PWM电机调速这东西,圈子里的朋友应该都不陌生。说原理,三句话能讲完——通过改变方波的高电平占空比,改变电机两端的平均电压,进而控制转速。但真到了自己动手做项目的时候,频率选多少、H桥怎么搭、驱动芯片怎么选、死区要不要配、代码怎么写才能稳,一步一步踩过去,每一步都有能绊倒人的坑。这篇文章就把整个链路摊开讲:从PWM调速的底层逻辑,到硬件选型和方案对比,再到51、STM32、Arduino三套代码实现,最后把频率选择、死区配置、飞车保护这些实战里最容易出问题的点单独拎出来说。适合手上有电机、想自己做调速板,又不满足于只抄一段代码跑通的读者。
1. PWM调速的核心思路:方波里的“有效电压”
1.1 先搞懂占空比和平均电压的关系
PWM的全称是Pulse Width Modulation,脉冲宽度调制。它输出的不是连续的模拟电压,而是一串周期性的方波。这串方波里有三个关键参数:
- 周期T:一个完整波形的时间,单位秒(s)
- 频率f:每秒波形重复次数,f = 1/T,单位赫兹(Hz)
- 占空比D:高电平时间(Ton)占整个周期的百分比,D = Ton / T × 100%
占空比这个值才是调速的关键。电机接在PWM输出和地之间,它看到的电压并不是一瞬间的方波,而是一个平均值。这个平均电压约等于:
[ U_{eff} = U_{max} \times D ]
举个直观的例子:一颗12V的直流电机,给它输入占空比30%的PWM信号,那么等效平均电压就是12 × 0.3 = 3.6V。电机的转速大致正比于端电压,所以理论上3000rpm的电机在30%占空比下会降到900rpm左右。当然实际值会因为摩擦、反电动势、驱动器压降等因素略有偏差,但趋势是单调的:占空比越大,平均电压越高,转速越快。
我调试时习惯直接用示波器去看PWM输出,高电平宽度一眼就清楚了。没有示波器的话,用万用表直流电压档也能量出平均电压,数值基本等于U_max × D,这个值可以帮你快速验证代码逻辑对不对。
1.2 电机为什么吃这一套:感性负载的电流惯性
很多新手会疑惑:方波明明是0V和12V来回跳,电机为什么不会一抖一抖地转?这里的关键在于电机本身的特性——它是一个典型的感性负载。
电感有一个核心特性:电流不能突变。当PWM高电平时,电源向电机供电,电流上升;当PWM切到低电平时,电源断开,但电机的绕组电感会阻止电流瞬间归零,电流通过续流二极管或H桥的下管继续流动,缓慢下降。只要PWM频率足够高,电流波动就很小,电机看到的实际上是一个非常平稳的连续电流,而不是断续的脉冲。
可以这么理解:电机靠电流产生转矩,转矩决定转速。电感把方波电压“滤”成了近似平稳的电流,所以电机不是在“抖动”,而是在一个稳定的平均电流下匀速转动。这也解释了为什么PWM调速在电机驱动里特别适合——它利用了负载本身的物理特性,而不是硬生生地靠外部滤波电路来平滑电压。
1.3 为什么调速不用调压:三种方案对比
既然要控制电机端电压,为什么不直接用可调电源?我见过不少新手其实是被这个思路卡住的。放在实际工程里,有三种调速思路:
- 串联电阻调速:在电源和电机之间串一个可调电阻,分压来降低电机电压。简单是简单,但电阻上消耗的功率等于电流的平方乘以电阻,能量全变成热量散掉了。低速时尤其浪费,而且电阻值一变,负载一变,转速也跟着漂。
- 线性稳压器调速:用LM317这类线性稳压IC调输出电压。精度比电阻好,但线性稳压器本质上也是靠管压降把多余的电压“吃掉”,效率和发热问题依旧,大电流场景基本用不了。
- PWM开关调速:MOS管或三极管工作在饱和/截止两个状态,要么全通要么全断,通态损耗很小,截止时漏电流极小,开关切换过程的损耗也远小于线性方案,整体效率能做到90%以上。
这就是为什么实际产品里,小到玩具车、电动牙刷,大到电动汽车主驱动,清一色都在用PWM调速。它不是“最精确”的方案,但它是“效率、成本、电路复杂度”平衡下来最现实的选择。
2. 硬件选型:H桥、驱动芯片与MOS管驱动
2.1 H桥的基本原理与防直通设计
PWM只能调速,不能换向——要控制电机正反转,得靠H桥。H桥由四个开关管(MOS管或三极管)组成,电机接在桥臂中间,形状像字母H,所以叫H桥。
- 正转:左上(Q1)和右下(Q4)导通,电流从电源经过电机左边流到右边
- 反转:右上(Q2)和左下(Q3)导通,电流方向反过来
- 刹车:同时导通同一边上下两管(或对地短路),让电机绕组短接快速停止
这里有一个绝对不能碰的禁忌:同一侧的上管和下管(Q1和Q2)绝对不能同时导通。一旦导通,电源直接通过这两个管子对地短路,电流会瞬间飙升到几十安培,把MOS管甚至整个板子烧掉。这个现象有个名字叫直通(Shoot-through)。
为了防止直通,硬件上要保证上下桥臂的切换有足够的时间间隔——先彻底关断上管,再打开下管。这个间隔就是死区(Dead Time),后面第4节我会专门展开讲。
2.2 从L298N到TB6612:三款驱动芯片怎么选
自己用四个MOS管搭H桥不是不行,但驱动电路(特别是栅极驱动电平匹配、续流保护、死区控制)对新手来说太容易翻车。工程上我更推荐直接用封装好的H桥驱动芯片,省心得多。下面是市面上最常见的三款:
| 芯片 | 输出电流 | 供电电压 | 逻辑电平 | 典型场景 | 特点 |
|---|---|---|---|---|---|
| L298N | 2A/通道(峰值3A) | 5-35V | TTL 5V | 教学实验、大功率玩具 | 管压降大、发热严重、效率偏低 |
| TB6612FNG | 1.2A/通道(峰值3.2A) | 逻辑2.7-5.5V,电机6-15V | TTL/3.3V | 小型机器人、智能小车 | 内阻约0.5Ω,效率高,带刹车功能 |
| DRV8833 | 1.5A/通道(峰值2A) | 2.7-10.8V | TTL/3.3V | 小型电机、玩具 | 自带过流保护,PWM频率可以很高 |
我的建议是:如果做教学或大功率演示,用L298N,便宜好买,资料也多;如果是做小型机器人、云台、智能车,优先TB6612,体积小、效率高,特别是电池供电的场景,省电优势非常明显;DRV8833适合讲究集成度和保护功能的项目,故障状态引脚可以直接反馈给单片机。
注意一点,这些芯片的电源引脚和逻辑引脚要分开供电,逻辑电源负责芯片内部控制电路,电机电源负责电机本身。接线时一旦把电机电源错接到逻辑引脚,芯片大概率直接烧掉。
2.3 555定时器生成PWM和逻辑电平MOS方案
在没有单片机的场景下,一颗NE555定时器就能搭出一个纯硬件的PWM发生器。核心思路是用两个二极管分别控制充放电回路,让充电和放电走不同的路径,调节电位器就能改变占空比,同时基本不影响频率。555的输出端可以直接驱动小功率电机,或者接一个MOS管去驱动大电流电机。
这个方案我以前上课时经常用,好处是电路直观、不需要写任何代码,五分钟就能搭出来;缺点是频率和占空比受温度和电源电压影响漂移,没有故障保护,不能做PID闭环。它的定位是学习工具,或者做一个临时性的手动调速器,真到产品阶段还得靠MCU控制。
如果要自己用MOS管搭驱动,我建议从逻辑电平的N-MOS入手,比如AO3400A。这种管的栅极阈值电压很低,5V甚至3.3V的PWM信号就能让它完全导通,导通内阻只有20-40mΩ,电流定额4A左右,对小功率直流电机、风扇、玩具车绰绰有余。需要注意的是,AO3400A是N-MOS,只适合低边驱动——电机接电源正极,MOS管接在电机负极和地之间,PWM直接加在栅极上。想做高边驱动就得搞自举电路,新手别一上来就挑战这个。
3. 代码实现:三种平台的PWM写法
3.1 51单片机软PWM:定时器中断控制占空比
51单片机本身没有硬件PWM外设(部分增强型型号除外),所以要靠定时器中断软件模拟。基本思路是:开一个固定周期的定时器中断,比如10us中断一次,中断服务程序里维护一个计数器和占空比比较值,计数器小于比较值输出高电平,否则输出低电平。
下面是标准的51软PWM模板,晶振12MHz,P1.0口输出PWM:
#include <reg51.h> sbit PWM_OUT = P1^0; unsigned char duty = 50; // 初始占空比 50% unsigned int cnt = 0; const unsigned int period = 100; // 一个PWM周期对应的中断次数 void Timer0_Init(void) { TMOD = 0x02; // 定时器0,8位自动重装模式 TH0 = 256 - 10; // 12MHz晶振,每10us产生一次中断 TL0 = 256 - 10; ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR(void) interrupt 1 { cnt++; if (cnt >= period) { cnt = 0; } PWM_OUT = (cnt < duty) ? 1 : 0; } void main(void) { Timer0_Init(); while (1) { // 主循环里只需要改duty变量,比如按键调速、 // 串口指令调速、PID运算结果写duty等等 } }这里几个细节要搞清楚:
- period是100,所以PWM频率 = 1 / (10us × 100) = 1kHz。如果觉得频率太低(后面会讲为什么太低的频率不好),就把period改小,比如period=20,频率就变成5kHz。代价是占空比分辨率从1%变成5%,每档精度变粗了。
- duty的范围是0到100,对应0%到100%占空比。想要更细的分辨率,就加大period,但频率会降下来,这就是软件PWM的精度和频率之间的矛盾。
- 中断服务程序里做的事情越少越好,别放延时、别放串口发送,不然中断还没跑完下一个中断就来了,PWM波形就会毛刺。
3.2 扩展思路:怎么用51模拟出16位高精度PWM
有些场合需要更高精度的占空比控制,比如舵机角度控制、云台微调,8位PWM(分辨率1/256)往往不够。51单片机的定时器是16位的,理论上能模拟出16位分辨率的PWM,但做法不是简单地加一个计数器。
推荐的做法是分两级处理:第一级定时器工作在16位模式,产生一个高频率的时基中断,比如10us;第二级在中断里维护一个16位的“周期计数器”,同时用一个16位的“比较寄存器”。当计数器在0到compare之间时输出高电平,达到period后清零。这种方法可以让占空比分辨率做到1/65536,频率部分牺牲掉。
#include <reg51.h> sbit PWM_OUT = P1^0; unsigned long cnt = 0; unsigned int compare = 16384; // 占空比比较值,范围0~65535 const unsigned int period_16 = 65536; const unsigned int unit_cnt = 10; // 每次中断对应的时间单位 // Timer1 16位模式,1us中断一次 void Timer1_Init(void) { TMOD = 0x10; // 定时器1,16位模式 TH1 = (65536 - 1000) / 256; // 大致1ms中断一次,具体按晶振计算 TL1 = (65536 - 1000) % 256; ET1 = 1; EA = 1; TR1 = 1; } void Timer1_ISR(void) interrupt 3 { TH1 = (65536 - 1000) / 256; TL1 = (65536 - 1000) % 256; cnt++; if (cnt >= period_16) { cnt = 0; } PWM_OUT = (cnt < compare) ? 1 : 0; } void main(void) { Timer1_Init(); while (1) { // 通过调节compare实现高精度占空比变化 } }这种用法比较取巧,它牺牲了实时性换分辨率,因为主循环里改compare的瞬间,可能会和中断里的读取产生竞争。实际工程里如果对时序要求严格,还是建议用带硬件PWM的增强型51(比如STC系列)或者直接上ARM内核的MCU。
3.3 STM32硬件PWM:从初始化到互补输出死区配置
STM32的定时器自带硬件PWM输出,CPU不需要参与波形产生,这是它比51软PWM先进的核心点。以最常用的STM32F103加HAL库为例,配置一个频率10kHz、占空比50%的PWM只需要这几步。
TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz,计数时钟 htim2.Init.Period = 100 - 1; // 1MHz / 100 = 10kHz PWM频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 50; // 初始占空比 = Pulse / Period = 50% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); } // 动态调速:任何时候调用下面这句话即可 // __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, newDuty);解释几个关键点:
- Prescaler和Period的乘积决定了PWM频率。72MHz主频,预分频72得到1MHz计数时钟,Period设为100就得到10kHz。这个对照关系一定要算清楚,改频率时别只动一个参数。
- Pulse寄存器决定占空比,它必须小于Period,否则输出一直为高。这一点在动态调速时特别容易踩,比如PID输出超调把Pulse推过了Period,电机突然全速飞转。
- PWM1模式的含义是:计数值小于Pulse时输出有效电平,大于Pulse时输出无效电平。反过来PWM2模式是小于Pulse时无效,大于时有效。理解这个逻辑对调试输出极性很有帮助。
STM32还有一类应用会用到互补输出CH1N(对应热搜词里的“stm32 pwm generation ch1n”)。CH1N的输出波形和CH1相反,专门用来驱动H桥上下桥臂。使用互补输出时必须配置死区时间,否则上下管直通。只有TIM1和TIM8这类高级定时器才有互补输出通道,配置方式和通用定时器类似,额外多一个死区参数:
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig; sBreakDeadTimeConfig.DeadTime = 100; // 死区时间 = 100 / (死区时钟频率) HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig);那句很典型的“stm32 高级定时器 pwm 中心对齐模式和 adc 采样时刻点设置”本质上也是定时器配合ADC同步采样的应用:在中心对齐模式下,计数器向上和向下计数都会更新PWM输出,PWM波形以周期中点对称,适合无刷电机换相和全桥应用。ADC采样时刻设定在PWM的中间点,可以避开开关噪声最大的边沿时刻,采样到的电流值最干净。这个优化在电机电流环里非常实用。
3.4 Arduino/ESP32:十行代码跑通的极简方案
如果只是快速验证电机的调速逻辑,Arduino是最省事的平台。AVR系列的analogWrite()就是硬件PWM输出,不需要配寄存器,一行代码就搞定:
const int pwmPin = 9; // 注意:只有带~标记的引脚才支持PWM void setup() { pinMode(pwmPin, OUTPUT); } void loop() { // 从0到255逐渐加速,再减速回来 for (int duty = 0; duty <= 255; duty++) { analogWrite(pwmPin, duty); delay(5); } for (int duty = 255; duty >= 0; duty--) { analogWrite(pwmPin, duty); delay(5); } }Arduino的analogWrite()频率默认约490Hz或980Hz(取决于引脚),太低了,驱动电机通常会发出让人难受的啸叫声。如果想提高频率,需要用定时器寄存器改底层配置,或者换ESP32这种可以用ledcSetup()自定义频率的芯片:
const int pwmPin = 4; void setup() { ledcSetup(0, 20000, 8); // 通道0,20kHz PWM频率,8位分辨率 ledcAttachPin(pwmPin, 0); // 把引脚4绑定到LEDC通道0 } void loop() { for (int duty = 0; duty <= 255; duty++) { ledcWrite(0, duty); delay(5); } for (int duty = 255; duty >= 0; duty--) { ledcWrite(0, duty); delay(5); } }ESP32的LEDC模块本质上就是一个硬件PWM生成器,既能调频率又能调分辨率,很多舵机控制库底层就是基于它实现的。做电机控制原型验证时,我经常先用ESP32跑通逻辑,再移植到正式MCU上。
4. 频率、死区、故障保护:那些容易踩的坑
4.1 频率不对,电机要么抖要么烫
PWM频率的选择有两个主要约束:一个是听觉,一个是效率。
先说听觉。人耳的听觉范围是20Hz到20kHz,PWM频率一旦落在这个范围里,电机绕组在电流脉冲作用下会产生高频啸叫,声音尖锐刺耳,听着非常难受。工程上一般避开1kHz到8kHz这个最敏感的区间,选择10kHz以上。我见过有人为了追求完全静音,直接把频率拉到25kHz甚至更高,但高频带来的结果是MOS管开关损耗急剧上升——开关瞬间管子的电压电流同时不为零,瞬时功率很大,频率越高,单位时间内开关次数越多,发热越明显。
再说低频率的问题。如果PWM频率低于1kHz,电机在低速时会出现可见的顿挫感,一卡一卡地转动。这是因为电流波动过大,转矩脉动超过了电机本身的惯性滤除能力。频率越低,这个问题越明显。
给一个实用参考值:
| 负载类型 | 推荐频率 | 说明 |
|---|---|---|
| 标准RC舵机 | 50Hz(周期20ms) | 信号频率固定,控制脉宽1-2ms |
| 小型直流电机 | 10-20kHz | 避开人耳敏感区,效率可接受 |
| 带减速箱的直流电机 | 10kHz左右 | 变速箱本身会放大噪音 |
| 无刷电机(ESC) | 16-25kHz | 高频换相,具体看电调设计 |
4.2 死区不是锦上添花,是保命配置
死区这个概念在普通单端PWM驱动里用不上,只要一做H桥或者半桥驱动,死区就是必须处理的硬性问题。
还是回到H桥那个电路。假设当前正转,左上管Q1导通,右下管Q4导通;现在要切换成反转,右下管Q4关断,右上管Q2导通,这时Q1和Q2就同时处于一个桥臂。如果Q4关断后立刻打开Q2,那么Q1和Q2之间存在一个极短暂的共同导通窗口——电源正极到地之间没有电阻,只有两个正在导通的MOS管,瞬间电流会暴涨到安培级别,轻则发热,重则直接炸管。
解决的办法就是插入死区时间:从Q4关断到Q2导通之间,留出几百纳秒的等待时间,这个时间必须大于MOS管自身的关断延迟时间。
STM32高级定时器配置死区是通过TIM_BreakDeadTimeConfig完成的,我之前在3.3节里已经给了代码片段。如果不使用互补输出而是用软件分别控制两个IO口,就得自己在代码里加延时,这其实是比较危险的做法,因为中断会打断延时的精确性,极有可能在某个特定时序下出现直通。所以我的原则是:凡是要驱动H桥,一律用带硬件死区的定时器通道,不要省这个功能。
4.3 “飞车”和常见故障排查速查表
热搜词里的“pwm电机飞车”是很多人在实际调试中真实遇到的问题。电机飞车就是电机突然全速运转,无法停止,通常原因有几类:
- 程序跑飞或复位后,IO口默认状态不确定,直接输出高电平,电机全速转
- PID闭环里占空比饱和,Pulse值超过Period,输出持续高电平
- H桥芯片逻辑电平受干扰,使能脚误触发
- 电源跌落导致单片机复位,复位后初始化代码没跑,PWM管脚悬空被干扰拉高
针对性的解决办法:软件上加看门狗,超时未刷新就进入安全状态;硬件上给PWM输出加下拉电阻,让默认电平为低;继电器或MOS管驱动端的控制脚做逻辑互锁;PID输出做限幅处理,Pulse值必须严格限制在0到Period-1之间。
下面整理了一张排查表,都是我实测中遇到过的问题:
| 现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 电机不转,PWM有输出 | 使能脚未拉高/驱动芯片没有电源 | 万用表量芯片供电脚 | 检查ENA/ENB引脚和电机电源 |
| 电机全速飞车 | 程序跑飞/占空比饱和/IO默认电平 | 示波器抓输出波形、复位测试 | 加看门狗、管脚下拉、PID输出限幅 |
| 电机低速一顿一顿 | PWM频率过低 | 示波器看频率 | 提到10kHz以上 |
| MOS管发热严重 | 开关频率过高/栅极驱动不够 | 手摸或红外温枪测温度 | 降低频率、加强栅极驱动能力 |
| 上电瞬间电机转半圈 | IO默认电平不确定/驱动芯片上电误动作 | 示波器抓上电瞬间波形 | 加下拉、MCU初始化后延时再使能 |
| 舵机抖动 | 信号占空比漂移/供电电压不足 | 万用表量工作电压 | 独立电源供电、加电解电容 |
| 调速时电机尖叫 | PWM频率落人耳敏感区 | 听声音、看波形 | 频率提到20kHz左右 |
4.4 我在调试中踩过的几个实际问题
调了这么多年PWM驱动,印象最深的是有一次给一个中型直流电机做H桥调速,用的是TB6612加STM32的互补PWM。代码逻辑看起来完全没问题,占空比50%输出也对,但电机一启动,示波器上电机两端波形出现明显的尖峰毛刺,持续了几十毫秒后恢复正常。后来排查发现是电机启动瞬间电流过大,把电源电压拉低了几百毫伏,TB6612逻辑电路跟着掉电,PWM输出瞬间乱掉。解决方法是电机电源和逻辑电源分开供电,或者在电机电源端并一个大电容做瞬间能量缓冲。
还有一个常见坑是频率选择上,早期为了图静音,把PWM频率提高到32kHz,结果带来两个问题:一是MOS管温度明显上升,二是低速时电机扭矩变小了。后来查资料才理解,频率过高时每个PWM周期的时间太短,电感电流还没充上来就放电了,电流峰值上不去,平均转矩自然就掉了。后来降到20kHz,静音效果还在,扭矩和发热都正常了。
这些经验常规文档里不会写,只有实际调过才知道。所以我一直建议:调PWM驱动,示波器是必需品,电流探头最好也备一个,先看波形再猜代码,排查效率能翻几倍。
从原理到代码,再到调试经验,这条链路的完整度已经能支撑你独立完成一个PWM电机调速项目了。如果你手头正好有电机和开发板,建议按这个顺序动手:先用Arduino或者51跑通调速逻辑,再用示波器看波形确认频率和占空比,最后再上H桥做正反转和死区配置。每一步都有明确的验证点,出问题也知道在哪一步排查。