资讯动态

STC89C52RC循迹小车实战:TCRT5000五路循迹与PWM差速调参

发布时间:2026/9/17 5:09:59 来源:尧图企业网站定制
简介STC89C52RC单片机智能小车黑线循迹实验程序源代码是一套基于51单片机的红外循迹小车完整工程面向电子竞赛、课程设计及机器人入门学习者。程序使用Keil开发适配STC89C52RC主控配合L293D电机驱动芯片与TT直流减速电机实现白底黑线路径的自动检测与循迹行驶。压缩包共15个文件大小1.84MB包含C语言源文件、car.h头文件、hex烧录文件、Keil工程文件以及实验说明文档和txt程序说明其中hex文件可直接烧录至单片机运行工程文件便于在Keil中修改调试说明文档则梳理了实验原理与接线要点。资源已获3203人学习下载作者在自研小车上亲测可用具备实际参考价值。通过阅读代码和说明可掌握红外循迹模块的信号处理方式、电机PWM调速及转向控制思路便于在此基础上二次开发。1. 黑线循迹是“传感器阈值、转向算法、电机响应”三件事的接力前几天调一辆基于 STC89C52RC 的循迹小车黑线循迹环节折腾到半夜最后发现问题不在传感器而在 EN 引脚根本没收到 PWM。很多从课程设计起步的人一听到“黑线循迹”以为就是把传感器读数翻译成“左转、直行、右转”几个判断真正做起来其实是三件事在接力先用反射式光电模块把黑线和白底变成稳定的 0/1 电平再让单片机按既定周期输出左右轮 PWM最后用一个状态机把“小车相对黑线偏了多少”换算成两个轮的差速。本文就按这个顺序把 STC89C52RC、TCRT5000 五路循迹模块、L298N 驱动这套组合完整讲一遍。工创赛智能物流小车、课程设计里的搬运小车第一关大多也是它。2. 从光到电平TCRT5000 怎么把黑线变成 STC89C52RC 能读的 0/12.1 反射式光电检测为什么黑线吸光、白底反光TCRT5000 内部封装了一只红外发射管和一只光电三极管发射管发出红外光打到地面反射光被接收管探测到。黑线用的黑色漆面在红外波段吸收率远高于白底所以白底反射强、接收管导通程度高黑线反射弱、接收管接近截止。模块板上的比较器 LM393 负责把这个模拟信号和电位器设定的参考电压做对比输出一个干净的数字电平而不是把模拟电压直接送进单片机。这个方案之所以在 51 里普及是因为 STC89C52RC 没有内置 ADC数字输出最省事。检测距离是这个环节最容易被忽略的参数。TCRT5000 的典型检测距离只有 0.5~1.2cm安装时要让传感器底面离地面 0.8~1cm。太高了红外光发散黑线边缘信号模糊白底和黑线的电压差变小太低了地面一块小石子就能把模块顶变形。装好之后用万用表量模块 DO 输出白底和黑线上应该有一个明确跳变而不是缓慢过渡。如果输出抖动先处理高度和电位器不要急着怀疑程序。2.2 三路、五路、灰度传感器预算和赛道难度决定方案市面上的循迹方案大致分三类三路数字模块、五路数字模块、灰度传感器配合 ADC。三路最少外侧两路管转弯、中间一路管直行接线最简单但连续 S 弯里容易丢线五路把弯道细分成缓弯和急弯能给出更细致的差速响应灰度板能输出每一路的具体灰度配合比例控制最平滑但 STC89C52RC 没有 ADC需要外接 ADC0832 或换带 ADC 的 STC 型号成本和代码量都上去了。方案输出形式与 STC89C52RC 接口适用场景三路 TCRT5000 模块DO 数字 0/13 个 GPIO 直读课程设计、简单单圈赛道五路 TCRT5000 模块DO 数字 0/15 个 GPIO 直读竞赛选拔、普通循环赛道灰度传感器 ADC 芯片模拟电压转数字SPI 或并口接 ADC高速车、需要平滑比例转向我一般直接推荐五路。五路一字排开时中间传感器对准车头中线相邻传感器间距按赛道黑线宽度设计。黑线宽 2cm 左右时间距取 1.5~1.8cm 比较合适间距太小过弯时好几个传感器同时在线状态区分度差间距太大直线上三个中间传感器都可能离开黑线丢失正前方信息。2.3 接线与电平归一化先让 P1 口读出来的数可信STC89C52RC 的 P1 口是准双向口读外部电平之前必须先写 1否则读回的是内部锁存器的旧值。这个问题很隐蔽代码看起来在“读引脚”实际上一部分位读的是上次的写值表现就是传感器偶尔灵、偶尔不灵。下面是一份常用接线功能引脚说明五路模块 DO1~DO5P1.0~P1.4电平表示是否检测到黑线L298N ENAP2.0左电机 PWM 输入L298N IN1 / IN2P2.1 / P2.2左电机方向L298N ENBP2.3右电机 PWM 输入L298N IN3 / IN4P2.4 / P2.5右电机方向另一个绕不开的问题是电平极性不统一。LM393 输出是开集极模块板上有没有上拉、比较器接同相端还是反相端决定了检测到黑线时 DO 输出高还是低。常见模块两种极性都有所以我习惯在代码里做一次电平归一化sbit line1 P1^0; // 最左 sbit line2 P1^1; sbit line3 P1^2; // 正中间 sbit line4 P1^3; sbit line5 P1^4; // 最右 unsigned char read_sensor(void) { unsigned char s 0; P1 0xFF; // 准双向口读前先置1 if (line1 0) s | 0x10; // 按“检测到黑线输出0”编写 if (line2 0) s | 0x08; if (line3 0) s | 0x04; if (line4 0) s | 0x02; if (line5 0) s | 0x01; return s; }这里P1 0xFF是准双向口切换输入状态的标准操作之后每个位代表一路传感器1 表示压线。如果你的模块黑线时输出高电平把五个条件里的 0改成 1就行。这样后面的状态机就不依赖具体模块极性了换模块不用重写逻辑。电源方面还要强调一点单片机 5V 由板载稳压提供电机电源由电池直接供给 L298N 的 VS两块地必须连在一起。千万不能让电机启动电流流过单片机稳压器否则急加速瞬间单片机会复位。2.4 电位器不是随便拧比较器阈值到底调的是什么模块上的电位器调的是 LM393 输入端的参考分压。这个电压决定“多强的反射才算白底”调高参考电压对反射强度的要求变高等于对黑线更敏感调低则相反。最稳的调法是把车停在白底上先把 DO 调到稳定输出一个电平再挪到黑线上确认电平翻转中间留出足够余量。调电位器有个常见误区拧到临界点附近让指示灯刚好翻转就以为调好了。实际上地面会有细微反光差异阈值卡在临界处时传感器在行驶中会频繁抖动看起来像程序有 bug。正确做法是先找到翻转临界位置再向回拧 1/4 圈让输出在两种地面上都“坐实”。这一步做完后面第四章会用串口再验证一次不靠眼睛看指示灯。3. 核心代码STC89C52RC 的定时器 PWM 与 5 路循迹状态机3.1 没有硬件 PWM 怎么办用定时器 0 做 200Hz 软件 PWMSTC89C52RC 没有硬件 PWM 模块和 STC15、STC8 系列不一样最常见的替代方案是拿定时器 0 做时基在中断里模拟两路 PWM。思路不复杂定时器每 50us 中断一次每次中断让计数变量pwm_cnt加 1加到 100 就回 0这样 PWM 周期是 5ms频率 200Hz。左右轮的占空比存在两个全局变量里中断里比较计数和占空比的大小决定 EN 引脚输出高还是低。选 200Hz 有两个理由一是直流减速电机的机械响应本身不快几百赫兹的 PWM 足够平滑调速再高电机也跟不过来二是软件 PWM 每次中断都要进现场、比较、翻转频率太高会吃掉大量 CPU 时间导致主循环里的循迹判断变慢。#define PWM_PERIOD 100 // 5ms 周期200Hz volatile unsigned char pwm_cnt 0; volatile unsigned char pwm_left 30; // 左轮占空比 30% volatile unsigned char pwm_right 30; // 右轮占空比 30% void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x02; // 定时器0方式28位自动重装 TH0 0xCE; // 12MHz12T模式机器周期1us TL0 0xCE; // 256 - 206 50us ET0 1; TR0 1; EA 1; } void Timer0_ISR(void) interrupt 1 { if (pwm_cnt PWM_PERIOD) pwm_cnt 0; ENA (pwm_cnt pwm_left) ? 1 : 0; ENB (pwm_cnt pwm_right) ? 1 : 0; }初值0xCE等于 206256 减 206 是 50也就是 50 个机器周期。这里的前提是单片机工作在默认 12T 模式、晶振 12MHz。如果用 STC-ISP 把芯片设成 6T 模式机器周期变成 0.5us同一个初值会让 PWM 频率翻倍到 400Hz占空比分辨率不变但周期变短调参时的直觉判断全部失效。所以一开始就要确认 ISP 下载界面里的“输入时钟/机器周期”设置保证和代码一致。中断里只做 PWM 翻转不要塞别的逻辑。主循环也尽量避免长延时。假设车速 0.3m/s主循环里加一个 delay(10)10ms 内车已经跑了 3mm高速过弯时这 3mm 足够让传感器越过黑线边缘。主循环的任务越短越好只做读传感器、算状态、更新占空比三件事。3.2 主循环三段式读传感器、定状态、算差速主循环的结构固定为“读一次传感器、做去抖、按状态表更新左右轮占空比”。去抖不用延时用连续两次读数相同才确认的办法如果当前读到的值和上一次不同只记住新值而不动作下一次读到相同值时再确认。这个去抖只多花一次轮询周期实际耗时几十微秒对循迹响应几乎没有影响。void main(void) { unsigned char sensor; unsigned char last 0xFF; // 初始值不能是0 unsigned char confirmed 0; Timer0_Init(); Motor_Init(); // 方向引脚初始化 P1 0xFF; // 传感器输入准备 while (1) { sensor read_sensor(); if (sensor ! last) { last sensor; continue; // 本轮不动作等确认 } confirmed sensor; track_control(confirmed); // 状态机更新 pwm_left/pwm_right } }last初始化成0xFF而不是 0是有讲究的。如果初始化成 0第一帧真实状态恰好是 0 时会被误判为“已经确认过”小车可能会跳过第一次必要的修正。confirmed变量保存的是稳定后的状态后续串口打印也用它排查问题时能直接看出当前传感器组合到底确认成了什么。3.3 5 路状态机从直行到急转弯的查表逻辑5 bit 状态里从高位到低位定义为“左、中左、中、中右、右”。中间位单线压线是直行中左或中右压线算缓弯最左或最右压线说明已经接近丢线需要大幅转向。查表关系如下状态(bit4..bit0)场景左轮占空比右轮占空比00100直行BASEBASE01100左侧缓弯BASE - SMOOTHBASE SMOOTH11000左侧急弯BASE - TURNBASE TURN00110右侧缓弯BASE SMOOTHBASE - SMOOTH00011右侧急弯BASE TURNBASE - TURN11111T 字或十字保持上一次修正保持上一次修正路径上如果遇到十字线所有传感器同时压线此时没有方向信息贸然转向可能挑错路。保持上一次的差速状态让车按原轨迹滑过交叉点是更稳妥的默认策略。对应代码#define BASE 30 // 基础占空比 30% #define SMOOTH 15 // 缓弯差速 15% #define TURN 35 // 急弯差速 35% void track_control(unsigned char st) { switch (st) { case 0x04: // 00100 直线 pwm_left BASE; pwm_right BASE; break; case 0x0C: // 01100 左侧缓弯 pwm_left BASE - SMOOTH; pwm_right BASE SMOOTH; break; case 0x18: // 11000 左侧急弯 pwm_left BASE - TURN 15; // 保底限幅 pwm_right BASE TURN; break; case 0x06: // 00110 右侧缓弯 pwm_left BASE SMOOTH; pwm_right BASE - SMOOTH; break; case 0x03: // 00011 右侧急弯 pwm_left BASE TURN; pwm_right BASE - TURN 15; // 保底限幅 break; default: break; // 未匹配状态保持上次占空比 } }左急弯时BASE - TURN 15这个15是限幅保护防止内侧轮占空比被减到 0。为什么不能归零下面单独说。switch 里没覆盖的状态比如 00101 这种离散双线通常表示传感器间距和黑线宽度不匹配保持上一帧差速比乱打方向安全得多。3.4 差速转向为什么内侧轮不要完全停转把内侧轮占空比直接改成 0 是很多新手的做法结果车在弯里明显顿一下出弯后还要重新加速。原因是两侧轮速差过大时小车会绕内侧轮接近原地打转轮胎和地面剧烈摩擦大量前进动能被消耗掉。差速转向的本质是让整车重心沿一条平滑圆弧切过去而不是在弯心里“拧”过去。内侧轮保留少量占空比也就是保留一点牵引力整车轨迹会更圆滑出弯速度也更均匀。另一个好处是响应更快内侧轮没有完全停转时如果黑线在外侧轮方向出现系统修正只需要改变占空比差不需要先克服静止摩擦。一般我会把内侧轮最低限制在 10%~15% 左右具体值视电机死区而定。占空比低于这个值时电机可能根本不转留了也白留。如果赛道里有直角弯仅靠差速还是不够常见的补强是入弯前提前减速而不是入弯瞬间猛打方向。这个减速要交给速度档位来做原理放在第 5 章和这里的差速逻辑是配套关系。4. STC89C52RC 循迹小车调参从会走到走得稳的检查顺序4.1 先用串口验证传感器极性而不是猜电平第一次上电我会先把串口调通让单片机把read_sensor()的结果发到电脑上。这一步同时验证三件事传感器接没接对、电源正不正常、电平极性到底是高有效还是低有效。定时器 0 已经被 PWM 占用串口波特率就交给定时器 1。初始化时注意TMOD赋值方式不能直接写TMOD 0x22否则会清掉定时器 0 已经设好的模式位。void Uart_Init(void) // 9600bps8N112MHz { SCON 0x50; TMOD 0x0F; // 只清 T1 模式位保留 T0 的配置 TMOD | 0x20; // T1 方式2 自动重装 TH1 0xFD; // 9600bps 初值 TL1 0xFD; TR1 1; } void send_byte(unsigned char c) { SBUF c; while (!TI); TI 0; } void send_hex(unsigned char v) { char code hex[] {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F}; send_byte(hex[(v 4) 0x0F]); send_byte(hex[v 0x0F]); send_byte(\n); }在 while(1) 里先不放track_control改成隔 100ms 调一次send_hex(confirmed)把稳定后的状态发出来。车分别停白底和黑线上记下两个读数就能确定read_sensor里的极性判断对不对。网上很多源码默认黑线输出低电平如果你的模块是反的程序看起来就像“瞎了一样”。这一步跑通之前不要往下调速度。串口接线是另一个高频翻车点USB-TTL 下载器的 TXD 接单片机 RXDP3.0RXD 接单片机 TXDP3.1共地。STC 的下载是冷启动下载点“下载/编程”后再给单片机板重新上电但串口调试不需要重复这个流程程序一旦烧进去直接打开串口助手看数据即可。波特率、校验位、停止位三项和代码里保持一致9600、N、1。4.2 速度参数表BASE、SMOOTH、TURN 三个值怎么配循迹调参最忌讳一次改多个参数。我按“先低速定状态、再加 BASE、再动差速”的顺序走第一轮把 BASE 设为 25、SMOOTH 设 10、TURN 设 25让车以较低速度完整跑一圈。如果每个弯都能过只是慢说明状态机和传感器设置都对之后以 5 为单位往上加 BASE每次只加一个参数跑一圈观察哪个弯先出问题。参数初始建议现象判断调整方向BASE25~30直线就跑偏降低 BASESMOOTH8~15缓弯反复修正、摇头降低 SMOOTHTURN25~35急弯冲线或原地打转先减 TURN再降 BASE传感器高度0.8~1cm白底偶发误判黑线抬高 0.1cm 再试如果出现高频摇头也就是直线段车头左右摆通常不是传感器问题而是 SMOOTH 太大。差速比例相当于闭环增益增益过大时系统会围绕目标线振荡。处理办法是降 SMOOTH不是降 BASE因为降 BASE 会让所有弯道一起变慢。反过来如果缓弯回正明显迟钝车头越过中线才有修正动作说明 SMOOTH 太小加 5 再试。TURN 的调法和 SMOOTH 不同。TURN 只影响最外侧传感器压线时的响应过弯飞线先加 TURN如果加到 40 以上仍然飞线问题多半是 BASE 太高入弯前来不及修方向。把 BASE 降回一档通常比继续增大 TURN 更有效。调完一组参数后电池电压变化也会影响实际车速所以每次换电池后至少要低速验证一圈。4.3 四个典型故障现象与排查顺序上电后常见的问题其实很有限。第一个是“车完全不动”先看方向引脚初始化再量 L298N 的 VS 有没有电池电压然后查 ENA/ENB 引脚有没有 PWM 方波。L298N 的 5V 逻辑电源不接时IN 脚电平悬空电机当然不动这个坑排在检查顺序第二位。现象第一排查点第二排查点第三排查点上电完全不动L298N 5V 逻辑电源ENA 引脚 PWM 输出电池电压跌落只向一个方向转传感器阵列是否装反左右差速写反某一路 DO 恒 0 / 恒 1直线摇头SMOOTH 过大传感器阈值临界左右轮胎磨损差异过弯必飞线BASE 过高TURN 不足传感器间距与线宽不匹配“只向一个方向转”最迷惑人。车能转说明电机和驱动基本正常问题多数出在传感器阵列装反或者状态机判断的左右方向和实际转向方向相反。处理办法是拿起小车用手分别挡住左右两侧传感器看串口打印的返回值是否对应正确位置再核对状态表里左右轮占空比是否符合预期。用串口打印pwm_left和pwm_right比用眼睛追车快得多。“过弯必飞线”还要检查程序上的隐患主循环里有没有别的模块在阻塞。常见的蓝牙模块、超声波模块如果放在同一个 while(1) 里比如超声波等回波等了十几毫秒这期间循迹判断完全停摆车在弯道上已经冲出去了。这类功能必须拆到低优先级分支或者用定时器轮询切换循迹主循环任何时候都不能被长时间阻塞。5. 让 STC89C52RC 循迹小车收弯更稳速度档位、EEPROM 与日志验证5.1 按弯道大小切速度档位而不是全程一个速度低速调通之后全程一个 BASE 很难同时兼顾直道速度和弯道稳定。常见做法是把基础速度切成两个档位检测到中左或中右传感器压线时从直道档降到缓弯档检测到最外侧传感器压线时再降一档给急弯用。切档逻辑放在track_control前半段根据当前状态位判断弯道深度再决定后面差速计算用哪组 BASE。5.2 把调试好的速度参数写进 STC89C52RC 内置 EEPROMSTC89C52RC 内置 4KB EEPROM起始地址 0x2000按 512 字节分扇区。操作方式和 AT24C02 完全不同不需要 I2C 时序直接在寄存器层面触发 IAP。写之前必须先整扇区擦除不能单独擦一个字节这是 Flash 的基本特性。EEPROM 适合存 BASE、SMOOTH、TURN 这类调参结果换电池后读出来直接用省去重新跑圈的麻烦。void IapEraseSector(unsigned int addr) { IAP_CONTR 0x80; // 使能 IAP IAP_CMD 0x03; // 扇区擦除命令 IAP_ADDRL addr 0xFF; IAP_ADDRH (addr 8) 0xFF; EA 0; // 触发期间关中断 IAP_TRIG 0x5A; IAP_TRIG 0xA5; // 连续触发命令 _nop_(); _nop_(); EA 1; IAP_CONTR 0x00; // 关闭 IAP } void IapWriteByte(unsigned int addr, unsigned char dat) { IAP_CONTR 0x80; IAP_CMD 0x02; // 字节写命令 IAP_ADDRL addr 0xFF; IAP_ADDRH (addr 8) 0xFF; IAP_DATA dat; EA 0; IAP_TRIG 0x5A; IAP_TRIG 0xA5; _nop_(); _nop_(); EA 1; IAP_CONTR 0x00; }擦除时传入的地址必须是扇区起始地址0x2000、0x2200 这种 512 的整数倍。写入地址可以连续但写完一个扇区内的数据后如果要改数据本身必须先把整个扇区擦掉再重新写。EA 0关中断很关键否则 IAP 触发期间被定时器中断打断可能造成误擦写。这套操作只在按下“保存参数”键时执行一次不要放进循迹主循环。5.3 用串口日志确认每个弯的响应时间调参不能只靠肉眼。我会在状态确认之后把状态值和新的占空比一起通过串口发出来日志里能直接看出从“检测到弯道”到“占空比变化”之间的响应时间。超过 50ms 就要回头查主循环里有没有阻塞代码延迟正常但弯道仍然飞线问题集中在 TURN 数值上。把日志里每个弯的响应时间按 5.1 的档位表对照如果中弯平均响应时间超过 60ms就把 SMOOTH 档提高 5% 再跑一圈。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价