简介:一份完整的STM32智能循迹避障小车课程设计报告,面向嵌入式初学者、电子竞赛参赛者及单片机课程设计学生,解决智能小车循迹、避障、PWM调速等核心设计问题。报告以STM32F103ZE为主控芯片,详细阐述红外对管循迹检测、超声波避障、H型可逆PWM变换器调速及MDK(Keil)编程实现,内容涵盖方案论证、硬件电路、仿真设计和主程序代码,结构完整可直接参考。资源共1个PDF文档,约819KB,内容精炼便于阅读,适合毕设或竞赛方案借鉴。目前已有18146人学习下载,这份从原理到代码的设计报告,可帮助读者快速掌握智能小车从硬件搭建到软件调试的完整思路。 很多做嵌入式课设或者电子设计竞赛的同学,第一次接触“基于STM32的智能循迹避障小车”这个题目时,都会有个错觉:这不就是一辆小车加几个传感器,让它沿着线走、遇到障碍停一下嘛,听起来好像没什么难度。
但等真正把元器件焊到板上、把代码烧进去、把车放到赛道上试跑的时候,各种问题就全冒出来了——车为什么不走直线、为什么一到强光下就乱拐、为什么明明检测到障碍了却还是撞上去、为什么电机一转单片机就重启。这些问题,设计报告里基本不会写,却是决定小车能不能稳定跑完全程的关键。
这篇内容我打算把一套真正能落地、能跑稳定的方案完整拆开讲:从整体架构和器件选型开始,到循迹和避障两大核心模块的原理与代码逻辑,再到调参过程中的经验和坑。不管你是准备交课程设计报告,还是想拿这套东西去打比赛,都应该能从里面找到直接能用的东西。
1. 一套能稳定跑完全程的小车,器件到底该怎么选
先说结论:STM32F103C8T6这颗芯片完全够用,不用再往上加预算换F4系列。很多初学者觉得F4主频高、性能强,用起来肯定更稳,但实际上循迹避障小车这种任务,核心难点根本不在算力,而在传感器信号的处理逻辑和控制策略。F103的72MHz主频,跑循迹PID和超声波避障逻辑绰绰有余,而且C8T6的封装小,焊接和布线都方便,网上参考资料也最多,出了问题好排查。
电机驱动这块,最常见的搭配是TB6612FNG或者L298N。我自己的实际体验是,TB6612比L298N好用得多。L298N压降大、发热严重,两路电机全速跑一会儿,散热片就烫得不敢摸,而且它的逻辑电平兼容性还容易出问题。TB6612的MOSFET架构压降小,体积也小,直接让STM32的PWM引脚控制,不需要额外的电平转换电路。唯一要注意的是TB6612的供电电压范围是2.7V到5.5V,千万别直接给它接12V。
传感器选型也是有讲究的。循迹模块市面上一堆,什么单路、双路、四路、八路红外都有。如果要跑比较复杂的赛道(十字路口、直角弯、断线),我建议至少上四路灰度传感器;如果只是基础直线加简单弯道,三路也够。但我个人不太推荐那种特别便宜的单个红外对管模块,它的检测距离和环境光抗干扰能力都很差,太阳底下跑一圈,阈值全漂了。循迹这块,我后面会给出更具体的选型和布局建议。
避障这块,市面上最常用的是HC-SR04超声波模块。它的测距范围大概2cm到400cm,精度在3mm左右,对小车避障场景来说绰绰有余。要注意的是,HC-SR04是5V供电的,但它的TRIG和ECHO引脚返回的电平也是5V,STM32的GPIO是5V容忍的,直接接没问题,但保险起见还是建议用电阻分压把ECHO的电平降到3.3V再接进PA0之类的引脚,防止某些体质比较弱的芯片出现引脚损伤。
最后是电源方案。这是整个小车最容易翻车的地方,必须单独说。如果用两个18650锂电池串联供电(7.4V),千万别直接拿这个电压给STM32供电。最稳妥的做法是:7.4V进电机驱动板的VM引脚给电机供电,同时从7.4V串联一个AMS1117-3.3稳压模块给STM32供电。电机和单片机必须共地,否则PWM信号的电平参考不一致,电机跑起来会各种乱抖。
提示:电机的启动电流峰值很大,两个电机同时全速启动时,瞬间电流可能冲到2A以上。如果单片机和电机共用同一个稳压源,电压会被瞬间拉低,轻则复位,重则程序跑飞。所以“电机供电”和“逻辑供电”分区是个非常好的习惯。
2. 循迹模块不是“检测到黑线就行”,灰度值的二值化处理才是关键
循迹的原理听起来很简单:红外发射管发射红外光,红外接收管根据反射光强度输出不同的电平或模拟电压,黑线吸光反射弱,白底反射强,以此判断当前传感器是在线上还是在线上以外。但实际跑起来,问题就出在这个“判断”上。
市面上很多便宜的循迹模块,输出的直接是数字信号(0或1),灵敏度电位器调到一个固定位置后,它的判断阈值就被固定死了。问题是,不同环境下环境光的强度不一样,电池电压的变化也会影响红外发射管的发射强度,所以这台车上调好的阈值,换块电池、换个场地,可能就不准了。
这也是我建议用带模拟输出的灰度传感器(比如五路灰度模块)的原因。它输出的是一路模拟电压值,你可以通过STM32的ADC实时采集,然后在代码里做动态二值化。简单说就是:每次上电的时候先做一次环境采样,记录下当前环境下“白底”和“黑线”各自对应的ADC值,然后取两者的中间值作为动态阈值。这样即使场地光照变了,也能保证判断的准确度,不需要每次跑之前都手动拧电位器。
循迹传感器的布局也是有讲究的。如果是三路传感器,最理想的状态是让中间那一路始终压着黑线走,左右两路作为纠偏参考。如果是五路,左右两侧还可以多出两个“急弯检测”位,提前判断赛道是往左拐还是往右拐,响应速度比三路快一个档次。
我做这套系统时,用的循迹逻辑是最经典的“查表法 + PID纠偏”:五路传感器从左到右编号1到5,把它们的数字值组合成一个5位的二进制数,然后在代码里维护一张表,把这个组合值映射到“当前偏差”。
// 五路循迹传感器状态映射表(1为压线,0为离线) // 状态码计算方式: (S1 << 4) | (S2 << 3) | (S3 << 2) | (S4 << 1) | S5 // 偏差定义: 0表示正中,负数偏左,正数偏右 const int track_map[32] = { // 全空或全满的情况 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 单独压线 0, -3, -2, 0, -1, 0, 0, 0, 0, 1, 0, 2, 3, 0, 0, 0 };这张表看起来不复杂,但手写两张表容易错位,调试的时候很折磨。更省事的方法是在初始化阶段写一段自检程序,把五个传感器分别用手挡一下,把对应的状态码打印到串口,然后对着状态码填表。这样能保证每个状态码映射到的偏差值跟实际位置是对应的。
PID纠偏是让小车稳定走直线的核心。循迹场景下一般用PD控制就够了,比例项负责给出转向力度,微分项负责抑制摆动。I项在很多小车上反而容易引发振荡,因为循迹是动态连续过程,误差积分很容易过头,导致小车在直线段“蛇形走位”。
int last_error = 0; float Kp = 25.0f, Kd = 8.0f; int pid_control(int current_error) { int delta_error = current_error - last_error; int output = (int)(Kp * current_error + Kd * delta_error); // 限幅,防止PWM值超界 if (output > 500) output = 500; if (output < -500) output = -500; last_error = current_error; return output; }这个output参数最终会叠加到左右电机的PWM占空比上:如果output为正,说明车偏右了,那就左轮提速、右轮减速;output为负则相反。Kp和Kd的整定,我建议先只给Kp,把小车的速度放在一个比较低的值(比如20%占空比),看它的反应;如果小车在直线段来回摆,就加大Kd;如果过弯时反应迟钝,就适当加大Kp。这套参数调好后,可以让小车在1.2m/s的速度下稳定走完一个标准十字赛道,不会冲出轨道。
3. 避障逻辑不能“检测到就躲”,要设计一个简单的状态机
超声波避障看起来比循迹还简单——测距,如果太近就转向。但如果真的只写一个“距离小于阈值就转弯”,小车的动作会非常僵硬:走到障碍前停下来,原地转个90度,再往前走,结果刚转完又检测到墙,又停下来转……最后原地卡死。这种逻辑只能算“能躲”,完全谈不上“智能”。
真正可用的避障策略,至少要是一个简单的状态机,把整个运行过程拆成几个明确的阶段:
- 直线行驶状态:超声波连续测距,如果前方距离大于安全阈值(一般30cm),保持直行。
- 接近障碍状态:前方距离小于30cm时,不急着立刻转弯,先减速,再根据超声波的读数判断障碍物的范围。
- 决策转向状态:减速后检测左侧和右侧的距离,选择空旷的一侧作为转向方向。
- 绕过恢复状态:转向完成后,重新进入直线行驶状态,但此时需要结合循迹传感器判断是否已经绕过了障碍,回到原来的轨迹上。
这套状态机的思路,本质上就是靠“延时 + 距离记忆”来模拟简单的记忆能力。我在实际写代码的时候,用了几个全局变量记录转向方向和转向持续的时间,保证小车不会在刚绕过障碍物的瞬间马上又判定“前方有障碍”。
超声波测距本身也有不少细节要注意。HC-SR04的触发方式是:给TRIG引脚一个10us以上的高电平,模块内部会自动发送8个40kHz的超声波脉冲,然后拉高ECHO引脚,ECHO高电平持续的时间就是超声波从发射到接收的往返时间。距离(cm) = 高电平时间(us) / 58。
// 超声波测距函数:单位cm float get_distance_cm(void) { uint32_t time_us = 0; // 拉高TRIG引脚,至少10us HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 等待ECHO变为高电平 while (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET); // 用定时器记录高电平持续时间 __HAL_TIM_SET_COUNTER(&htim2, 0); __HAL_TIM_ENABLE(&htim2); while (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET) { if (__HAL_TIM_GET_COUNTER(&htim2) > 12000) // 超时保护,约2m { __HAL_TIM_DISABLE(&htim2); return 999; } } __HAL_TIM_DISABLE(&htim2); time_us = __HAL_TIM_GET_COUNTER(&htim2); return (float)time_us / 58.0f; }这段代码里有几个非常关键的点:
第一,直接用一个死循环while等待ECHO引脚跳变是可行的,但一定要加超时保护。如果超声波模块没接好或者前方就是一片空旷的墙,ECHO引脚可能一直保持低电平,这时候程序就会卡死在while循环里,小车的其他任务全部停摆。我加了一个2m的超时保护,三秒之内测不到就返回一个很大的值,当作前方没有障碍。
第二,利用定时器计数来测量高电平时间比用delay循环精度高得多。STM32的定时器在主频72MHz下,一个计数周期大概是13.8ns,而一个delay空循环的时间很难精确控制,测出来的距离波动会很大。
第三,超声波模块的触发间隔不能太小。每次发射超声波后,声波还有余振,如果连续测量间隔太短,会出现信号串扰,测出来的距离忽大忽小。我的做法是每次测距之间至少间隔50ms,实测下来数据稳定很多。
避障状态机还有一层需要考虑的就是转向的幅度。很多初学者用的是一个非常粗暴的逻辑:左轮正转、右轮反转,原地转90度。这种转向方式在直线赛道上问题不大,但在有循迹线的场地上就会出现问题——车转完之后,循迹传感器已经彻底脱离黑线了,这样整个系统就在循迹和避障两个模式之间打架。
我当时想到的折中方案是:当超声波前方距离小于30cm时,先不脱离循迹线,而是让车减速,同时用左右轮差速的方式,缓慢地往空旷的一侧偏离。也就是说,避障时车是以一个较大半径的弧线绕过障碍物,而不是原地直角转弯。这样等绕过障碍之后,循迹传感器大概率还在线的附近,可以比较平滑地切回循迹模式。
4. 传感器供电和信号冲突:最容易翻车的地方
如果上面那些逻辑代码写得没问题,但小车跑起来还是各种诡异,那八成是硬件电路的问题。做嵌入式项目,软件出bug通常还能通过调试日志查到,硬件隐患有时候是查都查不着的。
最常见的问题就是共地。STM32的PWM信号送到TB6612的PWMA/PWMB引脚,如果STM32的GND和TB6612的GND没有连在一起,PWM信号的电平参考点就不一致,轻则电机转速忽快忽慢,重则电机根本不动。直接在面包板上用一根杜邦线把两边GND连起来就能解决,这几乎是我每次帮人调试必查的一个点。
第二个坑是超声波模块和电机驱动同时使用时,电源纹波的问题。HC-SR04对电源的纹波比较敏感,电机的碳刷(或者无刷电机的换相)会引入很多高频噪声,如果超声波模块的电源直接从电机电源那一路取,测距数据会出现随机性的跳变。解决方案很简单:给超声波模块单独用一个100uF电解电容和一个0.1uF陶瓷电容滤波,靠近模块电源引脚放置。别小看这两个电容,实测下来能明显减少数据的抖动。
第三个坑是舵机与电机的电流冲突。如果避障方案用的是舵机云台带动超声波模块左右扫测(这种方案在小车上也很常见),舵机启动的时候会有一个比较大的电流冲击,如果电源余量不足,这个冲击会把STM32的供电电压拖低,导致芯片复位。最典型的现象就是:小车一转弯,单片机和舵机同时重启,OLED屏幕闪一下,一切从头开始。做避障小车,至少要保证电池放电能力在3A以上,同时把舵机和电机的供电从逻辑供电分开。
第四个坑相对隐蔽一些,那就是GPIO引脚电平的冲突。循迹模块的信号输出,如果模块的供电电压是5V,输出高电平就是5V,而STM32的引脚虽然大多是5V容忍的,但是仍有部分引脚(尤其是ADC输入引脚)不是全范围容忍的,直接接5V有一定风险。最好给所有传感器的信号输出,都串一个1k欧姆电阻,或者用两个电阻做分压,把高电平拉到3.3V再进单片机。这个做法看起来很“保守”,但对芯片的保护是非常实在的。
5. 调试卷参的工程方法:别靠玄学,要有记录
循迹小车这套系统,代码写完只算完成了一半,剩下的一半全在调参。很多同学调参是靠“感觉”:Kp不对就调大点试试,不行再调小点,调了半天也调不明白,最后干脆死磕一个看起来还行的数值,发论文交课设。但真正有工程素养的调参方式,是靠记录和单变量原则。
我自己的习惯是:先在串口上加一个调试接口,把循迹传感器的原始ADC值、二值化后的状态码、PID计算出的output值、左右PWM值、超声波测距值,全部通过串口调试助手或VOFA+实时打印出来。这样小车在跑的时候,你可以在电脑上实时看到它的每一项参数变化,出问题的时候能快速定位是传感器问题、计算问题还是执行机构问题。
单变量原则很重要。调参数的时候,一次只改一个变量,改完记录下来,跑一圈,记录结果,再改下一个。不要Kp和Kd同时调,否则你根本不知道是哪个参数让小车变稳了。我一般是这样操作的:
- 第一步,固定速度在比较低的值(PWM占空比25%左右),把所有Kp、Kd清零。
- 第二步,只加大Kp,让小车能沿着线走出S形,但允许它小幅摆动。
- 第三步,在Kp的基础上加Kd,用来抑制摆动。Kd的值一般是Kp的三分之一到二分之一,所以我通常从Kp * 0.3这个值开始试。
- 第四步,确认直线稳定后,逐步提高速度,每提升一档速度就重复一遍第二步和第三步。
- 第五步,如果某一档速度下怎么调都稳不住,说明速度已经超出这套机械结构的极限了,不要硬扛,降回上一档速度。
避障的阈值参数同样需要记录。安全距离设得太大,小车频繁转向,根本没机会走直线;设得太小,刹车距离不够,直接撞上障碍。这个值跟车速和轮胎的摩擦系数都有关,没有一个通用的标准值。我的做法是先在电脑上模拟,把超声波测距值和电机的PWM值同步记录,分析一下从检测到障碍到小车完全停稳,最短需要多少距离,然后把这个距离留出20%的余量,作为安全阈值。
调参还有一个特别容易被忽视的因素:电池电量。锂电池在满电和快没电的时候,电压差能到0.5V以上,这会直接导致电机PWM占空比相同的情况下,实际转速不一样。如果你用一整个下午调好的参数,第二天换了一块满电的电池再去跑,可能就完全不是那回事了。所以我建议调参前先把电池充满,并且在调参过程中定时测量电池电压,如果电压掉了0.2V以上,就换一块满电的电池再继续。竞赛中很多队伍在赛前发现车变“飘”了,十有八九就是电池电量的问题。
6. 循迹与避障的模式切换:两个功能如何共处
不少课程设计的要求是“既能循迹,又能避障”,这就涉及两个功能怎么切换的问题。最常见的方案是加一个拨码开关或者按键,通过GPIO读取电平来切换模式。这个方案简单可靠,适合交课设,但不够“智能”。
稍微进阶一点的方案是“循迹优先,避障打断”的优先级机制:循迹模式下,超声波模块始终在工作,但避障逻辑只有在距离小于安全阈值时才介入。一旦介入,循迹PID的输出会被挂起,避障转向逻辑接管控制权,等障碍绕过去之后,再恢复循迹逻辑。这个方案比较考验状态机的设计,但只要把第三章那套状态机写顺了,加一个“中断标志位”就能实现。
这里有个很常见的误区:想把避障做成外部中断,直接在中断服务函数里操作电机。超声波测距本身耗时比较长(一次测距从触发到收到回波最多能到20ms),放到中断里很容易把其他更紧急的中断搞乱,而且触发超声波的时间间隔不确定,放在中断里逻辑会很难调。我建议把所有传感器数据采集都放到主循环里,用一个定时器中断做10ms的时基,主循环每次循环检查一下时基标志,再决定是否采集数据和更新控制量。
这种“对时时间片轮询”的结构,在写代码的时候会稍微多一点工作量,但好处是逻辑清爽,后期调试效率高得多。尤其当你想给小车加显示功能(用OLED显示实时距离和模式)、加蜂鸣器提示、加蓝牙遥控的时候,这种结构能让所有功能都有条不紊地协同,不会出现“加一个功能,另一个功能卡死”的情况。
循迹和避障还有许多同学没考虑到的一个联动细节:绕障之后的循迹恢复。如果你用的是“大半径弧线绕障”,绕过障碍之后小车的位置可能已经偏移出了黑线,循迹传感器读到的全都是白底,偏差值反复跳动,小车就会在线的边缘左右迷茫地摆动。这种情况我建议的做法是:绕障结束后,不要立刻切回循迹,而是让小车继续直线走一小段距离(比如50ms),同时不断读取循迹传感器的状态,一旦检测到有传感器压到黑线,再切回循迹PID。这个“滞后切换”的小技巧,实测能很大程度减少绕障后的震荡。
调试过程中还有一些细节值得注意:比如在小车上放一块OLED显示当前状态,能让你在跑动过程中快速判断车处于哪个模式;再比如给传感器加一个遮光罩,防止强光直射红外接收管,否则在窗边或者强光灯下,循迹传感器很容易误判。还有,万用表测量各路供电的电压值——电机转动瞬间电压是否跌落严重——这个习惯可以帮助你提前发现供电瓶颈,而不是等到小车现场罢工才去排查。
我在实际做这套系统的时候,最大的感受是:这种东西难不在知识点本身,而在于把所有小问题全部叠加在一起以后,整个系统能否稳定运行。STM32的代码逻辑其实都是固定的,PID算法网上随便一搜就有,超声波测距的例程也到处都是,真正能拉开差距的,是你有没有提前意识到供电要分区、传感器要滤波、阈值要动态校准、调参要记录数据。把这些工程细节处理好了,小车自然跑得又直又稳;处理不好,哪怕代码逻辑写得再漂亮,上了赛道照样会翻车。
如果你正准备做类似的题目,我建议拿到板子先别急着写代码,先把硬件平台的每一路电源都测量一遍,确认供电稳定、共地可靠之后,再开始写驱动和逻辑,你会发现后期调试顺利得多。
本文还有配套的精品资源,点击获取