简介:这是一套面向智能车竞赛与嵌入式学习者的STM32循迹小车完整工程,以72MHz主频的STM32F103ZET6为核心控制器,机械结构采用舵机加双编码电机方案,赛道覆盖直道、环岛、S弯与连续直角弯等典型元素。程序通过一字排列的灰度传感器阵列识别黑线并判断车身位置,以位置式PID作为主控算法,分别驱动舵机转向与双电机差速,实现弯道平稳行驶,适合想深入理解传感器采集、PWM输出与闭环控制的进阶学习者参考。资源包共207个文件,约5.48MB,包含36个c源文件、38个h头文件、38个crf与39个d编译中间文件、39个o目标文件,以及uvprojx工程、sct分散加载、hex与axf可执行文件、map映射表等,完整保留了Keil工程结构与编译产物,便于直接打开复现与二次修改。目前已有8434人学习下载,可从中获取赛题级控制方案、PID调参思路与工程组织方式。
1. 从一堆散件到能跑的车:STM32F103ZET6 循迹小车到底在做什么
很多人第一次拿到 STM32F103ZET6 循迹小车套件时,都会经历同一个瞬间:电机、轮子、红外对管、驱动板、主控板摊了一桌子,接线图看着都懂,一上电要么原地打转,要么冲出赛道。问题往往不在某个模块坏了,而在于「感知—决策—执行」这条链路没有真正闭环。循迹小车本质是一个极简的嵌入式控制系统:红外传感器阵列把黑线位置变成电平信号,STM32F103ZET6 读取这些信号并做偏差判断,再通过 PWM 控制左右轮差速,让车体持续回到黑线中心。它适合刚接触 STM32 的开发者练手,也适合作为 PID 控制、传感器融合、电机驱动的入门载体。这篇文章不讲空泛概念,而是按我实际调试的顺序,把选型、接线、代码、参数和翻车点一次讲透,让你能照着复现一台稳定循迹的车。
2. 硬件选型与接线:为什么 ZET6 不是唯一选择,但它是最好起步的
2.1 主控、传感器、驱动和电机的匹配逻辑
STM32F103ZET6 属于 F1 系列里的高配型号,144 引脚、512KB Flash、64KB SRAM,定时器资源丰富,光是通用定时器和高级定时器就能同时输出多路 PWM。循迹小车真正吃紧的不是算力,而是定时器通道和 GPIO 数量。常见做法是:用 TIM1 或 TIM3 输出两路 PWM 给左右电机,用 TIM2 做微秒级延时或编码器接口,红外传感器阵列直接接普通 GPIO 并开启外部中断或定时扫描。电机驱动常用 L298N 或 TB6612,前者便宜但压降大,后者效率高但接线更细。电机选直流减速电机,减速比一般在 1:48 到 1:120 之间,转速太低循迹迟钝,太高容易冲线。
提示:ZET6 的 5V 容忍引脚有限,红外模块和驱动模块的电源最好独立走一路,不要从主控板 3.3V 直接拉大电流。
2.2 最小系统接线表与电源处理
下面这张表是我在面包板和洞洞板上都验证过的接线方式,引脚可以根据实际 PCB 调整,但电源和地线的处理逻辑不变。
| 模块 | 引脚 | 接到 ZET6 | 说明 |
|---|---|---|---|
| 红外传感器 1 | OUT | PA0 | 左外侧,黑线输出高或低取决于模块 |
| 红外传感器 2 | OUT | PA1 | 左内侧 |
| 红外传感器 3 | OUT | PA2 | 中间 |
| 红外传感器 4 | OUT | PA3 | 右内侧 |
| 红外传感器 5 | OUT | PA4 | 右外侧 |
| L298N | IN1/IN2 | PB0/PB1 | 左电机方向 |
| L298N | IN3/IN4 | PB2/PB3 | 右电机方向 |
| L298N | ENA/ENB | PA8/PA9 | TIM1_CH1/CH2 PWM |
| 电源 | 12V | 电机驱动 | 电池正负极 |
| 电源 | 5V | 传感器 | 经降压模块 |
| 电源 | 3.3V | 主控 | 板载稳压 |
电源部分是最容易翻车的地方。电机启动瞬间电流能到 1A 以上,如果和主控共用一路 LDO,STM32 会因为电压跌落而复位,表现就是「一加速就重启」。我一般会单独给电机驱动供 12V,再用一片 MP1584 或 LM2596 降到 5V 给传感器,主控板自己用 3.3V 稳压。所有模块的地必须共地,否则 PWM 信号没有参考电平,电机时转时不转。
2.3 传感器布局与阈值初调
五路红外是循迹小车里最稳的配置,三路容错太低,七路以上对新手焊接和调试都不友好。传感器离地高度建议 8 到 15 毫米,太高容易受环境光干扰,太低会刮到地面。上电后先别写代码,用万用表或串口打印每一路在黑白地上的电平,确认黑线对应的是 0 还是 1。很多模块输出逻辑相反,代码里判断条件写反了,车就会往反方向修正。
// 红外传感器读取与简单阈值判断 // 假设黑线输出低电平,白地输出高电平 #define SENSOR_PORT GPIOA #define S1 GPIO_Pin_0 #define S2 GPIO_Pin_1 #define S3 GPIO_Pin_2 #define S4 GPIO_Pin_3 #define S5 GPIO_Pin_4 uint8_t read_sensor_mask(void) { uint8_t mask = 0; if (GPIO_ReadInputDataBit(SENSOR_PORT, S1) == Bit_RESET) mask |= 0x01; if (GPIO_ReadInputDataBit(SENSOR_PORT, S2) == Bit_RESET) mask |= 0x02; if (GPIO_ReadInputDataBit(SENSOR_PORT, S3) == Bit_RESET) mask |= 0x04; if (GPIO_ReadInputDataBit(SENSOR_PORT, S4) == Bit_RESET) mask |= 0x08; if (GPIO_ReadInputDataBit(SENSOR_PORT, S5) == Bit_RESET) mask |= 0x10; return mask; }这段代码把五路电平压成一个字节,后面做偏差计算时直接查表或按位判断,比逐个 if 清晰得多。参数上唯一要改的是Bit_RESET和Bit_SET,取决于你的模块在黑线上输出什么。如果读出来全是 0 或全是 1,先查供电和地,再查传感器上的电位器有没有调到临界点。
3. 控制逻辑落地:从五路红外到差速 PWM 的完整代码路径
3.1 偏差建模:把传感器状态映射成方向误差
五路传感器一共 32 种组合,但真正有意义的是黑线落在哪个位置。常见做法是给每一路分配一个权重,比如最左 -2、左 -1、中 0、右 1、最右 2,然后对当前触发的传感器权重求平均,得到偏差值 error。如果五路都没触发,说明丢线,需要根据上一次的 error 决定往哪边找。这个模型比「左偏就右转」的硬编码更平滑,也更容易接 PID。
// 五路权重偏差计算 // 返回 -2 到 2 之间的偏差值,255 表示丢线 int8_t calc_error(uint8_t mask) { const int8_t weight[5] = {-2, -1, 0, 1, 2}; int8_t sum = 0; uint8_t count = 0; for (uint8_t i = 0; i < 5; i++) { if (mask & (1 << i)) { sum += weight[i]; count++; } } if (count == 0) return 255; // 丢线标志 return sum / count; }权重表可以根据赛道宽度和传感器间距微调。如果车在直道上左右摆,说明权重跨度太大,把 ±2 改成 ±1.5 或缩小中间传感器的权重。如果弯道跟不上,说明权重不够,需要加大外侧权重或提高控制频率。
3.2 PWM 差速控制:左右轮速度怎么算
拿到 error 之后,最直接的做法是给左右轮一个基础速度加修正量。基础速度决定直道快慢,修正系数决定转向灵敏度。我一般先用纯比例控制跑通,再考虑加积分和微分。下面这段代码把 error 映射成左右轮 PWM 占空比,并做了限幅,防止占空比超过定时器周期。
// 左右轮 PWM 差速控制 // base_speed: 基础速度 0-100 // kp: 比例系数,先取 15 左右试 void motor_control(int8_t error, uint8_t base_speed) { int16_t left, right; if (error == 255) { // 丢线:按上一次方向原地找线 left = 0; right = 0; return; } int16_t correction = error * 15; // kp = 15 left = base_speed + correction; right = base_speed - correction; // 限幅到 0-100 if (left > 100) left = 100; if (left < 0) left = 0; if (right > 100) right = 100; if (right < 0) right = 0; set_pwm(TIM1, 1, left); // 左轮 set_pwm(TIM1, 2, right); // 右轮 }base_speed建议从 40 开始试,太快会直接冲出去,太慢电机可能不转。kp是玄学参数,不同电机、不同电池电压下都不一样。电池从满电到没电,同样的占空比转速会差很多,所以要么加电压检测做补偿,要么接受性能衰减。
3.3 定时器与中断配置:控制周期怎么定
控制周期决定了系统的响应速度。太快,传感器来不及稳定;太慢,车已经冲出赛道才修正。常见做法是用 TIM2 或 SysTick 产生 5ms 到 10ms 的中断,在中断里读传感器、算 error、更新 PWM。这样主循环可以空出来做串口调试或按键处理。
// TIM2 中断配置,5ms 控制周期 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); uint8_t mask = read_sensor_mask(); int8_t error = calc_error(mask); motor_control(error, 45); } }中断里不要放串口打印或延时,否则会拖慢控制周期。调试时可以把 error 和 mask 存到全局变量,在主循环里打印。如果发现车反应迟钝,先看中断是不是被其他高优先级任务阻塞了。
4. 避坑与排查:循迹小车最常见的五个翻车现场
4.1 上电就原地打转
现象:电机一上电就全速转,车原地打转或直接冲出去。原因通常是方向引脚初始化电平不对,或者 PWM 占空比初始值给了 100。解决:在初始化时把 IN1 到 IN4 全部拉低,PWM 比较值设为 0,等传感器读完第一帧再启动电机。另外检查 L298N 的使能跳线帽有没有插上,有些模块默认不使能。
4.2 直道上左右摆,像喝醉一样
现象:车能循迹,但在直道上频繁左右修正,走不直。原因是比例系数 kp 太大,或者传感器权重跨度太大。解决:先把 kp 降到 8 到 10,再把外侧权重从 ±2 改成 ±1,观察摆动是否减小。如果还是摆,检查传感器离地高度是否一致,五路模块有没有装歪。
4.3 弯道直接冲出去
现象:直道正常,一到弯道就冲出去,完全不修正。原因是控制周期太长,或者基础速度太快。解决:把控制周期从 10ms 降到 5ms,基础速度从 60 降到 40,再试。如果弯道是急弯,五路传感器可能不够用,需要加宽传感器间距或换成七路。
4.4 电机时转时不转,伴随主控复位
现象:电机启动瞬间主控重启,或者电机声音断断续续。原因是电源被拉垮,电机和主控共用一路电源。解决:电机驱动单独供电,主控和传感器用另一路,所有地共地。在电机电源两端并一个 470uF 到 1000uF 的电解电容,吸收启动电流冲击。
4.5 黑线识别不稳定,受环境光影响大
现象:同一段赛道,室内灯一开一关,传感器输出就变了。原因是红外模块的阈值电位器调得太临界,或者环境光太强。解决:把传感器离地高度降到 8mm 左右,减少环境光进入;重新调电位器,让黑白地的电平差至少 1V 以上;如果还是不行,给传感器加黑色热缩管遮光。
5. 进阶调参与验证:让循迹从能跑变成跑得稳
5.1 用串口波形看 error 和 PWM 的关系
调车最有效的手段不是盯着车看,而是把 error 和左右 PWM 打到串口,用上位机画波形。正常循迹时 error 应该在 0 附近小幅波动,PWM 左右差不超过 20。如果 error 长时间饱和在 ±2,说明 kp 不够或基础速度太低;如果 error 高频抖动,说明传感器有噪声或控制周期太短。
// 主循环里打印调试数据 printf("err=%d, L=%d, R=%d\r\n", error, left_pwm, right_pwm);串口波特率用 115200,打印频率控制在 20Hz 左右,太快会拖慢主循环。观察波形时重点看 error 过零是否平滑,如果每次过零都伴随剧烈振荡,就要降低 kp 或加一点微分。
5.2 从纯比例到 PD 的过渡条件
纯比例控制能跑,但直道容易等幅振荡,弯道容易超调。加微分项可以预测 error 的变化趋势,提前减速或反向修正。我一般会在 error 波动超过 ±1 且频率稳定时加 D 项,kd 从 kp 的 1/10 开始试。积分项在循迹里很少用,因为赛道不是长时间单向偏差,积分容易饱和。
| 参数 | 作用 | 起步值 | 调整方向 |
|---|---|---|---|
| base_speed | 直道速度 | 40 | 摆振就降,迟钝就升 |
| kp | 转向力度 | 12 | 冲线就降,不拐就升 |
| kd | 阻尼 | 1.5 | 振荡就升,反应慢就降 |
| 控制周期 | 响应速度 | 5ms | 丢线就缩短,抖动就加长 |
5.3 赛道验证:从直线到 S 弯的逐步加压
不要一上来就跑完整赛道。先跑 1 米直线,确认不摆;再跑 90 度直角弯,确认能拐;再跑 S 弯,确认连续修正不丢线;最后跑完整闭环。每换一种赛道,先降速跑通,再逐步提速。电池电压从 12V 降到 10V 时,同样的 PWM 转速会下降,如果比赛要求稳定,最好加一路 ADC 检测电池电压,在代码里做占空比补偿。
我自己的习惯是每次改完参数只动一个变量,改完跑三圈,记录丢线次数和单圈时间。丢线次数比单圈时间更重要,因为一次丢线可能直接冲出赛道。希望帮到你。
本文还有配套的精品资源,点击获取