资讯动态

STM32F103ZET6循迹小车实战:从灰度传感器标定到PD调参

发布时间:2026/9/8 9:35:45 来源:尧图企业网站定制
简介面向STM32学习者与智能车竞赛爱好者这套基于STM32F103ZET6的循迹小车代码完整覆盖灰度传感器采集、PWM输出、舵机与双编码电机差速控制的实现链路。车体借助一字排列的灰度传感器阵列识别赛道黑线配合位置式PID算法稳定通过直道、环岛、S弯和连续直角弯适合参考完整工程与PID调参思路的开发者。压缩包共207个文件、大小5.48MB含Keil C源码.c/.h、编译产物.hex/.axf、工程配置.uvprojx及启动文件等可直接编译烧录便于二次修改。已有8429人学习下载。工程内定时器配置、灰度采集、PWM输出、PID调节等模块分层清晰有助于理解整体控制逻辑源码、配置文件与可执行文件齐全可作为毕业设计或智能车比赛的备赛参考有效节省从零搭建的时间。 第一次给STM32F103ZET6循迹小车通电那个晚上我记得特别清楚。轮子没往前走车在原地疯狂转圈像极了一个放歪的陀螺。旁边朋友还补了一句“你这车是薛定谔的直行吧不测永远不知道会往哪偏。”后来我从传感器标定、PWM接线、控制逻辑一路排查花了一个周末才让它稳稳跑完一圈。这篇文章就是把那段经历和现在比较成熟的方案一起整理出来从灰度传感器原理、芯片资源规划、电机驱动接线到连续偏差计算和实际调参排错适合刚做完51小车想转STM32、或者准备电赛/课设要做循迹车的朋友。看完你至少能少踩一半我踩过的坑。1. 循迹小车到底在“循”什么灰度传感器与赛道识别1.1 五路灰度传感器怎么摆放最合理循迹的本质是检测赛道底色和引导线之间的反射率差异。最常见的方案是TCRT5000红外反射模块模块上有一个红外发射管和一个光电接收管发射管持续发出红外光光线照到白色赛道大部分被反射回来接收管导通程度高照到黑色引导线则被吸收反射回来很少。把输出电平或模拟电压读进单片机就能判断当前传感器下方是黑线还是白底。单路传感器只能告诉你“现在有没有压线”车子会像喝醉一样左右摆头。所以实用方案至少是5路一字排列在车头下方。间距要根据赛道线宽来定一般赛道黑线宽度在2cm左右5路传感器中心间距取1.5cm到1.8cm比较合适。如果间距太大两个传感器可能同时落在黑白交界处读出来的数据模棱两可间距太小黑线会同时压在三四路传感器上偏差分辨率反而下降。安装高度一样重要我试过离地3cm和离地1cm效果天差地别。离地太高红外光散射严重白色区域和黑色区域读数差距变小阈值很难选离地1到1.5cm是比较甜的范围。装好后最好用热缩管或者黑胶带在传感器两侧做一个简单的遮光罩只留一个向下的检测窗口能明显降低环境光干扰。1.2 阈值标定别靠电位器要读ADC市面上很多TCRT5000模块会带一个LM393比较器直接输出TTL高低电平板子上还带一个蓝色电位器可调阈值。这种方案接线简单但我不推荐在STM32上直接读开关量原因有两个第一电位器调出来的阈值没有量化依据全靠手感和运气换一个光照环境就要重新拧第二开关量丢失了“离黑线多近”的连续信息对后面做连续偏差控制非常不利。更好的做法是买能够引出模拟电压的模块或者直接读传感器接收管的分压点。STM32F103ZET6内部有3个12位ADC随便分配3到5个通道就够了。程序上做一个标定流程把车放在纯白区域读一次记录最大值再放到黑线区域读一次记录最小值。判断阈值就取(max min) / 2。这里还有个细节白底读数不一定比黑底高具体取决于模块输出的是反射电压还是反向电压所以标定时不要让程序假设方向直接记录高低值再算一个中间判断值即可。我自己的习惯是每个通道独立标定因为5路传感器之间哪怕型号一致个体差异也会导致白底读数相差几十个数。用统一阈值是偷懒后面直道画龙大概率就是这里埋下的。标定数据存到全局结构体里每次上电先跑一次标定函数或者通过按键触发标定这样更换场地时不用反复改代码重新烧录。2. 为什么是STM32F103ZET6资源分配与引脚避坑2.1 ZET6和C8T6的差别以及我给这辆车的资源规划有人会问做个循迹小车51单片机都能干为什么非用STM32F103ZET6我自己理解的答案是51能做但做得不舒服。循迹小车至少需要5路传感器采集、2路电机PWM输出、若干方向IO以及一个方便调试的串口打印。用51硬扛也能跑但ADC通道紧张、PWM要么靠定时器中断模拟要么靠内部PCA代码写起来非常别扭。STM32的优势在于外设资源是“溢出”的你可以把精力放在控制逻辑上而不是和硬件较劲。ZET6这颗芯片是F103系列里的大容量型号144脚封装512KB Flash64KB RAM5个串口4个通用定时器加2个高级定时器3个ADC引脚数量也非常充裕。C8T6价格更便宜做循迹也够用但ZET6的余量让我后期加了不少东西OLED显示实时偏差、蓝牙模块调参、编码器测速接口甚至预留了一个超声波避障的排针全程没有拆东墙补西墙。我当时的资源分配大致是这个表功能使用外设备注左电机PWMTIM1_CH1高级定时器互补输出不用只用比较输出右电机PWMTIM1_CH2与左电机共用TIM1方便同步电机方向控制普通GPIO每个电机2个方向IO共4个5路灰度传感器ADC1_IN0~IN4连续扫描采集周期1ms以内调试串口USART1打印传感器原始值和PID输出启动/标定按键GPIO外部中断长按进入标定模式这个表给的是一个参考并不需要完全照抄。核心思路是PWM用同一个定时器保证两路波形同步ADC用DMA或者规则组扫描避免在控制循环里卡时间串口一定要留调车时看不到内部变量纯靠猜会非常浪费时间。2.2 引脚复用和重映射被工程忽略的第一坑STM32的引脚不是想用就用的很多IO默认被调试接口或者其它复用功能占着。F103的JTAG默认占用PA13、PA14、PA15、PB3、PB4其中PB4是NJTRSTPA15是JTDIPB3是JTDO。如果你把这些引脚直接当普通GPIO用会发现在线调试和程序运行总会出怪问题这就是工程上最容易翻车的地方之一。循迹小车常用引脚里PA0到PA7、PB0、PB1、PC0到PC5这些通常比较安全。但如果你用了PA15、PB3、PB4这类引脚就必须在初始化代码里关闭JTAG复用只保留SWD或者干脆全关掉。相关代码一般是这样的void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // 禁用JTAG保留SWD如果不用SWD调试可以全关 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // ... 配置方向控制引脚 }另外F103的定时器通道引脚也有默认映射和重映射的区分。比如TIM3_CH1默认在PA6重映射可以到PB4TIM4很多引脚在PB6到PB9。网上常见的呼吸灯例子会提到TIM3引脚重映射就是因为如果PA6被ADC或者其它功能占用了可以通过AFIO把PWM输出重新映射到别的引脚。在规划引脚时我建议先翻一下对应型号的数据手册里的“Pin definitions and AFIO mapping”表把要用到的定时器通道引脚列出来再和ADC、串口、GPIO一起做一次冲突检查。省得焊完线才发现两个功能挤在同一个引脚上到时候飞线飞得怀疑人生。3. 从PWM波到轮子差速电机驱动链路搭建3.1 电机驱动选型和接线单片机IO口是给不出驱动电机的大电流的所以中间必须加电机驱动模块。市面上常见的有L298N、TB6612FNG、DRV8833这些。L298N便宜、皮实但内部用三极管做H桥饱和压降大供电电压一低就明显感觉轮子没劲。TB6612FNG是MOS管H桥压降小体积也小我后来一直用它。DRV8833更轻适合小型小车但最大电流稍低。做普通循迹车我建议优先考虑TB6612系列一方面是PWM响应干脆另一方面它逻辑电源可以直接用3.3V和STM32门当户对。接线有几个地方必须注意。第一电机电源和单片机电源要“共地”也就是驱动板的GND要和STM32的GND连在一起否则PWM信号没有参考电平电机根本不会转。第二电机电源不建议直接从STM32开发板的3.3V取电启动瞬间电流可以到一两安板载LDO会被拉垮轻则重启重则烧芯片。我给小车用的是一节7.4V锂电池经开发板稳压给单片机供电同时通过DC-DC降压到5V附近单独给电机驱动供电。第三PWM信号线和方向控制线尽量短避免和电机大电流线绑在一起走否则电机换向时的反电动势容易让单片机复位。驱动部分的示意接线如下不同模块脚位略有差异以实际模块丝印为准信号STM32引脚驱动板引脚说明PWM左PA8PWMA左电机调速IN1左PB5AIN1左电机方向1IN2左PB6AIN2左电机方向2PWM右PA9PWMB右电机调速IN1右PB7BIN1右电机方向1IN2右PB8BIN2右电机方向2电源GNDGNDGND共地必须接电机电源-VM独立供电不占单片机电源3.2 定时器PWM配置和电机控制函数PWM配置的重点是频率和占空比分辨率。我给电机用的PWM频率是10kHz因为常见直流减速电机的电感在这个频率下电流纹波比较小声音也不太刺耳。如果频率太低比如1kHz电机会发出明显的啸叫低速时还可能一顿一顿的。STM32F103ZET6的系统时钟是72MHz用TIM1输出两路PWM预分频PSC设为0自动重载值ARR设为7200-1这样PWM频率就是72MHz / 7200 10kHz占空比分辨率接近0.014%完全足够。初始化时需要注意高级定时器TIM1和TIM8需要额外开启TIM_CtrlPWMOutputs否则比较输出不会真正送到IO口。这个坑不少初学者会踩代码看起来都对了示波器却量不到波形其实就是忘了这一句。控制函数可以封装成下面这种形式调用时不用关心底层寄存器// speed范围-1000 ~ 1000正数前进负数后退0停止 void Set_Motor_Speed(int16_t left, int16_t right) { if (left 0) { AIN1_GPIO_HIGH(); AIN2_GPIO_LOW(); TIM_SetCompare1(TIM1, left); // 正转时直接用占空比 } else { AIN1_GPIO_LOW(); AIN2_GPIO_HIGH(); TIM_SetCompare1(TIM1, -left); // 反转时取绝对值给比较寄存器 } if (right 0) { BIN1_GPIO_HIGH(); BIN2_GPIO_LOW(); TIM_SetCompare2(TIM1, right); } else { BIN1_GPIO_LOW(); BIN2_GPIO_HIGH(); TIM_SetCompare2(TIM1, -right); } }把方向和速度拆开的思路是让上层控制逻辑只关心“左轮多少、右轮多少”这个抽象量不用管H桥怎么换向。后面写PID转向时调用这个函数就行代码会干净很多。这里还要补充一个测试方法焊完线先别直接把小车放地上跑。拿个支架把车架空用串口或者按键分别设成“左轮正转”“右轮正转”“左轮反转”“右轮反转”四个状态确认两个轮子的转向逻辑和你的预期一致。这一步看似简单却能省掉你后面排查“为什么一给转向就原地甩尾”的大量时间。4. 循迹控制逻辑连续偏差计算与PD转向4.1 用加权平均算出“差了多远”而不是非左即右很多51循迹小车教程的做法是传感器读到中间一路压线就直行读到偏左就右转读到偏右就左转像查表一样来做转向决策。这个方案在小速度、大曲率赛道上能跑但速度稍微上去一点车子就会在直线上左右震荡因为开关量没有告诉单片机“到底偏了多少”。同样是偏左压到左边第二路和刚刚擦到左边第一路需要修正的转向量是完全不同的。所以我改用连续偏差计算。把5路传感器从左到右记为s0~s4每一路有一个权重s0对应-2s1对应-1s2对应0s3对应1s4对应2。有车前把每路判断成bool量1表示压到黑线。偏差e的计算方式float Get_Deviation(void) { int weight_sum 0; int sensor_sum 0; int w[5] {-2, -1, 0, 1, 2}; for (int i 0; i 5; i) { weight_sum sensor[i] * w[i]; sensor_sum sensor[i]; } if (sensor_sum 0) { return last_deviation; // 没有压线保持上一次偏差 } last_deviation (float)weight_sum / sensor_sum; return last_deviation; }当黑线正好在中间时只有s2为1weight_sum为0e0。当黑线偏左时s1为1weight_sum -1e为负。如果黑线同时压到s1和s2weight_sum -1sensor_sum 2e-0.5表示只偏了一点点。这个连续值就是后面转向控制的输入。有了偏差e转向就顺理成章了。我用的是一套PD控制器P项根据当前偏差输出转向修正量D项用来抑制车身摆动int16_t steering (int16_t)(Kp * deviation Kd * (deviation - last_deviation)); left_speed BASE_SPEED - steering; right_speed BASE_SPEED steering;这里的符号要根据传感器安装方向调整如果发现车子越偏越厉害把steering前面的正负号反过来就行。BASE_SPEED可以先取300到400对应占空比30%到40%Kp从1.0开始往上加Kd从0.2开始试。4.2 十字、T字和断线特殊赛道的状态机处理连续偏差在普通直道和弯道上很好用但遇到跑道上常见的十字路口就会出问题。十字路口意味着5路传感器同时压到黑线sensor_sum等于5偏差公式会得到一个落在中间的0值听起来好像没问题但如果车身进入十字的角度偏了或者传感器间距和黑线宽度不匹配会出现一瞬间5路全黑然后又全白的情况这时控制逻辑容易误判成“丢线”进而做出错误转向。为了处理这种情况我引入了一个轻量的状态机状态包括正常循迹、十字直行、左转、右转、停止。只在5路全为1时进入“十字候选状态”在这个状态下保持直行一段固定时间或者固定距离直到脱离全黑状态。简单实现可以用一个计数器进入全黑后强制直行50ms50ms后如果还是全黑就继续直行否则恢复到正常循迹。这里的关键点是不去改变轮速差让车靠惯性直接通过十字。T字路口的策略就灵活了取决于比赛规则。如果规定T字必须左转那就在检测到左前方有连续黑线时给一个固定时间的左转同时关闭P控制避免PD不断打架。断线处理也是一样sensor_sum等于0时不要立刻让小车原地乱转而是沿用上次有效的偏差值并且把速度降下来一点因为大概率只是压到了线间缝隙或者黑线被磨损。再补充一个控制周期的概念。整个循迹循环最好做到1到5ms执行一次也就是200Hz到1000Hz。如果代码里用HAL_Delay(10)或者更长的延时车子响应就会慢半拍弯道冲出去的概率大幅增加。正确做法是让主循环不断扫描传感器、计算偏差、调用PD、设置电机速度整个循环本身尽量短不要放任何阻塞延时。需要定时的功能用定时器标志位别用延时。5. 实测调参从原地打转到稳定过弯的完整过程5.1 现象驱动排查原地打转、画龙、冲弯调车本质上是一个“现象-假设-验证”的过程别一上来就改参数先确认硬件链路是对的。我遇到最常见的现象是原地打转。这种问题很少是PID参数引起的我先用串口打印出5路传感器的值确认每一路在黑线和白底的读数有明显差异然后控制单侧电机正转、反转确认方向控制和PWM输出都正确最后检查电机驱动板的使能引脚和共地线。很多时候“原地打转”只是左右电机方向接反了或者驱动模块的STBY没拉高压根还没轮到PID出场。第二种现象是直道画龙车在直线上走S型。这个一般是传感器阈值不准或者Kp过大。阈值不准的表现是车在直道上时传感器读到的不是稳定的“1001”或者“010”而是杂乱抖动Kp过大的表现是偏差很小但转向量很大车会左右来回甩。解决方法是先把车放在白色赛道中间看串口打印的偏差是否接近0如果不是重新标定。然后Kp从很小值比如0.5开始试稳定后再增加到1.0每次增加后跑一段直道观察直到直道不需要明显修正就能走比较直。第三种现象是入弯冲出去通常是速度太快或者检测延迟太高。先降BASE_SPEED到250左右再检查控制循环里是否有不必要的延时最后检查传感器安装高度。传感器离地面越高车头已经压到弯道边缘偏差才被检测到晚了那么几十毫秒车速快一点就救不回来。把高度压到1cm以内效果立竿见影。电池电压也会影响手感。我那一版用7.4V锂电池满电8.4V和快没电7.2V时同样的PWM占空比实际转速差很多。所以如果你的车是电池供电正式比赛或演示前一定要充满电并且所有参数都在相近电压下整定。条件允许的话在电池和驱动之间加一个稳压模块能让参数稳定很多。5.2 换场地不用重新编译自动阈值标定小技巧最后分享一个我后来一直沿用的技巧可以说帮我在比赛现场省了一大半麻烦。传统的阈值标定是在程序里写死一个固定值比如“大于2000算黑线”。但不同场地、不同灯光下同一个传感器的读数可能差出500甚至更多。现场重新编译烧录非常费时间所以我做了一个自动标定模式。具体做法是在单片机上留一个按键长按进入标定模式。此时让小车上的OLED或者串口提示“White Calibration”把车放到纯白区域按一下按键程序记录当前5路ADC最大值的平均再提示“Black Calibration”把车放到黑线区域按一下按键记录当前5路ADC最小值的平均。标定完成后程序自动把每路的黑白临界值计算出来存到全局变量里退出标定模式就能直接循迹。整个过程大约10秒钟换场地只需要重新标定一次完全不需要动代码。这个思路也可以继续扩展比如把几个场地的标定值存在Flash里通过按键切换对应场地编号但循迹小车这种应用我觉得每次都重新标定反而更可靠因为环境光照变化是连续且不可预测的。阈值这东西宁可在现场花10秒重做也别赌上次的参数还能用。从最初原地转圈到后来稳定跑完一整圈我最大的体会是循迹小车90%的问题都不在算法而是在传感器标定、接线和供电这些最基础的地方。把底层数据用串口打出来看清楚再谈控制参数会顺利得多。如果你正准备做一辆STM32循迹小车希望这条踩出来的路线能让你少熬几个夜。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价