很多玩单片机的朋友,第一次接触 PWM,往往是从一盏 LED 开始的。手上只有高电平和低电平,想让灯暗一点,脑子里的第一反应是“把电压调小”,但单片机引脚输出的要么是 0 要么是 3.3V,于是有人开始串电阻,有人用 IO 口直接带负载,折腾一圈发现亮度是变了,可原理还是模模糊糊。等到做直流电机调速,需要让电机从静止转到某个转速,又发现“调电压”这条路同样绕不过 PWM。
这篇文章就聊聊 PWM 为什么能调速,从 LED 亮度变化到直流电机转起来,中间到底发生了什么。无论你是刚学 51、STM32,还是已经在用 HC32F460 这类国产单片机做产品,把 PWM 的占空比、频率、模式和驱动能力搞清楚,后面写代码、调硬件都会顺手很多。我会带上一些实际配置过程、参数计算和踩坑记录,手头有板子的可以直接跟着试。
1. PWM 到底在调什么:从一盏 LED 灯说起
1.1 人眼看到的“亮度”,其实是时间上的平均
PWM 的全称是脉宽调制,简单说就是固定频率,改变高电平持续的时间。一个周期 T 内,高电平持续时间为 t_on,占空比 D = t_on / T,用百分比表示。对纯电阻负载来说,平均功率等于 D 乘以峰值功率,也就是 D 乘以 V²/R。LED 不是纯电阻,它的伏安特性和颜色、温度都有关系,但只要开关频率足够快,高于人眼闪烁融合频率,通常 100Hz 以上,人眼就不会感知到亮度突变,而是感知到周期内的平均光输出。LED 的光通量和电流在很大范围内近似线性,所以占空比从 1% 到 99%,几乎能对应连续的亮度变化。
这就是 PWM 调光的基本原理:不是改变瞬时功率,而是改变单位时间内的能量供给。很多初学者把 PWM 理解成“数字模拟电压”,其实不够准确。PWM 输出的是完整的开关波形,负载上要么是满电压,要么是零电压,只是两者的比例在变。LED 的亮、电机的转、蜂鸣器的响,这些宏观结果都是时间平均效应叠加出来的。
1.2 为什么不用模拟调压:LED 的窄电压窗口和色偏问题
有人会问,既然平均电压决定亮度,那把电压从 3.3V 降到 2V 不也一样吗?对 LED 来说这条路很难走。LED 正向导通电压有一个比较窄的窗口,比如黄绿色 LED 大约 2V,蓝色、白色 LED 大约 3V。给它 2.5V,可能电流很小;给它 3.1V,电流可能已经窜到几十毫安。有经验的硬件工程师都清楚,LED 要稳定工作靠的是限流或恒流,而不是精密调压。调压过程中你还会发现,改变 LED 电流时色温、色坐标会漂移,尤其白光 LED 低电流时偏黄,高电流时偏蓝。
PWM 调光则不同,它让 LED 始终工作在同一个电流值,由限流电阻或恒流驱动决定,只是用开关方式快速通断。亮度变化时,LED 的色坐标保持稳定,寿命也更好。所以现在从手电筒到 LED 大屏,调光方案基本默认用 PWM,而不是模拟调节电流。这也是为什么你看到的 LED 芯片参数表里通常有“DIM 引脚”“PWM 调光”这类描述,而不是建议你串一个电位器去调亮度。
1.3 51 单片机驱动 LED,为什么不能用输出高电平的方式
这个问题在初学者群里几乎每周都出现。传统 51 单片机的 IO 口是准双向结构,内部上拉电阻比较弱,输出高电平时驱动能力非常有限;输出低电平时,引脚能灌入的电流往往比拉出的电流大不少。典型的老手册上,IOL 能到十几毫安,IOH 却只有几十到几百微安,不同型号差异很大。如果你用高电平去点亮 LED,10mA 可能把引脚电压拉垮,还会影响其他引脚。所以最常见的做法是:LED 正极接 VCC,负极经过限流电阻接到 IO 口,IO 输出低电平时点亮。
这个细节和 PWM 调光直接相关。用 51 的 IO 口做软件 PWM 时,如果采用低电平点亮,你会发现“点亮时间”对应的是 IO 口的低电平时间,而不是高电平时间。比如想让 LED 亮度为 70%,周期 10ms,点亮 7ms、熄灭 3ms,用低电平点亮时,IO 口输出低电平那 7ms 才是亮。很多新手在这里调反,把占空比从 0 调到 100,灯却从全亮变成全灭。解决办法一是把极性取反,二是直接选支持硬件 PWM 和极性可配的 MCU。
1.4 LED 闪灯驱动芯片和大屏灰度,其实也是 PWM
热词里还有“led 闪灯驱动芯片”和“led 大屏安装”,这里顺带提一下。LED 闪灯驱动芯片内部的核心模块之一就是 PWM 发生器和恒流源,通过内部寄存器控制每个通道的开关时间,实现呼吸灯、流水灯、闪烁效果。LED 大屏就更典型了,每个像素由红绿蓝三颗灯芯组成,灰度等级靠的是在极短的时间片内用不同宽度的 PWM 脉冲驱动灯芯。常见做法是“扫描+恒流+PWM 灰度”,利用人眼视觉暂留,让 16 位灰度的画面看起来连续自然。
所以 PWM 在 LED 领域不是可有可无的“调亮功能”,而是整个显示系统的底层机制。理解好这一点,再看后面电机调速,思路就通了:PWM 只是把能量分时投给负载的一种通用手段,负载是 LED、电阻还是电机,宏观表现不同,但底层逻辑一致。
2. 从 LED 搬到直流电机,调速的本质没变
2.1 直流电机的“阻力”:反电动势和电感
直流电机和 LED 看着差别很大,但 PWM 能调速的原因是一样的:让功率管周期性导通,负载得到的平均功率可控。电机内部有电枢电阻 Ra 和电枢电感 La,转子转动时还会产生反电动势 E = Ke × ω。当 PWM 导通时,电源电压加在电机上,电流上升,产生转矩;PWM 关断时,电感和续流二极管维持电流继续流,电机依靠惯性继续转。只要开关频率远大于电机的电气时间常数,电枢电流就基本连续,电机两端平均电压近似等于 D × VCC。
平均电压升高,电枢电流平均值和转速都会上升;平均电压降低,转速下降。注意它不是完全的线性关系:电机存在静摩擦和死区电压,占空比太小时可能不转;机械负载变化时,同样占空比下的转速也会变化。所以 PWM 调速是“开环调压”的典型方式,适合风扇、水泵、玩具马达这类要求不高的场景。如果要精确稳速,必须加编码器反馈,用 PID 去修正占空比,后面我们会细说。
2.2 占空比决定“平均电压”,频率决定“电流平滑程度”
很多教程只讲占空比,不讲频率。占空比相当于油门踩多深,频率相当于油门踏板抖多快。对电机来说,频率太低,电流纹波大,电机会一顿一顿,甚至转几毫秒停几毫秒,听起来就是咔咔的抖动;频率太高,MOS 管开关损耗、驱动损耗和铁损都上来,控制器发热,电机还可能啸叫。
经验值方面,玩具直流电机和散热风扇,PWM 频率在 1kHz 到 20kHz 之间比较常见。低于 1kHz 容易听到明显的换相噪声,高于 20kHz 进入超声范围,人耳听不见,但开关损耗明显增加。如果你的电机在某个频率下发出刺耳的声音,大概率是 PWM 频率落在音频范围内,并且和电机的机械谐振点接近。把频率往上调到 20kHz 以上,或者避开谐振点,声音会消失。另外,电机两端一定要并联续流二极管,这个不能省,否则关断瞬间电感产生的高压会击穿 MOS 管或单片机引脚。
2.3 为什么线性调压不行,必须用开关方式
有人问,我用一个可调电阻串在电源和电机之间,或者用线性稳压器调压,不也能调速吗?能用,但效率不行。可调电阻上分压的功率全部变成热量,电机轻载时电阻发热尤其严重。线性稳压也一样,压差越大损耗越大。PWM 的功率管只有导通和截止两个状态:导通时电流从 MOS 管流过但导通压降很小,截止时电流为零。理想情况下开关管本身不消耗能量,所以效率可以达到 90% 以上。这就是电机驱动、LED 驱动普遍采用 PWM 而不是模拟调压的根本原因:用高频开关换取极低的损耗。
到这里,LED 和电机两件事就统一了:PWM 调速调的不是“电压幅值”,而是“平均能量”。接下来进入实操,看看怎么在 51、STM32、HC32F460 上把 PWM 真正发出来。
3. 三种平台的 PWM 实操对照
3.1 51 单片机:定时器中断模拟 PWM,也能调速
增强型 51 单片机会有 PCA/CCP 硬件 PWM,但基础款 51 大多没有,只能用定时器中断模拟。思路并不复杂:设置一个固定周期的定时中断,比如 20us 一次,用一个变量累计。假设累计到 50 为一个完整周期,那么 PWM 频率就是 1/(20us × 50) ≈ 1kHz。占空比控制变量 duty,每次中断计数器加一,计数器小于等于 duty 时输出点亮电平,否则输出熄灭电平。代码骨架大致是:
void Timer0_ISR(void) interrupt 1 { static unsigned char cnt = 0; /* 重装定时器初值,具体值取决于晶振 */ TH0 = 0xFF; TL0 = 0x38; cnt++; if(cnt <= duty) LED = 0; /* 低电平点亮 */ else LED = 1; if(cnt >= period) /* period 设定 PWM 周期 */ cnt = 0; }这是最朴素的方法,缺点是中断频繁占用 CPU,占空比变化会产生轻微抖动。但对你理解 PWM 有很大帮助:肉眼看到的“占空比”就是单片机在中断里反复翻转引脚的结果。做产品时,尽量选带硬件 PWM 的单片机,STM32、HC32F460 甚至很多国产增强型 51 都有,硬件 PWM 不需要 CPU 参与,波形也更稳定。
3.2 STM32CubeMX 配置 PWM:模式 1 和模式 2 到底哪不同
用 STM32CubeMX 配置 PWM 非常方便。选择定时器,比如 TIM2,勾选 Channel1,Mode 设置为 PWM Generation CH1,然后在 Parameter Settings 里设置 Prescaler、Counter Period(也就是 ARR)和 Pulse(也就是 CCR)。PWM 频率 = 定时器时钟 / ((Prescaler+1) × (Counter Period+1))。假设定时器时钟 80MHz,Prescaler=79,ARR=999,频率 = 80MHz / (80 × 1000) ≈ 1kHz。占空比 = Pulse / ARR × 100%,Pulse 设置成 500 就是 50%。接着配置 GPIO,引脚会自动复用为定时器输出。
这里最容易搞混的是 PWM Mode 1 和 Mode 2。以向上计数和正极性为例:模式 1 是 CNT < CCR 时输出有效电平,否则输出无效电平;模式 2 刚好相反。所谓有效电平由 Polarity 决定,可以是高电平或低电平。实际效果就是模式 1 和模式 2 输出的波形相差 180 度,占空比互补。如果你要正逻辑的 50% 占空比,用模式 1 + High Polarity;如果你想把波形反相,就换成模式 2。CubeMX 生成代码后,调用 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1) 启动,修改占空比就改 CCR1 寄存器,比如:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 600); /* 60% 占空比 */还有一个细节:CubeMX 里 PWM 输出引脚默认配置为复用推挽输出。如果你的 LED 接法是低电平点亮,就要对应选择低电平有效的 Polarity,否则代码里看着占空比是对的,实际灯却反着亮。
3.3 HC32F460 单片机 PWM 配置历程:PA8 和定时器复用
这几年国产单片机在项目里越来越常见。HC32F460 是华大半导体的一颗 Cortex-M4 内核芯片,主频可以做到 200MHz,定时器资源多,做电机控制很合适。它的 PWM 配置思路和 STM32 类似,但库函数名和寄存器结构不同,很多从 STM32 转过来的人第一反应是找 CubeMX,结果发现没有,只能看参考手册和官方例程。
以官方库的定时器 PWM 输出为例,大致流程是:使能外设时钟,配置引脚复用功能,初始化定时器 PWM 通道,设置周期寄存器、占空比寄存器和极性,最后使能输出。比如你想让 PA8 输出 PWM,要先在数据手册里查 PA8 对应的定时器复用功能,然后在初始化函数里把引脚从普通 GPIO 切到复用模式。代码骨架大致是这样:
/* 伪代码,具体寄存器名以 HC32F460 固件库为准 */ stc_pwm_init_t stcPwmInit; stcPwmInit.enClkDiv = PwmClkDiv1; stcPwmInit.enPeriod = 1000; /* 周期计数值 */ stcPwmInit.enDuty = 500; /* 占空比计数值 */ stcPwmInit.enPolarity = PwmHighPolarity; PWM_Init(&stcPwmInit); PWM_Start();这里特意用伪代码,是因为不同 SDK 版本命名差异很大,直接照抄别人的代码容易抄错。我的建议是:先跑通官方例程里带 PWM 或 TIMER 的工程,做一次 LED 亮度渐变,再改周期和占空比。HC32F460 的 PWM 时钟源可以和系统时钟不同步,要注意分频系数计算,别直接用主频去除。引脚复用必须查表,PA8 能输出 PWM 不代表任意引脚都能输出 PWM,这是和 STM32 一模一样的原则。
另外,HC32F460 的定时器 PWM 支持互补输出和死区配置,做 H 桥电机驱动时用得着。配置死区时,先确认驱动芯片的输入延时要求,一般死区设置在几百纳秒到 2 微秒之间,太小会直通,太大会让波形畸变、效率下降。
3.4 驱动能力:单片机引脚不能直接拖电机
PWM 波形只是控制信号。单片机的 IO 口直接驱动 LED 没问题,直接驱动直流电机就是灾难。电机正常工作电流可能是几百毫安甚至几安培,而单片机 GPIO 最大输出只有几个毫安到二十几个毫安,强拉会导致引脚过流、芯片发热甚至烧毁。正确做法是加一级驱动电路,用 MOS 管或者专用电机驱动芯片。
以最简单的 N 沟道 MOS 管低边驱动为例:PWM 引脚接栅极,电机一端接电源正极,另一端接 MOS 管漏极,源极接地,电机两端反并联续流二极管,栅极到源极之间加 10k 到 100k 下拉电阻防悬空。MOS 管导通时电机得电,PWM 占空比控制导通时间。选 MOS 管时注意栅极电压是否兼容 3.3V 逻辑,很多单片机 IO 高电平只有 3.3V,普通 MOS 管阈值电压 2 到 4V,可能导通不完全,导致管子发热。所以要么选逻辑电平 MOS 管,要么用三极管或栅极驱动芯片抬压。
做大功率 LED 产品时也一样,最好用专门的 LED 驱动芯片,而不是让 MCU 直接 PWM 大电流 LED。LED 驱动芯片自带恒流和调光引脚,效率和一致性更好,MCU 只需要给一个 PWM 调光信号,功率部分交给专用芯片。
4. 进阶用法:死区、PID 和那些容易忽略的坑
4.1 互补 PWM、死区和故障保护:电机控制的生死线
当你从单个 PWM 控制 LED,升级到用 H 桥控制直流电机正反转,事情就不只是调占空比了。H 桥四个开关管交替导通,如果同侧上下管同时导通,电源会直接短路,电流瞬间冲到几百安培,烧管冒烟只在几微秒之间。所以控制器输出的两路 PWM 必须严格互补,并且在一路关闭到另一路开启之间插入死区时间。大多数 MCU 的定时器都支持死区插入,STM32 用 Break 和 DeadTime 配置,HC32F460 也有类似功能。
另一个容易被忽略的是故障保护。电机过流、过压、堵转时,最好让 PWM 输出立刻变成安全状态,而不是等主程序反应过来。好的 MCU 支持 PWM 刹车或故障输入引脚,比如把比较器输出接到 PWM 故障脚,触发后硬件自动关断输出,CPU 中断只是通知软件做记录。这个功能在英飞凌的 CCU6、STM32 的高级定时器上都有对应名字,叫 PWM 刹车、FAIL 或 TRAP。做动力相关产品,这个功能不能省,软件置电平的速度远不如硬件刹车快。
4.2 PID 调速:PWM 只是执行器,不是控制算法
很多人做完开环调速后发现一个现象:占空比设成 50%,电机空载转 3000 转,负载一加上去转速掉到 2000。这不是 PWM 失效了,而是开环控制本来就没有修正能力。要让转速稳定,必须闭环。常用方案是编码器测速,把实际转速和目标转速比较,误差送进 PID 控制器,PID 输出一个合适的 PWM 占空比。也就是说,PWM 是系统的“手”,PID 是“大脑”。
实际写代码时,PID 输出往往是浮点数,而占空比寄存器只能取整数,要做限幅处理。比如输出限制在 0 到 ARR 之间,防止积分饱和。参数整定我的经验是:先只调 P,从小到大,看到转速开始震荡,再加大 P 到震荡刚好消失;然后加 I 消除稳态误差,D 用于抑制超调。电机响应快,PID 周期一般用 5ms 到 20ms,比 PWM 周期慢很多。千万别在 PWM 中断里面做 PID 运算,那样既浪费资源,又会让控制周期不合理地变短,用固定时间基准比如 systick 去更新占空比即可。
4.3 LED 驱动器中的 FB、CS 引脚和 PWM 调光的关系
热词里有个“led 驱动器芯片 fb cs 脚调整”,这里简单说一下。FB 是反馈引脚,通过外部电阻分压采样 LED 电流或输出电压,芯片内部误差放大器根据 FB 电压决定内部开关的占空比,实现恒流或恒压。CS 是电流采样引脚,通常接采样电阻,电阻两端电压与电流成正比,到达阈值就关断开关管,实现逐周期限流。所以 FB 和 CS 属于模拟反馈,最终也在调整芯片内部 PWM 的占空比,和 MCU 直接输出 PWM 调光是两条路径。
如果用 MCU 给这类 LED 驱动芯片调光,常见做法是找 DIM、EN 或 PWM 引脚,把单片机的 PWM 接上去。低速 PWM,几百赫兹,直接改变平均电流;如果 PWM 频率太高,超过芯片的响应带宽,就需要把 PWM 滤波成模拟电压送到 FB 或 REF 引脚。我看到不少人直接把几十 kHz 的 PWM 接到 DIM 引脚,结果光通量变化不明显,就是因为芯片内部把高频 PWM 滤平了。先查芯片手册的调光频率范围,再决定用哪种方式,这比盲目加大占空比更有效。
4.4 用 PWM 监控硬盘温度?其实是温控风扇调速
搜索热词里有个问题叫“pwm 怎么监控硬盘的温度”。严格说,PWM 不能直接监控温度,它是一根控制线。在很多主板上,风扇接口的第 4 针就是 PWM 控制线,主板通过温度传感器,比如贴在硬盘、CPU 散热器上的 NTC,读到温度,再用 PWM 控制风扇转速:温度高就提高占空比,温度低就降到 30% 左右。监控温度靠的是热敏电阻或数字温度传感器,PWM 只是执行降温动作。
如果你用单片机做温控风扇,流程就是:ADC 读 NTC 电压,查表或计算温度,然后映射到 PWM 占空比,再加迟滞,防止风扇在临界点反复启停。这里有个小技巧:风扇 PWM 频率别选太低,低于 1kHz 风扇内部可能会有明显噪音;很多风扇模块要求 PWM 频率在 25kHz 左右,用之前先看规格。所谓“监控硬盘温度”,本质上是一个温度采集 + PWM 调速的闭环系统。
5. 常见问题与排查记录
5.1 为什么我的 PWM 输出只有高电平或低电平
最常见的原因有三个。第一,定时器没启动,STM32 里要调用 HAL_TIM_PWM_Start,HC32 里要调用 PWM_Start,不是初始化完就有波形。第二,比较值 CCR 大于等于周期值 ARR,占空比变成 100% 或 0%;如果你中途把 CCR 改到超过 ARR,波形就会锁死在全开或全关状态。第三,引脚复用功能没配好,GPIO 没有切到复用模式,比如 PA8 还是普通 GPIO,定时器信号根本没被引出来。
排查方法不复杂:先用示波器或逻辑分析仪看引脚波形。没有示波器的话,可以先把频率调到很低,比如 1Hz 左右,用万用表看看平均电压,或者直接听蜂鸣器的声音变化,再逐步调高频率。不要上来就怀疑芯片坏,八成是配置问题。
5.2 电机用 PWM 调好后还是发热严重,怎么查
先量 MOS 管栅极波形,如果上升沿和下降沿很缓,说明驱动电流不足,管子在中间线性区停留时间太长,损耗全在这里。解决办法是降低栅极电阻、加推挽驱动,或者换专门的栅极驱动 IC。再查 PWM 频率,频率太高,开关损耗增加;频率太低,电流纹波大,铜损增加。发热往往是多种因素叠加,不要只换管子不查驱动信号。
还有一个常见情况是占空比有效但电机没转,发热却很快。这是因为平均电压低于电机启动死区电压,电机转子不动,电枢电流一直很大,能量全变成铜损。这时候要么提高启动占空比,要么先用全速启动再切到低速 PWM。
5.3 电机转起来有“吱吱”声,是坏了吗
大概率不是坏了,是 PWM 频率落在音频范围内。把频率从 1kHz 改到 20kHz 以上,声音通常会消失。如果电机还配了减速齿轮,还要检查齿轮啮合和轴承润滑,但 PWM 频率是第一嫌疑。另外,避免让 PWM 占空比长期低于某个阈值,比如 10% 以下,电机可能因为死区电压不转,但一直有功率消耗,时间久了会过热。
如果用的是有刷电机,还要检查电刷和后盖是否有磨损痕迹。PWM 驱动有刷电机时,电流纹波会在电刷处产生火花,高频噪声也可能干扰控制电路。这时候可以在电机电源端加一个小容量的陶瓷电容,并在电机外壳和控制地之间做好接地。
5.4 问题速查表
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| PWM 不输出 | 定时器未启动、引脚复用错误、CCR 大于等于 ARR | 示波器量引脚,检查定时器寄存器 |
| 占空比和预期相反 | PWM Mode 1/2 配错、Polarity 没选对 | 检查模式配置和有效电平 |
| LED 亮度变化非线性 | 驱动器调光引脚频率超限 | 查芯片手册的调光频率范围 |
| 电机发热或异响 | 频率不合适、死区不够、驱动不饱和 | 测栅极波形,调整频率,加死区 |
| 转速不稳 | 开环控制、负载变化、占空比过小 | 加编码器,使用 PID 闭环 |
最后分享一个我自己的调试习惯:刚开始调 PWM,哪怕只是点亮一个 LED,也要先用示波器或逻辑分析仪看波形,确认周期、占空比和极性是你想要的那一个,再去看亮度、转速这些物理结果。因为软件里看到的寄存器值并不总是等于引脚上的波形,极性问题、复用问题、启动顺序问题,都可能让输出和预期相差一个反相或几微秒。把波形看明白,LED 和电机的问题就解决了一大半。PWM 这套东西,从原理到落地其实不复杂,关键是每个细节都要有数据支撑,不要靠猜。