
简介面向智能车竞赛与嵌入式初学者的STM32循迹小车完整工程资料包。方案以72MHz主频的STM32F103ZET6为核心控制器采用一字型灰度传感器阵列识别赛道黑线配合舵机转向与双编码电机差速驱动并以位置式PID作为主要控制算法可应对直道、环岛、S弯和连续直角弯等典型赛道元素。压缩包共207个文件大小约5.48MB内含Keil工程源码.c/.h、编译生成的目标文件.o/.d、工程配置文件.uvprojx/.uvoptx以及可直接烧录的hex文件并保留了map、lst等链接与列表输出便于核对编译细节和程序结构代码按模块组织方便阅读与二次开发。该资源已吸引8435人浏览学习受欢迎程度较高。对想要快速搭建循迹小车、理解PID参数整定和差速控制逻辑的开发者来说这套资料既能提供完整代码参考也能帮助排查工程配置问题是值得收藏的实战项目无论用于备赛还是课程设计都能直接参考其完整闭环。1. 循迹小车为什么值得做成“舵机电机”的竞速底子做循迹小车很多同学第一个想到的是“四驱差速转向”两个后轮转速不一样就能拐弯结构简单、代码也好写。但如果你要的是“循迹竞速”也就是在白色赛道上跑出尽量快的圈速差速方案很快就会撞到天花板转向响应慢、循迹精度靠轮子打滑来凑速度一上来就容易冲出赛道。这时候用“舵机控制前轮转向 电机驱动后轮”的模型车架构反而更适合高速循迹这也是不少竞赛车队实际采用的方案。本文围绕基于STM32F103ZET6的循迹小车展开从硬件选型、传感器布局、PID控制到赛道策略给出一个可以直接照着搭的完整设计。适合正在做课程设计或毕业设计的学生也适合想从差速小车升级到高速循迹方案的开发者参考。2. 总体方案设计STM32F103ZET6的资源盘活与车体架构选择2.1 为什么选STM32F103ZET6做主控首先需要明确一件事做循迹小车主控不需要多高性能但外设资源要够用。STM32F103ZET6属于大容量型号144引脚封装Flash有512KB、RAM有64KB主频72MHz。循迹小车的传感器读取、舵机PWM输出、电机PWM输出、编码器采集、蓝牙调试这些任务它的外设完全扛得住而且库函数资料多遇到问题容易搜到参考。从资源分配角度我做方案时会这样安排灰度传感器模块用GPIO读取8路数字信号或者用ADC读取模拟量取决于传感器模块本身是数字输出还是模拟输出。舵机需要一个定时器产生50Hz的PWM占空比在2.5%到12.5%之间对应0度到180度。通常用TIM1的通道1。驱动电机需要一个频率高一点的PWM来控制速度比如20kHz减少电机啸叫用TIM2或TIM3。还要配合两个GPIO控制方向。编码器测速如果要做速度闭环需要两个定时器工作在编码器模式TIM4和TIM5比较常用。调试接口USART1接蓝牙模块或者无线串口用来在跑车的时候看PID输出和传感器状态。这套分配下F103ZET6的定时器和GPIO都还留有大量余量后续想加超声波避障、OLED显示状态、按键切换模式都不用换主控。2.2 车体架构舵机转向和电机驱动的职责划分循迹小车的车体结构直接决定了控制策略怎么写。差速小车靠左右轮速差转向转向半径由轮速差决定而舵机转向车型的前轮有一个最大转角舵机角度和转向半径呈近似线性关系。这个区别在控制上非常关键差速方案转向相当于一个积分量轮速差持续存在才会持续转弯控制周期要求更高否则会左右摆动手感很差。舵机方案舵机角度可以直接映射为目标转向角控制环只需要把当前角度调整到目标值天然适合PD控制响应快且超调小。在竞赛场景下舵机转向车型的另一个优势是支撑结构稳定。舵机装在底盘前方通过连杆带动前轮转向后轮由电机通过齿轮或直驱驱动整车重心分布可以做得更靠后加速出弯时前轮不容易打滑。顺带说一个血泪经验如果用差速小车跑高速后轮打滑是整个控制环路里最难解决的“玄学问题”而舵机车型可以把打滑问题主要局限在后轮驱动轮上通过限幅和加速度控制来缓解。2.3 传感器布局8路灰度探头的安装策略循迹小车最常用的传感器是灰度传感器核心是红外对管——一组红外发射管加接收管利用黑线和白底对红外光的反射率差异来输出不同的电平或电压。典型的灰度传感器模块上有电位器可以调节灵敏度阈值也有比较器芯片做数字输出。8路灰度探头的安装位置我一般建议这样探头排成一条直线横向间距约1.5厘米覆盖总宽度约12厘米刚好能覆盖常见的白色赛道配黑色引导线引导线宽度通常在2厘米到3厘米之间。中间两路探头负责直线循迹左右各三路负责提前探测弯道方向。探头离地高度控制在1厘米到2厘米之间太近容易刮到地面太远反射率下降红外光会扩散。安装时要注意一个细节探头不能完全垂直于地面稍微向前倾斜一个角度让红外光打到白底后反射回接收管的角度更稳定。这个角度在1.5厘米离地高度下通常选20度到30度。为什么是8路而不是4路或16路4路的覆盖宽度太窄小车稍微偏一点就丢失全部信号16路的信息确实更丰富但读取和状态判断的复杂度上升对于51单片机可能有问题对于F103ZET6来说其实也能处理但布线难度更高。8路是竞速小车上最常见的选择因为状态组合可以比较完整地映射到转向角度后续做补线策略也方便。3. 路径检测模块实现灰度数据的读取、阈值处理与中线计算3.1 灰度数据的采集与去抖无论传感器模块是数字输出还是模拟输出第一步都是把原始信号稳定地读进主控。数字输出的模块内部有比较器直接接GPIO输入即可模拟输出则需要接ADC通道。前者代码简单后者可以灵活调节灵敏度适合在不同光照环境下使用。8路数字信号可以用GPIOA的低8位一次读入然后做一个软件去抖。常见的做法是连续读取三次取中间值或者两次相同的结果作为有效值。代码示例uint8_t Gray_Read(void) { uint8_t raw GPIOA-IDR 0xFF; // 一次读取PA0-PA7共8路 uint8_t stable; static uint8_t last_raw 0; uint8_t cnt 0; // 连续读取3次统计每一位的稳定结果 for (uint8_t i 0; i 3; i) { raw GPIOA-IDR 0xFF; cnt (cnt 8) | (raw last_raw ? 1 : 0); last_raw raw; } stable raw; return stable; }这个代码的原理是连续三次读取如果两次读取结果一致就认为信号稳定否则用最后一次结果。实际测试中灰度传感器在黑白边界附近会有几十毫秒的抖动区间软件去抖能避免舵机在边界处高频摆动。另一个更实用的办法是识别“哪几路压线”用一个8位变量记录状态每一位代表一路探头1表示压到黑线0表示压到白底。在控制循环里直接对这个状态做映射即可。3.2 中线计算的两种方式加权平均与状态映射拿到8路压线状态后需要把它转换成一个数值用来表示“黑线相对于车体的横向位置”。这个数值就是中线偏差是PID控制器的输入。方式一加权平均法。把每一路探头的位置索引作为权重压线的探头索引加权求和再除以压线数量。比如第3路和第4路压线权重分别是3和4计算结果为3.5表示黑线在中线偏右半个探头间距。这个方法的优点是连续性好能输出小数级别的偏差适合PID精细调节。方式二状态映射法。直接把8路状态的每一种组合对应到一个预定义的角度增量。比如只第4路压线正中偏差为0第3路压线偏差为-1第2路压线偏差为-2依此类推。这个方法的优点是响应快、确定性强缺点是组合数多8路全状态有256种处理起来麻烦通常只映射单路压线和双路压线的情况。我在竞速方案里推荐的做法是混合式正常循迹时用加权平均法计算连续偏差在弯道中如果出现多路压线则切换到状态映射法直接给定一个较大的转向角预设值。这样直线段控制细腻弯道段响应迅速。3.3 阈值自适应一个经常被忽略的细节灰度传感器的输出很大程度上依赖赛道表面的反射率。白色哑光纸和白色高光纸的反射率差异很大室内灯光和自然光的红外干扰也不同。如果阈值固定不变换一个场地就可能出现整片误判或整片丢失。通常的做法是在小车启动前做一个初始化校准让小车在赛道上扫一遍采集8路传感器在白底和黑线上的电压范围取两者的中间值作为判断阈值。如果传感器模块是模拟输出这个校准过程还能顺便记录每路传感器的基线偏差——不同探头之间因为元件离散性会导致输出电压不完全一致需要做归一化处理。typedef struct { uint16_t white_val[8]; uint16_t black_val[8]; uint16_t threshold[8]; } Gray_Calib; void Gray_Calibrate(Gray_Calib *calib) { // 每个通道采集10次白底最大值和黑线最小值 // 实际代码里会先让用户按键触发扫过黑白区域后取统计值 for (uint8_t i 0; i 8; i) { calib-threshold[i] (calib-white_val[i] calib-black_val[i]) / 2; } }这个初始化过程花不了两秒钟但能避免跑了十分钟发现传感器全飘了、数据从“正常”变成“玄学”的尴尬。有些团队会直接把阈值做成可调参数通过蓝牙下发修正在线调这在赛场上属于后悔药级别的功能建议预留。4. 控制核心实现舵机PD转向环与电机速度环的协同调参4.1 舵机的PWM初始化与角度映射舵机控制看起来简单实际上第一步就容易翻车舵机的PWM频率必须是50Hz左右也就是周期20ms占空比变化范围约2.5%到12.5%。很多同学把PWM频率配成几百赫兹甚至几千赫兹舵机要么疯狂抖动要么直接不转。使用定时器生成50Hz PWM的配置要点定时器时钟72MHz预分频系数设为71得到1MHz计数频率。自动重装载值设为19999得到50Hz的PWM频率同时计数分辨率是1微秒。舵机中位对应的占空比是7.5%即1500微秒左右极限分别是1000微秒和2000微秒。代码片段如下void Steering_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能TIM1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 72MHz / 71 / 20000 50Hz TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_Period 19999; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // PWM模式1输出比较预装载使能 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 1500; // 初始中位 TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_Cmd(TIM1, ENABLE); }舵机角度与PWM占空比不是严格的线性映射不同品牌的舵机线性度也有差异。实际使用中会让小车放地上从小到大给舵机加角度观察前轮是否在中位两端对称如果不对称用软件做一次角度补偿。4.2 PD控制器的C语言实现与参数意义循迹小车转向控制最常用的控制器是PD也就是比例加微分一般不加积分。原因很简单循迹的误差是动态变化的没有稳态残差问题加积分反而会在弯道处造成超调和振荡。PD控制器核心代码typedef struct { float kp; float kd; float last_error; float output; float out_max; } PID_TypeDef; void Steering_PID(PID_TypeDef *pid, float error) { float d_term (error - pid-last_error); pid-output pid-kp * error pid-kd * d_term; pid-last_error error; // 输出限幅防止舵机指令超出机械极限 if (pid-output pid-out_max) pid-output pid-out_max; if (pid-output -pid-out_max) pid-output -pid-out_max; }这段代码有几个需要说明的点第一误差error的来源是前面计算的中线偏差加权平均法输出的是“当前位置与目标位置的偏差”单位是探头间距的倍数范围通常在-3.5到3.5之间。第二微分项用的是误差变化量而不是误差对时间的导数原因是控制周期固定误差变化量本身就是微分的离散形式。这里不需要除以控制周期因为它等效于把kd放大了一个系数调参时直接通过kd来消化。第三输出限幅out_max要跟舵机机械限位对应。舵机中位1500微秒假设左右各允许转动30度对应约200微秒的变化范围那么输出限幅就设为200防止PID在高速过弯时给出一个根本执行不了的舵机角度。调参的顺序也是一门学问。我的习惯是先调kp从小到大让小车能在直线上稳定循迹出现轻微摆动即可然后加kd逐渐增大直到弯道出弯时不再有明显的左右甩动。如果发现小车在进弯时反应迟钝多半是kp不够如果发现小车在直线段高频抖动多半是kd过大放大了一丁点噪声。4.3 电机速度环为什么竞速小车要加编码器闭环驱动电机的PWM输出和舵机类似但频率要高一些一般用20kHz。电机的速度控制有两种思路开环给固定PWM占空比或者用编码器做闭环PID。开环方案在电池电压充足的时候很稳定但跑着跑着电池电压下降同样占空比下电机转速会下跌速度环就失效了。加编码器闭环的好处有两层一是稳速。用TIM4编码器模式读取电机转速STM32的定时器自带正交解码功能只需要把编码器的A相和B相接在定时器的两个通道上硬件会自动计数不需要外部中断。二是能实现“弯道降速”策略。竞速小车不追求全程跑最高速而是直线快速、弯道减速。通过编码器把当前车速反馈回来在检测到大偏差时给定一个目标速度曲线速度环自动把车速压下来出弯后再升上去。void Speed_Control(float target_rps) { float current_rps Encoder_GetSpeed(); // 编码器换算为实际转速 float error target_rps - current_rps; static float integral 0; integral error; integral CLAMP(integral, -50.0f, 50.0f); // 抗积分饱和 // 增量式PID输出叠加到当前PWM上 float delta speed_pid.kp * (error - speed_pid.last_error) speed_pid.ki * error; speed_pid.output delta; speed_pid.output CLAMP(speed_pid.output, -1000, 1000); speed_pid.last_error error; Motor_SetPWM((int)speed_pid.output); }这段代码里用的实际上是增量式PID输出的是PWM的增量而不是绝对值。这样做的优点是即使pid参数不合适也不会一下把PWM冲到满值导致小车猛冲出去。调参时速度环的kp比转向环小一个数量级因为被控对象是电机机械系统响应慢kp太大会出现电机啸叫。4.4 转向环与速度环的优先级先稳住舵机再谈速度在实际跑车中两个环同时工作如果速度环没有收敛车速时快时慢转向环的误差变化率会被放大导致舵机输出的微分项波动剧烈。反过来如果转向环不收敛小车蛇形走位速度环也会因为负载波动而振荡。所以我的调车顺序非常固定先把小车架起来让后轮悬空速度环开到目标速度闭环稳定然后把小车放地上速度环关闭只调舵机PD让车能循迹最后两个环同时开启从小目标速度开始逐步加速。整个过程每调整一次速度目标值都要同时看一下转向环的微分项是否在合理范围。5. 常见问题与避坑指南从传感器失灵到PID震荡的排查思路5.1 现象传感器阈值固定导致全盘误判小车在实验室白纸上跑得好好的拿到比赛场地之后上电一看8路传感器全部输出“压线”或者全部输出“白底”小车原地打转或直接冲出赛道。原因灰度传感器模块的电位器调节的是比较器参考电压不同场地的白底反射率差异很大。如果你用的是数字输出模块出场默认电位器位置可能只适配某一种光照环境如果用的是模拟输出模块ADC读到的白底电压范围可能整体偏移。解决在程序里加入初始化校准流程启动后小车保持在赛道上程序自动采集白底和黑线的电压范围并计算阈值。如果传感器模块是数字输出不能读模拟量就把电位器调到一个中性位置在比赛场地用螺丝刀微调直到地面白底时指示灯全灭、黑线时对应指示灯亮起——这个方法比较原始但确实管用。5.2 现象舵机PWM配置正确但抖动严重舵机上电后持续发出“嗡嗡”声用手摸舵机外壳能感觉到高频振动转动角度在中位附近来回抖动。原因最常见的是PWM频率不对舵机标准频率是50Hz如果你把定时器配成100Hz甚至更高舵机内部的电路会不断尝试重新定位表现就是抖动还有一种情况是PWM信号有毛刺可能是时钟配置里PWM输出引脚和另一个定时器共用了通道。解决用示波器看PWM波形确认周期确实是20ms。没有示波器的话写一个最简单的测试程序固定输出1500微秒脉宽如果舵机还在抖大概率是PWM配置问题。还有一个隐蔽坑电机的PWM地线没有和舵机地线共地舵机信号参考电位浮空也会造成抖动检查电源地和信号地是否连在一起。5.3 现象PID参数调好之后高速过弯还是冲出赛道小车在低速时循迹很顺速度一提高直角弯直接冲出去或者S弯连续摆动失控。原因这通常不是PID参数本身的问题而是“转向响应速度跟不上车速”。舵机的物理响应时间在50毫秒到100毫秒级别车速2米/秒时100毫秒车已经前进了20厘米而探头排列覆盖宽度只有12厘米传感器已经检测不到线了。也就是说问题出在信息滞后而不是控制算法。解决一是降低进弯速度在检测到连续多路压线时提前减速二是增加前瞻距离把传感器支架往前延伸让探头比前轮更早探测到弯道三是使用补线策略——当最外侧探头压线时直接把偏差量设为一个预设的较大值而不是依赖PID逐步逼近。这个做法相当于在算法里做了“弯道预判”实车效果提升非常明显。5.4 现象电池电压下降后电机明显变慢小车刚开始速度正常跑了两三分钟后越跑越慢即使PWM输出没变。原因锂电池放电过程中电压会从12.6V降到10V左右电机转速和供电电压近似成正比。开环PWM控制补偿不了这个电压跌落。解决如果已经加了编码器速度环这个现象会自动消失速度环会通过提高占空比来补偿电压下降如果用的是开环可以加一个电压监测ADC根据电池电压修正PWM比例系数。这算不上真正的闭环但能缓解最明显的掉速问题。注意编码器速度环的PID输出上限要留够余量否则电压低时PID饱和还是会出现掉速。5.5 现象蓝牙调试数据正常但小车实际表现异常代码里打印的传感器状态和解算出来的偏差数值看起来都对但小车跑起来和预期完全不符。原因大部分情况是“调试状态”和“运行状态”的程序路径不一致。比如调试时传感器输出通过串口打印运行循环里又读了一遍GPIO两处读取的GPIO引脚或去抖逻辑不同导致实际控制用的数据和你在串口上看到的不一样。解决把数据源统一成一个函数无论调试还是运行都调用同一个读取接口。然后做一个关键验证——用串口把“PID输入偏差”和“PID输出指令”同时打出来让小车固定在一个偏移位置检查输出的舵机角度是否符合直觉。这个操作能一次性验证传感器、PID计算、PWM输出三个环节的通路。6. 验收与答辩用可量化的测试赛道和调参记录证明方案有效到了临近交付或者答辩的时候很多同学会陷入一个误区反复调车试图跑出一个完美成绩却忘了准备“如何证明这套设计是可行的”的呈现材料。课程设计和毕业设计的评审关注点是需求理解是否准确、方案是否合理、实现是否完整、结果是否可靠。按照下面这套验收流程走下来基本能把所有可能被问到的问题前置堵住。第一步搭建标准测试赛道。用黑色电工胶带在白色KT板上贴出包含直线段、90度直角弯、S弯、十字交叉、大回环五种元素的赛道直线段长度不少于1米弯道曲率覆盖高中低三档。赛道宽度取30厘米到40厘米引导线宽度2.5厘米。这项工作的意义是让测试可复现——换一个场地、换一个时间跑同一个赛道成绩差异能说明系统的稳定性。第二步定义量化指标并逐项测试。建议至少记录四个指标完赛率连续跑10圈的成功圈数占比平均圈速完整一圈的时间最大舵机响应延迟通过串口把传感器变化时间戳和舵机指令时间戳打出来计算掉线次数8路传感器全部无压线的累计计数。每调整一组PID参数就完整测一遍这四项指标记录在一个表格里。最终的验收表可以这样展示测试项验收标准实测结果直线循迹稳定性直线段不发生S形摆动偏差保持在±1路探头内通过直角弯通过率连续10次过弯不冲出赛道9次通过1次因入弯速度过高冲出S弯平均偏差全程加权平均偏差小于1.5路探头平均值0.8路十字交叉处理直行通过不断线通过平均圈速至少达到0.8米/秒0.9米/秒第三步梳理演示主线。答辩时不要从“什么是灰度传感器”讲起直接从问题切入为什么选择舵机转向方案、如何把8路传感器信号变成PID输入、PID参数是怎么调的、最终测出来的数据说明什么。每一个结论都能对应到调试过程中的一张截图或者一段代码。我会建议把串口打印的偏差曲线用串口绘图工具录制一段视频答辩现场放10秒就够比讲一百句话都有说服力。还有一个容易被问到的核心问题“你的PID参数为什么是这几个值”如果回答“调出来的”显得没有深度正确的打开方式是展示调参过程初始kp0.5时小车振荡增大到1.0后振荡消失但过弯响应慢加kd8后出弯稳定kp和kd的比例大约在1比5到1比10之间。这套数据链能体现你对控制算法的理解是完整的而不只是会调用库函数。最后说一个习惯从那以后我每次调完车都会把最终的PID参数、传感器布局、机械连杆位置拍照存档并且把每轮测试数据做成一个表格放在工程目录里。答辩前翻一遍这些记录几乎能回答评审提出的所有细节问题。做项目最怕的不是没解决问题而是解决了问题却没有记录下是怎么解决的。希望这篇基于STM32F103ZET6的循迹小车设计方案能帮你把硬件、算法和验收逻辑串起来少走几步弯路。本文还有配套的精品资源点击获取