简介这是一份面向嵌入式初学者与电子竞赛选手的STM32循迹避障小车完整工程基于STM32F103C8T6最小系统板搭建采用L298N驱动两路直流电机12V供电通过Timer3输出两路PWM实现调速并集成四路红外循迹、超声波测距配合舵机自动转向的避障方案同时支持手机蓝牙调试助手远程控制小车运动方向。资源包共227个文件以c源码、h头文件、o目标文件、crf交叉引用文件及s启动文件为主另含hex烧录文件、uvprojx工程文件与map映射文件压缩包约7.38MB工程结构完整、代码注释详尽便于对照理解各外设驱动与主控逻辑。目前已有1677人学习下载适合希望掌握电机驱动、PWM调速、传感器融合与蓝牙通信等综合应用的读者参考实践。1. 从一堆散件到能跑的车循迹避障小车的真实门槛在哪很多人第一次拿到 STM32C8T6 最小系统板、几块钱的红外对管、一个 L298N 驱动模块和四节 18650 电池时脑子里想的是「焊完就能跑」。实际通电那一刻车要么原地打转要么直冲桌角要么循迹时在十字路口反复横跳。基于 STM32C8T6 的循迹避障小车核心不是把模块插满而是把「感知—决策—执行」这条链路调稳红外对管告诉你黑线在哪超声波告诉你前方有没有墙STM32 用定时器和中断把这两路信息揉成左右轮的速度差L298N 再把 PWM 变成电机力矩。它适合已经学过 GPIO 和定时器、想找一个能把中断、PWM、传感器滤波串起来的综合练手项目的人。如果你连 Keil 新建工程、下载器接线都还没跑通先补那一步否则后面每一步都是玄学。2. 硬件选型与接线为什么 C8T6 够用以及哪些模块不能省2.1 主控、驱动、传感器的三角关系STM32C8T6 是 48 脚封装GPIO 数量对于「两路电机 PWM 四路循迹输入 一路超声波 Trig/Echo 一路调试串口」刚好够用不需要上 C8T6 以上的型号。选它的理由很直接定时器资源够TIM1 出四路 PWM 中的两路给左右电机TIM2 做超声波回波捕获TIM3 做循迹采样定时剩下的 SysTick 给主循环节拍。驱动模块常见做法是 L298N 或 TB6612前者便宜、发热大、压降明显后者效率高但接线更密。如果你用四节 18650 串联约 14.8VL298N 的 5V 输出还能给 C8T6 供电但电机启动瞬间的电流跌落会让主控复位所以主控最好单独一路 5V 降压。循迹传感器用 TCRT5000 红外对管输出数字量黑线吸光、白底反光调节电位器让指示灯在黑白交界处刚好翻转。避障用 HC-SR04Trig 发 10us 高电平Echo 回高电平高电平持续时间除以 58 就是厘米数。2.2 接线表与供电去耦下面这张表是我实际接线时用的映射你可以按自己板子改但别把电机电源和主控电源混在一起。功能STM32C8T6 引脚外设引脚备注左电机 PWMPA8 (TIM1_CH1)L298N ENA频率 10kHz左电机方向PB0L298N IN1高电平正转左电机方向PB1L298N IN2与 IN1 反相右电机 PWMPA9 (TIM1_CH2)L298N ENB频率 10kHz右电机方向PB10L298N IN3高电平正转右电机方向PB11L298N IN4与 IN3 反相循迹左PA0TCRT5000 DO上拉输入循迹中左PA1TCRT5000 DO上拉输入循迹中右PA2TCRT5000 DO上拉输入循迹右PA3TCRT5000 DO上拉输入超声波 TrigPB6HC-SR04 Trig推挽输出超声波 EchoPB7HC-SR04 Echo浮空输入/捕获调试串口 TXPA9 复用前先确认CH340 RX波特率 115200注意PA9 如果同时被 TIM1_CH2 和串口占用要么换串口到 USART2要么换 PWM 到 TIM3。我一般把串口放 USART2 的 PA2/PA3但 PA2/PA3 又被循迹占了所以最终方案是循迹用 PB12~PB15串口用 PA9/PA10PWM 用 TIM3_CH1/CH2 的 PB4/PB5。接线前先在纸上把复用关系画一遍比烧录后调不出来再回头查省两个小时。2.3 电机与轮径的机械匹配很多人忽略轮径和减速比。常见黄色 TT 马达配 65mm 轮空载转速约 200rpm实际带车约 120rpm对应线速度约 0.4m/s。循迹时如果速度超过 0.6m/s红外对管的响应时间约 10ms加上主循环周期会让车在弯道直接冲出去。所以 PWM 占空比上限我一般限制在 70%给纠偏留余量。轮距也要量左右轮中心距 12cm 和 15cm 的转向灵敏度差很多代码里的差速系数要跟着改。3. 让车自己找线循迹算法的状态机与 PWM 差速实现3.1 四路红外怎么读成「偏差量」四路循迹的输出是四个数字量黑线为 1或 0取决于模块白底相反。不要用 if-else 硬编码十六种情况而是把四路加权成一个偏差值。比如从左到右权重 -3、-1、1、3检测到黑线的通道权重相加得到 error。error 为 0 表示居中负值偏左正值偏右。这样即使只有两路压线也能算出连续偏差比离散状态机平滑。// 循迹偏差计算sensor[0]最左sensor[3]最右 // 黑线输出为1白底为0 int16_t get_line_error(void) { int16_t weight[4] {-3, -1, 1, 3}; int16_t error 0; uint8_t count 0; for (int i 0; i 4; i) { if (sensor[i] 1) { // 检测到黑线 error weight[i]; count; } } if (count 0) return 0; // 丢线交给上层处理 return error / count; // 归一化避免多路压线时偏差过大 }这段代码的关键在最后除以 count。如果不除四路同时压线时 error 为 0但两路压线时 error 可能是 -2 或 2差速会突然变大。归一化后偏差范围稳定在 -3 到 3 之间方便映射到 PWM 增量。丢线时返回 0 是权宜之计实际要在主循环里判断 count0 持续超过 200ms 就进入「丢线找回」状态减速、原地小角度旋转直到重新检测到黑线。3.2 差速 PWM 的映射与限幅得到 error 后基础速度 base 设为 50%对应 TIM1 的 CCR 值 500假设 ARR999左右轮速度// base_speed 范围 0~1000对应 0~100% 占空比 // Kp 是比例系数先取 80再根据弯道调 void set_motor_by_error(int16_t error) { int16_t base_speed 500; // 50% int16_t Kp 80; int16_t left base_speed - Kp * error; int16_t right base_speed Kp * error; // 限幅防止负数或超过 ARR if (left 0) left 0; if (left 999) left 999; if (right 0) right 0; if (right 999) right 999; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, left); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, right); }Kp 从 80 开始调太小则弯道反应慢车会压线甚至冲出去太大则直道左右抖动像喝醉。调的时候先让车低速跑用串口把 error 和左右 PWM 打出来看直道 error 是否在 -1 到 1 之间波动弯道是否能及时给出 ±3 的偏差。如果直道就抖先把 Kp 降到 50再检查红外对管高度——离地 1cm 左右最稳太高会受环境光干扰太低会蹭到地面。3.3 定时器中断里的采样节拍循迹采样不要放在主循环里裸跑主循环里还有超声波和串口打印周期不稳定。我一般用 TIM3 每 5ms 中断一次在中断里读四路 GPIO 并计算 error存到全局变量。主循环只负责根据 error 更新 PWM。这样采样周期固定滤波也好做连续三次读到相同 error 才更新避免单次干扰导致电机抽搐。// TIM3 中断回调5ms 一次 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { static int16_t last_error 0; int16_t raw get_line_error(); // 简单中值滤波连续两次相同才采纳 static int16_t prev_raw 0; if (raw prev_raw) { last_error raw; } prev_raw raw; g_line_error last_error; } }这里的中值滤波只做了一层实际如果环境光变化大可以改成连续三次采样取中间值。注意中断里不要做浮点运算和串口打印否则会阻塞其他中断。4. 避障与循迹的优先级超声波怎么不跟循迹打架4.1 超声波测距的触发与捕获HC-SR04 的 Echo 高电平时间用输入捕获测最准。TIM2 配置成捕获模式Trig 发 10us 脉冲后Echo 上升沿捕获到 CCR1下降沿捕获到 CCR2差值乘以计数周期就是时间。如果不想用捕获也可以用外部中断加定时器计数但精度差一些。测距周期不要低于 60ms否则上一次的回波还没消失下一次触发会收到余波读数跳变。// 超声波测距返回厘米超时返回 -1 float get_distance_cm(void) { uint32_t start, end; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 等待 Echo 上升沿超时 30ms uint32_t timeout 30000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) GPIO_PIN_RESET) { if (--timeout 0) return -1; delay_us(1); } start __HAL_TIM_GET_COUNTER(htim2); timeout 30000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) GPIO_PIN_SET) { if (--timeout 0) return -1; delay_us(1); } end __HAL_TIM_GET_COUNTER(htim2); uint32_t ticks (end start) ? (end - start) : (0xFFFF - start end); return ticks / 58.0f; // 72MHz 下 1us 约 58 个计数具体看预分频 }这段代码用轮询而不是捕获好处是逻辑直白坏处是占用 CPU。实际跑的时候主循环里每 100ms 调一次期间循迹中断照常跑不会冲突。注意 delay_us 要用定时器实现不要用空循环否则编译器优化等级一变延时就不准了。4.2 避障优先级的仲裁逻辑循迹和避障同时开的时候必须定优先级。我的做法是超声波距离小于 20cm 时避障优先强制覆盖循迹的 PWM 输出让车停下或后退转向距离在 20~40cm 之间减速并把循迹的 base_speed 降到 30%大于 40cm 才全速循迹。这样不会出现「一边循迹一边撞墙」的尴尬。// 主循环里的仲裁 float dist get_distance_cm(); if (dist 0 dist 20.0f) { // 避障优先停车然后右转 set_motor(0, 0); HAL_Delay(200); set_motor(400, -400); // 原地右转 HAL_Delay(300); } else if (dist 20.0f dist 40.0f) { // 减速循迹 g_base_speed 300; set_motor_by_error(g_line_error); } else { g_base_speed 500; set_motor_by_error(g_line_error); }注意 set_motor 里要处理负数负数表示反转L298N 的 IN1/IN2 要对应翻转。原地右转时左轮正转、右轮反转PWM 绝对值给 400 左右太大容易烧驱动。4.3 传感器融合的时序安排循迹 5ms 一次超声波 100ms 一次串口打印 500ms 一次。这三个周期不要用同一个定时器否则中断嵌套会乱。TIM3 给循迹TIM4 给超声波触发主循环里做串口打印。超声波测距期间会关中断吗不会但 Echo 等待循环里如果被循迹中断打断测出来的时间会偏大。所以我在超声波测距前先关掉循迹中断测完再开虽然会丢一两次循迹采样但 100ms 内丢 1 次 5ms 采样对循迹影响可以忽略。5. 避坑与排查从「能跑」到「跑得稳」的五个血泪教训5.1 电机一转主控就复位现象下载完程序车不动的时候串口正常一给 PWM 电机启动串口就断主控重启。 原因电机启动电流瞬间拉到 1A 以上电池电压跌落L298N 的 5V 输出跟着掉C8T6 的欠压复位触发。 解决主控单独用一路 5V 降压模块比如从电池经 MP1584 降到 5V电机电源和主控电源只在电池端共地不共用 L298N 的 5V。同时在 C8T6 的 5V 和 GND 之间并一个 470uF 电解电容加 0.1uF 陶瓷电容。5.2 循迹在直道画龙弯道冲出去现象直道上车左右摆弯道上不减速直接冲出赛道。 原因Kp 太大导致直道过调弯道时 error 饱和但 base_speed 没降差速不够。 解决直道抖动先把 Kp 从 80 降到 50再检查红外对管是否四路高度不一致。弯道冲出去要在 error 绝对值大于 2 时主动降 base_speed比如 base_speed 500 - 100 * abs(error)让车在急弯自动减速。5.3 超声波读数偶尔跳成 0 或 999现象串口打印的距离大部分正常偶尔出现 0cm 或 999cm。 原因Echo 等待循环没有超时保护或者周围有同频超声波干扰比如另一辆车也在测距。 解决等待循环加超时超时返回 -1上层丢弃这次读数。连续三次读数取中间值。如果两辆车同时跑把触发周期错开比如一辆 60ms另一辆 70ms。5.4 丢线后原地打转找不回来现象车冲出赛道后原地转圈再也回不到线上。 原因丢线判断只看了 count0没有限制旋转方向和时长。 解决丢线后先停车 200ms然后按最后一次 error 的方向小角度旋转比如左转 30 度再直行一小段如果还没找到线就反向转。旋转 PWM 不要超过 400否则转太快红外对管来不及采样。5.5 电池电量下降后行为完全变了现象满电时跑得好好的电压降到 7V 左右时循迹迟钝、避障距离变短。 原因电机转速下降同样的 PWM 占空比对应线速度变慢但循迹的 Kp 和 base_speed 是写死的。 解决加一路电池电压检测分压到 ADC电压低于阈值时自动把 base_speed 提高 10%或者干脆换更好的电池。更省事的做法是每次跑之前用满电电池别在低电量下调参数。6. 进阶调参与验证用串口波形把「感觉」变成数据调车最怕凭感觉。我后来养成一个习惯所有关键变量都通过串口打到上位机用波形看。具体做法是每 20ms 发一帧格式为「error, left_pwm, right_pwm, distance」波特率 115200用任意串口示波器工具解析。这样直道抖动、弯道响应、避障触发时机都一目了然。// 每 20ms 调用一次打印 CSV 格式 void debug_output(void) { static uint32_t last_tick 0; if (HAL_GetTick() - last_tick 20) return; last_tick HAL_GetTick(); printf(%d,%d,%d,%.1f\r\n, g_line_error, __HAL_TIM_GET_COMPARE(htim1, TIM_CHANNEL_1), __HAL_TIM_GET_COMPARE(htim1, TIM_CHANNEL_2), g_last_distance); }看波形时重点看三个地方直道 error 是否在 ±1 内高频小幅波动如果是Kp 还可以再降弯道 error 从 0 变到 ±3 的时间是否小于 100ms如果大于采样周期要缩短或 Kp 要加避障触发时 left_pwm 和 right_pwm 是否立刻归零如果有延迟检查主循环里超声波调用是不是被其他延时阻塞了。另一个验证方法是把车架起来轮子悬空用手转动轮子模拟地面摩擦看 PWM 变化是否跟手。这个方法能快速排除机械卡死和驱动接线错误。我一般先架空调参再落地跑能省一半调试时间。最后说一个我踩过的坑不要一边充电一边跑。充电模块的共地噪声会让红外对管误判车会突然抽风。希望帮到你。本文还有配套的精品资源点击获取