STM32循迹避障小车源码解析:从外设分配到PD控制
发布时间:2026/9/10 9:20:57 作者:尧图编辑部 阅读量:1,286

简介基于STM32的循迹避障小车毕设源码包面向计算机、电子类专业正在准备毕业设计或课程项目的学生也适合需要完整嵌入式实战案例的进阶学习者。项目源自大四高分毕设评审99分经导师认可代码完整可运行硬件原理图、PCB、结构模型与源码均整理在内能够直接用于功能演示与二次开发。包内共322个文件以C语言源码、STM32标准外设库头文件及编译链接产物.o/.crf/.axf/.hex为主同时包含Keil工程配置、原理图与PCB文档.schdoc/.pcbdoc、3D模型文件.stl/.sldprt和说明文档.pdf/.md从代码到实物层次完整压缩包整体64.22MB结构清晰便于按模块查阅。已有164人学习下载适合拿来作为期末大作业、课程设计或毕设参考也可为循迹、避障、电机驱动等STM32典型应用提供可直接运行的示例。1. 为什么这个STM32循迹避障小车值得拆开看把一份完整跑通、评过99分的STM32循迹避障小车源码当研究对象比看零散教程有价值得多。这套资料不是随手拼的stm32f10x_adc.c 负责循迹灰度采样stm32f10x_tim.c 同时承担电机PWM和超声波输入捕获stm32f10x_flash.c 把校准阈值掉电保存连 i2c.c、can.c、fsmc.c 都是模板工程自带、按需裁剪的外设驱动说明作者保留了扩展接口而不是把删不掉的代码硬塞进去。对于要做毕业设计、课程设计或期末大作业的人最值得学的不是寻线算法本身而是一个完整工程里外设怎么分配、模块怎么组织、哪里留扩展。这篇把硬件资源分配、循迹与避障的代码实现、拿到工程后的编译烧录排错完整过一遍适合在校生也适合刚接触标准外设库的嵌入式新人。2. 硬件选型与STM32外设资源分配2.1 为什么是F103ZET6 标准外设库循迹避障小车是 STM32 毕设里出现频率最高的选题但很多人的代码只能在实验室跑通、换块板子就废。这套源码选的是 STM32F103ZET672MHz 主频、512KB Flash、64KB SRAM资源对小车主控来说溢出不少这不是浪费而是刻意留了余量循迹、避障跑完之后还能挂 OLED、ESP8266 甚至跑 Freemodbus。库用的是标准外设库而不是 HAL原因很实际——大量课程设计和毕业设计资料都基于标准库遇到问题搜GPIO_Init比搜MX_GPIO_Init能多找到十倍的结果而且标准库的结构对理解寄存器映射更友好。工程里 system_stm32f10x.c stm32f10x_rcc.c 决定了整个芯片的时钟走向这里值得先弄清楚因为后面所有外设的波特率、PWM频率、采样周期都挂在时钟树上。默认配置是外部 8MHz 晶振PLL 9 倍频到 72MHzAPB1 分频到 36MHzTIM2/3/4/5 的时钟源APB2 保持 72MHzGPIO、ADC1、TIM1 的时钟源。改外置晶振数值时不要把 system_stm32f10x.c 里的 PLL_M、PLL_N 孤立地调要连 HSE_VALUE 宏一起改否则串口波特率会整体偏移这是老手也容易忽略的地方。2.2 整车模块引脚分配拿到工程第一步不是看 main.c而是先建一张引脚占用表确认各模块没有抢占同一组外设。下面是这套小车源码中推荐的一套分配方案与标准库工程的默认配置对应改动最少模块引脚/外设功能说明左电机PWMPA6 / TIM3_CH1左轮速度10kHz右电机PWMPA7 / TIM3_CH2右轮速度10kHz电机方向PC0~PC3 GPIO四路IN引脚控制正反转五路循迹PC4~PC8 GPIO/ADCTCRT5000传感器阵列超声波TrigPA2 GPIOHC-SR04触发引脚超声波EchoPA3 / TIM2_CH4回波输入捕获OLED显示PB6/PB7 I2C1运行状态显示调试串口PA9/PA10 USART1115200-8N1打印日志这里有个容易被新手踩到的坑PA2/PA3 同时也是 USART2 的 TX/RX如果想把超声波和蓝牙模块一起挂上就会冲突。代码里超声波占用了 PA3 的 TIM2_CH4 复用功能蓝牙只能换到 USART1PA9/PA10串口调试和蓝牙共用一个串口时用跳线切换不要同时接。FSMC 和 I2C 在这个项目里处于模板保留状态I2C1 给了 OLEDFSMC 预留了外扩 TFT-LCDCAN 接口在驱动代码里初始化了但没有接总线后续要做多机通信或 Freemodbus 移植可以直接用上。提示GPIO 配置时PWM 引脚要开复用推挽GPIO_Mode_AF_PP方向引脚用推挽输出GPIO_Mode_Out_PP循迹输入引脚用浮空输入即可不要开上拉——上拉会直接影响灰度传感器的电平翻转点。2.3 电机驱动与TIM3 PWM初始化电机驱动常见两种方案L298N 和 TB6612。L298N 便宜但压降大约2V电池电压 7.4V 经驱动后到电机可能只剩 5V 多爬坡和转向时会明显乏力TB6612 管压降只有 0.5V 左右1.2A 连续电流够用发热低这套源码对应的驱动板方案也是兼容 TB6612FNG 的。选型时还要注意TB6612 的 VM 接电池正极VCC 接 3.3V 或 5V 逻辑电源两者不能接反否则芯片直接冒烟。PWM 初始化是独立封装的函数不依赖其他模块可以直接抄下来做电机起转测试void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tim.TIM_Prescaler psc; // 72MHz / (psc1) 得到计数频率 tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period arr; // PWM 频率 计数频率 / (arr1) tim.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; // 初始占空比 0防止上电猛冲 oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC2Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }初始化时把 preload 打开占空比更新会在计数周期边界同步生效不会出现运动中突然顿挫。调用时用TIM3_PWM_Init(7199, 0)PWM 频率就是 72MHz / 7200 10kHz。10kHz 对直流减速电机来说是合适的频率——低于 1kHz 能听到明显的电磁噪声且扭矩波动大高于 20kHz 驱动芯片的开关损耗上升但扭矩无明显提升。如果后期换了大功率电机这个频率可能需要下调到 5kHz。注意电池给电机供电时GND 必须和 STM32 开发板的 GND 相连共地否则 PWM 信号没有参考电平电机轻则抖动不转重则 GPIO 口被击穿。这是小车不上电转、上电乱转的头号原因。3. 循迹模块ADC灰度采样与PD转向修正3.1 TCRT5000 循迹原理与两种读取方式TCRT5000 是光电反射式传感器内部一个红外发射管加一个光电接收管。红外光照射到白色地面反射强接收管导通压降低照射到黑色胶带时大部分红外被吸收接收管截止电压升高。把接收端电压接到比较器或直接接 MCU就能区分黑白线。读取传感器有两种典型方式。一种是模块自带比较器输出数字电平黑线低电平/白线高电平GPIO 直接读引脚即可优点是代码简单、抗干扰强适合实验室白底黑线这种对比清晰的场景。另一种是模块只引出模拟电压ADC 采样MCU 通过 ADC 判断明暗程度可以输出 0~4095 的灰度值适合地砖颜色不统一或赛道有阴影的场地。这套源码里保留的是 ADC 方案对应 stm32f10x_adc.c因为 ADC 方案能拿到连续灰度值便于做阈值自适应这是评分时的一个加分点。3.2 ADC采样与阈值校准ADC 部分用 ADC1 的规则通道组连续采样。循迹传感器阵列的模拟输出接入的是 PC4/PC5 等引脚其中 PC4 对应 ADC1_IN14、PC5 对应 ADC1_IN15可以用独立模式单通道依次转换也可以开扫描模式一次把五路全部采完。注意ADC 引脚采样时间不能太短TCRT5000 的响应时间是微秒级的采样时间设置低于 55.5 周期时在高速行驶中会采到毛刺值。工程里的配置是采样时间 239.5 周期配合 12MHz ADC 时钟单通道采样率约 50kHz对循迹场景绰绰有余。读取五路灰度值的核心逻辑#define SENSOR_NUM 5 u16 sensor_adc[SENSOR_NUM]; // 五路原始ADC值 u16 threshold_low[SENSOR_NUM]; // 每路独立阈值 void read_sensors(void) { // 使用ADC1的注入组别一次触发依次转换五通道 // 注入组转换结果锁存读取期间不会被规则组打断 ADC_SoftwareStartInjectConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_JEOC) RESET); sensor_adc[0] ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1); sensor_adc[1] ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2); sensor_adc[2] ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_3); sensor_adc[3] ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_4); sensor_adc[4] ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_5); }注入组通道需要预先用ADC_InjectedChannelConfig()完成映射转换顺序和read_sensors()中的读取顺序要保持一致。使用注入组而不是规则组的原因PWM 和超声波中断会频繁抢占外设总线规则组 ADC 的转换结果寄存器有限转换慢时易被覆盖注入组有独立的结果寄存器适合在时序关键的主循环里一次性快照五路信号。阈值怎么定黑线和白地之间的 ADC 差值通常在 800~1500 之间工程里用每路独立阈值threshold_low[i]而不是统一用一个全局阈值。原因很实际五个传感器的安装高度、红外管衰减程度不可能完全一致统一阈值会出现中间三路正常、边缘两路永远误判的问题。校准时把车放在赛道上分别记录压黑线和压白地的值取中值作为初始阈值然后把这个值存进 Flash见 3.5 节下次开机免重复校准。3.3 五路加权偏差计算拿到五路二值化结果后要把哪一路看到黑线换算成一个连续的偏差量喂给 PD 控制器。最常用的是加权平均法中间传感器权重 0左侧为负右侧为正按激活状态加权平均得到归一化位置。// 将ADC值与阈值比较得到五路逻辑值 // 0白底1压线 static u8 sensor_binary[SENSOR_NUM]; static const s8 weight[SENSOR_NUM] {-2, -1, 0, 1, 2}; s16 get_line_position(void) { s32 sum 0, active 0; for (int i 0; i SENSOR_NUM; i) { sensor_binary[i] (sensor_adc[i] threshold_low[i]) ? 1 : 0; sum sensor_binary[i] * weight[i]; active sensor_binary[i]; } if (active 0) { // 全部白底说明冲出赛道返回一个极值让小车紧急寻线 return (last_position 0) ? 100 : -100; } last_position (s16)(sum * 100 / active); return last_position; }这段代码的关键在active 0的分支五路全白意味着车已经脱离黑线此时不能返回 0会让小车笔直开走而是沿用上一次的偏移方向返回极值让转向逻辑把小车的弯打回去寻线。sum * 100 / active把位置归一化到 -100~100100倍是为了让 PD 的 P 项系数不至于太小调参时看着更直观。3.4 PD转向控制与参数调节PWM 差速转向的实现思路把位置偏差换算成左右轮速度差偏差越大转向越猛。单纯 P 控制的问题是高速入弯时转向滞后车身会冲过弯心加上微分项 D 后偏差变化速率被提前感知入弯前就开始收油。static float kp 1.8f, kd 0.6f; static s16 last_pos 0; void motor_control_by_line(void) { s16 pos get_line_position(); float diff kp * pos kd * (pos - last_pos); last_pos pos; s16 speed_l BASE_SPEED (s16)diff; // 左轮 s16 speed_r BASE_SPEED - (s16)diff; // 右轮 // 限幅保护堵转电流过大会拉低电池电压 if (speed_l MAX_SPEED) speed_l MAX_SPEED; if (speed_r MAX_SPEED) speed_r MAX_SPEED; if (speed_l -MAX_SPEED) speed_l -MAX_SPEED; if (speed_r -MAX_SPEED) speed_r -MAX_SPEED; TIM_SetCompare1(TIM3, abs(speed_l)); TIM_SetCompare2(TIM3, abs(speed_r)); // 方向引脚根据speed_l/speed_r符号置高或置低 set_motor_dir(LEFT, speed_l 0); set_motor_dir(RIGHT, speed_r 0); }调参顺序建议从纯 P 开始先设 kd0kp 从 0.5 逐步增大直到小车在直道上不摆头、弯道上不冲出赛道此时记下 kp再累加 kd每次加 0.2观察入弯是否更平滑。如果小车在直道上左右震荡说明 kp 偏大或 kd 偏小如果入弯后转向不足冲出去说明 kd 过大把转向动作提前削减了。BASE_SPEED 的典型范围是 300~500占空比 0~7199 时的数值低于 300 时电机接近堵转临界点高于 550 时大部分赛道的 U 形弯会甩尾。现象调整参数直道抖动、蛇形kp 减小 20% 或 kd 增大 0.2入弯转向不足、冲出赛道kd 减小或 kp 增大弯道内侧轮反转限幅值设太高改回 500 基准循线稳定但响应迟钝kp 增大 10%观察两圈PD 参数不是固定的电池电压从 8.4V 掉到 7.2V 时电机响应变慢同一套参数表现会差不少。正式调试时用充满电的电池调完比赛或演示前再充满电避免参数漂移。3.5 阈值掉电保存到Flash每次开机都要把车放赛道上重新校准一次在答辩演示时非常尴尬。工程里把五路阈值写进了 stm32f10x_flash.c 驱动的功能范围启动时先用一小段内嵌逻辑检测 Flash 里是否有有效校准数据有就直接加载没有才进入校准模式。这里注意 F103ZET6 的 Flash 按扇区管理前 4 个扇区各 16KB写之前必须先擦除整扇区#define CALIB_FLAG_ADDR 0x0807F000 // 最后64KB扇区起始 #define CALIB_DATA_OFFSET 0x10 void threshold_save_to_flash(u16 *th, u8 len) { u32 addr CALIB_FLAG_ADDR; FLASH_Unlock(); FLASH_ErasePage(CALIB_FLAG_ADDR); // 擦除整页 // 先写标志0xA55A 表示阈值有效 FLASH_ProgramHalfWord(addr, 0xA55A); addr 2; for (u8 i 0; i len; i) { FLASH_ProgramHalfWord(addr, th[i]); addr 2; } FLASH_Lock(); }0x0807F000位于 F103ZET6 用户 Flash 的最末段放阈值不会跟代码区冲突但要注意程序本身编译后如果超过了这个地址烧录时会把这段覆盖掉导致校准数据被破坏。检查方法很简单编译后看 Keil 的 Build Output确认 Code 区的结束地址小于 0x0807F000。掉电保存逻辑可以延伸到行驶里程、最高速度记录等参数作为一个低成本数据记录思路。4. 超声波避障输入捕获测距与状态机切换4.1 HC-SR04 测距原理为什么选输入捕获HC-SR04 的工作时序Trig 引脚拉高 10us 以上触发测距模块自动发射 8 个 40kHz 超声波脉冲然后 Echo 引脚输出一个高电平脉冲脉冲宽度等于超声波从发射到接收的往返时间。距离 高电平时间 × 声速 / 2即每微秒对应 0.017cm。测距实现有两种常见方案。轮询延时方案用while(GPIO_ReadPin()SET)死等加delay_us()计时代码简单但有两个致命问题一是主循环被阻塞测距期间电机 PWM 无法更新小车会一卡一卡地走二是delay_us()本身依赖 SysTick与中断嵌套叠加时误差很大。这套工程用的是定时器输入捕获TIM2_CH4 配置为捕获模式自动记录 Echo 引脚上高电平的上升沿和下降沿时刻两个时刻的差值就是脉冲宽度全程 0 CPU 占用测距的同时循迹逻辑照常运行。4.2 TIM2 输入捕获初始化Echo 引脚接在 PA3TIM2_CH4必须开启 GPB 复用时钟和 TIM2 时钟。输入捕获的滤波器参数值得单独说HC-SR04 在复杂环境下 Echo 信号偶尔会有前沿抖动不滤波时会捕获到错误起始时刻测距值跳变几十厘米。void TIM2_IC_Init(void) { GPIO_InitTypeDef gpio; TIM_ICInitTypeDef ic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin GPIO_Pin_3; // PA3: TIM2_CH4 gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); ic.TIM_Channel TIM_Channel_4; ic.TIM_ICPolarity TIM_ICPolarity_Rising; // 先捕获上升沿 ic.TIM_ICSelection TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler TIM_ICPSC_DIV1; ic.TIM_ICFilter 0x0F; // 8个时钟周期采样滤波 TIM_ICInit(TIM2, ic); TIM_Cmd(TIM2, ENABLE); }配置成上升沿捕获后还要处理下降沿。标准库的输入捕获一次只能配置一种极性所以工程里采用捕获上升沿 - 记录时刻 - 重配为下降沿 - 再捕获的切换方式。重配极性用TIM_ICInit直接再调一次即可不需要关闭定时器。这里有个细节TIM_ICFilter 0x0F意味着信号需要连续 8 个时钟周期保持同一电平才算有效滤波太强会丢失超声波的短脉冲太弱又会引入毛刺0x0F 对 40kHz 超声波场景是合理的默认值。4.3 往返时间读取与距离换算完整的测距函数包含触发、捕获上升沿、捕获下降沿三部分。触发超声波之前先清一次捕获标志防止上一次残留的中断标志干扰本次测量float ultrasonic_get_cm(void) { u32 t1, t2; // 清标志避免残留捕获值影响本次计算 TIM_ClearFlag(TIM2, TIM_FLAG_CC4); GPIO_SetBits(GPIOA, GPIO_Pin_2); // Trig 拉高 delay_us(15); // 至少10us取15us留余量 GPIO_ResetBits(GPIOA, GPIO_Pin_2); // Trig 拉低 // 等待上升沿捕获 while (TIM_GetFlagStatus(TIM2, TIM_FLAG_CC4) RESET); t1 TIM_GetCapture4(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC4); // 切换为下降沿捕获 TIM_ICInit(TIM2, falling_ic_init); while (TIM_GetFlagStatus(TIM2, TIM_FLAG_CC4) RESET); t2 TIM_GetCapture4(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC4); // 恢复上升沿模式 TIM_ICInit(TIM2, rising_ic_init); // 溢出处理超出40ms未捕获到下降沿视为超量程 if ((t2 t1) || (t2 - t1 40000)) return -1; return (float)(t2 - t1) * 0.017f; }逻辑说明TIM2 计数频率 72MHz 会溢出65535 个计数约 0.91ms 翻一次所以测距函数里不能直接用 32 位计数器的值算而是必须在两次捕获之间切换极性并不断清标志。t2 - t1 40000对应高电平时间超过 40 毫秒即距离超过 6.8 米HC-SR04 量程只有 4 米左右超限直接返回 -1 交给上层处理避免小车把无效距离当成障碍物急停。4.3 循迹与避障的状态机切换避障的决策逻辑用状态机组织比在 main 里堆 if-else 清晰得多。系统只有三个状态正常循迹、避障转向、急停等待。状态之间的转换条件以距离阈值为准工况前向距离动作正常行驶大于 30cm循迹优先遇障预警20~30cm降速到 60%继续循迹强制避障小于 20cm停车原地转向 90°脱困恢复侧向距离大于 35cm回正恢复循迹状态切换的判定逻辑超声波数据是异步更新的每 50ms 触发一次测距但状态机只在主循环里查询最新值不在中断里切换——中断里只负责更新距离变量。这样避免了一个经典问题Echo 捕获中断是否处理完成和状态切换无关状态机永远读到的是上一次完整测距的结果。typedef enum { STATE_FOLLOW_LINE, STATE_SLOW_DOWN, STATE_AVOID_TURN, STATE_RECOVER } CarState; CarState car_state STATE_FOLLOW_LINE; float obstacle_dis 100.0f; void state_machine_update(float dis) { switch (car_state) { case STATE_FOLLOW_LINE: if (dis 20.0f) { car_state STATE_AVOID_TURN; avoid_turn_start(1); // 传入方向参数优先往左 set_base_speed(0); // 先停车 } else if (dis 30.0f) { car_state STATE_SLOW_DOWN; set_base_speed(BASE_SPEED * 0.6f); } break; case STATE_SLOW_DOWN: if (dis 35.0f) { car_state STATE_FOLLOW_LINE; // 障碍物已移开 set_base_speed(BASE_SPEED); } else if (dis 18.0f) { car_state STATE_AVOID_TURN; } break; case STATE_AVOID_TURN: // 转向动作持续1.5秒后重新检测 if (avoid_turn_timeout()) { car_state STATE_RECOVER; set_base_speed(BASE_SPEED); } break; case STATE_RECOVER: // 转向回正期间保持低速 if (dis 25.0f) { car_state STATE_FOLLOW_LINE; set_base_speed(BASE_SPEED); } break; } }避障和循迹的仲裁逻辑在 main 循环里体现超声波距离大于 30cm 时电机控制函数只调用motor_control_by_line()进入避障状态后电机控制切换到固定 PWM 差速旋转。这里要注意avoid_turn_start的方向参数要根据障碍物位置动态决定——常见做法是检测到障碍物后先原地左转 90°测距左转后仍然小于 20cm 就说明左侧也不通再回正右转 180°直接掉头。整个状态机设计的关键是不信任单次测距结果连续两次距离都越界才切换状态避免超声波传感器受到声波干扰时小车神经质一样急停。5. Keil工程导入、car.axf与四个容易踩的坑5.1 car.axf 文件在工程里扮演什么角色下载资料解压后car.axf是最容易引起困惑的文件。axf 是 ARM ELF 格式的可执行镜像由 Keil 在编译链接后自动生成它和 hex、bin 的区别在于hex 是纯代码镜像用于烧录axf 额外包含调试符号表和源码路径信息所以它既是可执行文件也是调试文件。用 ST-LINK 在线调试时Keil 实际加载的就是 axf离线烧录用 hex 或 bin 更稳妥也可以用 Keil 自带的 fromelf 工具把 axf 转成 hexfromelf --i32 --outputcar.hex car.axf拿到资料后建议这么做先不要改代码直接用 Keil 打开工程编译一遍确认编辑环境和你本地的芯片包、编译器版本兼容再把 car.axf 烧到板子上验证硬件是否正常。如果板子型号和原工程一致STM32F103ZET6axf 是可以直接跑的如果换了型号比如从 ZET6 换成 C8T6axf 就不能用必须重新编译。5.2 环境准备芯片包与ST-LINK打开工程前先确认三件事Keil 版本、设备芯片包、调试器驱动。工程基于标准外设库MDK 4.7 以上或 MDK5 都能编译但 MDK5 需要安装 STM32F1xx 的 Device Family PackST-LINK 驱动注意区分旧版 ST-Link Utility 和新版 STM32CubeProgrammer两套驱动不能同时混用装完重启 Keil。给经常在 C51 和 STM32 两个平台间切换的人一句实在话两个环境的芯片包互不干扰不用卸了重装MDK5 装完 STM32F1 支持包后打开 C51 工程会自动切换工具链。5.3 error: no stm32 target found 排查流程这个报错在插上 ST-LINK、点下载后弹窗出现原因排在前四位的依次是芯片供电异常、SWD 引脚被占用、ST-LINK 固件太老、芯片被锁死。小车项目里最容易犯的是电池组只给电机驱动供电没给开发板供电ST-LINK 的 3.3V 输出能力又不够导致识别失败。排查顺序先用万用表量板子 3.3V再在 Keil 的 Debug 设置里把 ST-LINK 的 SW 模式波特率从 4MHz 降到 1MHz如果 SWD 被重映射到了别的功能按住复位键的同时点下载等烧录进度条出现再松开复位。芯片锁死用 ST-LINK Utility 全片擦除即可解。5.4 串口打印验证传感器是否正常小车不上赛道时肉眼看不到传感器状态用串口打印是最快的验证手段。工程里 USART1 初始化在 usart.c 中波特率 115200重定向fputc后即可用printf打印日志// 主循环低速轮询打印 printf(dis%5.1fcm adc[0..4]%d %d %d %d %d state%d\n, obstacle_dis, sensor_adc[0], sensor_adc[1], sensor_adc[2], sensor_adc[3], sensor_adc[4], car_state);把车悬空用手在黑线和白地之间滑动串口助手里 adc 值应该有明显跳变超声波对着 30cm 处的手掌距离值应该稳定在 29~31cm 之间。这个技巧在答辩现场特别实用屏幕上实时滚动数据比口头解释这块代码能测距有说服力得多。5.5 基于这套源码扩展的三个方向如果要把成绩再往上推可以在现有工程上做三个方向的扩展每个都只需少量代码改动一是给循迹模块加模糊控制或 BP 网络替代固定 PD评分时智能算法是明显加分标签二是预留的 USART1 接 ESP8266 或蓝牙模块把车体姿态和距离数据推送到手机或云平台注意热点里常提到的stm328266 宿舍控制类项目就是同一条技术路线三是预留的 CAN 接口接第二块板子做主从编队这是把单个小车提升到多机协作课题的最短路径。三个方向都不需要动当前的主控板和驱动板原工程的引脚分配和代码组织足够支撑。本文还有配套的精品资源点击获取