简介:一份基于TCRT5000红外反射传感器与STC89C52单片机的循迹小车课程设计代码,适合单片机初学者、嵌入式课程设计及机器人实训。工程包含完整硬件驱动、红外数据采集、电机脉宽调制控制、循迹判断与比例积分微分控制参数整定思路,并给出多个测试迭代版本。压缩包共148个文件,约189KB,主要含有C语言源程序、头文件、Keil工程文件、烧录文件,以及编译生成的目标文件、列表文件和中间文件,有利于查看编译过程与工程结构。已有1082人学习下载。通过对比不同版本的调试过程,读者可掌握循迹阈值设定、电机响应速度调整和路面适应性测试等排错方法,是实践单片机与传感器联动开发的实用参考资料,模块化结构便于按功能拆解学习,也便于课程设计答辩与二次开发。
1. 为什么是TCRT5000和STC89C52的组合
做循迹小车最怕的不是代码写不出来,而是传感器信号不稳定、电机响应跟不上,最后车在地上画龙。TCRT5000红外反射传感器加STC89C52这个组合,是很多入门项目从原理图到实物的首选路径,原因很简单:TCRT5000模块自带比较器输出数字电平,不需要ADC换算,单片机只需要读高低电平就行;STC89C52又是经典的51内核,IO口直接接模块输出,逻辑上几乎没有中间层。这篇文章把这套组合从接线到循迹逻辑、再到调试和避坑完整拆开,照着步骤能把车调成沿黑线稳定走直线、走弧线。适合刚做完流水灯、想第一次让硬件动起来的开发者,也适合在做课程设计时需要快速交付一套能演示的实物。
2. 硬件接线与检测原理:从反射光到高低电平
2.1 TCRT5000模块为什么输出的是数字信号
很多第一次接触TCRT5000的人会误以为它是模拟传感器,需要接ADC引脚。实际上市面上常见的TCRT5000模块(小板子上带电位器和LM393比较器的那种)已经把反射光的强度转换成开关量了。模块正面有一颗红外发射管和一颗接收管,发射管持续发出红外光,接收管根据反射回来的光强产生不同大小的电流,这个电流经过比较器与电位器设定的阈值比较之后,输出端OUT直接给出高电平或者低电平。
这个设计的好处是STC89C52完全不用关心反射光的具体强度值,只需要在引脚上读到0或者1。黑线吸收红外光,反射回来的光弱,输出高电平;白色地面反射强,输出低电平。这个逻辑在不同模块之间可能相反,取决于模块上OUT引脚是否有反相处理,所以代码里一般先做一次实测来确定哪边是黑线。
2.2 STC89C52的IO规划与最小系统接线
STC89C52是5V供电的芯片,TCRT5000模块也支持5V供电,所以两者可以直接共用一个电源轨,不需要电平转换电路。常见做法是把模块的VCC接在单片机的5V上,GND共地,OUT引脚接在单片机任意一个普通IO口上。由于STC89C52的P0口是开漏输出,内部没有上拉电阻,读取外部信号时容易出现不确定电平,所以我一般把传感器接在P1口或者P2口,这两个口内部带上拉,省去外接电阻的麻烦。
电机驱动部分我用的是L298N模块,IN1到IN4接单片机IO口,ENA和ENB接PWM信号用来调速。如果只是想先让车跑起来不调速,ENA和ENB可以直接接5V让电机全速转,但这个做法会让循迹效果很难看,因为全速情况下小车过弯时几乎没有修正时间。实际项目里我习惯用定时器0产生PWM,频率选100Hz左右,占空比先给一个60%的基础值,再根据传感器状态动态调整左右轮速度。
注意:STC89C52的P0口如果必须使用,记得外接10k欧上拉排阻,否则高电平状态不稳定,传感器信号会被误读。
2.3 L298N的双电源接法:逻辑电源和电机电源必须共地
L298N模块上通常有两个电源输入端子,一个是电机电源(VS,常见接7-12V),另一个是逻辑电源(VSS,5V)。很多人在这里翻车:只给VS接上电池,VSS悬空,结果模块指示灯不亮,单片机怎么控制电机都不转。原因在于L298N内部的逻辑电路需要独立的5V供电,不能从VS稳压得到。
正确接法是电机电源接在VS和GND之间,逻辑电源接到VSS和GND之间,并且把L298N的GND和STC89C52的GND、TCRT5000的GND全部连在一起。这个共地是必须的,因为单片机输出到L298N IN引脚的信号是相对于单片机GND的电平,如果两个地不连,逻辑电平就没有参考点,模块收到的信号是悬浮的,电机自然不动。
我一般还会在电机电源端并联一个470uF电解电容,靠近L298N摆放。电机启动瞬间电流很大,会造成电源电压跌落,严重时单片机会复位,表现为车一动就重新从头执行程序。电容起的是储能缓冲作用,能吃掉一部分瞬态压降。
3. 循迹逻辑与代码实现:从读引脚到差速转向
3.1 双传感器的基础状态表
最简单的循迹方案是左右各放一个TCRT5000,中间隔一段距离(一般是两倍黑线宽度左右)。传感器检测黑线时输出高电平(或者低电平,先统一约定为检测到黑线为1),那么两个传感器的状态组合有四种:
- 左0右0:两个都在白地上,说明车在线的正中间或者已经跑出线外
- 左1右0:左侧压线,车向右偏,需要向左修正
- 左0右1:右侧压线,车向左偏,需要向右修正
- 左1右1:两个都压线,说明线很宽或者车正压在直线上
处理逻辑是:状态1和状态2做转向修正,状态0时维持之前的方向直行,状态4时直行。状态0特意不做急停是因为在跑直线时传感器偶尔会出现短暂离开黑线的情况,如果在那一瞬间急停或猛打方向,车反而会冲出赛道。
3.2 完整的循迹主循环代码
下面是我在模拟项目X里用过的代码框架,用Keil C51编写,目标芯片是STC89C52,晶振11.0592MHz。这个代码结构很简单,核心是一个while死循环加状态判断,配合PWM调速逻辑。
#include <reg52.h> // 定义传感器引脚 sbit sensorLeft = P1^0; // 左侧TCRT5000输出 sbit sensorRight = P1^1; // 右侧TCRT5000输出 // 定义L298N控制引脚 sbit IN1 = P2^0; // 左电机正转 sbit IN2 = P2^1; // 左电机反转 sbit IN3 = P2^2; // 右电机正转 sbit IN4 = P2^3; // 右电机反转 sbit ENA = P2^4; // 左电机PWM sbit ENB = P2^5; // 右电机PWM // PWM占空比参数 unsigned char leftSpeed = 65; // 左电机基础占空比 unsigned char rightSpeed = 65; // 右电机基础占空比 unsigned char leftTurnSpeed = 0; // 左转时左轮降速 unsigned char rightTurnSpeed = 0; // 右转时右轮降速 // 定时器0中断标志,每0.1ms置1 bit pwmFlag = 0; // 简单延时函数,11.0592MHz下大约延时x毫秒 void delay_ms(unsigned int x) { unsigned int i, j; for (i = 0; i < x; i++) for (j = 0; j < 115; j++); } // 初始化定时器0,用于生成PWM void timer0_init() { TMOD = 0x01; // 定时器0工作在模式1,16位定时 TH0 = 0xFF; // 初值设置,配合下面TL0得到约0.1ms中断周期 TL0 = 0xF0; ET0 = 1; // 使能定时器0中断 TR0 = 1; // 启动定时器0 } // 定时器0中断服务函数 void timer0_isr() interrupt 1 { TH0 = 0xFF; TL0 = 0xF0; pwmFlag = 1; // 标记一个PWM周期开始 } // 主函数 void main() { timer0_init(); // 初始状态:两个电机都不转 IN1 = 0; IN2 = 0; IN3 = 0; IN4 = 0; ENA = 0; ENB = 0; while (1) { // 读取传感器状态,1表示检测到黑线 unsigned char left = sensorLeft ? 1 : 0; unsigned char right = sensorRight ? 1 : 0; // 左右都压线或者都在白地:直行 if ((left == 1 && right == 1) || (left == 0 && right == 0)) { IN1 = 1; IN2 = 0; // 左电机正转 IN3 = 1; IN4 = 0; // 右电机正转 leftSpeed = 65; rightSpeed = 65; } // 左侧压线,车偏右,左轮减速 else if (left == 1 && right == 0) { IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; leftSpeed = 30; rightSpeed = 65; } // 右侧压线,车偏左,右轮减速 else if (left == 0 && right == 1) { IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; leftSpeed = 65; rightSpeed = 30; } // PWM输出控制 if (pwmFlag) { pwmFlag = 0; // 根据占空比输出PWM ENA = (leftSpeed > 0) ? 1 : 0; ENB = (rightSpeed > 0) ? 1 : 0; // 这里简化为固定占空比,更精确的做法是软件定时改变引脚电平 } delay_ms(10); // 主循环间隔,防止判断过于频繁震荡 } }这段代码的逻辑说明:PWM部分我用了一个简单的思路——每次定时器中断把pwmFlag置1,主循环里检测到这个标志后把ENA和ENB拉高,然后延时一段时间再拉低。延时时间长短由leftSpeed和rightSpeed的值决定,值越大高电平持续越久,电机平均电压越高,转速越快。这个做法牺牲了PWM精度,但胜在代码简单、容易理解。
参数说明里有个关键点:65这个占空比对应的是全速的65%左右,实际测试下来,这个值能让小车在普通桌面速度合适,太快了转弯时容易甩尾,太慢了过弯会卡在弯道里。30这个转向值不能设得太低,我试过10,结果转弯侧电机因为静摩擦力太大直接停住,车反而在原地转成了圆弧,完全偏离黑线。
3.3 12T模式下的延时误差
STC89C52默认是12T模式,也就是12个时钟周期执行一条指令,11.0592MHz晶振下机器周期约1.085微秒。上面代码里delay_ms里的115这个数字是按照这个速度估算的,实际误差大约在5%以内,用在循迹这样的慢速控制场景完全没问题。
但如果把STC89C52配置成6T模式(通过STC-ISP软件烧录时选择),机器周期变为原来的一半,同样的延时函数实际延时只有原来的一半左右。这时候如果传感器读取频率变高,遇到干扰信号时小车会出现无规律的抖动。解决方法是延时函数里的循环次数翻倍,或者干脆只使用默认的12T模式,少引入一个变量。
4. 循迹小车常见问题排查:五个典型翻车场景
4.1 现象:小车跑起来直接冲出去,不沿黑线走
这个现象最常见的原因是传感器安装高度不对。TCRT5000的检测距离只有0.5cm到1.5cm,模块离地面太低会一直反射,输出电平不变化;太高则接受管接收不到反射光,输出恒为高电平或者恒为低电平。
解决:把模块固定在小车底板前端,让发射管到地面的距离控制在1cm左右。调整的时候用一个螺丝刀拧模块上电位器,边拧边观察指示灯。在白纸上放一段黑胶带,传感器在黑白交界处来回移动,指示灯状态能变化就是调好了。指示灯亮说明输出低电平,灭说明输出高电平,具体哪个对应黑线看模块说明,一般黑线是亮、白地是灭。
4.2 现象:传感器放在黑线上指示灯亮,但单片机的引脚读不到变化
用万用表量一下模块OUT引脚和单片机引脚之间的电压。如果模块OUT是高电平但单片机引脚读0,说明两个参考地不在一起。TCRT5000模块的GND和单片机GND之间有电压差,可能是因为用了两套电源给它们供电。
解决:统一用同一块电池经过稳压芯片给单片机、传感器供电,把L298N的GND、单片机GND、传感器GND用一根短导线连在一起。不要用一个USB电源给单片机、另一个电池给电机驱动,这种多电源方案最容易出共地问题。
4.3 现象:小车能转弯,但每次过弯都会压线冲出
这说明转向修正太慢或者修正量不够。常见原因是主循环里delay_ms(10)这个延时太长,传感器状态变化后电机要等10毫秒才响应,车速快的时候这段时间已经冲出弯道了。
解决:把主循环里的延时去掉,改为用定时器做固定频率的控制周期。比如每5ms执行一次传感器读取和PWM更新,中断里不做复杂的逻辑判断,只做状态采样,这样响应速度能提高了。另外确认转弯时的差速足够大,我用的是65和30,如果弯道尖锐,可以把转弯侧降到20,代价是过弯后回直线的速度变慢。
4.4 现象:电机能转,但PWM调速没反应
ENA和ENB引脚如果直接接5V,L298N会全速输出,PWM信号根本不起作用。查一下这两个引脚是接到了单片机的IO口还是直接接到了正电源。另外L298N的ENA和ENB是使能引脚,不是速度引脚。需要调速时,每路电机需要三个信号:两个方向信号(IN1/IN2)加一个PWM速度信号(ENA)。IN1和IN2控制输出方向,ENA接受PWM控制平均输出电压。
解决:确认ENA和ENB连接到单片机IO口,且这两个引脚在代码里是输出模式。然后在主循环里用万用表量ENB引脚对地电压,改变占空比时电压平均值应该跟着变。如果电压始终是5V或者0V,查一下代码是否真的在循环里执行到PWM赋值那句。
4.5 现象:小车走完一圈后,第二次循迹起始位置偏了
这个属于机械问题多于电路问题。电机左右轮实际转速不完全一致,即使PWM占空比相同,因为电机生产批次差异,左右轮的实际线速度也有微小区别。长时间跑下来,偏差累积,小车轨迹整体往一边偏。
解决:在保持直行状态时,微调左右轮占空比差值。比如右轮比左轮快,就把rightSpeed调小一些,leftSpeed调大一些,让两个轮子在直线上跑出相同的线速度。我是用硬纸板搭了一条1米长的直线轨道,让小车反复跑,每次只调一个单位,直到跑完直线轮胎印不偏为止。
5. 系统调试方法:没有示波器也能看清传感器状态
5.1 用LED指示灯观察传感器响应
TCRT5000模块板载了两个LED,一个电源指示,一个输出指示。输出指示灯的亮灭直接反映OUT引脚电平,这是最直观的调试手段。我实际调试过程中发现,观察这个指示灯比用万用表量电压更高效,因为在小车高速运动的时候,人眼能通过灯的闪烁频率大致判断传感器响应速度。
如果发现传感器一直亮或者一直灭,先检查电位器是否旋到位了。电位器不是随便拧的,它决定比较器的阈值电压。把模块放在白纸上,拧电位器直到指示灯刚好处于临界状态(亮灭边界),然后放到黑线上看看指示灯是否翻转。这个临界点就是灵敏度的最优位置,既不容易被干扰误触发,又能可靠检测黑线。
5.2 用串口打印状态量
STC89C52自带UART模块,通过P3.0和P3.1可以输出调试信息。在循迹代码里加一段串口输出函数,把两个传感器的状态和当前的PWM占空比打印到电脑上,这样就能从时间序列看到小车的控制逻辑是否正确执行。
#include <stdio.h> // 串口初始化函数,4800波特率,11.0592MHz晶振 void uart_init() { SCON = 0x50; // 模式1,8位UART,允许接收 TMOD |= 0x20; // 定时器1工作在模式2,8位自动重装 TH1 = 0xFA; // 4800波特率对应的初值 TL1 = 0xFA; TR1 = 1; // 启动定时器1 } // 串口发送一个字节 void uart_send_char(unsigned char c) { SBUF = c; while (!TI); TI = 0; } // 串口发送字符串 void uart_send_string(unsigned char *str) { while (*str) { uart_send_char(*str++); } }这段代码放在主循环里,每次读取传感器后把状态通过串口发出去。用串口助手工具在电脑上以4800波特率接收,能看到类似"L1 R0 Speed:65 30"的字符串,代表左侧压线、右侧正常、左轮占空比65、右轮占空比30。这个信息在做转弯算法调试时特别有用,能确认代码逻辑分支是否正确进入,而不是靠肉眼猜。
注意:STC89C52的串口输出是TTL电平,不能直接接电脑的RS232串口或者USB转串口线,需要一个MAX232电平转换芯片或者USB转TTL模块,否则电脑收不到数据。
5.3 分步调试:把运动控制拆开测
循迹小车出问题时,不要直接跑完整流程,否则很难判断是传感器问题、控制逻辑问题还是电机驱动问题。我习惯分成三步来测:第一步只测电机,写一个最简单的代码让左右轮分别转和停,确认方向信号和PWM信号都正确;第二步固定传感器和电机,用手拿着小车在黑线上左右移动,观察代码是否正确切换了转向状态;第三步才把小车放地上跑完整循迹。
这种做法能大幅减少排查范围。如果第一步电机就不转,问题在L298N接线;如果第二步跟踪电机正常但地上不走线,问题在传感器灵敏度;如果地上跑起来偶尔失灵,才需要担心电源干扰和延时问题。
6. 进阶用法:从双路传感器升级到三路循迹
双路传感器在直线和大角度弯道上表现不错,但在遇到十字交叉线、锐角弯时经常会判断失误。我的做法是加一个居中的传感器,让它负责检测正前方的黑线,左右两个传感器负责检测偏离方向。对应的逻辑变成了:中间传感器压线时直行,左侧传感器压线时左转,右侧传感器压线时右转,左右同时压线时停车(因为可能遇到了终点线)。
三路传感器代码的改动其实不大:
sbit sensorCenter = P1^2; // 新增的中间传感器 // 三路循迹判断逻辑 if (sensorCenter) { // 中间压线,直行 IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; leftSpeed = 65; rightSpeed = 65; } else if (sensorLeft) { // 左压线,左轮降速右转 IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; leftSpeed = 30; rightSpeed = 65; } else if (sensorRight) { // 右压线,右轮降速左转 IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; leftSpeed = 65; rightSpeed = 30; } else { // 所有传感器都在白地上,直行,按上一次状态走 IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0; }这段逻辑比双路版本直观很多,它的核心变化是默认状态不再是左右轮等速,而是延用上一次直行状态。这就避免了在检测间隙里左右轮突然断电导致的车身抖动。
调三路时要注意传感器间距,间距太小会导致三个传感器同时压线,逻辑上会优先响应中间传感器,结果就是车压着线一路直行,根本不管弯道。我一般把三个模块间隔固定为黑线宽度的三分之一,然后用热熔胶固定在小车底盘上,实测在2cm宽的黑线上效果最好,过锐角弯时中间传感器先出线,左右传感器随即接替判断。
从那以后我每次写完循迹代码都会强制走一遍硬件检查表:先量共地,再调传感器阈值,然后单轮测试,最后才是整车路试,能省下至少一小时的无意义排错。希望帮到你上手这套TCRT5000加STC89C52的循迹方案。
本文还有配套的精品资源,点击获取