这个基于51单片机的智能小车项目,可以说是嵌入式开发的"Hello World"。作为一个从大学时代就开始折腾的老电子爱好者,我前前后后做过不下十种智能车方案。今天要分享的这个版本,特别适合刚入门的朋友练手——它集成了超声波避障、红外寻迹、温度采集、手动控制和LCD显示五大功能,代码量适中但涵盖面广,硬件成本不到200元就能搞定。
先说说这个小车的核心配置:
这个小车的硬件架构可以分为三个主要部分:
控制核心:STC89C52RC最小系统板,包含复位电路、晶振电路(11.0592MHz)和电源滤波电路。特别注意要在VCC和GND之间加104瓷片电容,这是我踩过的坑——早期版本没加这个电容,PWM控制电机时经常导致单片机复位。
传感器阵列:
驱动系统:
重要提示:电机驱动一定要用独立电源!早期我用同一个电源供电,电机启动瞬间的电压跌落会导致单片机异常复位。后来改用两节18650电池(7.4V)单独给电机供电,问题彻底解决。
车体我推荐用亚克力板制作的底盘,某宝20元左右就能买到配套的电机支架和万向轮。几个安装要点:
超声波模块要朝前安装,高度建议离地10-15cm,这个高度对20cm以上的障碍物检测最准确。
红外对管的安装特别讲究:
电机与轮子的装配要注意同心度,否则会出现行驶跑偏。我的经验是在电机轴和轮毂之间加一层电工胶布增加摩擦力。
超声波测距是避障的基础,HC-SR04模块的时序控制很关键。优化后的代码是这样的:
c复制// 超声波触发函数
void HC_SR04_Trigger() {
TRIG = 1;
_nop_(); _nop_(); _nop_(); _nop_(); _nop_(); // 10us延时
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
TRIG = 0;
}
// 获取距离函数
uint getDistance() {
uint distance;
HC_SR04_Trigger();
while(!ECHO); // 等待高电平开始
TR1 = 0; // 清零定时器1
TH1 = TL1 = 0;
TR1 = 1; // 启动定时器
while(ECHO); // 等待高电平结束
TR1 = 0; // 停止定时器
distance = (TH1<<8 | TL1) * 0.017; // 换算为厘米
// 数据滤波
if(distance > 200) distance = 0;
return distance;
}
这段代码有几个技术要点:
避障策略我采用的是分级响应:
寻迹的核心在于红外对管的状态检测和相应的控制策略。先看检测代码:
c复制// 红外状态检测
void checkIR() {
LeftIR = LEFT_IR; // 左传感器
RightIR = RIGHT_IR; // 右传感器
if(!LeftIR && !RightIR) { // 双黑线
goStraight();
}
else if(!LeftIR) { // 左偏
turnLeft(30); // 左转30ms
}
else if(!RightIR) { // 右偏
turnRight(30);
}
else { // 完全偏离
// 记忆上次转向方向
if(lastTurn == LEFT) turnLeft(50);
else turnRight(50);
}
}
实际调试中发现几个关键点:
DS18B20的单总线协议比较特殊,时序要求严格。经过多次优化后的温度读取函数:
c复制float read_temp() {
uint temp;
uchar LSB, MSB;
init_ds18b20();
write_byte(0xCC); // 跳过ROM
write_byte(0x44); // 启动转换
delay_ms(800); // 必须等待足够时间
init_ds18b20();
write_byte(0xCC);
write_byte(0xBE); // 读暂存器
LSB = read_byte();
MSB = read_byte();
temp = (MSB<<8) | LSB;
return temp * 0.0625; // 转换温度值
}
这里有几个坑要注意:
L298N的驱动逻辑相对简单,但有些细节需要注意:
c复制// 电机控制函数
void motorControl(uchar cmd) {
static uchar lastCmd = 0;
switch(cmd) {
case FORWARD:
P1 = 0x0F; // 两个电机正转
break;
case BACKWARD:
P1 = 0xF0; // 两个电机反转
break;
case LEFT:
P1 = 0x0A; // 左轮停,右轮转
break;
case RIGHT:
P1 = 0x05; // 右轮停,左轮转
break;
case STOP:
P1 = 0x00; // 全停
break;
}
// 防堵转延迟
if((lastCmd != cmd) && (cmd != STOP)) {
delay_ms(5);
}
lastCmd = cmd;
}
关键技巧:
手动控制采用最简单的按键方案,扩展性很好:
c复制void keyScan() {
if(KEY_LEFT == 0) {
delay_ms(10); // 消抖
if(KEY_LEFT == 0) {
motorControl(LEFT);
while(!KEY_LEFT); // 等待释放
}
}
// 其他按键类似...
}
进阶方案可以改用蓝牙遥控,只需要将按键检测替换为串口指令解析:
c复制void uartControl() {
if(RI) {
RI = 0;
switch(SBUF) {
case 'F': motorControl(FORWARD); break;
case 'B': motorControl(BACKWARD); break;
// 其他指令...
}
}
}
在51单片机上进行多任务管理,我采用时间片轮询的方式:
c复制void main() {
initAll(); // 初始化所有外设
while(1) {
if(timer10msFlag) {
timer10msFlag = 0;
// 10ms任务
keyScan();
refreshDisplay();
}
if(timer100msFlag) {
timer100msFlag = 0;
// 100ms任务
distance = getDistance();
temp = read_temp();
checkIR();
}
}
}
定时器配置:
c复制void timer0Init() {
TMOD |= 0x01; // 定时器0模式1
TH0 = 0xDC; // 10ms定时
TL0 = 0x00;
ET0 = 1; // 使能中断
TR0 = 1;
EA = 1; // 总中断
}
void timer0_isr() interrupt 1 {
static uchar count = 0;
TH0 = 0xDC;
TL0 = 0x00;
timer10msFlag = 1;
if(++count >= 10) {
count = 0;
timer100msFlag = 1;
}
}
LCD1602的显示刷新需要平衡实时性和稳定性:
c复制void refreshDisplay() {
static uchar count = 0;
char buf[16];
if(++count >= 5) { // 每50ms刷新一次
count = 0;
// 第一行显示距离
sprintf(buf, "Dist:%3dcm", distance);
lcdWriteString(0, 0, buf);
// 第二行显示温度
sprintf(buf, "Temp:%2.1fC", temp);
lcdWriteString(0, 1, buf);
}
}
几个优化点:
可能原因及解决方法:
调试技巧:
典型问题排查:
这个基础框架可以扩展很多有趣的功能:
我在实际项目中加入的温度联动控制就是个很好的例子:
c复制void tempControl() {
if(temp > 30.0) { // 温度过高
speedPercent = 110; // 加速10%"散热"
} else {
speedPercent = 100;
}
setMotorSpeed(speedPercent);
}
这个小功能在项目答辩时让评委眼前一亮,虽然实际散热效果有限,但展示了传感器数据与执行机构的联动思路。