news 2026/9/8 9:35:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103ZET6循迹小车实战:从灰度传感器标定到PD调参

简介:面向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 Flash,64KB RAM,5个串口,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是NJTRST,PA15是JTDI,PB3是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,重映射可以到PB4;TIM4很多引脚在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左电机方向1
IN2左PB6AIN2左电机方向2
PWM右PA9PWMB右电机调速
IN1右PB7BIN1右电机方向1
IN2右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对应-2,s1对应-1,s2对应0,s3对应1,s4对应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为1,weight_sum为0,e=0。当黑线偏左时,s1为1,weight_sum = -1,e为负。如果黑线同时压到s1和s2,weight_sum = -1,sensor_sum = 2,e=-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时进入“十字候选状态”,在这个状态下保持直行一段固定时间或者固定距离,直到脱离全黑状态。简单实现可以用一个计数器,进入全黑后强制直行50ms,50ms后如果还是全黑就继续直行,否则恢复到正常循迹。这里的关键点是不去改变轮速差,让车靠惯性直接通过十字。

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循迹小车,希望这条踩出来的路线能让你少熬几个夜。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 9:33:44

YOLOv8+RTSP实时流检测实战:从拉流解码到边缘部署完整指南

简介&#xff1a;面向视频监控、智能交通、工业自动化等场景开发者&#xff0c;资源提供了一套基于YOLOv8的RTSP实时视频流目标检测方案&#xff0c;覆盖视频流接入、模型推理、结果可视化等环节&#xff0c;适合具备一定深度学习基础、希望快速落地应用的工程人员。包体共467个…

作者头像 李华
网站建设 2026/9/8 9:32:54

毫米波OFDM 4D ISAC成像仿真:MUSIC算法与Matlab实现

简介&#xff1a;面向毫米波通信感知一体化研究需求&#xff0c;这份工程包实现了MUSIC算法与OFDM信号相结合的4D ISAC成像仿真&#xff0c;适合通信、雷达、信号处理方向的硕博生与工程师进行算法验证和系统级仿真。压缩包共58个文件&#xff0c;以40个Matlab脚本为核心&#…

作者头像 李华
网站建设 2026/9/8 9:32:34

GUI-MCP与HITL:让AI真正“会干活”的人机协同实践

1. AI不会点按钮&#xff0c;这个尴尬怎么破我估计不少人都经历过这个场景&#xff1a;大模型已经能写代码、写文章、做表格了&#xff0c;但让它帮你在某个系统里把报销流程走完——登录、进页面、找到对应入口、填单、上传附件、点提交——它就卡住了。模型再聪明&#xff0c…

作者头像 李华
网站建设 2026/9/8 9:31:41

行为树 C# 实现:从零手写一套可复用的游戏 AI 节点框架

一、从「套路脚本」到「节点框架」:为什么状态机写不下去 上一篇讲了行为树的原理(见《行为树 Behavior Tree:游戏 AI 背后的决策机制》),本篇用 C# 从零实现一套核心决策逻辑与引擎解耦的 BT(Behavior Tree,行为树)框架,示例用 Unity 集成,端到端跑通一个「巡逻-追…

作者头像 李华
网站建设 2026/9/8 9:31:39

行为树 (Behavior Tree):游戏 AI 决策机制的核心原理与工程实践

一、从一个巡逻敌人开始 想象你在玩一款动作游戏,遇到一个巡逻的敌人。它的行为是这样: 平时沿着固定路线巡逻 一旦发现你,转入追击模式 追上后,若血量低就逃跑;血量充足就发起攻击 攻击有一套连招逻辑,会根据你的距离选择近战还是远程 这一整套复杂的逻辑,在游戏行业中…

作者头像 李华
网站建设 2026/9/8 9:30:54

TVA具身架构详解(12):具身智能“原生大脑”的高效进化路径

前沿技术探索:TVA智能体(简称TVA) TVA智能体(亦称“AI智能体视觉”或“TVA视觉智能体”)是依托Transformer架构与“因式智能体”理论构建的通用视觉技术体系。它有机融合深度强化学习(DRL)、卷积神经网络(CNN)与因式分解算法(FRA),构成了具身智能的核心视觉中枢(…

作者头像 李华