1. 从一颗LED的呼吸感说起:PWM到底在干什么
很多人第一次接触PWM,是在调LED亮度的时候。代码里写一个TIM_SetCompare1(TIM3, 500),灯就暗了一半;改成900,灯又亮起来。看起来像是"调电压",但如果你拿万用表去量LED两端的电压,会发现它几乎没变——还是3.3V或者5V。那亮度为什么变了?
答案藏在时间维度里。PWM的全称是Pulse Width Modulation,脉冲宽度调制。它不去改变电压的高低,而是改变"高电平持续的时间占总周期的比例"。这个比例叫占空比(Duty Cycle)。占空比50%,意思是一个周期里有一半时间输出高电平、一半时间输出低电平。对于LED这种响应速度极快的器件,它根本"感觉"不到每一次开关,人眼更感觉不到——人眼的视觉暂留大约是几十毫秒级别,只要PWM频率高于100Hz左右,看到的就是一个稳定的平均亮度。
所以PWM调速、调光的本质,是用数字信号模拟出模拟量的效果。单片机最擅长的是输出0和1,最不擅长的就是输出1.65V这种中间值。PWM绕开了这个短板:我只会输出0和1,但我可以控制0和1的比例,后端的执行器件(LED、电机、加热丝)自己会把这份"比例"积分成平均效果。
这里有个容易被忽略的点:PWM不是真的在调压,而是后端器件在"帮"你调压。LED靠人眼积分,直流电机靠转子的机械惯性和电感积分,加热丝靠热惯性积分。如果后端器件的响应速度比PWM周期还快,那PWM就失效了——比如你拿PWM去驱动一个高速响应的电磁阀,它就会跟着一起"哒哒哒"地抖。理解这一点,后面调电机、调背光、做DAC滤波的时候,很多现象就顺了。
关键词里出现了STM32、GPIO、直流电机、LED,这几个词基本勾勒出了PWM最典型的学习路径:先用GPIO点灯,再用定时器输出PWM调光,最后拿PWM去驱动电机。这条路我走过不止一遍,也踩过不少坑,下面按这个顺序把原理和实操都摊开讲。
2. 定时器是怎么把"占空比"变成一根引脚上的方波的
2.1 从GPIO翻转说起:最笨但最直观的PWM
在讲定时器硬件PWM之前,先说清楚软件PWM,因为它是理解一切的起点。假设你要在PA8上输出一个1kHz、占空比50%的方波,用最原始的办法:
while(1) { GPIO_SetBits(GPIOA, GPIO_Pin_8); // 拉高 delay_us(500); // 高电平持续500us GPIO_ResetBits(GPIOA, GPIO_Pin_8); // 拉低 delay_us(500); // 低电平持续500us }1kHz的周期是1000us,高电平500us、低电平500us,占空比就是50%。想调亮度,把两个delay改成300和700,占空比就变成30%。这就是软件PWM,逻辑上完全正确。
但它的致命问题是:CPU被绑死了。这个while循环里干不了别的事,一旦你去做别的任务,delay就不准,波形就抖。而且delay_us本身受中断影响,稍微有个定时器中断进来,占空比就飘了。所以软件PWM只适合"演示原理"或者"对波形要求极低"的场合,比如某些对时序不敏感的指示灯。
2.2 硬件PWM:让定时器自己数数,CPU该干嘛干嘛
STM32这类MCU内部有专门的定时器外设,它能自己计数、自己比较、自己翻转引脚,全程不需要CPU插手。核心是三个寄存器在配合:
- ARR(Auto-Reload Register,自动重装载寄存器):决定PWM的周期。计数器CNT从0数到ARR,然后归零重新开始。
- CCR(Capture/Compare Register,捕获比较寄存器):决定占空比。CNT小于CCR时输出一种电平,大于等于CCR时输出另一种电平。
- PSC(Prescaler,预分频器):把定时器的时钟源分频,用来调频率。
PWM频率的公式是:
Fpwm = Fclk / ((PSC + 1) × (ARR + 1))
假设STM32的定时器时钟是72MHz,你想要1kHz的PWM:
- 先定PSC。如果PSC=71,那么分频后时钟是72MHz/72=1MHz,也就是计数一次1us。
- 再定ARR。要1kHz周期即1000us,ARR+1=1000,所以ARR=999。
- 占空比由CCR决定。CCR=500,占空比就是500/1000=50%;CCR=300,就是30%。
这套参数我建议你亲手算一遍,别直接抄。因为很多人抄了别人的代码,ARR=999、PSC=71,结果发现频率不对——原因往往是那颗芯片的定时器时钟不是72MHz,可能是84MHz、168MHz,或者挂在APB1上被二分频了。定时器时钟源到底是多少,一定要去查时钟树,这是新手最容易翻车的地方。
2.3 PWM模式1和模式2:一个反相开关的区别
STM32的定时器有个"PWM模式1"和"PWM模式2"的选项,很多人配的时候随手选一个,能用就不管了。其实区别很简单:
| 模式 | CNT < CCR 时 | CNT ≥ CCR 时 | 典型用途 |
|---|---|---|---|
| PWM模式1 | 有效电平(通常高) | 无效电平(通常低) | 常规调光、调速 |
| PWM模式2 | 无效电平(通常低) | 有效电平(通常高) | 需要反相的场合 |
所谓"有效电平",还要看CCER寄存器里的极性设置。极性为高、模式1,就是CNT<CCR时输出高;极性为低、模式1,就是CNT<CCR时输出低。模式和极性是两个独立的开关,组合起来有四种情况,配错一个,波形就跟你想象的反过来。
我踩过的坑:有一次调电机,代码里CCR从0加到999,电机却从满速降到停转,跟预期完全相反。查了半天才发现是极性配反了。所以调PWM的第一步,永远是拿示波器或者逻辑分析仪看一眼实际波形,别靠脑补。
2.4 为什么PA8能输出PWM,而PA9不一定行
关键词里有个"pa8输出pwm",这其实点到了一个关键概念:引脚复用(Alternate Function)。STM32的每个GPIO引脚背后都连着一个"复用功能选择器",PA8默认是普通IO,但你可以把它切到TIM1_CH1,这样定时器1的通道1就能直接驱动这根引脚。
但不是每个引脚都能复用成任意定时器通道。PA8能接TIM1_CH1,是因为芯片内部就是这么连的;你换成PA9,它可能只能接TIM1_CH2,或者根本接不了TIM1。具体哪个引脚对应哪个定时器通道,必须查芯片的"复用功能映射表"(Alternate Function Mapping),这份表在数据手册里,不在参考手册里,很多人找错地方。
配置的时候,GPIO要设成复用推挽输出(AF_PP),而不是普通的推挽输出。设成普通推挽,定时器再怎么数数,引脚也不会动,因为它根本没接到定时器上。这个坑我在带新人的时候见过太多次了。
3. 拿PWM调LED亮度:从"能亮"到"亮得舒服"
3.1 频率选多少:100Hz还是20kHz
调LED亮度,PWM频率不是随便定的。太低,比如50Hz,人眼能看出闪烁,尤其是眼睛快速扫过的时候,会有明显的"频闪感"。太高,比如100kHz,又没必要,还会增加开关损耗和EMI。
我的经验值:
- 普通指示灯:1kHz左右足够,看不出闪,实现简单。
- 摄像头会拍到的LED:至少4kHz以上,否则录像里会出现滚动黑条。
- 人眼长时间注视的背光:建议20kHz以上,彻底避开人眼敏感频段,减少视觉疲劳。
关键词里有个"pwm背光 干扰 显示",这是个很典型的实际问题。LCD背光用PWM调光时,如果频率落在音频范围内(比如2kHz),或者走线离显示信号线太近,就可能干扰显示,出现横纹、闪烁。解决办法通常是:把背光PWM频率提到20kHz以上(超出音频范围),同时背光走线远离数据线,必要时加RC滤波。
3.2 占空比和亮度的关系不是线性的
很多人以为占空比50%就是"一半亮度",实际上人眼对亮度的感知是近似对数的。占空比从0%到10%,亮度变化非常明显;从90%到100%,几乎看不出差别。所以如果你要做"呼吸灯"或者平滑调光,直接线性改CCR,效果会很别扭——前段变化太快,后段几乎没感觉。
解决办法是做伽马校正,把线性占空比映射到感知亮度。简单做法是查表:
// 8位伽马校正表(简化示意) const uint16_t gamma_table[16] = { 0, 1, 2, 4, 8, 16, 28, 44, 64, 90, 120, 158, 200, 248, 300, 360 };实际项目中,我一般用2.2或2.4的伽马值生成一张256项的查找表,CCR直接查表取值。这样呼吸灯看起来才顺滑。
3.3 LED驱动芯片的FB和CS脚:PWM调光的另一条路
关键词里提到"led驱动器芯片fb cs脚调整",这说的是另一类方案:不用MCU直接输出PWM驱动LED,而是用专门的LED驱动芯片,MCU通过PWM去调芯片的反馈(FB)或电流采样(CS)脚。
这种方案的好处是:LED的电流由驱动芯片闭环控制,MCU只负责给一个"目标值"。比如驱动芯片的FB脚基准是0.2V,你用PWM加RC滤波出一个0~0.2V的模拟电压接到FB脚,就能控制输出电流。或者更直接,用PWM去调CS脚的采样阈值。
这条路适合大功率LED、需要恒流的场合。缺点是响应速度受RC滤波限制,调光频率做不高。我做过一个项目,用PWM+RC调FB脚,RC时间常数取10ms,结果调光频率只能做到几十Hz,勉强够用。如果要做高速调光,还是得用驱动芯片自带的PWM调光脚(通常是EN脚或者专门的DIM脚)。
3.4 一个实测细节:PWM调光时LED的"余辉"
有个现象很多人没注意:PWM调光时,如果频率不够高,LED在低电平期间其实还有微弱余辉,尤其是荧光粉LED。这在暗环境下用高速相机拍,能看到明显的拖尾。对普通应用无所谓,但如果是做光学测量、或者配合高速相机,就得考虑用恒流线性调光替代PWM调光,或者把PWM频率提到很高。
4. 让直流电机转起来:PWM调速和LED调光的本质区别
4.1 电机是感性负载,不能直接拿GPIO怼
LED是二极管,正向导通、反向截止,相对"听话"。直流电机是感性负载,断电瞬间电感会产生反向电动势,电压可能高到把GPIO打穿。所以电机绝对不能直接接GPIO,必须经过驱动电路。
常见方案:
- H桥驱动芯片(如DRV8833、L298N):能正反转,适合需要换向的场合。
- MOS管+续流二极管:适合单向调速,成本低。
- 专用电机驱动模块:集成度高,适合快速验证。
关键词里有"stm32 drv8323",这是TI的一款三相栅极驱动器,用于BLDC电机。虽然和普通直流电机不是一回事,但PWM调速的底层逻辑是相通的:都是通过控制功率管的开关时间比例来调平均电压。
4.2 电机调速的PWM频率:为什么不能太低也不能太高
电机调速的PWM频率选择,和LED完全不同,因为电机的"积分"靠的是机械惯性和电感,不是人眼。
- 频率太低(<1kHz):电机会听到明显的"啸叫",因为绕组在音频范围内振动。低速时还可能感觉到"一顿一顿"的。
- 频率太高(>20kHz):开关损耗增加,MOS管发热,驱动芯片可能扛不住。而且高频下电感的续流效果变差,电流纹波反而可能变大。
我的经验值:有刷直流电机用8kHz~16kHz,这个区间超出人耳敏感范围,开关损耗也可接受。无刷电机(BLDC)通常用20kHz以上,因为要配合换相逻辑。
4.3 占空比和转速的关系:不是线性的,但比LED好
电机转速和占空比的关系,比LED亮度线性得多,因为电机的反电动势和转速成正比,而平均电压和占空比成正比。理想情况下,转速≈K×占空比。但实际上:
- 死区:占空比低于某个值(比如5%),电机根本转不起来,因为力矩不够克服静摩擦。
- 饱和:占空比接近100%时,转速受限于电源电压和电机特性,不再线性增加。
- 负载影响:同样的占空比,空载和带载转速差很多。
所以做闭环调速时,光靠PWM开环是不够的,得加编码器或者霍尔传感器做反馈。关键词里的"stm32定时器捕获测频率",就是用来读编码器脉冲的。
4.4 一个真实的坑:PWM调速时的"启动电流"
电机启动瞬间,反电动势为0,相当于短路,电流非常大。如果PWM占空比一上来就给很高,启动电流可能烧驱动芯片。正确做法是软启动:占空比从0慢慢加到目标值,比如每10ms加1%,给电机一个加速过程。
我在一个项目里偷懒,直接给80%占空比启动,结果DRV8833的过流保护反复触发,电机抖两下就停。后来加了软启动,问题解决。这个细节在数据手册里不会写,但实际做产品必须考虑。
5. 从寄存器到CubeMX:配置PWM的几条路径和各自的坑
5.1 直接写寄存器:最透明,但最容易配错时钟
直接操作寄存器配置PWM,步骤大致是:
- 使能定时器时钟和GPIO时钟(RCC寄存器)。
- 配置GPIO为复用推挽输出。
- 配置定时器的PSC、ARR。
- 配置PWM模式、极性、使能通道。
- 使能定时器。
// 以TIM3_CH1(PA6)为例的简化示意 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; GPIOA->MODER &= ~(3 << 12); GPIOA->MODER |= (2 << 12); // PA6复用模式 GPIOA->AFR[0] |= (2 << 24); // AF2 = TIM3 TIM3->PSC = 71; // 72MHz/72 = 1MHz TIM3->ARR = 999; // 1kHz TIM3->CCR1 = 500; // 50% TIM3->CCMR1 |= (6 << 4); // PWM模式1 TIM3->CCMR1 |= (1 << 3); // 预装载使能 TIM3->CCER |= (1 << 0); // 通道1使能 TIM3->CR1 |= (1 << 0); // 定时器使能这条路的好处是你清楚每一个bit在干什么,坏处是时钟树、复用映射、寄存器位定义全靠自己查,错一个就出不来波形。我建议新手至少手写一遍,之后再上CubeMX。
5.2 CubeMX配置:快,但要理解它生成了什么
CubeMX配置PWM,点几下鼠标就出来了。但很多人配完不知道它到底改了什么,出了问题无从下手。我的习惯是:CubeMX生成代码后,一定去翻一遍它写的初始化函数,对照上面那套寄存器逻辑看一遍。这样既快,又不至于变成"只会点鼠标"。
CubeMX里几个容易配错的点:
- Clock Configuration:定时器时钟到底是多少,一定要在这里确认。APB1的定时器时钟可能是APB1频率的2倍,别想当然。
- Prescaler和Counter Period:CubeMX里填的是PSC和ARR的实际值,不是分频系数。填PSC=71才是72分频。
- Pulse:就是CCR初值。
- GPIO Settings:确认引脚被自动配成了复用模式,有时候需要手动改。
5.3 用HAL库动态改占空比:别在中断里频繁写CCR
HAL库改占空比用__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, value)。这个操作本身很快,但如果你在中断里高频调用,可能和定时器的预装载机制打架。
正确做法是使能CCR预装载(CCMR的OCxPE位),这样你写CCR时,新值会在下一个更新事件才生效,不会出现半个周期的毛刺。CubeMX默认会开这个,但手写寄存器时容易忘。
5.4 一个排查思路:PWM没输出,按这个顺序查
PWM配好了但引脚没波形,我一般按这个顺序排查:
- 时钟使能了吗:定时器时钟、GPIO时钟,两个都要。
- 引脚复用对吗:查数据手册的AF映射表,别猜。
- 定时器使能了吗:CR1的CEN位。
- 通道使能了吗:CCER的CCxE位。
- CCR是不是0:CCR=0时,PWM模式1下输出恒低,看起来就像没输出。
- ARR是不是0:ARR=0定时器不计数。
- 有没有被别的外设占用:比如调试口、JTAG引脚冲突。
这个顺序能解决90%的"PWM没输出"问题。
6. 那些关键词背后的真实问题:从GPIO模式到PWM故障保护
6.1 GPIO的8种工作模式,PWM该选哪种
关键词里"gpio的8种工作模式"是个高频问题。STM32的GPIO有8种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。
PWM输出该选推挽复用(AF_PP)。为什么不是普通推挽?因为普通推挽是由GPIO数据寄存器控制的,定时器管不着;只有复用模式,引脚才交给定时器。为什么不是开漏?开漏需要外部上拉,上升沿慢,PWM波形会变形,除非你有特殊需求(比如电平转换)。
6.2 PWM故障保护:刹车输入和死区
关键词里"pwm故障保护"指的是定时器的刹车功能(Break)。在电机控制里,如果出现过流、过温,需要立刻关断PWM输出,保护功率管。STM32的高级定时器(TIM1、TIM8)有专门的刹车输入引脚,触发后硬件自动把PWM输出拉到安全电平,不需要CPU干预。
配置刹车功能要注意:
- 刹车极性:高电平触发还是低电平触发。
- 刹车后的输出状态:是强制拉低、拉高,还是高阻。
- 自动恢复还是需要软件清除。
这个功能在工业电机控制里是标配,做产品必须配。我见过因为没配刹车,过流时功率管炸掉的案例,损失不小。
6.3 PWM监控温度:一种"取巧"的测温法
关键词里"pwm怎么监控硬盘的温度"听起来奇怪,其实是一种利用PWM占空比传递信息的思路。比如某些温度传感器输出的是PWM信号,占空比和温度成正比,MCU用输入捕获测占空比,反推温度。或者反过来,MCU用PWM驱动一个加热元件,通过调节占空比维持恒温,同时读温度传感器做闭环。
这类应用的核心是PWM既能当输出(控制),也能当输入(测量)。输入捕获模式下,定时器记录上升沿和下降沿的时间戳,算出高电平时间和周期,占空比就出来了。
6.4 从51到STM32:为什么51驱动LED不能输出高电平
关键词里"51单片机驱动led时,为什么不能采用输出高电平的驱动方式",这是个经典问题。51的IO口是准双向口,输出高电平时驱动能力很弱(拉电流只有几十微安),而输出低电平时灌电流能力较强(可以到十几毫安)。所以51驱动LED通常采用灌电流接法:LED阳极接VCC,阴极接IO口,IO输出低电平点亮。
STM32的GPIO是真正的推挽输出,拉电流和灌电流都能到20mA左右,所以高低电平都能驱动。但即便如此,驱动大功率LED还是得加三极管或MOS管,别指望GPIO直接扛。
7. 把PWM用对:几条我反复验证过的经验
PWM这东西,原理半小时就能讲完,但真正用顺手,得靠项目里一次次踩坑。分享几条我自己的经验。
第一,永远先用示波器看波形。不管你是调LED、调电机还是调背光,配完PWM第一件事就是量波形。频率对不对、占空比对不对、极性对不对,一眼就看出来。靠脑补和靠"灯亮了就行",迟早出问题。
第二,频率选择要结合负载特性。LED看人眼,电机看听觉和开关损耗,背光看显示干扰,加热丝看热惯性。没有万能频率,只有适合当前负载的频率。
第三,占空比变化要平滑。不管是呼吸灯还是电机软启动,突变占空比都会带来问题:LED闪一下、电机抖一下、电源电压跌一下。加个斜坡,几行代码的事,能省很多麻烦。
第四,注意PWM对电源的干扰。大功率PWM负载会在电源上产生纹波,可能干扰MCU本身或者其他模拟电路。必要时加LC滤波、加大电容、把PWM负载和敏感电路分开供电。
第五,预装载和更新事件要理解清楚。改CCR、改ARR的时机不对,会出现波形毛刺或者周期跳变。使能预装载,让新值在更新事件生效,是最稳妥的做法。
第六,别忽视死区。做H桥或者三相驱动时,上下管同时导通就是短路。死区时间必须加,具体多少看功率管的开关速度,一般几百纳秒到几微秒。
PWM从LED亮度到电机转速,表面看是两个应用,底层是同一套"用时间换幅度"的逻辑。把这套逻辑吃透,再去看DAC、开关电源、 class D功放,会发现它们都是同一个思路的变体。这大概就是PWM最迷人的地方——简单,但无处不在。