☰
TCRT5000与STC89C52循迹小车实战:从传感器原理到PWM调速
2026/10/13 17:34:54 网站建设 项目流程

简介:面向TCRT5000红外反射传感器与STC89C52单片机的简单循迹小车课设,这份工程代码包提供了完整的实现方案,TCRT5000通过检测地面反射率分辨黑白路径,STC89C52作为主控完成数据处理与电机驱动,整套方案非常适合嵌入式及自动化方向的本科生课设参考。压缩包共收录148个文件,整体大小仅189KB,包含8个C语言源程序与8个头文件、4个Keil工程配置(uvproj/uvopt)、36个obj与lst编译中间文件,以及hex烧录文件、m51内存映射文件等,完整覆盖从源码到编译烧录的各个环节。目前已有1082人浏览学习,是轻量但完整的入门级案例。代码内含多次迭代调试痕迹(try1、try2、try3等),逐步优化循迹阈值与电机响应,覆盖传感器数据读取、电机PWM控制、基本循迹逻辑等模块。下载后可直接在Keil中打开工程,结合hex文件快速烧录到实际硬件,对理解MCS-51单片机中断、定时器以及红外传感器应用有不错的实践价值。

1. 用TCRT5000和STC89C52做循迹小车,这篇讲的就是怎么把车跑起来

很多第一次做循迹小车的开发者,往往把时间耗在“为什么传感器输出不对”和“为什么电机一启动单片机就重启”这两件事上。用TCRT5000传感器配合STC89C52写循迹小车代码,是入门玩家最常选的一条路:传感器模块便宜、接线简单、输出是现成的数字电平,单片机又是课堂和比赛里最普及的51内核芯片。这套组合能解决的诉求很直接:让小车沿着地面黑线走,不飘、不冲、不原地打转,同时让代码量控制在一个人半天能看懂的范围。这篇笔记面向手里已经有一块STC89C52开发板、想从硬件拼接到代码跑通一次完整闭环的开发者,也适合准备做课程设计或校内小车比赛的A同学。我尽量把选型理由、接线、代码结构、参数调整和最容易翻车的地方一次说清楚。

2. TCRT5000与STC89C52的选型逻辑:先搞清楚传感器到底在检测什么

2.1 TCRT5000的工作原理:它测的是反射率,不是距离

TCRT5000是一体式反射型光电传感器,一个塑料壳里封装了红外发射管和光敏晶体管。发射管通电后持续发出940nm左右的红外光,光敏晶体管接收被物体反射回来的红外线。注意,它检测的是“反射光强度”,不是距离。循迹场景的地面通常是浅色地板或者白纸,黑线则是黑色胶带或黑色喷漆。黑色表面吸收红外光,反射量很小;白色表面反射量大。传感器模块上自带一个比较器,通常是LM393,把光敏晶体管的模拟电压和电位器设定的阈值电压比较,输出数字高电平或低电平。所以你在单片机IO口读到的不是灰度值,而是一个已经判决好的“黑/白”信号。这个特征决定了后续代码只能做逻辑判断,不能做比例控制,除非你买的是模拟输出版本。

搞清楚这一点能省下很多调试时间。某开发者在第一次做的时候,以为TCRT5000测的是距离,把传感器抬到离地面3cm,结果输出信号完全乱跳,调电位器也没用。后来把传感器降到1cm左右,信号立刻稳定。TCRT5000的典型反射检测距离在0.5到1.5cm之间,超过2cm反射光强度衰减非常厉害,比较器翻转点变得极不稳定。安装高度是第一个要检查的物理参数,比代码里的阈值判断更优先。

2.2 模块输出逻辑要实测:黑线输出高还是低,各家的模块不一样

市面上绝大多数TCRT5000模块都带电位器和LM393比较器,但输出逻辑并非统一。有的模块设计成“检测到黑线输出高电平”,有的输出低电平。这取决于比较器的同相/反相输入端接的是光敏晶体管的集电极还是发射极。很多刚上手的同学照着网上的代码抄,结果车子的行为完全反了:压到黑线该转弯反而直冲。一个非常高效的解决方案是:上电后把传感器压在白纸上,用万用表量一下OUT引脚电平,再压到黑线上量一次,记录两个电平状态。这个状态决定了你的if判断怎么写。

我一般习惯用“检测到黑线为高电平”的模块,因为逻辑更直观。但如果你手里的模块是反的,不需要改硬件,只需要把代码里的判断条件对调。值得注意的是,模块上的电位器同时影响这个逻辑的稳定边界,旋得太浅会导致黑白都输出高电平,旋得太深会导致黑白都输出低电平。电位器旋到中间偏某一侧,让“白纸时输出稳定低电平、黑线时输出稳定高电平”为止。这是TCRT5000调试里唯一需要动手调的硬件参数,搞定了它,后面代码基本不用为传感器发愁。

2.3 STC89C52最小系统的资源分配与烧录要点

STC89C52是增强型8051内核,指令集和AT89C52完全兼容,但有一堆细节踩过才知道。首先是P0口,作为IO口使用时内部没有上拉电阻,必须外部接10k排阻上拉,否则P0输出高电平的时候带不动负载,L298N这类驱动芯片的逻辑输入可能识别不到高电平。第二是晶振,很多STC89C52开发板出厂默认用内部RC振荡,但内部振荡频率受温度影响大,做串口下载和定时器精确定时最好外接11.0592MHz晶振。第三是烧录方式,STC系列用串口下载,需要冷启动:点击下载后再给单片机通电,每次烧录前都要断电重来一次。

循迹小车这个项目对IO资源的占用很小。典型分配方案:4路TCRT5000模块的数字输出接P1.0到P1.3,两路电机方向控制信号各占2个IO,两路PWM信号占2个IO,总共消耗10个IO口。P2口完全够用。定时器T0用来产生1kHz的PWM中断,T1可以留着做测速或者串口调试。整个系统不需要外部RAM,不需要AD转换,STC89C52的8KB Flash跑这个程序只用了不到四分之一。选这款芯片不是因为性能强,而是因为配套资料多、烧录工具简单、5V供电直接匹配传感器模块和电机驱动。

3. 电机驱动与定时器模拟PWM:用中断凑出来的调速能力

3.1 电机驱动的选择:L298N和TB6612的控制信号完全兼容

循迹小车用的减速直流电机,两边各一个,型号一般是TT马达加金属减速箱。电机不能直接挂单片机IO口,需要驱动芯片。常见方案有两种:L298N和TB6612。L298N便宜、皮实,但内部是三级管压降,满电时发热明显,逻辑输入高电平门槛较高,需要确保单片机输出确实是5V。TB6612用MOSFET,压降小、体积小、支持更高的PWM频率,但价格贵一点。这两种驱动芯片在代码层面的控制逻辑完全一致:每个电机有2个方向IO和1个使能/PWM IO。L298N上是IN1/IN2加ENA/ENB,TB6612上是AIN1/AIN2加PWMA。因此下面代码里的电机控制函数可以无缝切换。

方向控制逻辑很简单:左电机IN1=1、IN2=0时正转,反过来就是反转,IN1=IN2时刹车。PWM引脚输入方波,调节占空比就是调节平均电压,也就是转速。STC89C52没有硬件PWM模块,这是选型时一个常被忽略的坑。同系列稍晚的STC12和STC15带硬件PWM,而STC89C52根本没有这个外设。解决方案是拿定时器T0做软件PWM,每次中断翻转或者比较输出,频率做到1kHz时对电机运行影响很小,噪音也听不太出来。

3.2 定时器T0模拟PWM:初值计算与中断开销

51单片机的定时器T0在模式1下是16位计数器,从初值加到65536溢出触发中断。要让中断频率为1kHz,先算机器周期:外接11.0592MHz晶振,12分频后机器周期约1.085us,1ms需要约922个计数。初值就是65536减922,等于64614,换算十六进制是0xFC66。所以初始化时写入TH0=0xFC; TL0=0x66。如果你用的是12MHz晶振,1ms需要1000个计数,初值是64536,十六进制0xFC18。这两个数字不要记混,很多人把晶振搞错导致PWM频率偏了,电机声音不对劲。

软件PWM的中断服务函数里只做一件事:维护一个计数器pwm_pulse,从0加到周期值100,每一轮里把当前计数和两个占空比变量left_duty、right_duty比较,决定ENA和ENB引脚输出高还是低。中断里不要做传感器读取和循迹判断,否则中断占用时间过长,主循环会被大量打断。传感器读取放主循环,50到100ms采样一次就够了。

3.3 电机控制层代码:方向与调速分离

把电机控制封装成函数,是让循迹主逻辑保持清爽的关键。以下是一份最小但完整的电机控制代码,直接复制到工程里就能用。

#include <reg52.h> // 电机驱动引脚定义:L298N 或 TB6612 通用 sbit IN1 = P2^0; // 左电机方向1 sbit IN2 = P2^1; // 左电机方向2 sbit IN3 = P2^2; // 右电机方向1 sbit IN4 = P2^3; // 右电机方向2 sbit ENA = P2^4; // 左电机 PWM sbit ENB = P2^5; // 右电机 PWM // PWM 全局变量,主循环里直接改这两个值就能调速 unsigned char left_duty = 0; unsigned char right_duty = 0; unsigned char pwm_pulse = 0; const unsigned char pwm_period = 100; // 定时器T0初始化:11.0592MHz 晶振,1ms 中断 void timer0_init(void) { TMOD = 0x01; // T0 模式1,16位定时器 TH0 = 0xFC; TL0 = 0x66; // 1ms 初值 ET0 = 1; // 开 T0 中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器 } // T0 中断服务函数:软件 PWM 输出 void timer0_isr(void) interrupt 1 { TH0 = 0xFC; TL0 = 0x66; // 重装初值 pwm_pulse++; if (pwm_pulse >= pwm_period) { pwm_pulse = 0; } if (pwm_pulse < left_duty) { ENA = 1; } else { ENA = 0; } if (pwm_pulse < right_duty) { ENB = 1; } else { ENB = 0; } } // 左电机方向控制:1正转,-1反转,0刹车 void left_motor(signed char dir) { if (dir > 0) { IN1 = 1; IN2 = 0; } else if (dir < 0) { IN1 = 0; IN2 = 1; } else { IN1 = 0; IN2 = 0; } } void right_motor(signed char dir) { if (dir > 0) { IN3 = 1; IN4 = 0; } else if (dir < 0) { IN3 = 0; IN4 = 1; } else { IN3 = 0; IN4 = 0; } }

这段代码的逻辑说明:left_duty和right_duty是0到100之间的占空比,也就是速度百分比。主循环里只要给这两个变量赋值,电机速度就会自动调整。方向函数独立于PWM,好处是转向逻辑可以同时设置方向和速度。比如右转时,左轮正转、右轮正转但占空比减小,车头就会偏右。参数上需要注意pwm_period设成100,意味着中断100次完成一个PWM周期,也就是100ms一个周期,这个数值对应的是PWM频率10Hz,会让电机有明显的一顿一顿感。实际的pwm_period建议设成10或20,让PWM频率更高。这里为了演示计数逻辑写了100,读者自行改为10即可,中断重装初值不变。

更正后的推荐参数是pwm_period取10,中断1ms触发一次,PWM频率100Hz,电机运行平滑无噪音。上述代码里把const unsigned char pwm_period = 100改成10就行了。占空比精度从1%降到10%,对循迹小车来说完全够用。

4. 循迹算法与完整代码:至少三路传感器,别用两路凑合

4.1 传感器状态读取:位操作与编码

循迹算法的输入是4路传感器的状态组合,先要有一个干净的读取函数。把传感器接在P1.0到P1.3,每个模块OUT引脚直接连单片机IO。TCrt5000模块输出的是TTL电平,不需要分压或上拉,但要注意模块和单片机必须共地。读取方式有两种:直接读P1整个端口再屏蔽高位,或者用位变量分别读。位变量方式每条语句多了几个字符,但可读性好很多。

sbit S1 = P1^0; // 最左侧传感器 sbit S2 = P1^1; sbit S3 = P1^2; sbit S4 = P1^3; // 最右侧传感器 // 返回4位状态:bit0对应S1,bit3对应S4,1表示检测到黑线 unsigned char read_sensor(void) { unsigned char val = 0; if (S1) val |= 0x01; if (S2) val |= 0x02; if (S3) val |= 0x04; if (S4) val |= 0x08; return val; }

这段代码的逻辑说明:把每个位变量收敛到一个无符号字符变量的bit上,后续判断就可以用十六进制比较。代价是几行if语句,但换来的是主循环里switch或if判断的简洁。如果你的模块是“检测到黑线输出低电平”,只需要把if(S1)改成if(!S1),其余三个同理,逻辑立刻反转。这一点已经在上一章强调过,这里再确认一次。

4.2 循迹策略对比:两路会甩尾,三路能直行,四路才谈得上弯道预判

网上很多简易教程只接两路传感器:一路偏左、一路偏右。两路的逻辑很简单,压到左线就右转,压到右线就左转,两边都没压到就直行。但实际跑起来你会发现车在直线段走出明显的Z字形,速度稍快就甩出赛道。原因在于两路传感器只能给出“偏了”的二值信息,给不出“偏了多少”,更给不出“是不是在赛道正中间”,控制系统没有直行的稳态判据。

三路传感器是性价比最高的入门配置:中间一路管直行,左右两路管纠偏。当只有中间传感器压线时,默认车在正中间,直行。左右某一路压线时,说明车偏向对应方向,做出转向调整。四路传感器则进一步支持更复杂的弯道识别,比如直角弯前,最外侧传感器会先于中间传感器压线,程序可以提前加大转向角度。我强烈建议直接上四路,TCRT5000模块单价很低,多接两路带来的价值远超成本。四路布局顺带解决了传感器间距太宽漏线的问题,两路传感器间距如果超过1.5cm,过细的胶带线很容易从中间漏过去,导致全白状态。

4.3 完整循迹代码:状态判断加转向决策

下面的代码是完整可用的循迹主程序,基于前面的电机控制层和传感器读取函数,把循迹决策写在主循环里。

// 循迹参数:默认速度和转向增量 #define BASE_SPEED 40 // 直行占空比 #define TURN_SPEED 25 // 转向时内外轮差 void main(void) { unsigned char state; timer0_init(); left_motor(1); right_motor(1); left_duty = 0; right_duty = 0; while (1) { state = read_sensor(); // 中线和左右近距离:小幅纠偏 if ((state & 0x02) && !(state & 0x04)) { // S2压线,车偏右,左轮加速、右轮减速 left_duty = BASE_SPEED + TURN_SPEED; right_duty = BASE_SPEED - TURN_SPEED; } else if ((state & 0x04) && !(state & 0x02)) { // S3压线,车偏左,右轮加速、左轮减速 left_duty = BASE_SPEED - TURN_SPEED; right_duty = BASE_SPEED + TURN_SPEED; } // 最外侧压线:大幅转向,应对急弯 else if (state & 0x01) { left_duty = TURN_SPEED + 10; right_duty = BASE_SPEED; } else if (state & 0x08) { left_duty = BASE_SPEED; right_duty = TURN_SPEED + 10; } // 全白:丢线处理,保持上一步动作,先不急着停车 else { left_duty = BASE_SPEED; right_duty = BASE_SPEED; } // 简单延时控制采样周期 delay_ms(20); } }

这段代码的逻辑说明:状态判断是典型的优先匹配,先看中间两路是否只有单边压线,再看最外侧是否压线,最后才是全白的兜底。转向幅度通过TURN_SPEED控制,50的直行速度和25的轮速差,在实际场地测试时起步较稳,不会一上电就窜出去。一个常见的问题是,全白状态处理得太简单:如果车已经冲出赛道,四个传感器全白,此时直行只会让车越跑越偏。更好的做法是记忆最后一次非全白状态的转向方向,然后向该方向持续小幅转向,直到重新捕获黑线。这个丢线恢复逻辑放到第6章进阶里展开。

代码里还缺一个delay_ms函数,STC89C52没有内置延时,简单的做法是用两层for循环,20ms在12MHz或11.0592MHz晶振下的循环次数大约为两万多次,但由于C51编译器优化差异,最好用示波器实测或者用定时器做时间基准。更推荐的做法是用T0中断里做计数器,每1ms加1,主循环里等待计数到20再清零,这样延时绝对精确。

5. TCRT5000循迹常见问题排查:供电、阈值、逻辑反转与上电时序

5.1 传感器阈值跳变导致小车蛇形走位

现象:小车在直线段不断左右摆动,速度稍微加快就冲出跑道。用万用表量传感器输出,发现在黑白交界处电平频繁跳变。原因:模块上电位器阈值调整不当,同时环境光(尤其是阳光或节能灯)干扰了光敏晶体管的输出。解决:先看电位器方向,大多数模块逆时针旋转降低阈值,顺时针提高阈值。调整时让传感器固定在地面上方1cm处,白纸输出低电平、黑线输出高电平,并且用手在传感器上方晃动时输出不翻转。软件上还可以加滞回判断:连续两次采样到黑状态才认为压线,连续两次采到白状态才认为脱离黑线,这能过滤大部分快速抖动。

5.2 电机启动瞬间单片机复位

现象:一开机或者电机突然加速时,LED灯闪一下、程序从头跑,有时候干脆白屏。原因:电机启动电流能到几百毫安甚至更大,电池或电源适配器内阻大,电压跌落超过单片机的复位阈值。这是TCRT5000和STC89C52小车上最常见的供电问题。解决:电机电源和逻辑电源分开走线,电机电源直接接驱动板电源端子,驱动板的5V输出不要给单片机供电。单片机单独用一块AMS1117-5.0或者7805稳压,电源输入端的电解电容不小于470uF,并在单片机VCC引脚旁边加0.1uF陶瓷电容。不要忘记共地:驱动板和单片机的地线必须连在一起,否则控制信号无参考电平。判断是否共地问题,只需要把板子放桌上,用手按紧两个GND端点,如果现象消失,就是共地不良。

5.3 传感器输出逻辑和代码假设不一致

现象:所有硬件连接看起来正确,但小车压线后的转向完全反向,或者某个方向特别迟钝。原因:模块比较器输出极性不同,代码里默认黑线为高,实际模块黑线为低。解决:上电后把每个传感器依次压到黑线上,用万用表或者串口打印确认各个模块的逻辑。模块出厂差异和电位器调节状态都会影响翻转结果。一个排查技巧是把4个传感器全部悬空,读取read_sensor返回值;再把4个全部压到黑线上,再次读取。两次返回值应该是0x00和0x0F(或相反),如果不是,说明某个传感器接线没插紧或者电位器没调好。某开发者的血泪经验:他花了一个晚上调整代码逻辑,最后发现只是S3的杜邦线插在面包板第二排但没插紧,接触不良导致时好时坏。

5.4 上电时序与引脚冲突

现象:一上电小车电机就转,不受安全开关控制;或者烧录程序后,传感器数值读取正常,但PWM输出引脚没有波形。原因:STC89C52上电瞬间IO口处于高电平状态,如果驱动芯片的IN引脚接的是单片机高电平默认状态,电机会在复位期间误动作。L298N的ENA若悬空或接高,电机也会不受控。解决:电机驱动板上的ENA和ENB必须由单片机PWM引脚控制,不能悬空接高;软件初始化时优先把方向引脚全部置低,然后再开定时器中断。时序上,在main函数最前面调left_motor(0)和right_motor(0),让两个电机刹车,再初始化其他外设。另一个引脚冲突问题是P3.0和P3.1默认是串口,如果你把这俩当普通IO用,烧录器在线下载时行为会异常。新板子调试时先把串口功能确认关闭,或者避开P3.0/P3.1接传感器。

6. 进阶调车技巧:从能走到能跑,最后检查这三个地方

循迹小车跑通直线和弯道之后,剩下的问题通常是速度上不去。速度一快,原来的占空比差值就不够用,车在弯道会往外偏。一个简单有效的做法是把固定TURN_SPEED改成动态的:弯道越急,左右轮占空比差越大。怎么判断弯道急不急?看最外侧传感器被压到的时间长度。如果S1持续压线超过100ms,说明车已经明显偏出,加大转向占空比;如果只是过弯瞬间压线20ms,小幅转向就够了。按这个思路维护一个int turn_gain变量,主循环里根据压线持续时间做积分调节,通常上两个级别的速度没有问题。

丢线恢复也值得认真处理。全白状态出现时,不要盲目直行,记住进入全白前最后一次非全白状态的转向方向。如果之前偏左,就向左轮减速右轮加速,以中等占空比画弧线扫描,直到重新读到传感器状态。如果扫描超过1秒还没找到线,说明车确实跑出赛道太多,这时候才停车报警。很多场比赛设计里有一种“断线区”,黑线故意断开一段,这个逻辑能直接派上用场。

最后一个验证技巧:在正式赛道跑之前,先做三个静态检查。一是手抬小车在轨道上方模拟压线,观察左右轮转速差是否符合预期转向;二是把四个传感器压到黑线上,观察PWM输出引脚波形是否变化;三是全速起步前先用手给一点阻力,确认电机有足够的扭矩克服地面摩擦。这三项过了,小车基本上赛道不会出大问题。我的调车习惯是每次都准备一卷黑色PVC胶带,在地面贴一个直线加直角弯的测试轨道,先让车以BASE_SPEED=30跑通,再逐步加到位。改一个参数、跑一圈、记录一次现象,比一次性改三个参数再瞎猜有用得多。希望这篇笔记能帮你把TCRT5000和STC89C52的组合一次调顺,把时间留给更进阶的赛道地图和运动控制,而不是消耗在接线和玄学参数上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询