☰
PWM电机调速实战:从占空比原理到H桥驱动与代码实现
2026/10/7 3:15:05 网站建设 项目流程

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桥驱动芯片,省心得多。下面是市面上最常见的三款:

芯片输出电流供电电压逻辑电平典型场景特点
L298N2A/通道(峰值3A)5-35VTTL 5V教学实验、大功率玩具管压降大、发热严重、效率偏低
TB6612FNG1.2A/通道(峰值3.2A)逻辑2.7-5.5V,电机6-15VTTL/3.3V小型机器人、智能小车内阻约0.5Ω,效率高,带刹车功能
DRV88331.5A/通道(峰值2A)2.7-10.8VTTL/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桥做正反转和死区配置。每一步都有明确的验证点,出问题也知道在哪一步排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询