用51单片机做智能小车可以说是电子爱好者的经典入门项目了。我第一次做这个小车是在大学电子设计课上,当时就被这种"赋予硬件生命"的感觉深深吸引。这个项目看似简单,但涵盖了单片机开发的核心技能点:GPIO控制、定时器中断、PWM调速、传感器数据采集、多任务调度等。
现在常见的智能小车功能越来越丰富,但核心模块无非几个:电机驱动、循迹、避障、无线控制和状态显示。我建议初学者先从基础功能做起,比如先让小车动起来,再加上循迹功能,逐步叠加模块。这样调试起来更有针对性,不会因为问题太多而手忙脚乱。
做这个项目你需要准备:
要让小车动起来,首先得搞定电机驱动。我最早用的是L9110S驱动芯片,后来换成了更常用的L298N。L298N可以驱动两个直流电机,支持正反转和PWM调速。
接线时要注意:
c复制// 电机控制引脚定义
sbit IN1 = P1^0;
sbit IN2 = P1^1;
sbit IN3 = P1^2;
sbit IN4 = P1^3;
// 电机正转函数
void MotorA_Forward() {
IN1 = 1;
IN2 = 0;
}
// 电机反转函数
void MotorA_Backward() {
IN1 = 0;
IN2 = 1;
}
PWM调速是控制小车速度的关键。51单片机可以通过定时器中断实现PWM。我一般用定时器0工作在模式1(16位定时器),产生固定周期的PWM信号。
c复制// PWM相关变量
unsigned char PWM_Duty_A = 0; // 电机A占空比
unsigned char PWM_Duty_B = 0; // 电机B占空比
unsigned char PWM_Counter = 0; // PWM计数器
// 定时器0初始化
void Timer0_Init() {
TMOD &= 0xF0; // 设置定时器0为模式1
TMOD |= 0x01;
TH0 = 0xFF; // 定时器初值,决定PWM频率
TL0 = 0x9C;
ET0 = 1; // 开启定时器0中断
EA = 1; // 开启总中断
TR0 = 1; // 启动定时器0
}
// 定时器0中断服务函数
void Timer0_ISR() interrupt 1 {
TH0 = 0xFF; // 重新装载初值
TL0 = 0x9C;
PWM_Counter++;
if(PWM_Counter >= 100) PWM_Counter = 0;
// 电机A PWM控制
if(PWM_Counter < PWM_Duty_A) {
MotorA_Forward(); // 高电平期间电机正转
} else {
MotorA_Stop(); // 低电平期间电机停止
}
// 电机B PWM控制同理
}
调试PWM时有个小技巧:先用LED观察PWM波形,确认占空比变化正常后再接电机。这样可以避免因程序问题导致电机异常转动。
循迹模块我推荐TCRT5000红外传感器,价格便宜效果也不错。它的工作原理很简单:红外发射管发射光线,遇到白色表面反射,黑色表面吸收。接收管根据反射光强度输出高低电平。
通常在小车底部安装3-5个TCRT5000,排列成一条直线。我的小车用了3个传感器,中间一个用于检测黑线,左右两个用于修正方向。
c复制// 循迹传感器引脚定义
sbit LeftSensor = P2^0;
sbit MiddleSensor = P2^1;
sbit RightSensor = P2^2;
// 循迹控制逻辑
void Track_Control() {
if(MiddleSensor == 0) { // 中间传感器检测到黑线
PWM_Duty_A = 50; // 两电机相同速度直行
PWM_Duty_B = 50;
}
else if(LeftSensor == 0) { // 左边传感器检测到黑线
PWM_Duty_A = 30; // 左轮减速,小车右转
PWM_Duty_B = 70;
}
else if(RightSensor == 0) { // 右边传感器检测到黑线
PWM_Duty_A = 70; // 右轮减速,小车左转
PWM_Duty_B = 30;
}
}
基础循迹容易产生"抖动"现象,小车会左右摇摆。我通过以下方法优化:
c复制// 简易PID循迹算法
int last_error = 0;
int integral = 0;
void PID_Track() {
int error = 0;
// 计算当前偏差
if(LeftSensor == 0) error = -2;
else if(MiddleSensor == 0) error = 0;
else if(RightSensor == 0) error = 2;
// PID计算
integral += error;
int derivative = error - last_error;
int output = Kp*error + Ki*integral + Kd*derivative;
last_error = error;
// 应用控制量
PWM_Duty_A = 50 - output;
PWM_Duty_B = 50 + output;
}
调试时建议先用串口打印传感器数值和PID计算过程,方便观察算法效果。
超声波避障我用的是常见的HC-SR04模块,测距范围2cm-400cm,精度约3mm。它的工作原理是:
c复制// 超声波模块引脚定义
sbit Trig = P2^5;
sbit Echo = P2^6;
// 测距函数
float Get_Distance() {
unsigned int time;
float distance;
Trig = 1; // 触发信号
Delay10us();
Trig = 0;
while(!Echo); // 等待回波开始
TR0 = 1; // 启动定时器计时
while(Echo); // 等待回波结束
TR0 = 0; // 停止计时
time = TH0*256 + TL0; // 计算总时间
distance = time * 0.017; // 距离=时间*声速/2
TH0 = 0; // 重置定时器
TL0 = 0;
return distance;
}
简单避障可以这样实现:
我后来加了个舵机让超声波模块可以旋转,这样测距更灵活:
c复制// 舵机控制函数
void Servo_Angle(unsigned char angle) {
unsigned int pulse = 500 + angle * 10; // 计算脉冲宽度
PWM = 1; // 输出高电平
DelayUs(pulse); // 延时脉冲宽度
PWM = 0; // 输出低电平
DelayMs(20 - pulse/1000); // 补足20ms周期
}
// 扫描周围环境
void Scan_Environment() {
float left_dist, right_dist;
Servo_Angle(30); // 转向左侧
DelayMs(500);
left_dist = Get_Distance();
Servo_Angle(150); // 转向右侧
DelayMs(500);
right_dist = Get_Distance();
Servo_Angle(90); // 回正
if(left_dist > right_dist) {
Turn_Left(); // 左侧空间更大,向左转
} else {
Turn_Right(); // 右侧空间更大,向右转
}
}
HC-05蓝牙模块支持AT指令配置,我通常这样设置:
蓝牙与单片机通过串口通信,51单片机需要用定时器1做波特率发生器:
c复制// 串口初始化
void UART_Init() {
TMOD &= 0x0F; // 设置定时器1为模式2
TMOD |= 0x20;
TH1 = 0xFD; // 9600波特率
TL1 = 0xFD;
TR1 = 1; // 启动定时器1
SCON = 0x50; // 串口模式1,允许接收
ES = 1; // 开启串口中断
EA = 1; // 开启总中断
}
// 串口中断服务函数
void UART_ISR() interrupt 4 {
if(RI) {
RI = 0; // 清除接收标志
char cmd = SBUF; // 获取接收数据
switch(cmd) {
case 'F': Motor_Forward(); break;
case 'B': Motor_Backward(); break;
case 'L': Turn_Left(); break;
case 'R': Turn_Right(); break;
case 'S': Motor_Stop(); break;
}
}
}
测速我推荐使用带编码器的电机,通过在固定时间内计数脉冲数来计算速度。编码器输出接单片机外部中断引脚。
c复制// 编码器计数
unsigned int pulse_count = 0;
// 外部中断0服务函数
void EX0_ISR() interrupt 0 {
pulse_count++; // 每个脉冲计数一次
}
// 定时器1中断计算速度
void Timer1_ISR() interrupt 3 {
static unsigned int speed_cmps;
speed_cmps = pulse_count * 3.14 * 6.5 / 20; // 计算速度(cm/s)
pulse_count = 0; // 重置计数器
// OLED显示速度
OLED_ShowNum(4, 4, speed_cmps, 3, 16);
}
OLED显示我用的是SSD1306驱动的0.96寸屏,通过I2C接口连接。显示效果清晰而且功耗低。
智能小车需要同时处理多个任务:电机控制、传感器读取、通信等。我的解决方案是:
c复制void main() {
System_Init(); // 系统初始化
while(1) {
if(flag_10ms) { // 10ms任务标志
flag_10ms = 0;
Track_Control(); // 循迹控制
Avoid_Obstacle(); // 避障控制
}
if(flag_100ms) { // 100ms任务标志
flag_100ms = 0;
Update_Display(); // 更新显示
Send_SensorData();// 发送传感器数据
}
}
}
// 定时器0中断服务函数
void Timer0_ISR() interrupt 1 {
static unsigned int count = 0;
TH0 = 0xFC; // 1ms定时
TL0 = 0x18;
count++;
if(count % 10 == 0) flag_10ms = 1;
if(count % 100 == 0) flag_100ms = 1;
if(count >= 1000) count = 0;
}
电机不转:
循迹不准确:
蓝牙连接不稳定:
测距数据跳动大:
做智能小车项目最重要的是耐心调试。我建议每添加一个新功能就单独测试,确保没问题再整合。遇到问题时可以用串口打印调试信息,或者用LED指示程序运行状态。