简介:基于51单片机的红外循迹与避障双功能小车资料,服务于嵌入式入门的实践教学,也适合电子设计竞赛选手参考。项目围绕红外传感器与电机控制展开,完整展示了循迹和避障的算法逻辑,以及从原理图到代码烧录的实现路径。压缩包共14个文件,大小约418KB,其中包含Keil工程文件、C语言源文件、可下载的HEX固件、程序编译生成的调试信息,以及实验场景图等,足以支撑从源码阅读到硬件验证的全流程。当前已有二千五百零二人学习下载,人气可观,说明其受到广泛关注。学习者可从中获得可复用的工程框架和具体的编程思路,尤其能练习中断、定时器、PWM调速和IO口控制等常用技术的综合应用,帮助快速上手小型智能车开发,少走弯路。
1. 51单片机双功能小车的定义与适用人群
做电子设计课设的人拿到“51单片机红外循迹红外避障小车”多半会以为要写两套代码:一套让车沿着黑线走,一套让车躲开障碍物。实际把硬件摆上桌就会发现,这辆所谓的双功能小车,核心传感元件只有一种——红外对管。循迹时对管朝下照地面,黑线吸光、白纸反光;避障时同一只对管水平朝前照,障碍物把红外光反射回来。双功能的“功能”不在传感器数量,而在模式切换和差速转向的调度逻辑,这恰恰是课设答辩时最容易被追问的地方。
适合读这篇的人分两类:第一类是正在做51单片机课程设计或电子竞赛入门车的学生,需要一套能复现的接线表和C代码;第二类是写过几年嵌入式、想快速确认传感器阈值标定和PWM不对称这类细节的工程师。下面的内容按硬件搭建、传感原理、Keil5下的C代码实现、现场调参的顺序推进,把常见的丢线、误触、电机抖动问题一次讲清楚。
2. 红外循迹与红外避障的本质:从反射光路到数字电平
2.1 同一只红外对管,完成两种感知
红外对管由发射管和接收管组成,发射管持续发出940nm左右的红外光,接收管根据反射回来的光强改变导通程度。循迹时对管垂直向下安装,白色或浅色地面反射率高,接收管导通强;黑色电工胶带几乎不反射红外光,接收管截止。模块上的LM393比较器把这个模拟差异转换成数字电平,单片机读到的就是干净的0或1,不需要再经过ADC。
避障时对管水平向前安装,前方没有障碍物时红外光发散后无法回到接收管,输出一种电平;有障碍物时反射光增强,输出翻转。这里有一个从原理上就能推导出的坑:避障模块的安装高度和仰角会直接影响检测距离,装得太低会一直收到地面反射导致误触发,装得太高又测不到矮障碍物。我一般把避障传感器装在车头前方4到6厘米高处,朝前微微上仰5度左右,让光轴避开地面。
2.2 传感器选型:TCRT5000和E18-D80NK的组合怎么定
循序迹和避障虽然原理相同,但检测距离要求差了一个数量级,所以市面上常见的方案是传感件混搭:循迹用TCRT5000模块,避障用带调制功能的E18-D80NK。
| 参数 | TCRT5000(循迹模块) | E18-D80NK(避障模块) |
|---|---|---|
| 工作电压 | DC 5V(部分模块支持3.3V) | DC 5V |
| 检测距离 | 1mm 到 25mm,实际建议离地10~20mm | 3cm 到 80cm,用电位器可调 |
| 输出形式 | 数字电平,TTL兼容 | 数字电平,NPN开漏输出 |
| 抗环境光 | 较弱,需要屏蔽罩或在暗光环境调试 | 强,内部有调制解调电路 |
| 适用场景 | 黑线灰度识别 | 障碍物有无判断 |
不要指望用E18-D80NK去做循迹,它的检测距离是以厘米为单位的,面对大面积的地面颜色差异很难给出稳定输出;反过来,TCRT5000做避障也只有2.5cm左右的有效距离,车速稍快就来不及刹车。如果预算有限非要统一用TCRT5000做避障,那小车速度必须压到极低,且要接受频繁误报。
2.3 阈值比较和软件标定:先调硬件再写代码
每个红外模块上都有一颗电位器,用来调节LM393比较器的参考电压。这个电位器是现场调试的第一个关口。以循迹模块为例,先把小车放在白色地面上,用螺丝刀缓慢旋转电位器,找到模块上指示灯刚好翻转的临界点,然后往回调1/8圈,让白地面的输出更稳定;再把车移到黑色胶带上,确认指示灯状态确实翻转。
这个标定动作解决的是“什么是黑”的问题,调完之后,模块输出的数字电平就固定了。代码里要做的只是读引脚,不需要ADC采样。读取部分在51单片机上加一句引脚定义即可:
sbit TRACK_LEFT = P0^0; // 左循迹传感器输出 sbit TRACK_MID = P0^1; // 中循迹传感器输出 sbit TRACK_RIGHT = P0^2; // 右循迹传感器输出 sbit AVOID_LEFT = P0^3; // 左避障传感器输出 sbit AVOID_RIGHT = P0^4; // 右避障传感器输出注意P0口传统8051默认是开漏结构,很多51开发板上已经有上拉排阻,直接用没问题;如果是自制最小系统板,务必给P0口加上4.7k到10k的上拉电阻,否则读回来的数据会随机跳动。
3. 整车搭建:电机驱动、电源分配与双功能接线
3.1 L298N驱动与电池供电的注意事项
电机驱动最常见的做法是L298N模块,它可以同时驱动左右两个直流减速电机。L298N上有两个使能端ENA和ENB,分别给左右电机提供PWM调速;每侧还有两个方向控制引脚IN1~IN4,组合成正转、反转和刹车。电流路径上,动力电池正极接L298N的12V输入,L298N板载5V稳压输出再供给单片机。
这里有一个新手几乎必踩的坑:电机堵转启动瞬间电流可达2A,如果单片机也从这个5V输出取电,电机会把5V拉低,造成单片机复位或传感器阈值漂移。我一般会做两级处理:一是在L298N的12V输入侧并联一个470uF电解电容,吸收启动冲击;二是把L298N的5V输出和单片机电源之间加一个100uF电容,同时把L298N的GND与单片机GND用粗导线短接。共地线做不到位,PWM波形上会叠加大量毛刺,传感器误判会频繁出现。
3.2 双功能小车接线对照表
双功能并不是把两套传感器全接到P2口那么简单,引脚规划的原则是:传感器占用P0口统一读取,电机方向控制占用P1口,PWM信号也从P1口输出,模式切换占用P3口的一个独立引脚。下面这套接线适用于STC89C52或AT89S52的最小系统板:
| 模块 | 模块引脚 | 单片机引脚 | 说明 |
|---|---|---|---|
| 左循迹 | OUT | P0.0 | 1表示压黑线 |
| 中循迹 | OUT | P0.1 | 1表示压黑线 |
| 右循迹 | OUT | P0.2 | 1表示压黑线 |
| 左避障 | OUT | P0.3 | 检测到障碍为低电平 |
| 右避障 | OUT | P0.4 | 检测到障碍为低电平 |
| L298N | IN1 | P1.0 | 左电机方向A |
| L298N | IN2 | P1.1 | 左电机方向B |
| L298N | IN3 | P1.2 | 右电机方向A |
| L298N | IN4 | P1.3 | 右电机方向B |
| L298N | ENA | P1.4 | 左电机PWM使能 |
| L298N | ENB | P1.5 | 右电机PWM使能 |
| 模式开关 | 拨码一端 | P3.7 | 高电平循迹模式,低电平避障优先 |
为什么PWM不接在L298N的固定PWM引脚上?因为51单片机不像STM32那样有硬件PWM输出,常用的做法是让定时器中断去翻转任意IO,所以P1.4和P1.5这样的普通引脚就能承担软件PWM功能。以后换到STM32F103ZET6做智能小车时,把这个软件PWM换成定时器通道输出即可,接线思路不变。
3.3 双功能模式的硬件切换方式
模式切换一般用两种硬件方案。最简单的是拨码开关,一端接P3.7,另一端接地,通电时读取一次引脚状态,程序里用一个mode变量记录下来;另一种是用独立按键,但按键需要处理消抖和长按,对课设来说性价比不高。拨码开关的优势在于复位瞬间电平就稳定,不需要在代码里加入防抖等待。
双功能的具体定义也需要在硬件层面说清楚。常见设计是三种模式:模式一纯循迹,模式二纯避障,模式三避障优先的循迹——也就是正常沿黑线走,遇到障碍物临时切换避障动作,绕开后再回到线上。第三种模式最能体现“双功能”的价值,把它作为默认屏蔽档位即可。
4. 双功能控制逻辑:Keil5下的C代码与策略切换
4.1 用51单片机的定时器生成双路软件PWM
STC89C52没有硬件PWM模块,最可靠的方式是用定时器0产生一个固定频率的时基中断,在中断里维护一个递增计数器,把这个计数器和目标占空比值比较,从而输出PWM。下面这段代码把PWM周期设为2ms,对应500Hz,这个频率下电机没有明显啸叫,L298N也能正常响应。
unsigned char pwm_count = 0; unsigned char speed_left = 70; // 左轮占空比 0~100 unsigned char speed_right = 70; // 右轮占空比 0~100 sbit PWM_L = P1^4; // L298N ENA sbit PWM_R = P1^5; // L298N ENB void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,模式1,16位 TH0 = 0xFF; TL0 = 0xA4; // 100us @ 11.0592MHz ET0 = 1; TR0 = 1; EA = 1; } void Timer0_ISR(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xA4; // 重装初值 pwm_count++; if (pwm_count >= 20) { pwm_count = 0; } PWM_L = (pwm_count < speed_left) ? 1 : 0; PWM_R = (pwm_count < speed_right) ? 1 : 0; }初值0xFFA4的计算过程:11.0592MHz晶振下机器周期为12/11.0592MHz约等于1.085us,100us需要计数92个机器周期,65536减92等于65444,也就是0xFFA4。计数器每溢出一次进入中断,pwm_count在0到19之间循环,speed值就是占空比的百分比。调整speed_left和speed_right就能让小车直行、加速或转圈,而不需要额外修改寄存器配置。
4.2 三路循迹的状态表与差速转向
循迹的转向逻辑是一个典型的两轮差速模型:想让车头往左偏,就降低左轮占空比、提高右轮占空比,车体围绕重心旋转。三路传感器的读数是三个bit,组合起来有8种状态,对应不同的转向强度。
#define STATE_STRAIGHT 0x02 // 010:中间压线,直行 #define STATE_LEFT_SLOW 0x06 // 110:左和中压线,快速左转 #define STATE_LEFT_TURN 0x04 // 100:仅左压线,中速左转 #define STATE_RIGHT_SLOW 0x03 // 011:右和中压线,快速右转 #define STATE_RIGHT_TURN 0x01 // 001:仅右压线,中速右转 #define STATE_LOST 0x00 // 000:全部丢线 void track_control(void) { unsigned char state = 0; state |= TRACK_LEFT ? 0x04 : 0; state |= TRACK_MID ? 0x02 : 0; state |= TRACK_RIGHT ? 0x01 : 0; switch (state) { case STATE_STRAIGHT: speed_left = 70; speed_right = 70; break; case STATE_LEFT_SLOW: speed_left = 30; speed_right = 90; last_dir = 1; // 记录最后一次转向方向 break; case STATE_RIGHT_SLOW: speed_left = 90; speed_right = 30; last_dir = 0; break; case STATE_LOST: recover_track(); // 丢线调头找回 break; default: speed_left = 55; speed_right = 85; break; } }这段代码的默认分支覆盖了只有单侧压线的中间态。注意state拼装时把P0.0赋给最高位,这样读到的状态码和上面的宏定义一一对应。逻辑上“黑线在左就往左修正”这条规则适用于大多数循迹场景,但如果线圈铺设方向相反,只需要交换左右传感器接线或改变配套的分支数值。
4.3 循迹丢线的调头找回
“红外循迹调头”是循迹小车最难的环节,因为传感器全丢线时,小车其实是“盲”的状态。常见做法是利用last_dir这个记忆变量:最后一次是左转时丢线,说明黑线往左甩出,小车应该继续原地左转;最后一次是右转时丢线,则反向操作。这样能在多数弯道场景中恢复路线。
void recover_track(void) { if (last_dir == 1) { // 左轮反转、右轮正转,原地左转 IN1 = 0; IN2 = 1; IN3 = 1; IN4 = 0; speed_left = 45; speed_right = 45; delay_ms(300); } else { IN1 = 1; IN2 = 0; IN3 = 0; IN4 = 1; speed_left = 45; speed_right = 45; delay_ms(300); } // 恢复直行,低速试探 speed_left = 50; speed_right = 50; }调头动作的核心不是转多久,而是每次只增加一个固定的旋转时长,让控制器重新进入循迹循环。如果在recover_track返回后依然丢线,下一次进入时应该适当延长旋转时间,我一般会在丢线计数的累积上做线性递增,第一次300ms,第二次400ms,直到传感器重新读到黑线为止。
4.4 避障传感器的防抖处理与三档避障逻辑
E18-D80NK这类数字量避障传感器的输出在障碍物边缘会抖动,直接读引脚会导致小车左右抖动。我一般会在避障检测函数里加入连续多次采样确认。下面以左避障为例:
unsigned char avoid_left_check(void) { static unsigned char cnt = 0; if (AVOID_LEFT == 0) // 模块检测到障碍时输出低电平 { if (cnt < 50) cnt++; } else { cnt = 0; } return (cnt >= 50); // 连续50次低电平,确认有障碍 }这个函数每次被调用时都会在定时器中断的间隙执行,50次连续采样累计大约50ms时间,足以滤除毛刺而又不会让避障反应迟钝。避障控制策略用一个简单的三态判断即可:
void avoid_control(void) { unsigned char l = avoid_left_check(); unsigned char r = avoid_right_check(); if (l && r) { // 两侧都有障碍,先后退再右转 IN1 = 0; IN2 = 1; IN3 = 0; IN4 = 1; delay_ms(200); IN1 = 1; IN2 = 0; IN3 = 0; IN4 = 1; delay_ms(200); } else if (l) { // 左侧有障碍,右转 speed_left = 80; speed_right = 30; } else if (r) { // 右侧有障碍,左转 speed_left = 30; speed_right = 80; } else { speed_left = 70; speed_right = 70; } }值得说明的是,这种局部避障不做路径规划,只做“遇障绕行”,适合51单片机这类资源有限的平台。在ROS小车或STM32小车上,同样的逻辑会被行为树或状态机取代,但“先判定空间、再分配差速”的思路是相通的。
4.5 双功能融合:避障优先还是循迹优先
双功能控制的核心是确定优先级,我认为合理的策略是避障优先。在障碍物密集区域,循迹功能的意义不大,小车首先要保证不撞;离开障碍区后再回到黑线上继续循迹。主循环的结构很简洁:
while (1) { if (mode == 0) { track_control(); } else if (mode == 1) { avoid_control(); } else { if (avoid_left_check() || avoid_right_check()) avoid_control(); else track_control(); } }mode的值由P3.7在初始化时读取,建议在init函数里用一句mode = SWITCH_PIN;读取,并且不要在运行中频繁切换,因为拨码开关在车辆震动时可能跳动,如果确实需要动态切换,务必加上50ms的软件防抖。这个结构与动态避障小车路径规划中常见的“全局规划器+局部避障器”分层一致,只不过在51单片机上,全局规划被简化为一条固定的黑线,局部避障被简化为一对红外传感器。
5. 调头、地线与阈值:三个必须现场调试的细节
5.1 先调阈值再查代码
代码写得再干净,传感器阈值不对,整台车就是个随机运动器。现场标定的标准动作是:把小车分别放在白色地面和黑线上,各测一次传感器输出的电平状态,确认两者的逻辑差异是1和0而不是都是1或都是0。如果循迹模块的输出逻辑和预期相反,最简单的办法是在代码里给每个引脚定义一个反相宏,比如#define TRACK_LEFT_ACTIVE (TRACK_LEFT == 0),而不是去改硬件。
电位器调好后,用螺丝刀在电位器上点一点热熔胶固定,防止车辆震动导致漂移。这一步看起来土,但在课设答辩现场能避免大量尴尬。
5.2 线宽与传感器间距的匹配
循迹稳定性与黑线宽度、三个传感器的间距强相关。用标准1.8cm宽的电工胶带铺线时,传感器间距需要满足一个关系:三路探头中至少保证中间探头能落在黑线内,同时左右探头在线外。间距太宽,小车在直线上也会左右摇摆;间距太窄,过弯时传感器容易同时丢线。
| 黑线宽度 | 传感器间距范围 | 铺线建议 |
|---|---|---|
| 1.5cm ~ 2.0cm | 1.0cm ~ 1.5cm | 使用标准电工胶带 |
| 2.0cm ~ 3.0cm | 1.5cm ~ 2.2cm | 转弯半径需大于15cm |
| 3.0cm以上 | 2.5cm以下即可 | 允许更激进的速度 |
用记号笔画的线反光特性不稳定,最好统一用哑光黑色电工胶带铺设,否则白纸和黑线的反射差异会让阈值标定失效。
5.3 验证调头动作与PWM不对称
调头动作看起来是左右轮反转,但因为两个电机的机械阻尼和减速比不会完全一致,同一个占空比下实际转速可能差20%。验证方法很简单:在代码里固定speed_left和speed_right为相同值,让小车沿直线跑1米,如果跑偏,就往慢的那一侧增加占空比偏移量。把这个偏移量定义成一个全局变量pwm_offset,在左右轮赋值处分别应用:
void set_speed(unsigned char left, unsigned char right) { speed_left = left + pwm_offset; // 补偿慢轮 speed_right = right - pwm_offset; }同样地,原地调头时左右轮目标方向相反,转向半径更小,但补偿逻辑依然适用。现场用慢动作视频记录小车原地转一圈的时间,微调补偿量,直到车头转角与设定值一致。最后把补偿值写死到初始化里,调头逻辑才算真正可靠。
本文还有配套的精品资源,点击获取