第一次接触51单片机循迹小车时,我完全被那些密密麻麻的代码和电路搞晕了。但经过几个项目的实战后,我发现这其实是个特别适合嵌入式新手的练手项目。循迹小车就像是一个会自己找路的智能玩具车,它能沿着地面上的黑线自动行驶,不需要人为控制。
这个小车的核心部件其实很简单:一块51单片机开发板、几个红外传感器、两个电机加上轮子,再配个电池组就行了。传感器负责"看"地面上的黑线,单片机就像小车的大脑,根据传感器传来的信号控制电机转动。你可能觉得这听起来很复杂,但跟着我做一遍就会发现,整个过程就像搭积木一样有趣。
我建议初学者先从理解硬件连接开始。把红外传感器接到单片机的IO口,电机驱动模块接上PWM引脚。传感器检测到黑线时会输出高电平,白底则是低电平(有些模块可能相反,这个后面会详细说)。单片机通过不断检查这些传感器的状态,就能判断小车是否偏离轨道,然后调整左右轮速度来纠正方向。
先来看源码开头的引脚定义部分,这是整个项目的基础配置:
c复制sbit IN1 = P1^4; // 电机驱动引脚
sbit IN2 = P1^3;
sbit IN3 = P1^2;
sbit IN4 = P1^1;
sbit ENA = P1^5; // 左轮PWM调速
sbit ENB = P1^0; // 右轮PWM调速
sbit Lsen = P2^7; // 左侧传感器
sbit Rsen = P2^4; // 右侧传感器
这里定义了控制两个电机的引脚,以及两个红外传感器的输入引脚。IN1-IN4控制电机转向,ENA和ENB用于PWM调速。我在第一次接线时就犯过错,把左右电机的引脚接反了,导致小车转向完全相反。建议大家在焊接前先用杜邦线测试,确认每个引脚功能正确。
小车的运动全靠电机控制,源码中定义了四个基本运动函数:
c复制void go_ahead() { // 直行
compareA = 80; compareB = 80;
IN1 = 0; IN2 = 1; IN3 = 1; IN4 = 0;
}
void turn_left() { // 左转
compareA = 80; compareB = 80;
IN1 = 0; IN2 = 1; IN3 = 0; IN4 = 1;
}
void turn_right() { // 右转
compareA = 80; compareB = 80;
IN1 = 1; IN2 = 0; IN3 = 1; IN4 = 0;
}
void stop() { // 停止
ENA = 0; ENB = 0;
}
这里有个细节需要注意:compareA和compareB的值都是80,表示PWM占空比为80%。在实际调试中,我发现左右轮转速即使设置相同,实际也可能不一样,这是因为电机本身存在个体差异。后来我通过调整这两个值,让小车能够真正走直线。
循迹的核心在于正确处理传感器信号。源码中的xunji()函数就是大脑:
c复制void xunji() {
if(Lsen == 1 && Rsen == 0) { // 左黑右白
turn_left();
}
else if(Lsen == 0 && Rsen == 1) { // 左白右黑
turn_right();
}
else if(a == 0 && Lsen == 1 && Rsen == 1) { // T字路口
turn_left();
a++;
}
// 其他情况处理...
}
这里有个关键点:Lsen和Rsen的值取决于你的传感器模块。有些模块检测到黑线输出1,有些则输出0。我第一次调试时就栽在这个问题上,小车完全反着跑。后来用万用表测了传感器输出才搞清楚。
遇到T字路口或十字路口时,小车需要特殊处理。源码中使用了一个状态变量a来记录路口通过状态:
c复制else if(a == 1 && Lsen == 1 && Rsen == 1) {
go_ahead(); // 直行一小段
a++;
}
else if(a == 2 && Lsen == 1 && Rsen == 1) {
stop();
Delay(200);
turn_right();
Delay(500); // 这个值需要根据实际情况调整
stop();
Delay(1000);
}
在实际测试中,我发现Delay的时间参数非常关键。500ms对我的小车来说转弯角度刚好,但换了不同轮胎后,这个值就需要重新调整。建议在调试时准备秒表,记录下不同延时对应的转弯角度。
PWM调速是控制小车速度的关键,源码中使用定时器0来生成PWM波:
c复制void Timer0_Init(void) {
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x01;
TL0 = 0x9C; // 100us定时初值
TH0 = 0xFF;
TF0 = 0; // 清除标志
TR0 = 1; // 启动定时器
EA = 1; // 开总中断
ET0 = 1; // 开定时器0中断
PT0 = 0;
}
这里配置定时器每100us产生一次中断。我刚开始不理解为什么选这个值,后来明白这是为了PWM频率在可听范围之外(约1kHz),避免电机发出刺耳的噪音。
PWM的具体实现是在中断服务函数中完成的:
c复制void Timer0_Routine() interrupt 1 {
TL0 = 0x9C; TH0 = 0xFF; // 重装初值
counter++;
if(counter == 100) counter = 0; // 10ms周期
if(counter > compareA) ENA = 0;
else ENA = 1;
if(counter > compareB) ENB = 0;
else ENB = 1;
}
这个函数每100us执行一次,counter从0累加到100,形成一个10ms的PWM周期。compareA和compareB的值决定了高电平的持续时间,也就是电机转速。比如compareA=80,表示80%的时间输出高电平,电机就以80%的功率运转。
调试循迹小车时,我遇到过几个典型问题:
小车原地转圈:通常是左右传感器接反了,或者电机极性接反。检查传感器输出和电机转向是否正确。
小车走S形路线:说明PID参数需要调整。虽然源码中使用的是简单逻辑控制,但可以尝试减小转弯时的速度差。
过弯时冲出轨道:增加转弯时的延时,或者降低转弯速度。我的经验值是compareA/B降到60左右转弯更稳。
遇到路口无法正确识别:调整传感器高度,确保两个传感器同时检测到黑线时才判断为路口。
经过多次调试,我总结出几个优化点:
电源滤波:在电机电源端并联一个大电容(我用的470μF),可以有效减少电机干扰导致的单片机复位。
传感器校准:不同地面反光率不同,建议用可调电阻调节传感器灵敏度,确保在白底和黑线上输出稳定。
速度分级:直行时用高速(compare=80),转弯时切到低速(compare=50),这样过弯更稳。
增加状态指示:我在P2口接了LED,不同状态亮不同灯,调试时非常直观。