简介:围绕P89V51RB2单片机的循迹小车实验报告,是一份面向电气工程与自动化学院学生的课程设计实践资料,完整展示了从系统总体设计、硬件电路搭建到软件驱动编写与整机调试的全过程。报告以三轮小车为平台,讲解红外探测法识别黑线、PWM控制电机调速、光电编码器测速以及超声波测距与LCD1602显示等关键模块,尤其适合单片机入门者参考电路设计与代码实现思路。压缩包内为1个PDF文件,整体大小约1.53MB,内容包含绪论、系统总体设计、硬件设计、软件设计、系统调试与结束语等章节,目录结构清晰。已有219人学习下载,可作为嵌入式技术实践、智能车竞赛备赛或相关课程设计的参考资料。
1. 拿到“循迹小车实验报告.pdf”这个标题时,先别急着补文档,很多问题出在车上
实验报告一般最后才写,但循迹小车这个项目真正卡的从来不是排版,而是车为什么跑不稳。红外对管装好了、电机能转、代码烧进去了,结果小车在直道上画龙,过一个锐角弯直接冲出去。这份报告的核心是把“循迹”这件事讲清楚:传感器怎么判断黑线、单片机怎么决定左右轮转速差、参数为什么这么设。本文面向正在做单片机课程设计、蓝桥杯备赛或刚接触 STC/STM32 小车控制的人,用可复现的代码和参数把循迹的检测、转向、调速三个环节走一遍,最后给出整车验证顺序和最容易翻车的边界情况。报告内容可以之后再填,但车要先能跑。
2. 循迹小车怎么“看见”黑线:红外对管与检测判据
2.1 两种常见循迹方案的读数差异
循迹小车的“眼睛”通常有两大类。第一类是红外对管,由红外发射管和接收管组成,发射管持续发出红外光,接收管接收反射回来的光强。地面是白色时反射强,接收管导通,输出低电平;地面是黑色时吸收红外光,反射弱,输出高电平。这种方案输出的是数字量,单片机只要读 I/O 口的电平即可,成本低、代码简单,是 51 单片机循迹小车最常见的配置。
第二类是灰度传感器,原理类似但输出模拟量,能区分不同灰度的颜色。它带来的好处是可以做“阈值自适应”,在光线变化的环境里通过算法自动调整判定标准,但需要单片机有 ADC。STC89C52 这类基础 51 芯片不带 ADC,要用 ADC 得外挂芯片或换 STC12 系列、STM32。做课设的话,我一般建议先上红外对管把逻辑跑通,再考虑灰度方案。
| 对比项 | 红外对管(数字量) | 灰度传感器(模拟量) |
|---|---|---|
| 输出信号 | 高/低电平 | 0~5V 模拟电压 |
| 接线复杂度 | 低,只需 I/O 口 | 需要 ADC 引脚 |
| 环境光影响 | 大,需调电位器 | 可通过软件校准 |
| 适合芯片 | STC89C52、STM32 | STM32、STC12 系列 |
| 单路成本 | 约 1~2 元 | 约 3~8 元 |
| 判定逻辑 | 电平判断 | 电压阈值比较 |
2.2 STC89C52 读取两路红外对管的代码与电平逻辑
下面是一段用 STC89C52(属于 STC 单片机家族)驱动两路红外对管的最小代码。硬件接线约定:左传感器接 P1.0,右传感器接 P1.1,传感器模块上的 VCC 接 5V,GND 接地,OUT 接 I/O 口。绝大多数市售红外对管模块自带比较器(如 LM393),输出已经是数字电平,不需要再加 ADC。
#include <REGX52.H> sbit LeftSensor = P1^0; // 左传感器输出引脚 sbit RightSensor = P1^1; // 右传感器输出引脚 // 电机控制引脚 sbit LeftMotorDir = P2^0; sbit RightMotorDir = P2^1; void Motor_Stop(void) { LeftMotorDir = 0; RightMotorDir = 0; } unsigned char Read_Sensor_State(void) { unsigned char state = 0; // 状态位定义:bit1 = left, bit0 = right // 1 表示检测到黑线(输出高电平),0 表示白地 if (LeftSensor == 1) state |= 0x02; if (RightSensor == 1) state |= 0x01; return state; } void main(void) { unsigned char sensor; while (1) { sensor = Read_Sensor_State(); switch (sensor) { case 0x00: Motor_Stop(); break; // 两路都在白地,停车或直行 case 0x02: // 只有左边压线 -> 右转 LeftMotorDir = 1; RightMotorDir = 0; break; case 0x01: // 只有右边压线 -> 左转 LeftMotorDir = 0; RightMotorDir = 1; break; case 0x03: // 两路都压线 -> 直行 LeftMotorDir = 1; RightMotorDir = 1; break; } } }这里先解释状态位的判定逻辑。红外对管模块在检测到黑线时输出高电平,所以if (LeftSensor == 1)意味着左传感器正压在黑线上。0x02表示左黑右白,车应该往右打方向;0x01反之;0x03两路同时压线,说明车头正对黑线,直行即可。注意 I/O 口若没有内部上拉,需要在模块 OUT 和 VCC 之间接一个 10k 上拉电阻,否则电平会漂浮导致误判。
这段代码的价值在于把“循迹”抽象成了状态查表,之后无论加几路传感器,都只是扩展状态位而已。缺点是左右轮转速一样,转向幅度不可调,弯道稍急就会冲出赛道,所以实际项目中要引入占空比控制。
2.3 传感器布局间距与高度对循迹的影响
布局是实验报告里最容易被忽略的一节。两个传感器的间距如果只有 2~3cm,小车在直道上轻微晃动就会导致两路同时脱离黑线,进入未知状态;如果间距拉到 8cm 以上,遇到小角度弯道时可能两边都没压到线,车就“瞎”了。我一般把间距设为黑线宽度的 1.2~1.5 倍,标准 PVC 胶带宽度约 2cm,传感器间距取 3~4cm 比较稳妥。
传感器离地高度同样关键。模块底部的红外发射管和接收管是斜对射设计,离地太近(小于 0.5cm)时接收管只能收到很少的反射光,白地和黑线的电平差变小;离地太远(大于 3cm)时红外光扩散,黑线也反射部分红外光,同样难区分。2cm 左右是最佳工作距离,这个可以直接写进实验报告的“实验条件”部分。调传感器上的蓝色电位器可以改变灵敏度阈值,顺时针旋转让判定更敏感,但过度敏感会把细微污渍也当成黑线,此消彼长需要实测。
3. 直道加速弯道减速:差速控制的 PWM 调参
3.1 两轮差速的基本公式与转向策略调整
只靠电平切换转向是“开关量控制”,车在直道上左右摇摆,过弯时内侧轮和外侧轮转速相同,转弯半径完全由摩擦力决定,很难跑出稳定的轨迹。要让循迹小车跑得平稳,需要用 PWM 独立控制左右电机的占空比,实现差速。差速的核心公式很简单:直线时左右占空比相等,转弯时一侧减速、另一侧保持或略微升速,转速差越大转弯半径越小。
转向策略分两步设计。第一步是“主从轮”,以弯道内侧轮为减速对象,外侧轮保持直行速度。第二步是“速度补偿”,如果发现外侧轮在急弯时速度下降(电池电压跌落导致),就把它稍微提高。开环阶段不需要编码器,直接给固定占空比就能跑。下面用 STM32F103C8T6 的定时器输出 PWM 来演示,因为 STM32 循迹小车普及率很高,且可以用定时器同时输出两路频率相同的 PWM。
3.2 用 STM32 输出 PWM 实现差速的最小配置
// 使用 TIM2 的 CH1(PA0) 和 CH2(PA1) 输出 PWM,频率 1kHz #include "stm32f10x.h" void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 72MHz/72 = 1MHz TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1MHz/1000 = 1kHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 两路 PWM 模式 1,初始占空比 0 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); } // 左轮占空比 left_duty,右轮占空比 right_duty,范围 0~999 void Motor_SetDuty(uint16_t left_duty, uint16_t right_duty) { TIM_SetCompare1(TIM2, left_duty); TIM_SetCompare2(TIM2, right_duty); }这段配置的逻辑是:先将 72MHz 系统时钟分频到 1MHz,再设置自动重装载值为 1000,最终得到 1kHz 的 PWM 频率。TIM_SetCompare1设置的是占空比数值,999 对应 99.9% 占空比,0 对应停机。PWM 频率选择 1kHz 是折中方案——电机驱动芯片(如 L298N、TB6612)响应这个频率没问题,也不容易产生人耳可闻的啸叫。
参数设置上有一个关键点:占空比不能直接等于目标速度。以 TB6612 为例,2.5V 的电机驱动电压大约对应 50% 占空比,但电机存在死区电压,占空比低于某个值(约 20%)时电机根本不会转。所以实际控制时要给一个基础占空比,比如直线巡航是 55%,转弯时内侧降到 30%,外侧提高到 65%,这样内外轮的转速差就能形成明显的转向力。
3.3 差速转向参数表与实测调整顺序
| 道路情况 | 左轮占空比 | 右轮占空比 | 说明 |
|---|---|---|---|
| 直道 | 550 | 550 | 两轮等速,微调消除跑偏 |
| 左急弯 | 380 | 600 | 左轮减速,右轮略加速 |
| 右急弯 | 600 | 380 | 右轮减速,左轮略加速 |
| S 弯连续 | 470 | 530 | 动态切换,幅度不宜过大 |
| 十字路口 | 550 | 550 | 冲过去,不做转向动作 |
调整顺序有讲究。先把直道占空比两边调到跑得直,此时如果发现车总往左偏,说明左轮摩擦力大或电机负载高,将左轮占空比加 20~30 即可。然后再调急弯参数,观察入弯时车头是否冲出白线,冲出就是内侧减速不够狠或打晚了。这里我建议用“先慢后快”的调法,把直线占空比降到 400,等急弯能稳定过再逐步升回 550。
3.4 编码器测速在这个阶段可以先不做
热词里频繁出现“单片机小车测速”“编码器测速”,但对循迹小车来说,编码器不是启动阶段必须的。开环 PWM 控制的问题是电池电压随放电下降,同样占空比下电机会越跑越慢,弯道参数会逐渐失效。如果实验报告需要有“速度闭环”这一节,可以用霍尔编码器加定时器输入捕获来测轮速,然后用 PID 把速度稳住。但这是第二个迭代版本的内容,先把开环跑通、拿到一组稳定的占空比参数,再谈闭环,代码复杂度会低很多。
4. 跑得稳不等于跑得快:阈值自适应与转向收敛
4.1 环境光变化下的动态阈值算法
红外对管最怕的不是黑线不明显,而是环境光变化。早上十点的阳光和傍晚五点的夕阳,会让同一块白地的反射电平差 0.5V 以上。固定电压阈值只在一段时间内有效,拉上窗帘和拉开窗帘完全是两种表现。常见的做法是用滑动窗口动态求阈值:单片机定期采样红外对管的模拟量(如果是 STM32 可以直接用 ADC),维护最近 N 个采样值的数组,取最大值和最小值的中间值作为当前判定阈值。
#define SAMPLE_SIZE 10 uint16_t samples[SAMPLE_SIZE]; uint8_t sample_index = 0; // 更新滑动窗口并返回当前阈值 uint16_t update_threshold(uint16_t current_value) { uint16_t max_val = 0, min_val = 0xFFFF; uint8_t i; samples[sample_index] = current_value; sample_index = (sample_index + 1) % SAMPLE_SIZE; for (i = 0; i < SAMPLE_SIZE; i++) { if (samples[i] > max_val) max_val = samples[i]; if (samples[i] < min_val) min_val = samples[i]; } // 阈值取中间值,也可用 (max+min)/2 return (max_val + min_val) / 2; }这段代码的计算开销很小,10 个元素的扫描在 72MHz 主频下不超过几微秒,不会影响控制周期。这里需要说明的是窗口大小的选择:窗口太小(比如 3),阈值会跟着瞬时噪声乱跳;窗口太大(比如 50),光线变化时阈值响应太慢,车已经跑偏了还没更新过来。10 到 20 是常用区间。滑窗法不要求传感器输出模拟量,数字量也可以做——把多次采样的 0/1 结果累加,超过一半算黑线,相当于一个软件滤波器,不过对光线变化的适应能力不如模拟量阈值方案。
4.2 积分分离 PID 用于转向修正量
开关量转向的局限是“要么全转、要么不转”,在直道上容易出现小幅度摇摆。引入 PID 后,传感器的状态不再只是“压线/没压线”,而是一个误差量。比如五路传感器,把每个传感器的位置加权成线形误差值:左侧传感器权重 -2,左侧中路 -1,中间为 0,右侧中路 +1,右侧 +2,误差越大说明偏离黑线越远。控制目标是让误差收敛到 0,输出量是左右轮的占空比修正量。
循迹小车建议用增量式 PID 或积分分离 PID,因为普通 PID 的积分项容易让车在过弯时产生严重的滞后超调,出弯后还在继续加大转向。积分分离的意思很简单:偏差大于某个阈值时关闭积分项,防止积分饱和;偏差小的时候再恢复积分,用来消除稳差。
float kp = 0.8f, ki = 0.08f, kd = 0.6f; float integral, last_error, output; float pid_calc(float error, float threshold) { float p_term, i_term, d_term; p_term = kp * error; if (error > threshold || error < -threshold) { // 偏差过大,关闭积分,防止过冲 integral = 0; i_term = 0; } else { integral += error; if (integral > 50) integral = 50; // 积分限幅 if (integral < -50) integral = -50; i_term = ki * integral; } d_term = kd * (error - last_error); last_error = error; output = p_term + i_term + d_term; if (output > 300) output = 300; // 输出限幅 if (output < -300) output = -300; return output; } // 假设基础占空比 base_duty = 550,误差左偏为负,右偏为正 // left_duty = base_duty - output; // right_duty = base_duty + output;上面的代码没有使用固定的 sin 或查表函数,所有计算都是浮点运算,在 STM32F103 上没有任何压力。但如果用的是 STC89C52,浮点 PID 计算会拖慢主循环,此时需要改成定点数或者把 kp、ki、kd 放大 100 倍用整数算。参数说明:kp决定转向力度,直道上太小会让车“找不到线”,太大又会在两个传感器之间来回弹跳;kd提供阻尼,抑制过冲;ki在循迹中通常只起微调作用,取kp的十分之一到二十分之一。
4.3 参数调整顺序:先 P 再 D 后 I
PID 调参有个直接的顺序,我一般按这个流程做,能节省不少时间。先把ki和kd设为 0,只保留kp,从小值慢慢加大,观察小车过弯时能不能跟上黑线。当车在线两侧出现规律性振荡时,说明kp已到边界,此时加入kd消除振荡。最后再加ki,并且只在小车直道上存在固定偏移时使用。
| 现象 | 调整方向 |
|---|---|
| 直道上左右连续摆动 | 增大 kd,微调 kp |
| 过弯转不过去,冲出线 | 增大 kp |
| 过弯之后仍偏向一侧 | 检查机械结构,增大 ki |
| 过弯猛烈抖动 | 减小输出限幅 |
| 动态阈值下仍误判 | 检查采样窗口长度和传感器高度 |
一个容易被忽略的细节是 PID 的控制周期。PWM 输出是 1kHz,但 PID 计算不需要那么频繁,10ms 到 20ms 计算一次就够了。如果每毫秒都算,微小的误差变化会被微分项放大,产生高频抖动。用定时器产生 100Hz 中断,在中断里做 PID 计算,主循环只负责显示或通信,结构会清爽很多。
5. 整车验证顺序与最有用的一个小技巧
5.1 从上电到连续过弯的测试顺序
拿到一套新组装的循迹小车,不要急着上赛道。按下面的顺序逐级验证,每一级过了再进下一级,能在十分钟内定位到具体问题。
- 传感器方向检测:用白纸和黑胶带分别放在传感器下方,确认输出电平与预期一致,若不正确,交换模块接线或改代码中的极性判断。
- 电机方向检测:左右轮分别短时给 30% 占空比,确认两个轮子的旋转方向都向前,否则交换电机线,之后不要动它。
- 直道测试:在 1 米长的黑线直道上运行,记录偏移方向和摆动幅度,只调直道基础占空比。
- 单弯测试:先跑 30 度缓弯,再跑 60 度中弯,最后加 90 度直角弯,每个弯道类型单独记录过弯成功率。
- 连续 S 弯测试:把 S 弯间距设置为小车长度的 1.5 倍,检验转向收敛性,这步是对 PID 参数的最终验收。
5.2 一个提高过弯成功率的实用技巧:弯前减速
我发现很多小车不是转向能力不够,而是速度太快导致传感器还没看到线就已经冲过去了。简单的办法是给传感器加“前瞻”:在车头前方 3~5cm 处再加一排传感器,提前感知弯道方向,单片机在处理时优先响应前瞻传感器的数据,相当于提前一个车身距离决定转向策略。这样做的机械成本只是一两个传感器模块,代码上只需在读传感器时多读几个 I/O 口,再用一个简单的优先级判断即可。
有一个在蓝桥杯单片机和 STC 单片机项目中很实用的做法:在循迹主循环里加一个“弯道检测”逻辑,当两路辅助传感器中的任何一路压线时,就把基础占空比从 550 降到 400,同时加大转向修正量。这本质上是给转向控制加了一个前馈路径,比单纯调 PID 更直接,也让整车的速度上限提升不少。
5.3 十字路口、起跑线和电池电压这三个边界
项目收尾前还需要处理几个容易翻车的边界场景。十字路口的特征是底部四路传感器同时压线,此时如果执行“全部压线=直行”的逻辑会直接冲过路口,但在需要计数转弯的场景下会出问题。常见做法是检测到四路同时为 1 时启动计数器,根据后续状态变化决定是直行还是转固定角度。起跑线检测则简单一些,只需加一个独立的光电开关或霍尔传感器,在车轮压到起跑线时触发停车信号即可。电池电压的影响用经验值处理:4 节 5 号电池供电时,新电池电压约 6.2V,占空比 500 对应的转速在电压降到 5.4V 时会下降约 15%,如果实验中换过电池,参数要重新校验。
本文还有配套的精品资源,点击获取