基于STM32F103ZET6的循迹小车:硬件选型、PID调参与避坑指南 简介这是一份面向智能车竞赛与嵌入式控制学习者的STM32循迹小车完整工程核心控制器为72MHz主频的STM32F103ZET6机械结构采用舵机加双编码电机方案赛道覆盖直道、环岛、S弯与连续直角弯等典型元素。程序以一字排列的灰度传感器阵列识别黑线并判断车身位置采用位置式PID作为主控算法分别驱动舵机转向与双电机差速适合想深入理解循迹竞速控制逻辑、PID参数整定与传感器布局的读者参考。资源包共207个文件约5.48MB包含36个c源文件与38个h头文件构成的核心驱动与算法代码、38个crf与39个o编译中间文件、39个d依赖文件以及uvprojx工程文件、sct分散加载脚本、hex与axf可执行输出和map映射文件完整保留了Keil工程从源码到烧录的目录结构。目前已有8434人学习下载可据此复现整车控制框架、对照调试PID与传感器阈值并借鉴环岛、S弯等赛道的处理思路。1. 基于STM32F103ZET6的循迹小车从选型到跑通的第一道坎很多人第一次做循迹小车都会卡在同一个地方传感器读数飘、电机一给 PWM 就复位、PID 调半天还是画龙。基于 STM32F103ZET6 的循迹小车之所以值得单独拿出来讲是因为这颗芯片资源足够多——72MHz 主频、512KB Flash、64KB SRAM、多达 11 个定时器、3 个 ADC、5 个 USART既能跑灰度传感器阵列也能同时驱动编码器测速和 OLED 调试界面不用像 8 位机那样处处省资源。它适合两类人一类是刚学完 GPIO 和定时器、想找个综合项目练手的嵌入式新手另一类是想把 PID 闭环、传感器融合这些控制概念落到实物上的同学。这篇笔记按“硬件怎么接、代码怎么写、参数怎么调、坑在哪”的顺序展开所有代码基于标准外设库换成 HAL 只需改初始化部分控制逻辑不变。2. 硬件选型与接线为什么是 ZET6 而不是 C8T62.1 主控、传感器、驱动的三角关系循迹小车的核心链路很短灰度传感器识别黑线主控读传感器并算偏差主控输出 PWM 给电机驱动电机带动轮子修正方向。链路短不代表好做问题往往出在“选型不匹配”。主控选 STM32F103ZET6最直接的理由是引脚多。一个 5 路灰度传感器占 5 个 GPIOTB6612 或 L298N 驱动占 4 到 6 个 GPIO编码器测速占 2 个定时器通道OLED 占 2 个 I2C 引脚再加上串口调试和按键C8T6 的 37 个可用 IO 会很紧张ZET6 的 112 个 IO 让你不用在引脚分配上做取舍。另一个理由是定时器多TIM1 输出两路 PWM 驱动电机TIM2 和 TIM3 接编码器做正交解码TIM4 做控制周期中断互不抢占。传感器常见做法是 5 路或 7 路灰度模块输出数字量或模拟量。数字量接线简单但只能知道“偏了”不知道“偏多少”模拟量能给出连续偏差配合 ADC 可以做更细的 PID。我一般建议新手先用数字量跑通逻辑再换模拟量做优化。电机驱动选 TB6612 而不是 L298N原因是 TB6612 效率高、发热小、支持 1.2A 持续电流对 130 或 260 电机足够。L298N 压降大电池电压 7.4V 时电机实际只有 5V 左右速度上不去。2.2 接线表与电源注意事项模块引脚接到 ZET6说明灰度传感器OUT1~OUT5PA0~PA4数字量输入上拉TB6612PWMAPA8 (TIM1_CH1)左电机调速TB6612PWMBPA11 (TIM1_CH4)右电机调速TB6612AIN1/AIN2PB0/PB1左电机方向TB6612BIN1/BIN2PB2/PB3右电机方向编码器A/B相PA6/PA7 (TIM3)左轮测速编码器A/B相PB6/PB7 (TIM4)右轮测速OLEDSCL/SDAPB8/PB9I2C 调试显示电源是第一个大坑。ZET6 需要 3.3V电机需要 6~9V传感器一般 5V。常见做法是 2 节 18650 串联给电机驱动再用 AMS1117-3.3 降压给主控。注意 AMS1117 压差约 1.1V电池降到 4.5V 以下时 3.3V 输出会不稳主控会复位。更稳的方案是用 MP1584 这类开关降压效率高、压差小。提示电机和主控共地是必须的但电机电源线不要和传感器信号线捆在一起走PWM 开关噪声会串进灰度传感器表现为读数随机跳变。3. 传感器采样与偏差计算把“看到线”变成“知道偏多少”3.1 数字量与模拟量的取舍数字量灰度模块内部已经做了比较器输出 0 或 1主控读 GPIO 就行。优点是代码简单、不占 ADC缺点是阈值固定遇到反光地面或深色赛道容易误判。模拟量模块输出 0~3.3V 或 0~5V主控用 ADC 采样后自己定阈值灵活但要多写一点代码。我一般会先用数字量把整车跑起来确认电机方向、PWM 极性、传感器顺序都对再换模拟量。这样出问题时能快速定位是控制逻辑错还是传感器错。3.2 五路传感器的偏差公式假设传感器从左到右编号 1~5压线为 1未压线为 0。常见偏差计算有两种加权法给每个传感器一个权重比如 -2、-1、0、1、2偏差 Σ(权重 × 状态) / Σ(状态)。全黑或全白时除零需要单独处理。位置法找到所有压线传感器中最中间的那个用它的位置减 2 作为偏差。比如只有第 4 路压线偏差 4 - 3 1以 1 为起点。加权法更平滑适合 PID位置法响应快适合简单逻辑。下面用加权法写采样函数。// 五路灰度传感器PA0~PA4返回偏差值 -2~2全白返回 0xFF 表示丢线 #define SENSOR_NUM 5 const int8_t weight[SENSOR_NUM] {-2, -1, 0, 1, 2}; uint8_t read_sensors(void) { uint8_t state 0; for (int i 0; i SENSOR_NUM; i) { // 传感器压黑线输出低电平这里取反 if (GPIO_ReadInputDataBit(GPIOA, 1 i) Bit_RESET) { state | (1 i); } } return state; } int8_t calc_error(uint8_t state) { int sum 0, count 0; for (int i 0; i SENSOR_NUM; i) { if (state (1 i)) { sum weight[i]; count; } } if (count 0) return 0xFF; // 丢线 return (int8_t)(sum / count); }read_sensors把 5 个 GPIO 拼成一个字节方便后续判断。calc_error用加权平均算偏差count 为 0 时返回 0xFF 表示全白丢线主控可以据此进入丢线处理比如停车或按上次方向猛打。注意权重数组的顺序要和传感器物理顺序一致接反了车会往反方向修正这是新手最常见的翻车点。3.3 采样周期与滤波传感器采样不要放在主循环里裸跑建议用定时器中断固定 5ms 或 10ms 采一次。数字量本身跳变少但机械振动会让传感器瞬间离地读数抖动。简单做法是连续采 3 次取多数或者用一阶低通滤波// 一阶低通alpha 越小越平滑但延迟越大 float error_filtered 0; float alpha 0.3f; void update_error(int8_t raw) { if (raw 0xFF) return; // 丢线不滤波 error_filtered alpha * raw (1 - alpha) * error_filtered; }alpha 取 0.3 左右对应约 3 个采样周期的延迟。如果车在直道上画龙先把 alpha 调小如果过弯反应迟钝把 alpha 调大。这个参数没有理论最优和车速、传感器间距、赛道曲率都有关只能实测。4. PID 控制与电机输出从偏差到 PWM 的映射4.1 位置式 PID 的代码结构循迹小车一般用位置式 PID输入是偏差 error输出是左右电机的差速。公式output Kp × error Ki × Σerror Kd × (error - last_error)Kp 决定修正力度Ki 消除稳态误差Kd 抑制超调。循迹场景里 Ki 通常很小甚至为 0因为赛道是连续的积分项容易累积导致过弯后猛冲。typedef struct { float Kp, Ki, Kd; float integral; float last_error; float output_max; } PID; PID pid {15.0f, 0.0f, 8.0f, 0, 0, 100.0f}; float pid_calc(PID *p, float error) { p-integral error; // 积分限幅防止累积过大 if (p-integral 50) p-integral 50; if (p-integral -50) p-integral -50; float derivative error - p-last_error; p-last_error error; float out p-Kp * error p-Ki * p-integral p-Kd * derivative; // 输出限幅 if (out p-output_max) out p-output_max; if (out -p-output_max) out -p-output_max; return out; }integral累加偏差限幅在 ±50 防止积分饱和。derivative用当前偏差减上次偏差注意第一次调用时 last_error 为 0会有一次冲击可以在初始化时把 last_error 设为第一次的 error。output_max限制输出幅度和 PWM 满量程对应。4.2 差速合成与电机方向控制PID 输出是一个带符号的修正量把它加到基础速度上int base_speed 60; // 基础 PWM 占空比 0~100 int left_speed, right_speed; void motor_update(float pid_out) { left_speed base_speed (int)pid_out; right_speed base_speed - (int)pid_out; // 限幅到 0~100 if (left_speed 100) left_speed 100; if (left_speed 0) left_speed 0; if (right_speed 100) right_speed 100; if (right_speed 0) right_speed 0; set_pwm(TIM1, left_speed, right_speed); set_direction(left_speed 0, right_speed 0); }base_speed是直道上的巡航速度太小过弯没力太大容易冲出赛道。一般从 40 开始试稳定后逐步加到 70。pid_out为正表示车偏右需要左轮加速右轮减速所以左加右减。如果发现修正方向反了把 PID 的 Kp 取负或者交换电机接线但推荐改代码里的符号接线保持规范。4.3 定时器配置与 PWM 频率TIM1 输出 PWM频率建议 10kHz~20kHz低于 5kHz 电机会有啸叫高于 20kHz 驱动芯片开关损耗大。配置时预分频和自动重装值要算对// 72MHz / (711) 1MHz 计数频率 // 1MHz / (991) 10kHz PWM 频率 TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 71; tim.TIM_Period 99; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, tim); // 占空比 compare / (period1) // 设置 60% 占空比compare 60 TIM_SetCompare1(TIM1, 60);预分频 71 得到 1MHz 计数周期 99 得到 10kHz。占空比 60% 对应比较值 60。注意 TIM1 是高级定时器需要额外使能 PWM 输出TIM_CtrlPWMOutputs(TIM1, ENABLE);漏掉这句是 TIM1 不出 PWM 的最常见原因血泪经验。5. 避坑与排查那些让车不按预期跑的原因5.1 上电就复位或跑几秒停住现象电机一转主控就重启或者跑几秒后 OLED 花屏。原因电机启动瞬间电流大电池电压被拉低AMS1117 输入低于 4.4V 后 3.3V 输出跌落主控欠压复位。解决电机电源和主控电源分开走线在主控电源入口并 100μF 电解电容和 0.1μF 陶瓷电容换用 MP1584 开关降压电池电量不足时及时充电。5.2 传感器读数全黑或全白现象OLED 显示 5 路全是 1 或全是 0车不动或乱转。原因灰度传感器阈值电位器没调或者传感器离地高度不对。数字量模块一般有可调电阻需要对着赛道调阈值。解决把车放在黑线上调电位器直到对应 LED 刚好亮再放到白底上确认 LED 灭。离地高度建议 8~15mm太低容易刮蹭太高灵敏度下降。5.3 PID 调参后车画龙或过弯冲出现象直道上左右摆或者弯道不减速直接冲出去。原因Kp 太大导致过冲Kd 太小抑制不住振荡或者 base_speed 太高过弯时差速不够。解决先把 Kp 从 5 开始逐步加到车能修正但不振荡再加 Kd 抑制超调Kd 一般是 Kp 的 0.5~1 倍过弯冲出就降 base_speed或者加弯道检测偏差绝对值大于 1 时降速。5.4 电机一个转一个不转现象给同样 PWM左轮转右轮不转或者反转。原因TB6612 的 STBY 引脚没拉高或者方向引脚接错。解决STBY 接 3.3V 或 GPIO 输出高用万用表测 AIN1/AIN2 电平正转时一个高一个低如果方向反了交换电机两根线或者改代码里的方向标志。5.5 编码器测速跳变现象速度显示忽大忽小闭环控制抖动。原因编码器 A/B 相接线太长没屏蔽或者定时器编码器模式配置错。解决编码器线用双绞线尽量短TIM 编码器模式要配置 TI1 和 TI2 都为上升沿计数周期设最大 65535每 10ms 读一次并清零。6. 进阶技巧用编码器做速度闭环与参数整定6.1 从开环到双闭环前面讲的 PID 只用了灰度偏差属于位置开环。加上编码器后可以做成“外环位置 内环速度”的双闭环外环 PID 输出作为目标速度内环 PID 根据编码器实测速度调整 PWM。这样在电池电压下降时内环会自动加大占空比维持速度直道一致性明显提升。内环速度 PID 的代码结构和位置环一样只是输入变成目标速度减实际速度输出直接给 PWM。采样周期建议 10ms和编码器读取周期一致。// 内环速度 PID输入目标速度和实际速度 float speed_pid_calc(float target, float actual) { static float integral 0, last_err 0; float err target - actual; integral err; if (integral 100) integral 100; if (integral -100) integral -100; float out 2.0f * err 0.5f * integral 0.1f * (err - last_err); last_err err; return out; }内环 Kp 一般比外环小因为速度环响应快Kp 太大会震荡。Ki 用来消除稳态速度误差Kd 抑制速度突变。6.2 参数整定的实操顺序我一般按这个顺序调只开外环位置 PID内环固定 PWM调 Kp 到车能跟线但不振荡。加 Kd 抑制直道摆动观察过弯是否平滑。开内环速度 PID目标速度设成外环输出先调内环 Kp 到速度跟随不滞后。加内环 Ki 消除速度静差Kd 一般给很小或 0。整体提速每加 10% 基础速度重新微调外环 Kp 和 Kd。整定过程中用 OLED 实时显示偏差、PID 输出、左右轮速度比串口打印直观。如果 OLED 刷新太慢可以只显示偏差和输出两个值。6.3 一个容易被忽略的细节死区补偿电机在 PWM 低于某个值时不转这个值叫死区。比如占空比 20% 以下电机没反应PID 输出在 0 附近时车其实不动积分项会累积到很大一旦超过死区就猛冲。解决办法是在 PWM 输出前加死区补偿int apply_deadzone(int pwm, int deadzone) { if (pwm 0 pwm deadzone) return deadzone; if (pwm 0 pwm -deadzone) return -deadzone; return pwm; }deadzone 实测得到一般 15~25。加上之后低速控制会顺很多这是调车调到最后才会发现的细节但效果立竿见影。注意死区补偿只加在最终输出上不要加在 PID 内部否则积分项会算错。整套跑下来硬件半天、代码一天、调参两天是正常节奏。别指望一次调好我自己的车在赛道上跑了三十多圈才把 Kp、Kd 和 base_speed 匹配到不画龙也不冲弯。每次改一个参数记下现象比盲目试快得多。希望帮到你。本文还有配套的精品资源点击获取