☰
PWM原理与STM32实战:从LED调光到电机调速
2026/10/27 9:18:22 网站建设 项目流程

1. 从一个现象说起:为什么调亮度不用调电压

很多人第一次接触PWM,脑子里冒出来的问题都差不多:既然LED亮度跟电压电流有关,那我直接用一个可调电阻或者DAC把电压调低一点,灯不就暗了吗?为什么非要搞一个方波出来,一会儿开一会儿关,这不是多此一举吗?

我刚开始玩STM32那会儿也是这么想的。手头有块板子,PA8接了个LED,想着用DAC输出个1.5V,结果灯要么不亮,要么亮度根本不受控,稍微调一点电压就全亮了。后来才明白,LED这种二极管的伏安特性是指数型的,电压稍微过一点,电流就飙上去了,你根本没法用电压去精细控制它的亮度。而电机更离谱,你给它降一半电压,它要么转不动,要么扭矩小得可怜,带不动负载。

PWM解决的就是这个问题。它不去改变电压的幅值,而是改变“通电时间”的比例。你可以把它想象成快速开关灯:如果一秒钟内灯亮0.5秒、灭0.5秒,只要这个开关速度足够快,人眼看到的就是一半亮度。电机也一样,通电一半时间,等效的平均电压就是一半,转速自然就下来了。这个“亮的时间占总时间的比例”,就是占空比。

所以PWM的本质,是用时间维度上的比例控制,去等效替代幅值上的连续调节。它绕开了LED和电机非线性特性的坑,用数字信号实现了模拟控制的效果。这也是为什么从LED调光到电机调速,从舵机控制到开关电源,PWM几乎无处不在。

这篇文章我会从LED亮度的变化讲起,一路拆到直流电机为什么能转起来,中间穿插STM32上PA8输出PWM的实际配置、GPIO模式的选型逻辑、以及我在调试过程中踩过的那些坑。不管你是刚点亮第一颗LED的新手,还是已经用过定时器但没深究原理的老手,应该都能从中找到一些有用的东西。

2. PWM的核心原理拆解:占空比、频率与等效电压

2.1 占空比到底控制了什么

占空比(Duty Cycle)的定义很直白:在一个PWM周期内,高电平时间与整个周期时间的比值。用公式表示就是:

占空比 D = T_on / (T_on + T_off) × 100%

假设PWM频率是1kHz,周期就是1ms。如果高电平持续0.3ms,低电平持续0.7ms,占空比就是30%。对于LED来说,30%的占空比意味着灯只有30%的时间在通电,人眼因为视觉暂留效应,感受到的亮度大约就是满亮度的30%。

但这里有个关键点:占空比控制的是平均功率,不是瞬时功率。LED在导通的那段时间里,电流还是满额电流,只是它导通的时间短了。所以你不能用“平均电流”去算LED的峰值电流,否则会低估它的实际工作应力。

对于直流电机,情况类似但更复杂一些。电机的等效平均电压等于电源电压乘以占空比:

V_avg = V_supply × D

比如12V电源,50%占空比,电机两端的平均电压就是6V。但电机是感性负载,电流不会突变,PWM的高频开关会让电流在电感的作用下变得平滑,最终表现为一个连续的转矩输出。这也是为什么电机PWM频率不能太低,否则电流纹波太大,电机会抖动、发热甚至发出啸叫。

2.2 频率选多少才合适

PWM频率的选择,本质上是在“开关损耗”和“控制平滑度”之间找平衡。频率太低,LED会闪烁,电机会抖动;频率太高,MOSFET或驱动芯片的开关损耗会增加,效率下降,EMI也会变差。

我整理了一个常见应用的PWM频率参考表,这些都是实际项目中验证过的范围:

应用场景推荐频率范围原因说明
LED调光1kHz ~ 5kHz高于200Hz人眼就不闪了,但太低会有频闪效应,5kHz以内开关损耗可忽略
直流有刷电机10kHz ~ 20kHz高于20kHz超出人耳听觉范围,避免啸叫;低于10kHz电流纹波大,转矩脉动明显
舵机控制50Hz标准舵机要求20ms周期,脉宽0.5ms~2.5ms对应角度
开关电源100kHz ~ 500kHz频率越高,电感电容可以越小,但开关损耗急剧上升
背光驱动20kHz ~ 100kHz避免音频噪声,同时满足调光精度要求

注意:如果你用PWM驱动LED,频率选在200Hz到5kHz之间是比较稳妥的。太低会有可见闪烁,太高则可能和驱动芯片的响应速度不匹配,导致调光线性度变差。

2.3 分辨率与占空比精度

占空比能调多细,取决于定时器的位数。STM32的通用定时器通常是16位的,ARR寄存器最大65535,意味着占空比可以分成65536个等级。如果你用8位定时器,就只有256个等级,调光时可能会看到明显的台阶感。

举个例子:假设PWM频率是1kHz,定时器时钟是72MHz,那么:

ARR = 72MHz / 1kHz - 1 = 71999

这个值在16位范围内(最大65535),所以1kHz下你没法用72MHz直接分频得到,需要先做预分频。比如预分频设为1,定时器时钟变成36MHz,ARR = 36000 - 1 = 35999,这样分辨率就是36000级,足够细腻了。

实际调光时,人眼对亮度的感知是非线性的,低占空比区域的亮度变化比高占空比区域更敏感。所以如果你要做平滑的调光效果,最好用伽马校正或者查表法,而不是线性地改CCR值。这个坑我在做呼吸灯的时候踩过,线性调的时候总觉得低亮度段变化太快,高亮度段又几乎没变化。

3. STM32上PA8输出PWM的完整实操

3.1 为什么选PA8:定时器通道的映射关系

STM32的PWM输出不是随便哪个GPIO都能做的,它必须连接到定时器的输出比较通道。以STM32F103为例,PA8对应的是TIM1的通道1(TIM1_CH1)。TIM1是一个高级定时器,支持互补输出、死区插入和刹车功能,用来做PWM输出绰绰有余。

选择PA8有几个实际考虑:首先它位置方便,很多最小系统板都把LED或者排针引到PA8;其次TIM1是高级定时器,时钟源独立,配置灵活;再者PA8的复用功能明确,不容易和其他外设冲突。

但这里有个容易忽略的点:PA8默认是普通GPIO,要输出PWM必须配置为复用推挽输出模式。如果你把它配成普通推挽输出,然后试图用软件去翻转电平,那出来的不是PWM,只是普通的GPIO翻转,频率和精度都差得远。

3.2 GPIO模式选择:复用推挽还是普通推挽

STM32的GPIO有8种工作模式,很多人刚接触时会被这8种模式搞晕。我简单梳理一下和PWM输出相关的几种:

模式适用场景是否适合PWM输出
普通推挽输出软件控制电平翻转不适合,无法连接定时器
复用推挽输出外设功能输出(PWM、UART TX等)适合,PWM标准配置
复用开漏输出需要线与或电平转换一般不用于PWM,除非有特殊电平需求
模拟输入ADC采样不适合

所以PA8输出PWM,必须配置为复用推挽输出(GPIO_Mode_AF_PP)。这个配置告诉芯片:这个引脚的控制权交给定时器外设,而不是GPIO数据寄存器。

实操心得:如果你配置完PWM发现引脚没输出,第一件事就是检查GPIO模式是不是设成了复用推挽。我见过太多人在这里卡住,代码逻辑全对,就是模式选错了。

3.3 定时器配置的完整流程

下面以STM32F103、系统时钟72MHz、PA8输出1kHz、50%占空比为例,走一遍完整配置。我用的是标准外设库,HAL库的逻辑类似,只是函数名不同。

第一步,开启时钟。TIM1挂在APB2总线上,PA8也挂在APB2:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE);

第二步,配置GPIO为复用推挽输出:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

第三步,配置定时器时基。这里需要算一下预分频和ARR:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1MHz / 1000 = 1kHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

预分频设为71,定时器时钟变成72MHz / 72 = 1MHz。ARR设为999,计数到1000溢出,所以PWM频率是1MHz / 1000 = 1kHz。这个计算过程很关键,很多人直接抄代码但不知道数值怎么来的,换个频率就不会算了。

第四步,配置PWM输出通道:

TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; // 50%占空比 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);

TIM_Pulse设为500,ARR是999,所以占空比是500/1000 = 50%。TIM_OCMode_PWM1的意思是:计数器小于CCR时输出高电平,大于等于CCR时输出低电平。模式2则相反。

第五步,开启定时器:

TIM_Cmd(TIM1, ENABLE);

对于高级定时器TIM1,还需要额外开启PWM输出使能:

TIM_CtrlPWMOutputs(TIM1, ENABLE);

这个函数很多人会漏掉,结果就是定时器在跑,但引脚死活没波形。高级定时器有刹车和死区控制,默认输出是关闭的,必须显式使能。

3.4 PWM模式1和模式2的区别

TIM_OCMode_PWM1和TIM_OCMode_PWM2的区别,简单说就是输出极性相反:

  • PWM模式1:计数器 < CCR时输出有效电平(高电平),计数器 >= CCR时输出无效电平(低电平)
  • PWM模式2:计数器 < CCR时输出无效电平(低电平),计数器 >= CCR时输出有效电平(高电平)

如果你把CCR设为500,ARR设为999,模式1下前500个计数输出高,后500个输出低,占空比50%。模式2下前500个输出低,后500个输出高,占空比也是50%,但相位差了180度。

实际用的时候,模式1和模式2的选择取决于你的驱动电路。比如你用低电平驱动的LED,可能就需要模式2或者设置输出极性为低。这个细节在调电机驱动的时候特别重要,因为有些驱动芯片是高电平使能,有些是低电平使能,搞反了电机就不转。

4. 从LED到电机:PWM调速的底层逻辑

4.1 LED亮度变化的本质

LED的亮度取决于流过它的电流。在PWM驱动下,电流是脉冲式的,但人眼有视觉暂留效应,感受到的是平均亮度。假设LED满电流是20mA,PWM占空比50%,那么平均电流就是10mA,亮度大约是一半。

但这里有个非线性问题:LED的亮度与电流并不是严格线性的,低电流时效率会下降。而且人眼对亮度的感知近似对数关系,所以50%的占空比看起来可能不止50%的亮度。这就是为什么做呼吸灯时,线性改变CCR值会感觉亮度变化不均匀。

我实测过一个简单的伽马校正表,用查表法把线性占空比映射到感知亮度,效果比直接线性调好很多。具体做法是预先算好一个256项的数组,每一项是校正后的CCR值,调光时直接查表。

4.2 直流电机为什么能转起来

直流电机的转动原理是电磁力:通电导体在磁场中受到安培力,产生转矩。转矩大小与电流成正比,转速与反电动势有关。当你用PWM驱动电机时,电机两端的电压是脉冲式的,但电机绕组是感性负载,电流不会突变。

在PWM高电平期间,电源通过驱动管给电机绕组充电,电流上升;低电平期间,绕组通过续流二极管放电,电流下降。如果PWM频率足够高,电流纹波很小,电机感受到的就是一个平滑的等效直流电压。

这个等效电压决定了电机的转速。占空比越大,等效电压越高,转速越快。这就是PWM调速的基本原理。

但实际调试时你会发现,占空比低于某个阈值时,电机根本不转,只是发出嗡嗡声。这是因为占空比太低,等效电压不足以克服电机的静摩擦力和负载转矩。这个阈值取决于电机本身和负载情况,一般有刷电机在10%~20%左右。

4.3 驱动电路不能省

STM32的GPIO输出电流有限,通常只有20mA左右,根本驱动不了直流电机。电机启动瞬间的电流可能达到几安培,直接接GPIO会烧掉芯片。所以必须加驱动电路,常见的有:

  • MOSFET驱动:适合大电流场合,效率高,但需要注意栅极驱动电压和续流二极管
  • H桥驱动芯片:如DRV8323、L298N等,支持正反转和调速,集成度高
  • 继电器驱动:只能开关,不能调速,不适合PWM场合

我用过DRV8323驱动无刷电机,也用过简单的MOSFET驱动有刷电机。MOSFET方案成本低,但需要自己搭续流回路,PCB布局不好容易烧管。H桥芯片贵一点,但保护功能完善,调试省心。

注意:不管用哪种驱动,电机两端一定要加续流二极管,否则PWM关断瞬间电感产生的反电动势会击穿驱动管。这个坑我烧过好几个MOSFET才记住。

4.4 背光干扰显示的问题

热词里有个“pwm背光 干扰 显示”,这个我在做ILI9341屏幕项目时遇到过。屏幕背光用PWM调光,结果发现显示内容有横纹干扰。排查下来是PWM频率和SPI时钟产生了拍频,或者PWM的开关噪声通过电源耦合到了显示电路。

解决办法有几个:一是提高PWM频率到20kHz以上,超出音频范围也避开SPI的敏感频段;二是在背光电源上加LC滤波,减少纹波;三是把PWM走线和SPI走线分开,避免平行走线。我最后用的是20kHz加RC滤波,横纹基本消失了。

5. 常见问题与排查技巧实录

5.1 PWM没输出的排查清单

现象可能原因排查方法
引脚完全没波形GPIO模式没设复用检查GPIO_InitStructure.GPIO_Mode
引脚有波形但频率不对预分频或ARR算错重新计算定时器时钟和周期
高级定时器无输出没开PWM输出使能调用TIM_CtrlPWMOutputs
占空比不可调CCR没更新或预装载没使能检查TIM_OCPreloadConfig
波形抖动严重时钟源不稳定或干扰检查晶振和电源滤波

5.2 电机调速的常见坑

第一个坑是死区时间。如果你用H桥驱动,上下管不能同时导通,否则电源直接短路。STM32的高级定时器支持死区插入,配置TIM_DeadTimeConfig可以自动插入死区,避免直通。

第二个坑是续流回路。PWM关断时,电机绕组的电流需要继续流动,如果没有续流二极管,电流会通过驱动管的反并联二极管流回电源,可能导致电源电压升高。所以电源端要加足够的滤波电容。

第三个坑是低频啸叫。PWM频率落在人耳听觉范围内时,电机会发出啸叫。把频率提到20kHz以上就能解决,但要注意驱动管的开关损耗会增加。

5.3 调试工具的选择

调试PWM最直接的工具是示波器。没有示波器的话,可以用逻辑分析仪,几十块钱的就能看波形和频率。我早期用过一个USB逻辑分析仪,配合开源软件,看PWM波形足够了。

另外,STM32的定时器支持输入捕获,可以用另一个定时器去测量PWM的频率和占空比,这样不用外部工具也能验证输出是否正确。这个方法在调试遥控信号解码时特别有用。

6. 一些实操心得与扩展思路

PWM这个东西,原理不复杂,但细节很多。我做了这么多项目,最大的体会是:参数计算要自己动手算一遍,不要直接抄。抄来的代码换个芯片、换个频率就跑不起来,因为你不知道那些数值是怎么来的。

另外,PWM的应用远不止LED和电机。舵机控制、开关电源、DAC模拟输出、音频放大,甚至红外遥控发射,底层都是PWM。你把定时器的PWM模式吃透了,这些应用都能触类旁通。

最后分享一个小技巧:如果你需要多路PWM输出,优先选同一个定时器的不同通道,这样它们共享同一个时基,频率一致,相位关系固定。如果选不同定时器,频率可能会有微小差异,长时间运行会累积相位误差。这个在做RGB调光或者三相电机驱动时特别重要。

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

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

立即咨询