
简介面向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 小车硬件的选型思路与整体框架整个小车系统可以拆成五个部分主控板、电机驱动、传感器、电源、车架。我这次用的配置如下也是网上流传最广的一套方案模块型号说明主控STC89C52RC40脚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 三路循迹的控制策略三路循迹模块的安装位置呈一字排开左、中、右各一个传感器。中间的传感器负责对准黑线左右两个负责检测偏离方向。逻辑控制其实就是一个非常简单的查表判断左传感器中传感器右传感器判断结果动作111全在黑线上十字路口直行000全在白底上丢线停止100左偏左转001右偏右转101中间在黑线上直行010中间对准黑线直行011车头偏左左转110车头偏右右转表中的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再驱动电机轮子转起来再读到传感器数值再把它们串起来。每一步都验证过了最终合在一起的每一分钟就都是心理有数的。这套方法才是这个源码包里最值钱的东西。本文还有配套的精品资源点击获取