1. 项目概述与硬件准备
大家好,今天我想分享一个特别实用的51单片机实战项目——用PWM精准控制小车运动和转向角度。这个项目特别适合刚接触嵌入式开发的朋友,我会把每个步骤都讲得清清楚楚,让你能轻松上手。
咱们这个项目的核心是通过51单片机生成PWM信号,控制L298N驱动模块来调节直流电机的速度,同时用SG-90舵机实现精确的角度转向。PWM就像是给电机和舵机发送的"密码指令",通过调整脉冲的宽度,就能让它们按照我们的想法来工作。
需要准备的硬件材料:
- 51单片机开发板(推荐STC89C52,便宜又好用)
- L298N电机驱动模块(双桥结构,能同时驱动两个电机)
- MG310直流电机 ×2(带编码器,方便后期做闭环控制)
- SG-90舵机 ×1(180度旋转范围,精度足够)
- 小车底盘和轮子套装
- 电源适配器(建议5V/2A以上)
- 杜邦线若干(公对公、公对母都要准备)
工具和软件:
- Keil uVision开发环境(经典好用)
- STC-ISP下载程序
- 万用表(检查接线必备)
- 螺丝刀等机械组装工具
我在实际做这个小车的时候,发现电源部分特别重要。L298N和舵机都是耗电大户,如果电源功率不足,会出现电机转速不稳或者舵机抖动的情况。建议给电机驱动和单片机分别供电,我用的是两节18650电池给L298N供电,USB线单独给单片机供电,这样稳定性最好。
2. PWM控制原理深入解析
PWM(脉冲宽度调制)是这个项目的核心技术,理解它的工作原理非常重要。我用个简单的比喻来解释:就像用开关控制灯泡的亮度,快速开关开关,调整亮的时间和不亮的时间比例,就能让灯泡呈现不同的亮度。PWM也是同样的道理,通过调整高电平和低电平的时间比例,来控制电机的平均电压。
PWM的三个关键参数:
- 频率:每秒钟脉冲信号的周期数。对于电机控制,一般用1k-10kHz;舵机需要50Hz(周期20ms)
- 占空比:高电平时间占整个周期的百分比
- 分辨率:占空比可调节的精细程度
对于SG-90舵机,控制信号要求非常精确。它需要50Hz的PWM信号,也就是周期20ms。其中高电平时间在0.5ms到2.5ms之间变化,对应着0°到180°的旋转角度。具体对应关系是:
- 0.5ms高电平 → 0°位置
- 1.5ms高电平 → 90°位置
- 2.5ms高电平 → 180°位置
在实际调试中,我发现不同厂家的舵机可能会有细微差异。有的舵机0.5ms对应0°,但有些可能是0.6ms,这就需要我们实际测试校准。最好的方法是用示波器观察波形,如果没有示波器,也可以通过实验慢慢调整找到最佳参数。
51单片机内部有三个定时器,我们可以充分利用它们。定时器0和1用来产生电机控制的PWM,定时器2专门用于舵机控制。这样分工明确,不会相互干扰。
3. 硬件连接详细指南
硬件连接是项目成功的基础,我刚开始做的时候就在这踩过坑。下面我把每个连接细节都说明白:
L298N模块连接:
- ENA、ENB:接单片机的PWM输出引脚(我用的P1.0和P1.1)
- IN1-IN4:接普通IO口控制电机方向(我用的P2.0-P2.3)
- OUT1-OUT4:接两个直流电机
- 电源输入:接外部5-12V电源正极
- GND:与单片机共地
SG-90舵机连接:
- 红色线(VCC):接5V电源
- 棕色线(GND):接地
- 橙色线(信号):接单片机P1.2引脚
重要提示:
- 一定要共地!所有模块的GND都要连在一起,否则信号会不稳定
- 电机电源和单片机电源最好分开,避免电机启动时的电压波动影响单片机
- 在线材选择上,电机电源线要用粗一点的,信号线可以用细线
- 建议先焊接好再插杜邦线,避免接触不良
我在连接时遇到过一个问题:舵机有时会莫名其妙地抖动。后来发现是电源问题,舵机工作时电流能达到500mA,如果电源内阻太大,就会导致电压波动。解决办法是在舵机电源引脚并联一个100μF的电解电容,效果立竿见影。
接线完成后,一定要用万用表检查一遍:
- 检查电源是否短路
- 确认电压值是否正确
- 测试各连接点是否导通
4. 定时器配置与中断设置
51单片机的定时器是我们生成PWM的关键。我习惯用定时器0和1模式2(8位自动重装模式),这样配置简单,中断频率稳定。
定时器0配置(电机PWM):
void PWM_Init() {
// 定时器0配置
TMOD &= 0xF0; // 清除TIMER0设置
TMOD |= 0x02; // 设置TIMER0为模式2
TH0 = 256 - 100; // 初始占空比设置
TL0 = 256 - 100; // 100对应约40%占空比
ET0 = 1; // 使能TIMER0中断
TR0 = 1; // 启动TIMER0
// 定时器1配置
TMOD &= 0x0F; // 清除TIMER1设置
TMOD |= 0x20; // 设置TIMER1为模式2
TH1 = 256 - 100; // 初始值
TL1 = 256 - 100; // 同上
ET1 = 1; // 使能TIMER1中断
TR1 = 1; // 启动TIMER1
EA = 1; // 开启总中断
}
定时器2配置(舵机控制):
void Timer2_Init() {
T2CON = 0x00; // Timer2 16位自动重装模式
TH2 = 0xFF; // 设置初始值
TL2 = 0xA4; // 约20ms周期
RCAP2H = 0xFF; // 重装值高字节
RCAP2L = 0xA4; // 重装值低字节
ET2 = 1; // 使能Timer2中断
TR2 = 1; // 启动Timer2
EA = 1; // 开启总中断
}
在实际编程中,中断服务函数的编写很重要。以定时器0中断为例:
void Timer0_ISR() interrupt 1 {
static unsigned int pwm_count = 0;
pwm_count++;
if(pwm_count < duty_cycle) {
MOTOR_PIN = 1; // 输出高电平
} else {
MOTOR_PIN = 0; // 输出低电平
}
if(pwm_count >= 100) {
pwm_count = 0; // 重置计数器
}
}
这里我设置了一个100份的分辨率,duty_cycle取值0-100对应0%-100%的占空比。这种软件PWM的好处是灵活,可以同时产生多路PWM,缺点是会占用CPU资源。
5. 电机控制函数实现
电机控制主要包括速度控制和方向控制。L298N模块有使能端和方向控制端,我们需要协调好这两者的关系。
速度控制函数:
void Set_Motor_Speed(unsigned char motor, unsigned char speed) {
if(motor == MOTOR_A) {
duty_cycle_A = speed; // 设置占空比
} else {
duty_cycle_B = speed;
}
}
方向控制函数:
void Set_Motor_Direction(unsigned char motor, unsigned char direction) {
if(motor == MOTOR_A) {
if(direction == FORWARD) {
IN1 = 1;
IN2 = 0;
} else {
IN1 = 0;
IN2 = 1;
}
} else {
if(direction == FORWARD) {
IN3 = 1;
IN4 = 0;
} else {
IN3 = 0;
IN4 = 1;
}
}
}
在实际使用中,我发现电机的启动需要特别注意。如果直接给100%占空比,启动电流会很大,可能烧坏驱动芯片。我通常采用软启动策略:
void Soft_Start(unsigned char motor, unsigned char target_speed) {
unsigned char current_speed = 0;
while(current_speed < target_speed) {
Set_Motor_Speed(motor, current_speed);
current_speed += 5; // 每次增加5%
Delay_ms(50); // 间隔50ms
}
}
还有一个实用技巧是刹车功能。当需要快速停止时,可以将两个方向引脚都设为高电平:
void Motor_Brake(unsigned char motor) {
if(motor == MOTOR_A) {
IN1 = 1;
IN2 = 1;
} else {
IN3 = 1;
IN4 = 1;
}
Set_Motor_Speed(motor, 0); // 同时速度设为0
}
6. 舵机角度精确控制
SG-90舵机的控制比电机要精细得多,需要更精确的定时。我通常用定时器2来专门处理舵机控制,确保20ms周期的准确性。
角度设置函数:
void Set_Servo_Angle(unsigned char angle) {
// 将角度转换为脉冲宽度
unsigned int pulse_width = 500 + angle * 11.11;
servo_pulse = pulse_width / 10; // 转换为定时器计数单位
}
在中断服务函数中实现脉冲生成:
void Timer2_ISR() interrupt 5 {
static unsigned int pulse_count = 0;
TF2 = 0; // 清除中断标志
pulse_count++;
if(pulse_count <= servo_pulse) {
SERVO_PIN = 1;
} else {
SERVO_PIN = 0;
}
if(pulse_count >= 200) { // 20ms周期
pulse_count = 0;
}
}
在实际使用中,我发现舵机有几个需要特别注意的地方:
- 角度校准:每个舵机都有细微差异,需要实际测试校准。我通常写一个校准函数:
void Servo_Calibrate() {
Set_Servo_Angle(0); // 理论上0°
Delay_ms(1000);
// 观察实际角度,如有偏差可调整参数
Set_Servo_Angle(90); // 理论上90°
Delay_ms(1000);
Set_Servo_Angle(180); // 理论上180°
Delay_ms(1000);
}
- 运动平滑处理:直接让舵机从一个角度转到另一个角度会产生抖动,可以添加平滑过渡:
void Smooth_Rotate(unsigned char start_angle, unsigned char end_angle) {
int current_angle = start_angle;
int step = (end_angle > start_angle) ? 1 : -1;
while(current_angle != end_angle) {
current_angle += step;
Set_Servo_Angle(current_angle);
Delay_ms(20); // 控制转动速度
}
}
- 功耗管理:舵机保持位置时仍然消耗电流,如果不需要保持力矩,可以定期关闭信号。
7. 小车运动集成测试
当电机和舵机都调试好后,就可以进行集成测试了。我通常按照以下步骤进行:
基本运动测试:
void Test_Basic_Movement() {
// 前进测试
Set_Motor_Direction(MOTOR_A, FORWARD);
Set_Motor_Direction(MOTOR_B, FORWARD);
Soft_Start(MOTOR_A, 80);
Soft_Start(MOTOR_B, 80);
Delay_ms(2000);
// 转向测试
Set_Servo_Angle(45); // 左转45°
Delay_ms(1000);
Set_Servo_Angle(135); // 右转45°
Delay_ms(1000);
Set_Servo_Angle(90); // 回正
// 后退测试
Set_Motor_Direction(MOTOR_A, BACKWARD);
Set_Motor_Direction(MOTOR_B, BACKWARD);
Delay_ms(2000);
// 停止
Set_Motor_Speed(MOTOR_A, 0);
Set_Motor_Speed(MOTOR_B, 0);
}
高级运动模式: 在实际项目中,我还会实现一些更复杂的运动模式:
- 定速巡航:
void Speed_Cruise(unsigned char target_speed) {
unsigned char current_speed_A = Get_Current_Speed(MOTOR_A);
unsigned char current_speed_B = Get_Current_Speed(MOTOR_B);
// PID调节保持速度恒定
while(1) {
Adjust_Speed_PID(MOTOR_A, target_speed);
Adjust_Speed_PID(MOTOR_B, target_speed);
Delay_ms(100);
}
}
- 精确转向控制:
void Precise_Turn(unsigned char angle, unsigned int radius) {
// 根据转向角度和半径计算左右轮速差
float ratio = Calculate_Turn_Ratio(angle, radius);
unsigned char left_speed = base_speed * (1 - ratio);
unsigned char right_speed = base_speed * (1 + ratio);
Set_Motor_Speed(MOTOR_A, left_speed);
Set_Motor_Speed(MOTOR_B, right_speed);
Set_Servo_Angle(angle);
}
- 避障模式:
void Obstacle_Avoidance() {
while(1) {
if(Check_Obstacle(FRONT)) {
// 检测到前方障碍物
Set_Motor_Speed(MOTOR_A, 0);
Set_Motor_Speed(MOTOR_B, 0);
Delay_ms(500);
// 选择转向方向
if(!Check_Obstacle(LEFT)) {
Turn_Left(45);
} else if(!Check_Obstacle(RIGHT)) {
Turn_Right(45);
} else {
Move_Backward();
}
}
Delay_ms(100);
}
}
在集成测试中,最常见的问题是电机干扰舵机。表现为舵机在电机启动时抖动,这是因为电源电压被拉低。我的解决办法是:
- 电机电源和控制系统电源完全分离
- 在电源入口处加大容量电容(1000μF以上)
- 软件上错开电机启动时间
另一个常见问题是小车走不直,即使两个电机设置相同速度也会偏。这是因为电机本身有差异,需要软件补偿:
void Motor_Compensation() {
// 通过实验测得的补偿值
const unsigned char comp_A = 5; // A电机需要增加5%的占空比
const unsigned char comp_B = 0;
Set_Motor_Speed(MOTOR_A, target_speed + comp_A);
Set_Motor_Speed(MOTOR_B, target_speed + comp_B);
}
8. 调试技巧与常见问题解决
在做这个项目的过程中,我积累了不少调试经验,分享给大家:
硬件调试技巧:
- 先用万用表检查所有电源电压是否正确
- 单独测试每个模块是否正常工作
- 注意观察LED指示灯状态,L298N模块上有电源和使能指示灯
- 用手轻轻转动电机,感受阻力是否正常
软件调试方法:
- 使用串口打印调试信息:
void UART_Init() {
SCON = 0x50; // 8位数据,可变波特率
TMOD |= 0x20; // 定时器1模式2
TH1 = 0xFD; // 9600波特率
TL1 = 0xFD;
TR1 = 1; // 启动定时器1
}
void Print_Debug(char* message) {
while(*message) {
SBUF = *message++;
while(!TI);
TI = 0;
}
}
- 添加状态指示灯:
void Status_Indicator() {
static unsigned int counter = 0;
if(++counter >= 500) {
counter = 0;
LED = !LED; // 闪烁指示灯
}
}
常见问题及解决方案:
- 电机不转:
- 检查使能引脚是否设置正确
- 测量电机两端电压
- 检查方向控制引脚电平
- 舵机抖动:
- 检查电源是否足够
- 添加滤波电容
- 检查信号线连接是否可靠
- PWM输出不稳定:
- 检查定时器配置是否正确
- 中断优先级设置是否冲突
- 是否有其他中断干扰
- 小车运动不平稳:
- 检查机械结构是否牢固
- 轮胎是否打滑
- 电机安装是否对称
我在调试中发现一个很有用的技巧:用手机慢动作视频拍摄小车运动,可以清楚地看到轮胎转动情况和舵机运动过程,很多问题通过慢动作回放就能发现。
另一个建议是做好版本管理,每次修改都保存一个备份。我曾经因为一次改动了太多地方,出现问题后找不到原因,最后只能重写代码。现在我会这样管理版本:
// 版本1.0:基础功能
// 版本1.1:添加软启动
// 版本1.2:优化转向控制
// 版本1.3:添加调试功能
最后提醒大家,静电防护很重要。特别是在干燥季节,人体静电可能损坏单片机。操作前最好触摸接地金属释放静电,工作台面铺防静电垫。

402

被折叠的 条评论
为什么被折叠?



