1. 从一个LED灯说起:为什么调亮度不是调电压
很多人第一次接触PWM,都是从“让一颗LED变暗一点”开始的。你手里有STM32或者51单片机,GPIO口接了一颗LED,串了一个限流电阻,程序里写个GPIO_SetBits灯就亮,写GPIO_ResetBits灯就灭。这时候你想让它“半亮”,第一反应往往是——把电压降一半行不行?
我试过,不行。或者说,用GPIO直接降电压这条路根本走不通。GPIO只有高电平和低电平两种输出状态,你要么给3.3V,要么给0V,没有中间值。就算你用DAC输出一个1.65V的模拟电压去驱动LED,亮度确实会变,但问题一大堆:DAC资源稀缺、驱动能力弱、效率极低,而且对电机这种负载来说,半电压意味着半转矩,电机可能直接堵转发热。
PWM的思路完全不一样。它不改变电压幅值,而是改变“通电时间”的比例。还是3.3V,但我让LED在1毫秒内只亮0.5毫秒、灭0.5毫秒,循环下去。因为人眼的视觉暂留效应,你看到的不是闪烁,而是亮度减半。这就是PWM最朴素的定义:用数字信号的时间比例,去等效模拟量的幅度。
这个思路一旦打通,你会发现它能干的事情远不止调LED亮度。直流电机的转速、舵机的角度、开关电源的输出电压、甚至加热丝的温度,底层都是同一套逻辑。标题里说的“从LED亮度变化到直流电机转起来”,其实就是PWM从“视觉等效”走向“能量等效”的过程。LED是光输出,电机是机械输出,但控制手段是同一个。
这篇文章我打算把PWM这件事从头到尾讲透。不管你是刚学STM32、还在纠结GPIO八种模式怎么选的新手,还是已经能用定时器输出PWM、但说不清楚“模式1和模式2到底差在哪”的进阶玩家,都能从中找到对你有用的东西。我会从原理讲到寄存器配置,从LED讲到电机,中间穿插我实际调试时踩过的坑和验证过的参数。
2. PWM的核心原理:占空比、频率与等效电压
2.1 占空比到底在表达什么
占空比(Duty Cycle)是PWM里最核心的概念,没有之一。它的定义很简单:在一个周期内,高电平时间占总时间的比例。比如周期是1ms,高电平0.3ms,占空比就是30%。
但我想强调的是,占空比本身不是目的,它只是手段。真正起作用的是“等效值”。对于一个LED,等效亮度约等于占空比乘以满亮度;对于一个直流电机,等效电枢电压约等于占空比乘以电源电压。注意这里我用了“约等于”,因为实际系统中还有非线性、惯性、死区等因素。
我见过不少初学者把占空比和频率搞混。占空比决定“等效值多大”,频率决定“这个等效值稳不稳”。举个例子,你用1Hz的频率、50%占空比去驱动LED,看到的是明显的闪烁,不是半亮。因为1秒亮1秒灭,人眼根本来不及做视觉平均。频率提高到100Hz以上,闪烁感才消失。所以调亮度靠占空比,消闪烁靠频率,这两件事要分开理解。
2.2 频率选多少才合适
频率的选择没有万能答案,它取决于负载特性。我整理了一个实际项目中常用的频率范围表,你可以直接参考:
| 应用场景 | 推荐频率范围 | 选择理由 |
|---|---|---|
| LED调光 | 200Hz - 2kHz | 高于200Hz人眼无闪烁感,太高会增加开关损耗 |
| 直流有刷电机调速 | 1kHz - 20kHz | 低于1kHz有啸叫,高于20kHz开关损耗明显 |
| 舵机控制 | 50Hz | 标准舵机协议固定50Hz,脉宽0.5-2.5ms |
| 开关电源 | 50kHz - 500kHz | 频率越高电感电容越小,但开关损耗上升 |
| 加热丝控温 | 0.5Hz - 10Hz | 热惯性大,不需要高频,低频减少开关次数 |
这张表是我自己项目里反复验证过的。特别说一下直流电机,我一开始用1kHz驱动,电机发出明显的“滋滋”声,后来提到8kHz,声音就基本听不到了。但频率也不是越高越好,我试过用20kHz驱动一个小功率电机,MOS管发热明显增加,因为开关次数多了,开关损耗上去了。最后定在8kHz,兼顾静音和效率。
2.3 等效电压的数学关系
对于直流电机,PWM驱动下的等效电枢电压可以用这个公式算:
V_eff = V_supply × Duty
比如12V电源,占空比60%,等效电压就是7.2V。电机的转速大致和电枢电压成正比,所以转速大约是满速的60%。但这里有个坑:电机不是纯电阻负载,它有反电动势。实际转速会比这个公式算出来的略高一点,因为反电动势会抵消一部分电枢电压。我在调试小车电机时发现,50%占空比下实际转速大约是满速的55%左右,不是精确的50%。
对于LED,等效亮度和占空比的关系更接近线性,但也不是完全线性。因为LED的亮度还受结温影响,大电流下发热会导致亮度下降。所以如果你要做精确的亮度控制,光靠PWM还不够,还得考虑温度补偿。
3. STM32上怎么把PWM输出出来
3.1 GPIO八种模式里,PWM该选哪个
STM32的GPIO有八种工作模式,初学者最容易在这里懵。我列一下这八种模式,然后告诉你PWM输出该选哪个:
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟输入
- 开漏输出
- 开漏复用输出
- 推挽输出
- 推挽复用输出
PWM输出要选推挽复用输出(GPIO_Mode_AF_PP)。为什么?两个关键词:推挽、复用。
推挽的意思是,输出级有上下两个管子,能主动拉高也能主动拉低,驱动能力强。开漏输出只能主动拉低,拉高要靠外部上拉电阻,上升沿会变慢,PWM波形会畸变。我试过用开漏输出驱动LED,波形上升沿明显变缓,占空比都不准了。
复用的意思是,这个引脚的控制权交给片上外设(这里是定时器),而不是GPIO数据寄存器。你写GPIO_SetBits没用,因为引脚已经不归GPIO管了。这一点新手特别容易搞错,代码里明明配置了PWM,但引脚没反应,就是因为忘了配成复用模式。
3.2 定时器是怎么生成PWM的
STM32的定时器生成PWM,核心靠三个寄存器:ARR(自动重装载寄存器)、PSC(预分频器)、CCR(捕获比较寄存器)。
先说频率怎么来的。定时器的计数时钟是总线时钟经过PSC分频后的结果:
f_cnt = f_clk / (PSC + 1)
然后计数器从0数到ARR,数满一个来回就是一个PWM周期:
f_pwm = f_cnt / (ARR + 1) = f_clk / ((PSC + 1) × (ARR + 1))
举个例子,STM32F103的APB1定时器时钟是72MHz,我想要8kHz的PWM频率。先定PSC,比如取0,那ARR就是:
ARR = 72,000,000 / 8,000 - 1 = 8999
如果取PSC=71,那计数时钟变成1MHz,ARR就是:
ARR = 1,000,000 / 8,000 - 1 = 124
两种都行,区别在于分辨率。PSC=0时,ARR=8999,占空比可以调9000档;PSC=71时,ARR=124,占空比只能调125档。分辨率越高,调速越细腻。所以我一般优先用小的PSC、大的ARR,除非ARR太大导致计数溢出。
占空比由CCR决定:
Duty = CCR / (ARR + 1)
CCR可以在运行时动态改,改完下一个周期就生效。这就是PWM调速的软件接口——你只需要改CCR的值。
3.3 PWM模式1和模式2的区别
这是很多人搞不清的地方。STM32的定时器每个通道可以配成PWM模式1或模式2,区别在于有效电平的定义。
- PWM模式1:计数器小于CCR时,输出有效电平;大于等于CCR时,输出无效电平。
- PWM模式2:反过来,计数器小于CCR时输出无效电平,大于等于CCR时输出有效电平。
那什么是“有效电平”?这又取决于另一个配置位:输出极性。极性可以配成高有效或低有效。
所以实际上有四种组合。我一般用PWM模式1加高有效极性,这样CCR越大,高电平时间越长,占空比越大,符合直觉。如果你用PWM模式2,CCR越大反而高电平越短,容易把自己绕晕。
有个实际场景需要注意:如果你驱动的是低电平点亮的LED(比如共阳极接法),那用PWM模式1加低有效极性会更方便,CCR越大亮度越高。我一开始没注意这个,代码里CCR调大了灯反而变暗,查了半天才发现是极性配反了。
3.4 用CubeMX配置PWM的完整流程
我平时用STM32CubeMX配PWM,效率高而且不容易漏配置。流程大致是这样:
- 选好定时器和通道,比如TIM1_CH1对应PA8。
- 把对应引脚配成复用推挽输出。
- 在定时器配置里,Clock Source选Internal Clock。
- Channel选PWM Generation CH1。
- 设PSC和ARR,算出想要的频率。
- 设Pulse(就是CCR初值),决定初始占空比。
- 生成代码,在main里调用
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1)。
这里有个坑我要提醒:HAL库启动PWM必须调用HAL_TIM_PWM_Start,光配置不启动是没有波形的。我见过好几个新手配置都对了,但引脚就是没输出,最后发现是忘了调这个函数。
还有,如果你想在运行时改占空比,用这个函数:
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_ccr);这个宏直接写CCR寄存器,比调HAL库函数快,适合在中断里用。
4. 从LED到直流电机:负载变了,思路怎么变
4.1 LED驱动:限流电阻和驱动方式的选择
LED是最简单的PWM负载,但也不是随便接就行。先说限流电阻,这个必须要有,否则LED容易烧。计算方法:
R = (V_supply - V_led) / I_led
比如3.3V电源,红色LED正向压降约1.8V,想要10mA电流:
R = (3.3 - 1.8) / 0.01 = 150Ω
这里有个热词提到“51单片机驱动LED时为什么不能采用输出高电平的驱动方式”。原因是51单片机的GPIO是准双向口,输出高电平时驱动能力很弱(通常只有几十微安),拉电流能力远不如灌电流。所以标准做法是低电平点亮:LED阳极接VCC,阴极通过限流电阻接GPIO,GPIO输出低电平时LED亮。STM32的推挽输出高低电平驱动能力都还行,但灌电流通常也比拉电流强一点,所以低电平点亮仍然是更稳妥的选择。
如果你要驱动大功率LED或者灯带,GPIO直接驱动就不够了,需要加驱动芯片。常见的LED驱动芯片有恒压和恒流两种。恒流驱动更适合LED,因为LED亮度对电流敏感,对电压不敏感。有些驱动芯片有FB(反馈)脚和CS(电流采样)脚,通过调整反馈电压来设定输出电流。这种芯片也可以用PWM调光,PWM信号加到EN脚或者直接调制FB脚。
4.2 直流电机驱动:为什么不能直接接GPIO
直流电机绝对不能直接接GPIO,原因有三个:电流大、有反电动势、有感性尖峰。
小电机堵转电流可能就有1A,STM32的GPIO最大也就20mA左右,直接接上去轻则烧引脚,重则烧芯片。电机转动时会产生反电动势,断电瞬间电感还会产生反向尖峰电压,可能击穿GPIO。所以必须加驱动电路,常见方案有:
- H桥驱动芯片:比如DRV8833、L298N,能正反转,适合小车。
- MOS管低边驱动:适合单向调速,成本低。
- 继电器:只能开关,不能调速,不适合PWM。
我用得最多的是DRV8833,双路H桥,单路持续电流1.5A,峰值2A,驱动小车轮子绰绰有余。它的控制逻辑很简单:一个PWM脚调速,一个方向脚控方向。PWM频率我一般设8kHz,占空比0到100%对应转速0到满速。
这里有个细节:电机启动时占空比不能太小。因为电机有静摩擦,占空比低于某个阈值时电机根本不转,只是发热。我实测一个小电机,占空比低于15%就不转了。所以软件上要做个死区补偿,比如把0-100%的输入映射到15%-100%的输出。
4.3 电机调速的闭环思路
开环PWM调速有个问题:电池电压下降时,同样的占空比转速会变慢。如果你要做恒速控制,就需要闭环。最简单的方法是加编码器测速,然后用PID调PWM占空比。
我做过一个小车项目,用霍尔编码器测轮速,PID周期10ms,PWM频率8kHz。调参的时候先调P,让系统有响应但不振荡;再加I,消除稳态误差;最后加少量D,抑制超调。最后转速波动能控制在±3%以内,比开环好太多。
但闭环不是必须的。如果你只是做个风扇调速、灯光调光,开环足够了。闭环的成本和复杂度都要高不少,要看项目需求决定。
5. 实操中踩过的坑和排查技巧
5.1 PWM没输出,先查这五个地方
PWM配置完没波形,是新手最常遇到的问题。我总结了一个排查顺序,按这个查基本能定位:
| 排查项 | 检查方法 | 常见问题 |
|---|---|---|
| 定时器时钟 | 查RCC配置 | 忘了使能定时器时钟 |
| PWM启动 | 查代码 | 忘了调HAL_TIM_PWM_Start |
| GPIO模式 | 查引脚配置 | 配成了普通输出而非复用 |
| 引脚映射 | 查数据手册 | 用错了引脚或重映射没开 |
| CCR值 | 查占空比 | CCR=0或CCR>ARR |
我印象最深的一次是调PA8输出PWM,配置全对就是没波形。最后发现是TIM1是高级定时器,除了HAL_TIM_PWM_Start,还需要额外使能主输出:
__HAL_TIM_MOE_ENABLE(&htim1);高级定时器(TIM1、TIM8)有刹车和死区功能,默认主输出是关的。这个坑我踩过一次就记住了。
5.2 PWM背光干扰显示的问题
热词里有个“pwm背光干扰显示”,这个我遇到过。用PWM调LCD背光亮度时,屏幕出现横条纹干扰。原因是PWM频率和LCD刷新频率产生了拍频,或者PWM的开关噪声耦合到了显示数据线上。
解决办法有三个:一是提高PWM频率到20kHz以上,超出人耳和显示刷新范围;二是给背光电路加LC滤波,把PWM平滑成模拟电压;三是背光PWM走线远离数据线,减少耦合。我一般用第一种加第二种组合,频率提到20kHz,再加个100uF电容,干扰基本消失。
5.3 电机PWM的故障保护
电机堵转时电流会飙升,如果没有保护,驱动芯片和电机都可能烧。我在DRV8833的方案里加了电流采样电阻,用STM32的ADC监测电流,超过阈值就立刻把PWM占空比降到0。
这里有个细节:故障保护要快。软件轮询ADC太慢,我改成ADC注入通道加中断,采样到过流立刻在中断里关PWM。从过流发生到PWM关闭,响应时间控制在100微秒以内,驱动芯片就不会过热。
另外,PWM故障保护还可以用定时器的刹车输入功能。高级定时器有BKIN引脚,检测到低电平自动关闭PWM输出,完全硬件响应,比软件快得多。这个功能在电机驱动里非常实用,建议用高级定时器做电机PWM时一定要配上。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| LED闪烁而非调光 | PWM频率太低 | 提高到200Hz以上 |
| 电机有啸叫 | PWM频率在人耳范围 | 提高到8kHz以上 |
| 电机低速不转 | 占空比低于启动阈值 | 加死区补偿 |
| PWM波形畸变 | GPIO配成开漏 | 改成推挽复用 |
| 占空比不准 | 时钟配置错误 | 核对PSC和ARR |
| 高级定时器无输出 | MOE未使能 | 调__HAL_TIM_MOE_ENABLE |
| 背光干扰显示 | PWM噪声耦合 | 提高频率加滤波 |
这张表是我自己项目里积累的,基本覆盖了PWM调试中80%的问题。遇到问题先查表,能省不少时间。
6. 一些进阶玩法和个人体会
PWM玩到后面,你会发现它不只是“调亮度”和“调速”。我最近在做的一个项目,用PWM控制DC-DC反馈脚来调输出电压。原理是把PWM经过RC滤波变成模拟电压,加到DC-DC的FB脚,改变反馈比例,从而调整输出。这种用法比用DAC灵活,因为PWM通道多,几乎每个定时器都能出。
还有一个方向是PWM监控。热词里提到“pwm怎么监控硬盘的温度”,这个思路其实是用PWM占空比来编码温度信息。温度传感器输出PWM,占空比随温度变化,MCU测占空比就知道温度。这种传感器抗干扰能力强,适合长距离传输。
我个人在实际操作中的体会是,PWM看着简单,但要用好,关键在三点:频率选对、模式配对、保护做足。频率选对决定了系统稳不稳,模式配对决定了控制方不方便,保护做足决定了系统可不可靠。这三点做到了,PWM基本不会出大问题。
最后分享一个小技巧:调试PWM时,如果手头没有示波器,可以用另一个定时器配成输入捕获模式,测PWM的频率和占空比,通过串口打印出来。我用这个方法在没有示波器的情况下调过好几个项目,精度虽然不如示波器,但看个大概足够了。STM32的定时器捕获功能很强大,测频率、测占空比、测脉宽都能做,值得花时间研究一下。