
简介针对STC89C52RC单片机智能小车循迹与测速综合实验整理的完整程序源代码适合51单片机学习者、智能小车DIY爱好者和电子竞赛备赛人群。程序采用Keil开发结合L293D电机驱动芯片、TT直流减速电机、测速模块以及1602液晶显示可实现循迹、测速与显示一体的功能。资源共15个文件包含Keil工程源文件.c/.h/.uv2、编译生成文件.hex/.OBJ/.LST/.M51、Word和TXT说明文档以及备份文件.bak等压缩包整体2.71MB目录结构清晰便于按需检索与二次开发。代码在作者实体智能小车上亲测可用对想快速上手小车项目、理解循迹与测速实现原理的读者来说既可直接烧录hex观察效果也可对照C源码学习模块化编程思路。目前已有2915人学习适合作为入门到进阶的参考例程。1. STC89C52RC智能小车循迹和测速在同一个主循环里为什么容易打架很多人做循迹小车时觉得难点在转向逻辑实际把测速加进去之后才发现真正卡人的是中断资源循迹要不停读IO测速要捕捉脉冲沿两件事挤在同一颗STC89C52RC上主循环稍慢一拍码盘脉冲就漏了。这颗增强型8051虽然便宜、资料多、手工焊接也没压力但它没有内置ADC外设全靠IO模拟和定时器硬拼。本文以「循迹 测速」这套最常见的课设/竞赛组合为对象把硬件接线、C代码骨架、阈值标定和调试手段一次讲透。适合三类人刚把最小系统板点亮的新手、准备电赛或工创赛的小组、以及想快速评估89C52够不够用的老工程师。你不需要先跑通整台车顺着中断和定时器的分配往下走就知道每一部分为什么这样写。2. 硬件链路五路循迹探头与槽型光耦测速模块如何在STC89C52RC最小系统板上接线2.1 为什么选五路循迹而不是三路或单路三路循迹左、中、右在直道和缓弯上够用但遇到十字路口或锐角弯时会丢线因为车辆偏离后没有任何传感器能看到线。五路循迹传感器L2、L1、C、R1、R2把视野拓宽到近6厘米对码盘测速带来的额外重量和惯性也有更好的预测空间。市面上的五路TCRT5000模块大多集成LM393比较器输出直接是数字高/低电平这正好避开了STC89C52RC没有ADC的短板——你不需要去读模拟电压只需把5个IO口配成准双向口读电平。接线原则是传感器输出接P1低五位电机PWM接P2口编码器信号接INT0或INT1。不要把编码器信号接到普通IO上靠轮询读电平脉冲宽度可能只有几十微秒主循环根本来不及扫到。2.1.1 最小系统板与传感器供电的共地问题STC89C52RC最小系统板通常由USB转TTL供电电机驱动模块L298N或TB6612由电池组供电。循迹传感器需要5V但它的参考地必须与单片机的地连通否则数字电平没有公共参考点IO口读到的是随机跳变。这也是排查「传感器明明对着黑线却不翻转」的第一检查项。接线参考表以5路输出低电平表示压线为例 ---------------------------------------------- 模块引脚 单片机引脚 说明 VCC 5V 与板子同源或共地 GND GND 必须与最小系统板共地 OUT1-L2 P1.0 左外侧 OUT2-L1 P1.1 左内侧 OUT3-C P1.2 中心 OUT4-R1 P1.3 右内侧 OUT5-R2 P1.4 右外侧 PWM左电机 P2.0 左电机调速 PWM右电机 P2.1 右电机调速 编码器A相 P3.2/INT0 下降沿计数 ----------------------------------------------接线时需要注意槽型光耦测速模块一般工作在5V输出是开漏或推挽具体看模块上的上拉电阻。如果是开漏输出必须在信号线与VCC之间加一个4.7k上拉电阻。编码器的A相接INT0B相如果不做方向识别可以不接实验程序里只测速不判断正反转省下一路外部中断。2.2 测速码盘的选型与脉冲频率计算测速的本质是单位时间内的脉冲计数而脉冲频率取决于码盘的镂空格数和轮子转速。常见的20格码盘配合槽型光耦H206轮径6.5cm时每转产生20个脉冲。若车速0.5m/s轮子每秒转约2.45圈脉冲频率约49Hz在100ms计数窗口内只能数到5个脉冲分辨率偏低。所以有两个改进方向一是换40格或60格码盘提高每转脉冲数二是缩短计数窗口到50ms并在软件里做滑动平均。但STC89C52RC的主频最高到24MHz机械周期是12分频大约2MIPS中断频繁进入会增加开销。常见做法是保持20格码盘用100ms窗口累计计数速度计算最终结果做一阶滤波兼顾分辨率和CPU负载。2.3 电机驱动与PWM通道分配L298N的使能端接PWMTB6612的PWMA/PWMB同理。实验小车用差速转向不需要单独的方向舵机左右轮速度差决定转弯半径。PWM频率不能太高STC89C52RC用定时器0模拟PWM时频率一般设在1kHz到10kHz频率太高定时器中断开销大太低电机有啸叫。常见做法是定时器0工作在方式116位定时每100us进一次中断做PWM脉宽计数周期200次即20ms可以实现0-100%占空比。这样安排后主循环只负责读循迹传感器和算转向PWM刷新交给中断完成避免PWM在转向计算时卡顿。3. 主程序实现定时器PWM、循迹判决与外部中断测速的C代码骨架3.1 初始化寄存器分配STC89C52RC没有硬件PWM模块某些新型号有增强PWM但经典89C52确认没有所以PWM必须用定时器模拟。资源分配方案定时器0方式1100us中断刷新PWM占空比。定时器1方式110ms中断累计100ms时间基准并清零脉冲计数。外部中断0下降沿触发脉冲计数加1。串口方式19600波特率调试输出。这条分配链保证了测速计数完全由硬件中断完成主循环即使因为循迹判决死循环也不会漏脉冲这是「循迹和测速不打架」的关键。#include STC89C5x.H #define PWM_PERIOD 200 // 20ms周期 / 100us步进 volatile unsigned char pwm_count_left 0; volatile unsigned char pwm_count_right 0; volatile unsigned char pwm_current 0; volatile unsigned int pulse_count 0; // 外部中断累计脉冲 volatile unsigned int speed_tick 0; // 10ms tick volatile unsigned int speed_result 0; // 100ms窗口内的脉冲数 volatile unsigned char speed_ready 0; // 新速度数据就绪标志 void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0方式116位定时 TH0 0xFF; // 12MHz下100us初值取65536-100 TL0 0x9C; ET0 1; // 开启定时器0中断 TR0 1; } void timer1_init(void) { TMOD 0x0F; TMOD | 0x10; // 定时器1方式1 TH1 0xD8; TL1 0xF0; // 12MHz下10ms初值 ET1 1; TR1 1; } void ext0_init(void) { IT0 1; // 下降沿触发 EX0 1; // 使能外部中断0 EA 1; // 总中断 } void uart_init(void) { SCON 0x50; // 方式18位数据允许接收 TMOD 0x0F; TMOD | 0x20; // 定时器1重新配置为方式2自动重装 TH1 0xFD; // 11.0592MHz下9600波特率 TL1 0xFD; PCON 0x00; // SMOD0 TR1 1; }注意timer1_init里的初值按12MHz算uart_init里按11.0592MHz算实际板子晶振只能选一个如果你用11.0592MHz就要把timer1_init的初值改成TH1 0xEC; TL1 0x7810ms对应9216个机器周期。这个矛盾点经常让初学者怀疑代码写错其实是晶振不匹配导致的定时偏差。3.2 循迹判决从传感器组合到转向量五路循迹传感器输出的核心逻辑是加权误差。把每路传感器的位置映射为权重值中心为0左内为-1左外为-2右内为1右外为2然后将所有压线传感器权重求和得到当前车体相对线的横向偏差。权重和为0说明车正对线为负说明车偏左需要向右修正。// 读取5路循迹传感器返回加权误差 // 返回值为0表示直行负值偏左正值偏右 signed char get_line_error(void) { unsigned char raw 0; signed char error 0; // 注意低电平有效某路为0表示检测到黑线 raw (P1 0x1F); // 只取P1.0-P1.4 if (!(raw 0x01)) error (-2); // L2 if (!(raw 0x02)) error (-1); // L1 if (!(raw 0x04)) error 0; // C if (!(raw 0x08)) error 1; // R1 if (!(raw 0x10)) error 2; // R2 // 全丢线所有传感器均为1(未压线) if (raw 0x1F) { // 使用上一次误差方向防止冲过线后原地打转 error last_error 0 ? 3 : (last_error 0 ? -3 : 0); } else { last_error error; } return error; }这里的丢线处理很关键。小车高速过弯时可能出现所有传感器都离开黑线的情况如果错误判为0去直行车会冲出赛道。常见做法是记录上一次有效误差的符号丢线时沿原方向加大转向量。last_error需要在主循环外定义成全局变量并在初始化时清零。3.3 测速计算脉冲数如何换算成速度外部中断0每检测到一个下降沿pulse_count加1。定时器1每10ms进入中断一次累计到10次100ms后把当前脉冲数存入speed_result然后清零脉冲计数。void ext0_isr(void) interrupt 0 { pulse_count; } void timer1_isr(void) interrupt 3 { static unsigned char cnt 0; TH1 0xEC; // 重新装载初值按11.0592MHz TL1 0x78; cnt; if (cnt 10) { // 100ms到 speed_result pulse_count; pulse_count 0; cnt 0; speed_ready 1; } } // 由speed_result换算速度单位cm/s unsigned int calc_speed_cm_s(unsigned int pulses) { // 20格码盘轮径6.5cm100ms窗口 // 每脉冲对应距离 PI * 6.5 / 20 1.021 cm // 速度 脉冲数 * 1.021 / 0.1s 脉冲数 * 10.21 // 为避免浮点用整数乘除pulses * 1021 / 100 return (pulses * 1021) / 100; }参数说明码盘格数、轮径、时间窗口三个值只要改任一个换算系数就变。码盘不是20格就把1021换成314*直径/格数的整数化结果。注意pulse_count是16位无符号数极速下200ms内不会溢出但如果你把窗口拉长到1s极速2m/s时脉冲约400个仍然安全。3.4 主循环把循迹和测速粘在一起主循环做三件事读传感器计算误差根据误差调整PWM占空比速度数据就绪时通过串口发送。PWM的更新不是直接改PWM寄存器而是修改两个全局占空比变量定时器0中断每秒200次去比较刷新输出。void main(void) { signed char line_err; int base_speed 120; // PWM基准占空比 120/20060% timer0_init(); timer1_init(); ext0_init(); uart_init(); while (1) { line_err get_line_error(); // 转向修正误差绝对值越大两边轮速差越大 if (line_err 0) { pwm_count_left base_speed (line_err * 8); // 左轮减速 pwm_count_right base_speed; } else if (line_err 0) { pwm_count_left base_speed; pwm_count_right base_speed - (line_err * 8); // 右轮减速 } else { pwm_count_left base_speed; pwm_count_right base_speed; } // 限制占空比范围 if (pwm_count_left 200) pwm_count_left 200; if (pwm_count_right 200) pwm_count_right 200; if (speed_ready) { speed_ready 0; send_speed(speed_result); } } }line_err * 8是比例系数误差为1时左右轮差8个占空比刻度误差为3时差24。比例系数过高车会蛇形摆动过低则弯道转不过去。这个值需要在实车上现场标定没有通用答案。pwm_count_left如果小于0会变成很大的无符号数所以前面要先判断误差方向再保证减法不减出负值更稳妥的写法是先检查line_err范围再乘系数实际项目中建议把占空比变量声明为signed int在赋值给PWM比较寄存器前再做饱和钳位。4. 参数整定与排错循迹阈值、PID方向修正和测速毛刺的实测处理4.1 循迹传感器阈值为什么要开机自适应TCRT5000的反射信号受环境光影响极大。室内灯光下和阳光下测同一根黑线反射电压可能差一倍。如果程序里固定比较器阈值赛场换一个亮度环境传感器就会误判。大部分模块上有个电位器可以调比较阈值但实战中更可靠的做法是开机后花2秒扫描一遍5路传感器背景值。// 开机自适应阈值采样32次求各通道基线电压 // 此代码在main初始化阶段调用需在进入循迹循环前完成 unsigned char threshold[5]; void calibrate_sensor(void) { unsigned int sum[5] {0, 0, 0, 0, 0}; unsigned char i, j; // 传感器全部悬空白底环境采样 for (i 0; i 32; i) { DelayMs(5); // 简单延时确保采样间隔 for (j 0; j 5; j) { if (P1 (0x01 j)) sum[j]; } } // 统计为1的比例超过50%记为高电平基线 for (j 0; j 5; j) { threshold[j] (sum[j] 16) ? 1 : 0; } }这段不是直接调模拟阈值而是让程序记住传感器在无黑线状态下的默认电平逻辑。模块输出低电平表示黑线有效那白底正常应是高电平如果校准后发现某一路在白底默认是低说明模块比较器阈值偏了应该先调电位器而不是改代码。4.2 转向控制比例控制还是加微分get_line_error返回的误差值乘以固定系数就能控制转向但这只在低速下稳定。车速提高到0.5m/s以上后车会因为惯性冲过中线形成左右摆荡。原因是比例控制只对当前偏差起作用没考虑偏差的变化趋势。signed int pid_output; signed char last_error 0; // 增量式PD控制输出转向修正量 // Kp8, Kd4 pid_output 8 * line_err 4 * (line_err - last_error); last_error line_err;Kd项起到阻尼作用当前误差相对上次误差变小时提前降低修正量抑制蛇形。Kp过大表现为高频摆动Kd过大会导致转向迟钝弯道转不过去。调参顺序是先把Kd清零在直道上加大Kp直到出现小幅抖动然后加Kd到抖动消失最后再结合弯道表现微调。4.3 测速毛刺来源与软件滤波测速脉冲不稳定通常不是代码问题而是机械问题。码盘偏心导致每转脉冲间隔不均匀、槽型光耦安装缝隙太大导致信号抖动、电机碳刷火花干扰信号线等都会让脉冲计数在短时间内异常。软件层面能做的有两件事一是计数窗口内做边缘去抖对10us以下的窄脉冲不计数二是对速度结果做中值滤波连续采集5次取中间值输出避免单次毛刺污染显示。// 简单的峰值保持脉冲滤波 // 进入中断后先读当前引脚电平两次采样确认电平稳定再计数 void ext0_isr(void) interrupt 0 { EX0 0; // 禁止外部中断防止重入 DelayUs(20); // 等待信号稳定 if (P3_2 0) { // 确认确实是低电平 pulse_count; } EX0 1; }这个写法能以牺牲少量计数精度为代价换稳定性。20us延时内漏掉一个极窄脉冲影响很小何况正常码盘输出脉宽远大于20us。缺点是中断服务程序时间变长如果你同时开启了多个高优先级中断需要注意延时会阻塞其他中断。更优雅的方案是直接用定时器0做周期采样判断电平变化不过那是状态机方案对89C52来说代码量偏大。4.4 常见故障排查表现象可能原因排查与处理循迹传感器全亮但程序无反应IO模式未配置默认高阻输入将P1口写0xFF切换为准双向电机一启动单片机复位电源压降导致最低工作电压不足电池和单片机分别供电共地信号先过光耦测速读数始终为0编码器信号没接到INT0或中断未使能检查P3.2引脚电压是否有脉冲跳变先测信号线速度显示减半码盘格数配置与实际不符确认码盘实际格数重新计算换算系数转角处车轮反转占空比变量溢出变成负数改用signed int并在赋值前做钳位注意电机启动瞬间电流可以达到运行电流的3-5倍这会让共用电源的STC89C52RC电压跌到复位阈值以下。该项目中的常规解法分成两路供电加共地或者在电机电源端并一个大容量电解电容吸收冲击。5. 调试技巧串口回传与逻辑分析仪验证测速脉冲的最终手段调试循迹小车时最怕黑盒运行车跑歪了你不知道是传感器读错还是转向算错。串口回传是最便宜的观测手段。STC89C52RC内置UART用USB转TTL接P3.0和P3.1把循迹误差和速度值实时发出来电脑端用串口助手查看。串口发送函数需要注意格式兼容很多串口助手默认按字符串解析直接发二进制数值会显示乱码。我习惯把数据拼成逗号分隔的文本行末尾加\r\n这样还能直接导入Excel画曲线。一个精简的发送函数如下void send_speed(unsigned int val) { // 输出格式SPD:xxx\r\n char buf[12]; buf[0] S; buf[1] P; buf[2] D; buf[3] :; buf[4] 0 val / 100; buf[5] 0 (val % 100) / 10; buf[6] 0 val % 10; buf[7] \r; buf[8] \n; for (unsigned char i 0; i 9; i) { SBUF buf[i]; while (!TI); // 等待发送完成 TI 0; } }波特率建议固定在9600一方面11.0592MHz晶振正好整除另一方面9600波特率下传输距离和抗干扰都够用。如果你用12MHz晶振9600波特率的误差约0.16%短时间调试问题不大但如果传大量数据积累漂移可能出现帧错位此时把波特率降到4800更稳。调测速脉冲时最好准备一个逻辑分析仪把编码器A相信号和循迹传感器的第3路输出同时抓下来。方法是让小车静止用手缓慢转动轮子观察脉冲沿是否密集且均匀。如果发现某一段脉冲明显稀疏基本可以断定码盘有杂质遮挡或安装间隙不均匀。用逻辑分析仪还能直接测出脉冲频率和你代码里的speed_result对比一次就能定位是硬件问题还是换算系数问题。最后在下载程序时留意STC-ISP的冷启动提示STC89C52RC需要先断电再上电触发引导如果你的板子不支持USB自动断电必须手动操作。否则点了下载一直提示「正在检测目标单片机」检查项目里当前没有源代码管理提供程序进行注册这类提示往往是开发环境的问题跟烧录无关可以先忽略。本文还有配套的精品资源点击获取