简介:面向51单片机学习者和智能小车爱好者,这份源代码完整实现了红外循迹与超声波避障的组合控制。程序基于STC15W4K56S4单片机,在KEIL环境下开发,搭配L293D电机驱动、1602液晶显示、直流减速电机以及红外循迹、红外避障和超声波测距模块。运行逻辑清晰:前方无阻碍时小车循迹行驶,检测到障碍物立即停车,移开障碍物后自动恢复循迹,可直接用于课程设计或竞赛原型验证。
资源包共62个文件,压缩包大小约150KB。文件类型覆盖C源码、H头文件、HEX烧录文件、KEIL工程文件及说明文档,还包含lst、obj、m51等编译过程文件,便于二次修改与重新编译。源码按功能模块拆分,如电机驱动、超声波测距、红外避障、循迹控制和液晶显示等,配合必看说明可快速上手。
该资源已有3431人学习下载,作者在自建小车上亲测可用。下载后可直接导入KEIL查看逻辑,也可烧录HEX到实际硬件验证,适合想掌握51单片机多传感器融合控制的开发者参考。 拿到这套“51单片机智能小车红外循迹+超声波避障程序源代码”的时候,我第一反应是,这几乎是每一个做嵌入式或者电子设计入门的人,都绕不开的一个完整项目。很多人最开始玩单片机,就是点个流水灯,或者做个电子时钟,但真正让你从“点亮一颗LED”过渡到“一个能自动跑的小车”,这个项目刚好卡在两者中间。它涵盖了传感器读取、电机驱动、PWM调速、定时器中断、逻辑判断,甚至还有一点简单的避障策略,麻雀虽小五脏俱全。
如果你正在准备课程设计、电子设计竞赛,或者单纯想搞一台能在桌面上追着黑线跑的小车,这套源码的参考价值是实实在在的。它在硬件上用的是经典的51单片机(STC89C52或者AT89S52都行),配合TCRT5000红外循迹模块和HC-SR04超声波传感器。这两个模块是整个项目的灵魂,也是难点所在。我先说个结论:这个项目不难,但坑很多,尤其是供电和传感器阈值调节,能折磨人两三天。下文我会把整个项目的硬件搭建、模块原理、代码核心逻辑,以及我调试过程中踩过的坑,全部拆开讲清楚。
1. 项目整体设计与硬件架构拆解
1.1 为什么选51单片机而不是STM32或Arduino
很多人一上来就纠结主控选型,觉得51单片机性能太弱,不如直接用STM32或者Arduino。我个人的看法是,这个项目用51恰恰是最合适的选择。STM32虽然性能强,但对于一个循迹加避障的小车来说,绝大部分算力都是浪费的。Arduino确实开发快,但很多学校的课程设计和竞赛默认平台就是51,而且51的寄存器操作方式能让你把每一个外设的原理都吃透,比如定时器怎么配置、中断怎么触发、PWM怎么模拟。你把这些搞明白了,后面去学STM32其实就是换了个库的事。
还有一点,51单片机的手册和例程极其丰富,网上随便一搜就是几百个版本。对于新手来说,遇到问题容易找到参考。而且STC89C52这种芯片的成本只有几块钱,即使烧错了几个IO口,换一片也不心疼。
1.2 小车硬件的选型思路与整体框架
整个小车系统可以拆成五个部分:主控板、电机驱动、传感器、电源、车架。我这次用的配置如下,也是网上流传最广的一套方案:
| 模块 | 型号 | 说明 |
|---|---|---|
| 主控 | STC89C52RC | 40脚DIP封装,12MHz晶振 |
| 电机驱动 | L298N | 双H桥驱动,支持PWM调速 |
| 直流电机 | 4个TT马达(两驱或四驱) | 减速比1:48,扭矩够用 |
| 循迹模块 | TCRT5000三路红外对管 | 带LM393比较器,数字输出 |
| 超声波模块 | HC-SR04 | 测距范围2cm-400cm |
| 舵机(可选) | SG90 | 用于超声波云台左右扫描 |
| 电源 | 7.4V锂电池组 + LM2596降压模块 | 电机直供,逻辑部分降压到5V |
整个系统的连接方式是这样的:电池正极经过一个总开关分成两路,一路直接给L298N电机驱动供电(用于电机),另一路经过LM2596降压到5V后给单片机主板、循迹模块、超声波模块供电。这里最关键的一点是共地,即降压模块的负极与L298N的负极必须连在一起,否则传感器和单片机之间的电平参考不一致,逻辑就会乱套。很多人的小车跑起来不受控,十有八九就是地线没接好。
TCRT5000循迹模块的输出是数字信号,直接接单片机的P1口。HC-SR04超声波模块的Trig和Echo分别接两个IO口,测量距离时只需要拉高Trig10微秒,然后接收Echo高电平的持续时长即可。
1.3 供电方案的确定与避坑
供电是整个项目里最容易出问题的地方,尤其是电机和单片机共用电源的情况。TT马达启动瞬间电流能到1A甚至更高,电压跌落非常明显。如果单片机和传感器跟电机共用一个电源且没有做降压隔离,小车一启动单片机就会复位,表现就是车跑起来没几秒就重启。
我的做法是,7.4V锂电池直接给电机驱动供电,LM2596降压到5V给单片机。比较便宜的方案是用三端稳压芯片7805,但7805压差要求较高,7.4V输入输出5V时效率不高而且发热明显,功耗全浪费在发热上了。LM2596是开关电源,效率高,发热低,实测下来电池续航比7805方案多20%以上。
注意:无论采用哪种方案,共地都不能省。不共地,信号线上测到的电压永远是不确定的,系统必然随机性失控。
2. 红外循迹模块的原理与控制逻辑
2.1 TCRT5000到底在检测什么
TCRT5000内部集成了一个红外发射管和一个光电接收管。发射管持续发射红外光,红外光照射到地面后,由于黑色和白色的反射率不同,接收管接收到的红外光强度也不同。模块上自带一个LM393电压比较器,将接收管上的电压与一个可调阈值电压进行比较,最终输出高电平或低电平。
当红外光被黑色轨道吸收时,接收管几乎收不到反射光,比较器输出高电平(通常标记为黑线检测);当小车行驶在白色地面上时,反射光强,接收管导通,比较器输出低电平(白底)。模块上的蓝色电位器就是用来调节这个比较阈值的。阈值调得太灵,地面有一点污渍都会被当成黑线;调得太钝,黑线又检测不到。我的一般做法是:把模块放在白底上方约1.5cm处,旋转电位器直到输出刚好跳变,然后再往灵敏方向微调半圈。
2.2 三路循迹的控制策略
三路循迹模块的安装位置呈一字排开,左、中、右各一个传感器。中间的传感器负责对准黑线,左右两个负责检测偏离方向。逻辑控制其实就是一个非常简单的查表判断:
| 左传感器 | 中传感器 | 右传感器 | 判断结果 | 动作 |
|---|---|---|---|---|
| 1 | 1 | 1 | 全在黑线上(十字路口) | 直行 |
| 0 | 0 | 0 | 全在白底上(丢线) | 停止 |
| 1 | 0 | 0 | 左偏 | 左转 |
| 0 | 0 | 1 | 右偏 | 右转 |
| 1 | 0 | 1 | 中间在黑线上 | 直行 |
| 0 | 1 | 0 | 中间对准黑线 | 直行 |
| 0 | 1 | 1 | 车头偏左 | 左转 |
| 1 | 1 | 0 | 车头偏右 | 右转 |
表中的1代表检测到黑线,0代表白底。这里有一个细节,左右传感器的安装间距要和实际轨道的线宽匹配,一般黑线宽度在1.5cm到2.5cm之间时,传感器间距15mm左右比较合适。间距太宽,小车会在黑线上走“之”字形;间距太窄,转向反应又太迟钝。
2.3 为什么不用PID也能跑直线
很多新手一看到循迹小车,就问是不是要用PID控制。我坦率地说,对于51单片机配合TCRT5000这种简单数字量的应用场景,PID反而有点杀鸡用牛刀的感觉。数字传感器的输出只有0和1,没有连续的偏差量,你看到的就是左边偏了就往右打,右边偏了就往左打,本质上是bang-bang控制(开关控制)。这种控制方式在小车速度不高(PWM占空比40%左右)的情况下完全够用。
如果后续想让小车的循迹更丝滑,可以在代码逻辑上做一些变通,比如检测到轻微偏转时先给一个较小的转向PWM,检测到重度偏转时再给全力转向。这种做法相当于一个模糊的P控制,虽然没有标准PID那么精细,但比单纯的开关控制好很多,而且不增加代码复杂度。
2.4 循迹模块的物理安装方向
TCRT5000的安装高度建议在距离地面1.5cm到2.5cm之间。离得太近,小车稍微颠簸一下模块就贴地了,信号被遮挡;离得太远,红外光发散严重,黑白反射差异被稀释,比较器不好判断。安装方向要保证发射管和接收管垂直于地面,歪斜会导致接收光强度下降,阈值判断不稳定。
模块固定我推荐用铜柱加螺丝锁死在底盘上,不要用热熔胶。热熔胶在电机振动下容易脱落,而且一脱落就是三路全歪,排查起来非常痛苦。
3. 超声波避障模块的原理与数据采集
3.1 HC-SR04的测距时序细节
HC-SR04的测距原理用一句话概括,就是记录超声波从发射出去到反射回来的时间,然后乘以声速再除以2。但工程上实现的细节比这句话要具体得多,很多人的代码看似逻辑正确,实测却不行,问题就出在时序控制上。
正确的时序如下:
- 先将Trig引脚拉低至少2微秒,确保模块处于复位状态。
- 将Trig引脚拉高至少10微秒,模块内部会自动发射8个40kHz的方波脉冲,并等待接收回波。
- 发送完方波之后,模块会主动将Echo引脚拉高,Echo拉高的时间就是声波往返的时间。
- 单片机需要精确测量Echo引脚高电平的持续时间。
在代码中,这套时序写成:
void Ultrasonic_Trigger(void) { TRIG = 0; // Trig拉低 delay_us(5); // 保持低电平 TRIG = 1; // Trig拉高 delay_us(15); // 保持高电平,至少10us TRIG = 0; // 拉低,结束触发 } float Ultrasonic_GetDistance(void) { unsigned int time_us = 0; Ultrasonic_Trigger(); // 发送触发信号 while (ECHO == 0); // 等待Echo拉高 TR0 = 0; TH0 = 0; TL0 = 0; // 清零定时器 TR0 = 1; // 启动定时器开始计时 while (ECHO == 1); // 等待Echo拉低,即等待回波 TR0 = 0; // 停止计时 time_us = (TH0 << 8) | TL0; // 读取定时器值 return time_us * 0.017; // 距离(cm),详见下节推导 }3.2 距离计算公式的推导
声速在空气约340m/s,也就是0.034cm/微秒。Echo高电平的持续时间是声波从发出到接收的往返时间,所以距离的计算公式是:
距离(cm) = 时间(微秒) * 0.034 / 2 = 时间(微秒) * 0.017
很多例程里面直接写distance = time * 0.017,这个0.017就是这么来的。对于12MHz晶振的单片机,定时器每微秒计数一次(12个时钟周期),所以用TH0和TL0拼出来的数值本身就是微秒数,不需要再除以12。但如果你用的是11.0592MHz晶振,就必须做频率修正,否则距离始终会偏大或偏小。
3.3 超声波测距的避坑经验
HC-SR04的精度在实际使用中受很多因素影响。第一是环境温度,声速跟温度正相关,温度每上升1度,声速大约增加0.6m/s。在室内恒定温度下影响不大,但在室外环境温差明显的地方,测距值会出现漂移。第二是反射面形状,超声波打到斜面或圆柱面上,很大一部分能量会被反射到其他方向,导致测不到回波,Echo会超时拉高,程序就会卡死在while等待循环里。
所以代码里必须加超时判断。我的处理方式是,在等待Echo拉高和等待Echo拉低的两个while循环中都加一个超时计数变量,如果超过30ms(对应约5米距离)还没有反应,直接退出循环并返回一个固定的最大值(比如99cm),避免单片机卡死在等待中。
unsigned int timeout = 0; while (ECHO == 1) { if (++timeout > 30000) { // 约30ms超时 TR0 = 0; return 99; // 超时返回大值 } }3.4 避障策略:左转优先还是右转优先
有了距离数据之后,避障策略就变成逻辑判断了。我常用的策略是先让超声波测前方距离,如果大于设定阈值(比如25cm),小车直行;如果小于阈值,停车,然后原地向左转90度(或者右转),再次测距,如果新方向安全则前进,否则向反方向转。
这个策略有一个隐藏的问题,就是超声波模块是固定的还是带舵机云台的。固定安装时,超声波只能检测正前方一个方向,小车在遇到斜侧面的障碍物时,如果正前方的检测窗口刚好漏过了障碍物边缘,就会撞上去。我的做法是给超声波模块加一个SG90舵机,左右各转60度,形成三个方向的扫描。虽然51单片机驱动舵机需要写PWM,但用定时器加IO翻转就能实现,不算复杂。实测下来,三向扫描的避障成功率比单方向检测高很多,尤其面对墙角这类场景效果明显。
4. 程序源代码的结构分析与核心代码讲解
4.1 模块化编程的文件规划
拿到源码包后先看文件结构,这是快速理解代码脉络的捷径。一套健康的51工程项目文件应该包含以下几个层次:
main.c:主循环,负责初始化和调度delay.c/delay.h:延时函数,微秒和毫秒级motor.c/motor.h:电机控制,包括前进、后退、左转、右转、PWM调速tracking.c/tracking.h:循迹传感器的读取和策略判断ultrasonic.c/ultrasonic.h:超声波测距的触发和距离计算uart.c/uart.h:串口调试(可选)
文件分模块带来的最大好处是,你可以单独测试某一部分功能。比如先用一个简单的测试程序确认电机正反转方向,再单独用串口打印出超声波的距离数据,最后再合并整体逻辑。如果不分模块,所有代码堆在main.c里,一旦出现问题,排查难度翻倍。
4.2 主循环的执行调度
主循环的逻辑应该保持简单,尽量不让阻塞性函数占住整个CPU。我写的main.c核心如下:
void main(void) { unsigned int distance; Timer0_Init(); // 初始化定时器0 PWM_Init(); // 初始化PWM输出 Motor_Init(); // 初始化电机引脚 while (1) { Tracking_Handle(); // 循迹逻辑判断 distance = Ultrasonic_GetDistance(); // 测距 if (distance < 25) // 前方障碍物过近 { Motor_Stop(); Car_Turn_Left(300); // 左转300ms delay_ms(100); } else { Motor_Go_Straight(); // 直线前进 } } }这里有一个常见的架构陷阱:如果把循迹和避障放在同一个主循环里,并且超声波测距的while等待循环时间太长,单片机就做不了其他事情,逻辑会显得非常“卡顿”。一个比较好的优化方式是,给整个循环加一个固定节拍,比如每50ms执行一次循迹判断,每200ms执行一次超声波测距。这样两个功能的干扰会降到最低。
4.3 定时器与中断的正确配置
51单片机产生PWM的方式比较原始,最常见的就是用定时器中断,每隔一个固定时间翻转IO口来模拟PWM波。举个例子,如果想让一个电机引脚输出50%占空比、周期为10ms的PWM信号,那就用定时器产生5ms中断,中断服务函数里翻转一次IO。
void Timer0_ISR(void) interrupt 1 { TH0 = 0xEE; // 重新装载初值 TL0 = 0x00; // 对应约5ms中断 pwm_counter++; if (pwm_counter >= 100) { pwm_counter = 0; } if (pwm_counter < motor_speed_left) { IN1 = 1; // 左电机正转控制 } else { IN1 = 0; } }motor_speed_left变量就是占空比的等效表达,0到100之间的数值越大平均电压越高转速越快。用这种软件PWM的好处是占空比随时可调,缺点是比较占中断资源,所以中断服务函数里面不要放复杂逻辑。
4.4 源码中需要注意的易错点位
我见过特别多的例程在起始处没有先设置IO口的初始电平,导致上电瞬间电机乱转一下。这是很多新手忽略的细节:单片机在复位瞬间,IO口默认是高电平,如果电机驱动芯片的使能端和输入引脚是高电平有效,那么电机上电就会瞬间转一下再停下来。编程时要在主函数的最开始,先把所有电机控制引脚拉低,使能引脚也拉低,然后再初始化其他外设。
第二个容易出错的点是舵机PWM的周期。SG90舵机需要20ms周期的PWM信号,1.5ms的高电平对应90度。用定时器中断模拟舵机PWM时,定时器初值的计算要精确到微秒,如果脉冲宽度偏差超过0.3ms,舵机角度就会严重偏移。
5. 常见故障、调试技巧与经验总结
5.1 小车走不出直线的排查
小车走不直是最常见的问题,但原因可能有很多,我提供一个从简到繁的排查顺序:
- 左右电机的接线极性是否一致。L298N的OUT1/OUT2接左电机,OUT3/OUT4接右电机,接反了左右转向就会镜像,表现出来就是走不直甚至原地绕圈。
- 左右电机转速差异。TT马达虽然是同一个型号,但个体差异导致转速不可能完全一致,即使用相同的PWM占空比,小车也会向慢的一侧偏移。解决方式是实测左右轮在不同占空比下的实际速度,在代码里补偿。
- 电池电量下降带来的电压跌落。实测下来,电池从满电7.4V降到6.5V左右时,同样的PWM占空比下电机转速会有明显下降,如果代码里的补偿值是固定写死的,电量变化后又会偏。
5.2 红外循迹模块输出一直不变
模块输出一直为高或者一直为低,先检查模块上的电源指示灯和信号指示灯有没有正常亮起。如果没有亮,大概率是3.3V供电还是5V供电接错了。TCRT5000模块有的版本支持3.3V到5V宽电压,有的只支持5V,电压不足时发射管亮度不够,接收管完全收不到反射信号。
如果供电正常但输出一直不对,把模块拿到黑线正上方,边旋转电位器边看信号指示灯有没有变化。如果不管怎么调都没反应,大概率是模块本身坏了,换一个模块测试。TCRT5000这个器件本身很皮实,但模块上焊接的贴片电阻和电容器件比较脆弱,焊接温度过高导致虚焊的情况不在少数。
5.3 超声波测距数据跳变的处理
超声波测距数据偶尔跳变是正常现象,因为有环境杂波干扰或反射面不规则。处理办法有两种:一是连续测5次,去掉最大值和最小值,然后取剩下3次的平均值;二是加一个简单的滤波判断,如果当前值和上一次值的差超过某个阈值(比如20cm),丢弃,继续用旧值。我个人推荐第二种,因为计算量小,而且对实时避障来说更有用,不会因为一个瞬时的大跳变而做出过激的转向反应。
5.4 电量消耗过快的建议
小车用的是18650锂电池组,标称容量2000mAh左右,实测满电连续跑大概在40分钟左右。如果你发现续航远低于这个数,先检查L298N的散热片温度,烫手的话说明电机堵转或者PWM频率太低导致开关损耗大。可以把PWM频率提高到10kHz以上,降低功率管的开关损耗,同时对电池也是一个保护。
5.5 调试时最好加一个串口打印
我在开发时必开串口打印,30块钱的USB转TTL模块就能干这个活。把循迹三路传感器的实时状态、超声波的距离数据、当前电机的PWM占空比全部通过串口发到电脑串口助手,一眼就能看出问题是出在传感器还是执行器还是逻辑。不要用矩阵灯或者示波器去猜,调试效率太低。
void UART_Send_Byte(unsigned char dat) { SBUF = dat; while (!TI); TI = 0; } void UART_Send_String(char *str) { while (*str) { UART_Send_Byte(*str++); } }一段简单的发送函数,在循环里格式化出一串字符发出去:
sprintf(buffer, "L:%d M:%d R:%d Dis:%dcm\r\n", sensor_left, sensor_mid, sensor_right, distance); UART_Send_String(buffer);这样调起来心里就有底了,小车跑偏了,你能看到是哪路传感器误触发;避障撞车了,你能看到是距离阈值设大了还是转向时间不合适。
6. 源码该从哪里开始改
拿到源码包第一件事,不是直接烧录,而是先打开motor.c文件,把左右电机的PWM占空比或者转速补偿写好。因为后续所有调试都建立在“小车能沿着直线走”的基础上。走不直的话,循迹和避障的逻辑再完美都白搭。
然后再调整循迹模块的电位器,传感器的安装高度和间距。最后再调试避障的距离阈值,我常用的参数是:前方30cm内开始减速,25cm内停车转向,转向后测到新方向大于40cm才继续直行。不同车架和电机性能略有差异,这个参数需要实测微调。
提示:在实际调车之前,把车轮拆了架空测试是很好的习惯。架空时电机空转,负载小,电流小,能避免很多供电和电流冲击引起的问题。等逻辑调通后,再装上轮子地面跑。
7. 一点个人体会
这个项目其实就代表了一条完整的学习路线:从单片机外设驱动到传感器原理,从执行机构控制到闭环策略设计,全部经历一遍。很多过来人说“51单片机过时了”,但如果你真的把一个51小车从零做到能稳定循迹、灵活避障,你再回头看STM32、ESP32这些平台,会发现难点根本不在主控性能,而在你对硬件原理和软件架构的理解够不够深。
我在做完这个小车之后最大的收获,是学会了怎么把一个大问题拆成一个个能单独验证的小模块。先点亮LED,再驱动电机轮子转起来,再读到传感器数值,再把它们串起来。每一步都验证过了,最终合在一起的每一分钟就都是心理有数的。这套方法,才是这个源码包里最值钱的东西。
本文还有配套的精品资源,点击获取