51单片机PWM调速实战:从定时器模拟到PID闭环
简介基于51单片机控制PWM直流电机调速的完整源代码面向电子工程、自动化控制及嵌入式入门学习者主要解决直流电机调速中PWM占空比调节、定时器配置与中断控制等关键问题。压缩包共含19个文件以Keil工程文件.uvproj、.uvopt、C语言源码、编译输出文件.hex、.lst、.m51及备份文件为主总计38KB便于直接打开工程查看和烧录验证。源码在关键位置配有注释附带的注释说明可帮助初学者理解51单片机定时器产生PWM信号的原理以及通过改变占空比实现电机转速调整的完整流程。资源还涉及启动、加速、减速和停止的平稳性等调试思路适合课程设计、毕设或项目实践参考。目前已有5334人学习使用整体代码简洁、结构清晰是快速掌握51单片机PWM调速的实用资料。1. 51单片机输出PWM调速不是降电压是斩波做直流电机调速时新手最容易掉进去的坑是盯着“调电压”三个字不放。电压一降电机力矩跟着塌陷转速掉到一半时堵转力矩往往只剩四成不到负载稍微变一下转速就漂而用51单片机输出PWM让电机两端电压以固定频率在0和电源电压之间高速切换平均电压可控但峰值电压始终顶着电源带载特性比线性降压好得多。这里说的51单片机以AT89C52、STC89C52这类经典51为对象它们没有硬件PWM外设调速源代码的核心是定时器中断模拟PWM一个定时器产生稳定节拍一个比较值决定占空比按键或串口改这个比较值。这篇内容适合刚点亮LED、想真正让电机转起来的单片机学习者也适合想评估51做小型调速够不够用的工程师。下面按驱动、频率、代码、仿真、闭环、收尾技巧的顺序走中间给的命令、参数和源代码都能直接抄。2. 直流电机调速先定频率与驱动H桥、平均电压和占空比2.1 驱动电路先于代码H桥与续流二极管普通51的IO驱动能力只有20mA左右直流电机轻则几十毫安、重则几安直接拿P1.0去推电机电流不够只是其一电机线圈在PWM关断瞬间产生的反向电动势会把IO打坏。驱动方案里L298N是双H桥芯片逻辑输入和功率输出隔离一片能同时驱动两路电机缺点是大电流时饱和压降接近2VL9110S是低压模块适合5V、1A以内的小电机模块上已经带续流二极管分立H桥用两个PNP上管加两个NPN下管能看清导通路径和死区适合教学但要自己处理同臂直通问题。无论用哪种PWM都加在H桥某一侧输入上另一侧给固定电平电机两端得到的就是斩波后的平均电压。H桥里有一个必须捅破的常识上下两个开关管绝对不允许同时导通否则电源直接短路。L298N内部自带死区逻辑而分立H桥必须在换向代码里先让所有管子输出低电平延时几十微秒后再切换这个动作就是常说的PWM死区。如果不加电机还没转电源先烧。注意硬件上永远做好续流保护。电机是感性负载PWM关断瞬间电感电流不能突变必须通过反向并联的二极管或H桥内部二极管给电流续路否则电压尖峰直接冲击芯片。2.2 PWM频率往哪选从100Hz到20kHz的取舍PWM调速的数学模型很简单平均电压 U_motor D × U_peakD T_on / T。占空比0.5时电机端电压平均值就是电源电压的一半转速也近似一半。真正需要认真定的是PWM频率它直接决定噪声、力矩平滑度和开关损耗。PWM频率电机表现适用场景100Hz ~ 500Hz能转但噪声明显电流纹波大低速时一顿一顿Proteus仿真、教学演示、大惯量负载1kHz ~ 10kHz噪声基本消失电流连续调速线性好常规小型直流电机最常用的区间20kHz 以上超出人耳听频范围但占空比分辨率难做高开关损耗上升需要硬件PWM和专用MOS驱动51软件PWM做不到频率太低还有另一个问题PWM周期长电流纹波大电机发热增加低速段甚至会出现齿槽效应带来的振动。频率高了电机电感感抗变大电流波形更平滑但半桥驱动器的开关损耗也随之上升。对常见的玩具电机、减速电机、风机我一般把频率放在1kHz到10kHz之间而用AT89C52这类经典51做软件模拟PWM时受中断开销限制常用区间是200Hz到1kHz能跑但别指望10kHz。2.3 51单片机接L298N的最小接线用L298N做最简单的单方向调速接线只有六根P1.0接IN1作为PWM输入P1.1接IN2并置低电平电机接OUT1和OUT2ENA是使能脚直接接高电平或接到另一个IO口上拉VM接电机电源正极VS接5V逻辑电源L298N的GND必须和51开发板的GND共地。这里最容易漏掉的就是共地GND不共逻辑电平没有参考点电机只会抖不会转。P1.1的电平决定转向IN1的PWM占空比决定转速这种“单边PWM”是调速项目里最省事的接法。如果需要正反转就把方向控制脚也接成IO口换向前先让PWM输出低电平再切换方向最后恢复PWM顺序反了就是一次危险的直通。3. 定时器模拟PWM的源代码51单片机调速的核心实现3.1 软件PWM怎么用中断搭出来经典51没有硬件PWM但有一个近乎万能的办法把PWM周期切成N等份定时器每等份中断一次中断里把计数器加一再用计数器和比较值做比较决定引脚输出高还是低。if (pwm_counter pwm_compare) { PWM_PIN 1; } else { PWM_PIN 0; }pwm_counter从0加到N后归零输出高电平的时间就是 pwm_compare 个中断节拍占空比就是 pwm_compare / N。改占空比不需要动硬件只需要改pwm_compare这个变量。这也是整个调速源代码最核心的机制占空比不是算出来的是中断里比出来的。为什么用小于号不用小于等于主要为了在pwm_compare等于0时能拿到全低电平等于N时能拿到全高电平边界行为更规整。3.2 可复制的51单片机调速源代码下面这段代码在12MHz晶振、12T模式的AT89C52/STC89C52上可以直接编译运行。定时器0方式1产生50us中断每50个中断节拍为一个PWM周期PWM频率400Hz占空比每档2%。#include reg52.h #define PWM_CYCLE 50 // 一个PWM周期含50个中断节拍 #define PWM_DUTY_MAX 48 // 占空比上限避免输出恒高 sbit PWM_OUT P1^0; // PWM输出接L298N的IN1 sbit MOTOR_DIR P1^1; // 方向控制接L298N的IN2 sbit KEY_UP P3^1; // 加速按键 sbit KEY_DOWN P3^0; // 减速按键 volatile unsigned char pwm_counter 0; volatile unsigned char pwm_compare 25; // 初始占空比50% void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0方式116位手动重装 TH0 0xFF; TL0 0xCE; // 12MHz/12T模式下约50us一次中断 ET0 1; EA 1; TR0 1; } void timer0_isr(void) interrupt 1 { TH0 0xFF; TL0 0xCE; if (pwm_counter PWM_CYCLE) { pwm_counter 0; } if (pwm_counter pwm_compare) { PWM_OUT 1; } else { PWM_OUT 0; } } void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) { for (j 0; j 123; j); // 12MHz下经验值 } } void main(void) { timer0_init(); MOTOR_DIR 0; // 方向固定为正向 while (1) { if (KEY_UP 0) { delay_ms(10); // 消抖 if (KEY_UP 0) { if (pwm_compare PWM_DUTY_MAX) { pwm_compare; // 占空比加2% } while (KEY_UP 0); // 等待按键释放 } } if (KEY_DOWN 0) { delay_ms(10); if (KEY_DOWN 0) { if (pwm_compare 5) { pwm_compare--; // 占空比减2%下限5%保证能起步 } while (KEY_DOWN 0); } } } }pwm_compare用volatile修饰是因为它在中断和主循环两个执行流里都被访问防止编译器把它优化进寄存器导致按键改了但中断读到的还是旧值。按键部分先检测低电平再延时10ms消抖二次确认后修改pwm_compare最后等待释放这样一次按键只产生一次步进。若想改频率把定时器初值0xFFCE和PWM_CYCLE按比例调整即可换11.0592MHz晶振时50us对应机器周期是46个初值要同步修正为0xFFD2。提示PWM_DUTY_MAX故意留了两档余量。占空比到100%意味着PWM输出恒高虽然电机全速但失去了PWM的斩波特性且突然满载对电源冲击大工程上极少让占空比长期顶着上限跑。3.3 PWM分辨率、频率和中断开销的三角关系软件PWM有一个绕不开的三角矛盾占空比分辨率越高、PWM频率越高定时器中断就越密集。经典51的12T模式下机器周期是晶振的12分频12MHz时一个机器周期1us中断服务函数从进入、重装初值到恢复现场大约要20到40个机器周期这意味着中断周期短到50us时CPU已经有一大半时间泡在中断里。中断周期PWM周期拍数PWM频率占空比分辨率中断开销50us50400Hz2%较高主循环还能响应按键50us100200Hz1%同上100us50200Hz2%减半更从容20us501kHz2%中断服务本身就可能超20us不现实这是用51做PWM调速的边界想做到1kHz同时保持1%分辨率在AT89C52上几乎不可能。实际项目中我一般只推荐两个方案教学演示用400Hz、2%分辨率够直观真要10kHz级别用STC15系列自带硬件PWM的51增强型或者直接换带高级定时器的单片机别再跟软件中断较劲。4. 用Proteus验证51单片机PWM调速再从开环升级到PID闭环4.1 用Proteus搭电路让示波器看到PWM信号写好的C代码先在Keil里编译出hexOptions for Target里勾选Output选项卡的Create Hex File编译后工程目录下会生成对应名称的hex文件。接下来在Proteus里新建工程放置AT89C52并双击加载这个hex文件。仿真电路按这个搭AT89C52的P1.0经1kΩ电阻接2N2222三极管基极发射极接GND集电极接直流电机负极电机正极接12V电源电机两端反向并联一个1N4007做续流二极管。示波器A通道接P1.0B通道接集电极再在电机回路串一个电流表。运行仿真后示波器上能同时看到P1.0的低压方波和集电极的高压斩波按键调节占空比时波形宽度会随之变化电流表和电压表数值同步跳动这就是最直观的开环验证。这个低边驱动电路等效于单向PWM调速的完整信号链实际项目里常用它代替L298N做低成本风机控制。如果仿真里波形显示不连续或看不到窄脉冲把示波器Time/Div调到1ms左右同时把Proteus的动画仿真步长调小默认步长下高频PWM窄脉冲很容易被丢弃。仿真没问题后再焊实物排错成本低得多。4.2 加编码器测速把INT0当转速输入开环调速的缺点是转速随负载漂占空比不变负载变大转速就掉。闭环的前提是能测到转速。霍尔传感器或光电编码盘每转一圈输出固定数量脉冲把脉冲接到P3^2也就是INT0外部中断引脚用下降沿触发定时器1每100ms统计一次脉冲数就能算出转速。volatile unsigned int pulse_count 0; unsigned int speed_rpm; #define PULSE_PER_REV 20 // 码盘每转脉冲数 void ext0_isr(void) interrupt 0 { // INT0下降沿触发 pulse_count; } void timer1_isr(void) interrupt 3 { // 每100ms一次 speed_rpm (unsigned int)((unsigned long)pulse_count * 600UL / PULSE_PER_REV); pulse_count 0; }定时器1每100ms中断一次1秒就是10次pulse_count乘以10是每秒脉冲数再乘60变成每分钟脉冲数除以PULSE_PER_REV得到RPM。我用600UL表示10×60用long先乘后除是为了避免16位乘法溢出。这里一个值得注意的细节如果每转只有1个脉冲100ms测得的转速分辨率会非常差低速时pulse_count只有几个数曲线抖动剧烈应该改用每转多脉冲的光电码盘或者把测速周期拉长到200ms、500ms。4.3 增量式PID给PWM调速闭环有了转速反馈就能把按键直接改pwm_compare的开环逻辑替换成PID闭环逻辑。工程里最常用的是增量式PID每次控制周期只计算占空比的增量不对历史误差累加做全量计算输出限幅也更好处理。float kp 0.5f, ki 0.02f, kd 0.1f; int e0, e1, e2; int delta; unsigned char pwm_target 25; void pid_update(void) { int speed_now (int)speed_rpm; int speed_ref 300; // 目标转速可改 e2 e1; e1 e0; e0 speed_ref - speed_now; delta (int)(kp * (e0 - e1) // 比例项 ki * e0 // 积分项 kd * (e0 - 2 * e1 e2)); // 微分项 pwm_target delta; if (pwm_target 90) pwm_target 90; if (pwm_target 5) pwm_target 5; }比例项Kp负责快速响应误差积分项Ki消除稳态误差微分项Kd抑制超调。增量式PID输出的是delta要累加到pwm_target上这一点和位置式PID完全不同。51的float运算虽然慢但每100ms才跑一次完全够用。整定顺序我一般固定先Ki和Kd设0Kp从0.2开始往上调看转速曲线发现稳定误差消不掉再加Ki从0.01起步一次加0.01最后加Kd压超调Kd一般不超过Kp的五分之一。如果speed_rpm本身波动大微分项会被放大成高频扰动这时候先给测速值做4次均值滤波再决定要不要上D项。调完PID后pwm_target代替了原来按键直接设置的pwm_compare按键的功能变成设定speed_ref而不是直接改占空比这是开环控制与闭环控制的本质区别输出量不再由人直接决定而是由误差动态算出。5. 51单片机PWM调速收尾技巧软启动、堵转保护与示波器验证5.1 软启动与堵转保护直流电机直接给满占空比启动冲击电流可以达到额定电流的5到8倍小电源会被瞬间拉低机械结构也会受冲击。工程上常见做法是让占空比从0逐步爬升到目标值也就是软启动。用单独的变量current_compare表示实际输出的占空比target_compare表示期望值在每个控制周期让current_compare向target_compare逼近一格。unsigned char target_compare 40; unsigned char current_compare 0; void soft_start_step(void) { // 每10ms调用一次 if (current_compare target_compare) { current_compare; } else if (current_compare target_compare) { current_compare--; } }占空比实际上用的是current_compare而中断里输出PWM时也读这个变量。爬升速度可以通过调用间隔或步长调整一般小电机从启动到全速控制在0.3到1秒太快起不到缓冲作用太慢会让电机长时间处于低速大电流状态。堵转保护思路类似占空比大于20%时如果0.5秒里测速脉冲数始终为0就认为堵转立刻把motor_enable标志清掉并停止PWM输出延时1秒再重新允许启动。实现上要在中断里检查这个标志直接在主程序里给PWM_OUT赋0会被下一次中断覆盖掉。bit motor_enable 1; void timer0_isr(void) interrupt 1 { TH0 0xFF; TL0 0xCE; if (!motor_enable) { PWM_OUT 0; pwm_counter 0; return; } // 原有PWM比较逻辑 } void stall_check(void) { // 每100ms调用 if (pwm_compare 20 pulse_count 0) { stall_count; if (stall_count 5) { // 持续0.5s无脉冲 motor_enable 0; // 关断PWM delay_ms(1000); motor_enable 1; stall_count 0; } } else { stall_count 0; } }5.2 示波器验证PWM波形时的几个检查点代码跑通了最后用示波器确认几个关键指标别只看电机转不转。探头夹PWM输出引脚地线夹GND记得不是夹在电机电源上。先看波形是不是标准方波占空比高电平宽度是否随按键单调变化再用示波器的频率测量功能核对PWM频率和设计的400Hz是否一致。然后是电压幅值P1.0的高电平应该在接近5V如果只有3V检查IO模式和负载L298N输出的波形高电平会比电源电压低1.5到2V这是正常压降。最后测电机两端的平均电压用直流电压表显示值应当近似等于占空比乘以电源电压比如12V电源、50%占空比时测到约6V说明整个链路是通的。示波器上出现毛刺或者波形边缘抖动先检查共地是否可靠其次是驱动芯片的VCC与GND之间有没有加100nF去耦电容这两处是PWM噪声的头号来源。本文还有配套的精品资源点击获取