1. 从一颗LED的"半亮"说起:PWM到底在干什么
很多人第一次接触PWM,都是在调LED亮度的时候。代码里写个TIM_SetCompare1(TIM3, 500),灯就暗了一半;改成900,灯又亮起来。看起来像是"输出了一个中间电压",但拿万用表一量,引脚上明明是3.3V和0V在反复跳。这个矛盾点,恰恰是理解PWM的钥匙。
PWM的全称是Pulse Width Modulation,中文叫脉冲宽度调制。它不改变电压的高低,只改变"高电平占一个周期的时间比例"。这个比例叫占空比(Duty Cycle)。假设周期是1ms,高电平持续0.5ms,占空比就是50%。对LED来说,它亮0.5ms、灭0.5ms,只要这个切换频率足够快,人眼因为视觉暂留根本分辨不出来,看到的就是"半亮"。
这里有个关键数字:人眼的闪烁融合频率大约在60Hz到90Hz之间,因人而异。所以PWM频率只要高于100Hz,调光看起来就是连续的。但如果你把频率设成20Hz,那LED就会明显闪烁,像老式日光灯管坏掉那样。我早期做呼吸灯的时候图省事,定时器分频没算对,频率掉到了30Hz,肉眼看着就是一顿一顿的,调了半天代码才发现是频率问题,不是占空比问题。
那为什么不用DAC直接输出模拟电压来调亮度呢?原因很实际:DAC输出的是真正的模拟量,但它的驱动能力很弱,带不动大电流负载,而且成本比普通GPIO高。PWM本质上是数字信号,只有高和低两个状态,用最普通的推挽输出GPIO就能产生,效率极高——三极管要么完全导通要么完全截止,几乎没有中间状态的功率损耗。这一点在驱动电机时尤其重要,后面会详细展开。
所以PWM的核心价值可以概括成一句话:用数字手段,等效地实现模拟控制。它把"调电压"这个模拟问题,转化成了"调时间比例"这个数字问题,而数字问题在单片机上处理起来又便宜又精确。
1.1 占空比、频率、周期三个参数的关系
刚上手的人容易把这三个概念搅在一起。我用一个生活化的类比:把PWM信号想象成水龙头开关。
- 周期:你开关水龙头的总时间,比如每2秒完成一次"开-关"循环。
- 频率:周期的倒数,2秒一个循环就是0.5Hz。频率越高,循环越快。
- 占空比:一个周期里"开"的时间占比。开1秒关1秒,占空比50%。
在STM32里,这三个量由定时器的三个寄存器决定:
| 参数 | 对应寄存器 | 计算公式 |
|---|---|---|
| 频率 | ARR(自动重装载值)、PSC(预分频) | Fpwm = Fclk / ((PSC+1) * (ARR+1)) |
| 占空比 | CCR(捕获比较值) | Duty = CCR / (ARR+1) |
| 周期 | 由频率决定 | T = 1 / Fpwm |
举个具体例子。STM32F103的TIM3挂在APB1总线上,时钟是72MHz。我想产生1kHz的PWM,ARR设成999,那么PSC要设成71。验算一下:72000000 / (72 * 1000) = 1000Hz,正好。如果这时候CCR设成300,占空比就是300/1000 = 30%。
注意:ARR和PSC都是从0开始计数的,所以公式里要加1。这个"加1"坑过无数新手,算出来的频率总是差那么一点点,多半就是这里漏了。
1.2 为什么调LED和调电机用的是同一个原理
LED和直流电机看起来八竿子打不着,但PWM控制它们的逻辑是相通的:都是通过快速开关,控制"平均能量"的输入。
对LED,平均电流决定了亮度。占空比50%,平均电流就是满电流的一半,亮度大约也是一半(严格说亮度与电流不是完全线性,但趋势一致)。
对直流电机,平均电压决定了转速。占空比50%,电机两端的平均电压就是电源电压的一半,转速大约降到一半左右(实际还受负载、反电动势影响,后面细说)。
区别在于:LED是纯阻性负载,响应快,几kHz到几十kHz的PWM都能用;电机是感性负载,有电感、有反电动势、有机械惯性,PWM频率的选择要讲究得多,太低会听到啸叫,太高又会让驱动管发热。这个差异,是后面章节要重点拆解的内容。
2. 从GPIO翻转到一个完整的PWM波形:定时器在背后做了什么
如果你用最笨的办法产生PWM——在while循环里手动拉高拉低GPIO,加个delay——你会发现两个问题:一是CPU被完全占死,什么都干不了;二是频率和占空比极不稳定,稍微来个中断就抖一下。所以真正做项目,没人这么干,都是用定时器硬件来产生PWM。
STM32的通用定时器(比如TIM2、TIM3、TIM4)和高级定时器(TIM1、TIM8)都自带PWM输出功能。它的工作流程是这样的:定时器内部有一个计数器CNT,从0开始每个时钟周期加1,加到ARR的值就归零重新开始,如此循环。这个CNT的循环就构成了PWM的周期。
同时,每个通道有一个比较寄存器CCR。硬件会实时比较CNT和CCR的大小:当CNT < CCR时,输出高电平;当CNT >= CCR时,输出低电平。这样,CCR的值就直接决定了高电平的持续时间,也就是占空比。整个过程不需要CPU参与,纯硬件完成,所以波形非常稳定。
2.1 PWM模式1和模式2:一个容易搞反的细节
STM32的每个PWM通道可以配置成模式1或模式2,区别就在于极性:
- PWM模式1:CNT < CCR时输出有效电平,CNT >= CCR时输出无效电平。
- PWM模式2:反过来,CNT < CCR时输出无效电平,CNT >= CCR时输出有效电平。
这里的"有效电平"又取决于你设置的输出极性(高有效还是低有效)。所以模式1+高有效,就是"CNT小于CCR时输出高";模式1+低有效,就是"CNT小于CCR时输出低"。
我见过不少人在调电机驱动时把这两个搞混,结果占空比写0电机全速转,写满电机反而停。排查半天以为是接线问题,其实是模式配反了。记住一个判断方法:先确定你的驱动电路是高电平导通还是低电平导通,再决定PWM模式和极性。比如用NPN三极管驱动,基极高电平导通,那就用模式1+高有效,CCR越大导通时间越长。
2.2 频率怎么选:LED、电机、背光各有各的脾气
PWM频率不是随便定的,不同负载对频率的敏感度完全不同。我把常见场景整理成一张表:
| 应用场景 | 推荐频率范围 | 原因 |
|---|---|---|
| LED调光 | 1kHz - 20kHz | 高于100Hz无闪烁,太高会增加开关损耗 |
| LCD背光 | 20kHz以上 | 避开人耳听觉范围,防止电感啸叫 |
| 直流电机调速 | 5kHz - 20kHz | 太低有啸叫和抖动,太高开关损耗大 |
| 舵机控制 | 50Hz | 标准舵机的固定要求,周期20ms |
| 开关电源 | 几十kHz - 几百kHz | 频率越高电感电容可以越小 |
这里重点说LCD背光。很多人调背光时能听到"滋滋"声,就是因为PWM频率落在了人耳可听范围内(20Hz-20kHz),背光升压电路里的电感在频率激励下产生了机械振动。解决办法很简单:把频率提到20kHz以上,超出听觉范围,声音就消失了。这个坑我在做一块带背光的屏时踩过,当时以为是屏质量问题,换了三块屏都有声,最后才反应过来是PWM频率的问题。
电机这边还有个额外考虑:PWM频率如果和电机的机械共振频率接近,会引起明显抖动。一般5kHz到20kHz是比较安全的区间,既能避开听觉,又不会让MOS管发热太厉害。
2.3 死区时间:高级定时器为什么比通用定时器贵
如果你只是驱动LED或者单向转的电机,通用定时器足够了。但如果你要做电机正反转,用H桥驱动,那就得考虑死区时间。
H桥有上下两个开关管,如果上管还没完全关断、下管就导通了,电源和地之间会瞬间短路,这叫"直通",轻则烧管,重则炸板。死区时间就是在上下管切换之间插入的一小段"都关断"的空白时间,确保不会直通。
STM32的高级定时器(TIM1、TIM8)硬件支持死区时间插入,通用定时器则没有。这就是为什么做电机控制、数字电源这类项目,大家优先选带高级定时器的型号。死区时间一般设几百纳秒到一两微秒,具体看你的MOS管开关速度,太快了起不到保护作用,太慢了会损失效率、波形失真。
3. 让直流电机真正转起来:PWM调速背后的电气逻辑
理解了PWM怎么产生,接下来要解决的是"怎么用它驱动电机"。这里有个关键认知:单片机的GPIO引脚是驱动不了电机的。STM32一个引脚最多输出20mA左右,而一个小型直流电机启动瞬间可能要几百mA甚至上安培。直接接上去,要么电机不转,要么引脚烧掉。
所以中间必须加驱动。常见的方案有三种:三极管/MOS管、专用电机驱动芯片(如L298N、DRV8833、DRV8323)、以及H桥电路。选哪种取决于你的电机功率、是否需要正反转、以及成本预算。
3.1 单向调速:一个MOS管就够了
如果电机只需要单向转,最简单的方案是用一个N沟道MOS管做低边开关。PWM信号接到MOS管的栅极,电机一端接电源正极,另一端接MOS管漏极,MOS管源极接地。
当PWM为高,MOS管导通,电机通电;PWM为低,MOS管截止,电机断电。由于PWM切换很快,电机因为机械惯性,转速不会跟着开关跳变,而是稳定在某个平均转速上。占空比越大,平均电压越高,转速越快。
这里有个细节:电机是感性负载,断电瞬间会产生反向电动势,可能击穿MOS管。所以必须在电机两端并联一个续流二极管(比如1N5819肖特基二极管),给反向电流提供泄放回路。这个二极管不加,短时间可能没事,但长期运行MOS管迟早会坏。我见过有人省这个二极管,结果跑了两天管子就冒烟了。
3.2 正反转调速:H桥和死区的配合
需要正反转就得用H桥。H桥由四个开关管组成,通过控制哪两个导通,可以改变电机两端电压的极性,从而实现正转和反转。
用PWM调速时,通常有两种策略:
- 单极性PWM:一个桥臂常通,另一个桥臂用PWM调制。比如正转时左下管常通,右上管PWM。这种方式调速平滑,但只能单向调速。
- 双极性PWM:两个桥臂都用PWM,互补输出。这种方式响应快,但开关损耗大,且必须加死区。
对于STM32,如果用高级定时器的互补输出通道,硬件会自动插入死区,省心很多。如果用普通GPIO软件模拟H桥时序,那死区就得自己在代码里加延时,而且延时不准,风险很大。所以做电机项目,我强烈建议用带高级定时器的型号,或者直接用集成H桥的驱动芯片,把死区交给芯片处理。
3.3 为什么电机调速不是线性的
很多人以为占空比50%电机就转一半速度,实际测下来往往不是。原因有几个:
第一,电机有启动死区。占空比太低时,平均电压不足以克服静摩擦和负载,电机根本不转。这个临界点可能在20%到30%之间,取决于电机和负载。
第二,反电动势的影响。电机转起来后会产生反电动势,方向与电源相反,抵消一部分电压。转速越高,反电动势越大,实际电流越小。所以占空比和转速的关系是曲线,不是直线。
第三,负载变化。空载和带载时,同样的占空比转速完全不同。带载时转速会掉下来,需要闭环控制才能稳住。
所以如果你要做精确调速,光靠开环PWM是不够的,得加编码器测速,做PID闭环。这也是为什么航模、机器人这类项目,电机控制往往是个独立的复杂话题。
4. 那些年我在PWM上踩过的坑:从波形异常到干扰排查
理论讲完了,说点实战中真正让人头疼的问题。PWM看起来简单,但一旦和实际电路结合,各种幺蛾子就出来了。
4.1 波形出不来:先查时钟,再查引脚复用
新手最常见的"PWM没输出",排查顺序应该是这样的:
- 定时器时钟开了没:STM32的外设时钟默认是关的,用之前必须
RCC_APB1PeriphClockCmd打开。忘了这一步,定时器根本不工作。 - GPIO复用配了没:PWM输出引脚要配成复用推挽输出(AF_PP),不能配成普通推挽。配错了,引脚就是普通IO,当然没波形。
- 引脚和定时器通道对上了没:不是随便哪个引脚都能输出PWM的,得查数据手册的复用表。比如PA8对应TIM1_CH1,你非要用它输出TIM3的PWM,那肯定不行。
- ARR和CCR设了没:如果CCR是0,占空比就是0,输出一直是低电平,看起来就像没输出。
我建议养成习惯:写完PWM初始化代码,先用示波器或者逻辑分析仪看一眼波形,确认频率和占空比都对,再接负载。没有示波器的话,接个LED也能大致判断——如果LED亮度能随CCR变化,说明PWM基本正常。
4.2 PWM背光干扰显示:一个典型的电源耦合问题
这个坑比较隐蔽。现象是:LCD背光用PWM调光时,屏幕上出现横向条纹或者闪烁,背光越暗条纹越明显。
根本原因是PWM开关瞬间产生的电流突变,通过电源线或地线耦合到了显示电路。背光LED的电流可能几百mA,开关瞬间的di/dt很大,在走线电感上产生电压尖峰,干扰了敏感的显示信号。
解决办法有几个层次:
- 提高PWM频率:把频率提到20kHz以上,让干扰频率远离显示刷新频率,条纹会减轻。
- 加滤波电容:在背光电源脚附近并一个10uF到100uF的电容,吸收开关瞬间的电流波动。
- 分离地线:背光的地和显示逻辑的地分开走,最后单点汇合,避免大电流地线干扰小信号地。
- 改用恒流驱动:如果背光芯片支持模拟调光(调电流而不是调PWM),优先用模拟调光,从根上消除开关干扰。
我处理过一个项目,屏幕条纹怎么都消不掉,最后发现是背光走线和SPI数据线并排走了很长一段,把PWM干扰直接耦合到了数据线上。把走线分开后,问题立刻解决。所以PCB布局在PWM应用里非常关键,不能只盯着代码。
4.3 电机一转,单片机就复位:电源和地的问题
这个现象也很典型:电机不转时一切正常,电机一启动,单片机就重启或者程序跑飞。
原因通常是电机启动瞬间的大电流拉低了电源电压,导致单片机供电跌落到复位阈值以下。或者是电机换向时产生的电磁干扰通过地线串入了单片机。
解决思路:
- 电机和单片机分开供电:用独立的电源给电机,单片机用另一路,共地但不共电源。
- 加大电源滤波:电机电源端并大电容(几百uF到几千uF),单片机电源端并小电容(0.1uF+10uF)。
- 加光耦隔离:PWM信号通过光耦传给电机驱动,彻底隔断电气连接。
- 电机两端加RC吸收:在电机引脚上并一个几十欧电阻串0.1uF电容,吸收换向尖峰。
这些措施不一定全上,但至少要做到电源分离和滤波。我早期做小车项目时,图省事用一个电池给所有东西供电,结果电机一加速单片机就重启,折腾了好久才明白是电源问题。
5. 从能跑到跑得好:PWM进阶优化与选型建议
把PWM跑通只是第一步,真正做产品还要考虑效率、精度、可靠性。这一章聊聊进阶的优化思路。
5.1 分辨率与频率的权衡
PWM的分辨率取决于ARR的值。ARR越大,CCR可以调的档位越多,分辨率越高。但ARR大了,在时钟频率固定的情况下,PWM频率就会降低。这就是分辨率和频率的矛盾。
比如72MHz时钟,ARR=999时频率1kHz,分辨率1000级;ARR=99时频率10kHz,分辨率只有100级。如果你要做精细调光,100级可能不够,暗部会看到明显的跳变。
解决办法有两个:一是提高定时器时钟,比如用72MHz而不是36MHz;二是用更高位数的定时器,比如16位定时器ARR最大65535,比8位的256级细腻得多。选型时如果对调光精度要求高,一定要看定时器的位数。
5.2 用DMA更新CCR:做呼吸灯和波形发生
如果你的PWM占空比需要频繁变化,比如做呼吸灯、音频波形输出,用CPU一个个写CCR会很占资源。这时候可以用DMA,把预先算好的一串CCR值自动搬运到定时器,硬件自动更新占空比,CPU完全解放。
具体做法是:开一个数组存CCR序列,配置DMA从内存搬到TIMx_CCR1寄存器,定时器的更新事件触发DMA传输。这样就能产生任意波形的PWM,做呼吸灯、正弦波调制都很方便。这个技巧在音频和灯光效果项目里非常实用。
5.3 故障保护:让PWM在异常时自动关断
做电机和电源项目,安全很重要。如果出现过流、过温,PWM必须立刻关断,不能等软件慢慢处理。STM32的高级定时器有个刹车输入(Break Input)功能,检测到刹车引脚上的有效电平,硬件会立刻把PWM输出强制到安全状态,响应速度是纳秒级的,比中断快得多。
配置方法是把故障检测电路的输出接到定时器的BKIN引脚,使能刹车功能,设置刹车后的输出状态(通常是全部拉低)。这样即使软件跑飞,硬件也能保护功率级。这个功能在工业控制里几乎是标配,做产品一定要用上。
5.4 选型建议:什么项目选什么芯片
最后给个实用的选型参考:
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 简单LED调光 | 通用定时器即可 | 不需要死区、刹车 |
| LCD背光 | 通用定时器+高频 | 20kHz以上避啸叫 |
| 单向电机调速 | 通用定时器+MOS管 | 成本低,够用 |
| 正反转电机 | 高级定时器或集成驱动 | 需要死区保护 |
| 无刷电机/FOC | 高级定时器+专用驱动 | 需要互补PWM和刹车 |
| 数字电源 | 高级定时器+高分辨率 | 需要精确死区和快速保护 |
选型时不要一味追求高配,够用就好。但如果项目涉及功率级控制,高级定时器带来的死区和刹车功能,能帮你省掉很多外围电路和软件麻烦,长期看是划算的。
PWM这个东西,入门容易精通难。会写几行配置代码让灯亮起来,和真正理解它在各种负载下的行为,中间隔着大量的实践和踩坑。我到现在也不敢说完全吃透了它,每次遇到新负载、新场景,还是得重新算频率、看波形、调参数。但正是这种"简单背后有深度"的特性,让它成为嵌入式领域最值得反复琢磨的基础技术之一。