【51单片机】直流电机PWM调速实战:从驱动电路到闭环控制
1. 直流电机驱动基础与硬件选型
第一次玩直流电机时,我直接拿杜邦线把电机接在51单片机的IO口上,结果电机纹丝不动,还差点烧了芯片。这个教训让我明白:驱动电路是电机控制的第一道门槛。常见的直流电机工作电压通常在3-6V,但启动电流可能高达500mA-1A,远超单片机IO口20mA的驱动能力。
市面上的驱动方案主要分两种:直接驱动和H桥驱动。直接驱动适合小功率电机,用ULN2003D这类达林顿阵列芯片就能搞定。它的内部结构就像七个"电流放大器",每个通道都包含达林顿管和续流二极管。我实测过用ULN2003D驱动5V/200mA的小电机,芯片只是微微发热,完全不需要散热片。
对于需要正反转的中功率电机(比如智能小车),L298N这类H桥驱动是更好的选择。记得有次做循迹小车,用L298N驱动两个130电机,即使堵转电流达到800mA,驱动芯片依然稳如泰山。不过要注意,H桥的使能端必须接PWM信号才能调速,单纯给高低电平只会让电机全速转动或刹车。
硬件连接有个容易踩的坑:务必给电机加滤波电容。我在实验室用示波器观察过,电机启停时会在电源线上产生高达20V的尖峰电压。最简单的解决方法是在电机两端并联一个100μF电解电容加0.1μF陶瓷电容组合,这个技巧让我少烧了三块单片机。
2. PWM调速原理与定时器配置
PWM调速就像快速开关水龙头调节水流大小。我做过对比实验:用普通IO口高低电平控制电机,转速只有"全速"和"停止"两档;而改用PWM后,转速可以像音量旋钮一样平滑调节。关键参数有三个:
- 频率:通常1kHz-10kHz,太低电机会抖动,太高驱动芯片会发热
- 占空比:0-100%对应转速从停止到全速
- 分辨率:8位(256级)够用,12位(4096级)更精细
51单片机没有硬件PWM模块,但用定时器模拟也很方便。以STC89C52为例,配置定时器0的代码我优化过好几个版本,最终这个最稳定:
void Timer0_Init() //1kHz PWM@11.0592MHz { TMOD &= 0xF0; //不清高四位 TMOD |= 0x01; //模式1 TL0 = 0xA4; //100us中断 TH0 = 0xFF; ET0 = 1; //开中断 EA = 1; TR0 = 1; //启动定时器 }中断服务程序里实现PWM的核心逻辑:
unsigned char pwmDuty = 50; //初始50%占空比 void Timer0_ISR() interrupt 1 { static unsigned char count = 0; TL0 = 0xA4; //重装初值 TH0 = 0xFF; Motor = (count < pwmDuty) ? 1 : 0; if(++count >= 100) count = 0; }实测发现,中断服务程序越短越好。曾经我在中断里加了数码管扫描,结果PWM波形出现明显抖动。后来改用状态机在主循环处理显示,波形就稳定了。
3. 交互设计:按键与数码管
好的交互设计能让调试效率翻倍。我习惯用四按键+四位数码管的经典组合:
- 按键1:增加转速
- 按键2:减少转速
- 按键3:切换方向(需H桥)
- 按键4:保存预设
按键消抖我推荐状态机写法,比延时更可靠:
unsigned char Key_Scan() { static unsigned char keyState = 0; switch(keyState){ case 0: if(!KEY1) keyState = 1; break; case 1: if(!KEY1) { keyState = 2; return 1; } else keyState = 0; break; case 2: if(KEY1) keyState = 0; break; } return 0; }数码管显示转速时,动态扫描要避开PWM中断。我的经验是把扫描放在主循环,用1ms延时控制刷新率:
void Display_Speed(unsigned char speed) { static unsigned char pos = 0; P2 = (P2 & 0xF8) | (pos & 0x07); //位选 P0 = segCode[speed % 10]; //段选 if(++pos >= 4) pos = 0; Delay_ms(1); }曾经遇到数码管显示闪烁的问题,后来发现是PWM中断频率(1kHz)和显示刷新率冲突。调整PWM到5kHz后问题解决,这也印证了时序设计的重要性。
4. 进阶:闭环控制与编码器
开环PWM调速有个痛点:负载变化时转速会波动。给电机加装增量式编码器后,就可以实现闭环控制。我用的600线编码器,通过STM32的定时器编码器接口捕获信号,换算公式为:
转速RPM = (脉冲数 × 60) / (编码器线数 × 采样周期)51单片机资源有限,可以用外部中断+定时器实现简易测速:
unsigned int pulseCount = 0; void EXTI0_ISR() interrupt 0 //编码器A相接INT0 { pulseCount++; } void Timer1_Init() //1秒速度计算 { TMOD &= 0x0F; TMOD |= 0x10; TH1 = 0x3C; TL1 = 0xB0; ET1 = 1; TR1 = 1; } void Timer1_ISR() interrupt 3 { static unsigned char speed = 0; speed = pulseCount / 600 * 60; //换算RPM pulseCount = 0; TH1 = 0x3C; //重装初值 TL1 = 0xB0; }PID算法能让控制更精准。虽然51跑浮点PID比较吃力,但用整型增量式PID效果也不错:
typedef struct { int SetPoint; int LastError; int SumError; int Kp,Ki,Kd; } PID; int PID_Calc(PID *pid, int feedback) { int error = pid->SetPoint - feedback; int dError = error - pid->LastError; pid->SumError += error; pid->LastError = error; return (pid->Kp*error + pid->Ki*pid->SumError + pid->Kd*dError)/100; }调试PID参数时,建议先用Ziegler-Nichols法估算初值。我总结的经验是:先调P消除静差,再加D抑制震荡,最后用I微调。
