STM32 PWM深度解析:从LED调光到电机调速的工程实践
1. 从一颗LED的"半亮"说起:PWM到底在干什么
很多人第一次接触PWM,都是在调LED亮度的时候。代码里写个__HAL_TIM_SET_COMPARE(&htim, TIM_CHANNEL_1, 500),灯就暗了一半;改成900,灯又亮起来。看起来像是"输出了一个中间电压",但拿万用表一量,引脚上明明是3.3V和0V在跳。这就是PWM最容易被误解的地方——它压根没有输出模拟电压,它输出的是一串方波,只不过这串方波"开"和"关"的时间比例在变。
PWM的全称是Pulse Width Modulation,脉冲宽度调制。核心参数就三个:频率、占空比、幅值。幅值由GPIO的电平决定,STM32上通常是3.3V;频率是周期一秒钟重复多少次;占空比是高电平时间占整个周期的比例。比如1kHz、50%占空比,就是每1ms一个周期,其中0.5ms高、0.5ms低。LED接在这个引脚上,因为人眼有视觉暂留(大约几十毫秒的积分效应),它看到的不是"一闪一闪",而是"平均亮度"。50%占空比,平均功率就是满功率的一半,看起来就是半亮。
这里有个关键点必须说清楚:PWM调速、调光的本质是"调平均功率",不是"调电压"。对LED来说,亮度基本正比于平均电流,所以占空比线性调亮度,效果很直观。对直流电机来说,转速近似正比于电枢两端的平均电压,而平均电压 = 电源电压 × 占空比,所以占空比调转速也成立。这就是为什么同一个PWM原理,既能调LED亮度,又能让电机转起来——它们吃的都是"平均能量"这碗饭。
但为什么不用DAC直接输出一个模拟电压去调呢?我早年也想过这个问题。原因很实际:DAC输出电流能力极弱,带不动任何负载,后面必须加功率放大;而且DAC是连续可调的模拟量,在噪声环境下容易漂移。PWM输出的是数字信号,只有高低两态,抗干扰能力强,后级只需要一个开关管(MOSFET)就能驱动大功率负载,效率还高——开关管要么导通(压降接近0),要么截止(电流为0),自身功耗极小。这就是PWM在电机驱动、LED调光、电源控制里几乎统治地位的原因。
提示:PWM不是"模拟输出",示波器上看到的是方波。如果你用万用表直流档测PWM引脚,读数接近"占空比×幅值",那是万用表在做平均,不代表引脚真的输出了那个电压。
理解了这一层,后面所有的配置、计算、踩坑才有落脚点。接下来我把LED和电机两条线拆开讲,因为它们虽然同源,但工程上的坑完全不一样。
2. LED调光这条线:从GPIO翻转手搓到定时器硬件PWM
2.1 为什么"软件延时翻转GPIO"只能玩玩,不能上产品
最原始的调光方式是在主循环里手动翻转引脚:
while (1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); delay_us(500); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); delay_us(500); }这段代码能亮,但问题一堆。第一,delay_us是死等,CPU被完全占住,别的任务别想跑;第二,频率极不稳定,一旦有中断打断,占空比就抖,LED会看到闪烁;第三,想同时调几路LED,软件延时根本忙不过来。我见过有人用这种写法做呼吸灯,结果一加串口打印,灯就开始抽风——因为串口中断把延时节奏打乱了。
所以只要涉及"稳定、多路、可调"的调光,必须上硬件定时器。STM32的通用定时器和高级定时器都能输出PWM,配置好之后,方波由硬件自动产生,CPU只管改比较值(CCR),完全不占CPU时间,频率和占空比都精准。
2.2 STM32上配置一路PWM的完整逻辑链
以最常见的TIM1_CH1输出到PA8为例(PA8正好是TIM1_CH1的默认复用引脚,这也是热搜里"pa8输出pwm"的由来)。配置分几步,每一步都有它的道理:
第一步,开时钟。TIM1挂在APB2上,GPIOA也挂在APB2上,两个时钟都要使能。很多人只开了定时器时钟忘了GPIO时钟,结果引脚没反应,查半天。
__HAL_RCC_TIM1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE();第二步,配GPIO为复用推挽输出。这里涉及"GPIO的8种工作模式"的选择问题。PWM输出必须选复用推挽(Alternate Function Push-Pull),因为引脚的控制权要交给定时器外设,而不是普通的输出寄存器。如果错选成普通推挽输出,定时器根本控制不了这个脚,你会看到引脚一直保持你手动写的电平。
GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF1_TIM1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);第三步,算定时器参数。这是最容易算错的地方。PWM频率公式是:
Fpwm = Ftim_clk / ((PSC+1) × (ARR+1))
其中Ftim_clk是定时器的计数时钟。假设STM32F103,TIM1挂APB2,APB2是72MHz,定时器时钟就是72MHz。想要1kHz的PWM,可以取PSC=71(分频到1MHz),ARR=999(1000个计数一个周期),这样Fpwm = 72M/(72×1000) = 1kHz。占空比则由CCR决定:CCR=500时占空比50%,CCR=900时90%。
htim1.Instance = TIM1; htim1.Init.Prescaler = 71; htim1.Init.Period = 999; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim1); TIM_OC_InitTypeDef sConfigOC; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);2.3 PWM模式1和模式2:一个反直觉的细节
热搜里"pwm模式1和模式2的区别"问的人特别多。简单说:模式1是"计数值小于CCR时输出有效电平",模式2是"计数值小于CCR时输出无效电平"。配合OCPolarity(有效电平是高还是低),一共能组合出四种输出形态。
为什么要有这个?因为有些驱动电路是低电平有效的。比如某些LED驱动芯片的使能脚是低有效,或者MOSFET用了反相驱动,这时候你就需要把极性反过来。我一般习惯:先确定后级电路是"高电平点亮"还是"低电平点亮",再决定用模式1还是模式2、极性HIGH还是LOW。别死记,画个波形图对着看最快。
2.4 调光频率选多少:不是随便定的
LED调光频率有个讲究。太低(比如100Hz以下),人眼会看到闪烁,尤其是余光扫过的时候特别明显;太高(比如几十kHz),开关损耗上去了,而且有些LED驱动芯片响应不过来。经验值:普通照明调光用1kHz到5kHz比较稳,既看不到闪,损耗也可接受。如果是给摄像头补光,那要避开摄像头曝光频率,否则画面会出现横纹,这个坑我在做视觉项目时踩过,调了半天才发现是补光PWM和曝光不同步。
另外,如果LED是接在MOSFET低边驱动,PWM频率高的时候要注意栅极驱动电阻和米勒效应,否则波形会变圆,占空比失真,亮度就不线性了。
3. 直流电机这条线:PWM调速为什么"低速没劲"
3.1 电机吃的是平均电压,但还有电感和反电动势
LED是纯阻性负载,PWM调平均功率很线性。电机不一样,它有电感、有反电动势、有机械惯性。当你用PWM驱动电机时,电枢电流并不是方波,而是被电感平滑过的三角波。频率够高时,电流连续,转矩平稳;频率太低时,电流断续,电机会一顿一顿的。
更关键的是反电动势。电机转起来会产生反电动势E,方向与电源相反。稳态下,平均电压 = I×R + E。占空比降低,平均电压降低,转速下降,E也下降。但注意:在极低占空比下,平均电压可能低于电机启动所需的克服静摩擦的电压,电机就转不动了,只会嗡嗡响。这就是"低速没劲"的根源,不是PWM的问题,是电机物理特性决定的。
解决办法有两个:一是提高PWM频率让电流更连续,二是用闭环控制(编码器反馈)动态补偿。开环调速想低速稳,基本做不到。
3.2 驱动电路:GPIO绝对不能直接接电机
这是新手最容易犯的致命错误。STM32的GPIO单个引脚最大输出电流也就20mA左右,而一个小直流电机启动电流轻松上几百mA甚至几安。直接接上去,轻则引脚烧,重则整个MCU报废。
正确做法是用MOSFET或者专用驱动芯片(比如DRV8323这类,热搜里也提到了)。典型低边驱动:PWM接MOSFET栅极,电机一端接电源正,另一端接MOSFET漏极,源极接地。PWM高电平导通,电机通电;低电平截止,电机断电。这里必须加续流二极管,因为电机是感性负载,断电瞬间电感会产生反向高压,没有续流路径会击穿MOSFET。
// 电机调速就是改CCR,和LED一模一样 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty); // duty 0~9993.3 频率选择:电机和LED完全不同
电机PWM频率不能太高。因为MOSFET开关有损耗,频率越高损耗越大;而且电机绕组有电容效应,太高频会漏电流。常用范围是1kHz到20kHz。低于1kHz电机会听到啸叫(人耳可听到),高于20kHz虽然听不到但开关损耗明显。我一般取8kHz到16kHz,避开听觉最敏感的2kHz到5kHz区间,同时损耗还能接受。
另外,如果是H桥驱动做正反转,还要注意死区时间。上下桥臂同时导通会直接短路电源,必须靠死区时间隔开。STM32的高级定时器(TIM1/TIM8)硬件支持死区插入,这个功能在电机驱动里是刚需。
注意:用PWM驱动电机时,电源端一定要并大电容(几百uF以上)。电机换向和PWM开关会在电源线上产生尖峰,不滤波的话MCU可能被干扰复位,这就是热搜里"pwm故障保护"要解决的问题。
4. 那些配置完不亮、不转的排查链路
4.1 引脚没输出:先查时钟和复用
配置完PWM,示波器一搭,啥都没有。我排查的顺序是:先确认定时器时钟和GPIO时钟都开了(用调试器看RCC寄存器最直接);再确认GPIO模式是AF_PP而不是OUT_PP;然后确认Alternate功能号对不对——STM32每个引脚的复用功能号不一样,PA8是AF1,PB0可能是AF2,查数据手册的复用表,别猜。
4.2 有波形但占空比不对:查ARR和CCR
如果波形频率对但占空比不对,八成是CCR设错了,或者忘了HAL_TIM_PWM_Start。还有一种情况:CCR大于ARR,那占空比就是100%,一直高电平。我见过有人ARR设999,CCR设1000,结果灯全亮,还以为是代码没生效。
4.3 电机转但一卡一卡:查频率和电源
电机一顿一顿,先看PWM频率是不是太低(低于1kHz),再看电源功率够不够。用电池供电时,电机启动瞬间电压被拉低,MCU可能复位,表现就是转一下停一下。这时候要在电机电源和MCU电源之间做隔离,或者加大电容缓冲。
4.4 干扰显示:PWM背光和屏幕打架
热搜里"pwm背光 干扰 显示"是个经典问题。LCD背光用PWM调光时,如果PWM频率和SPI/I2C通信频率产生拍频,屏幕上会出现滚动横纹。解决办法:把背光PWM频率调高到20kHz以上,或者和显示刷新率错开。我在用ILI9341这类屏时就遇到过,读ID是A1A1确认屏没问题,但背光一调就花屏,最后是把背光PWM从1kHz提到25kHz才干净。
5. 从能跑到跑好:几个实战里攒下的经验
第一个经验:CCR的更新时机。在PWM运行中改CCR,如果ARR没到就改,可能出现半个周期的畸形波。STM32有预装载寄存器(OCPreload),使能后CCR的修改会在下一个周期生效,波形干净。调光调速度时建议开预装载。
第二个经验:多路PWM用同一个定时器。TIM1有4个通道,频率共享(同一个ARR),但占空比各自独立(各自CCR)。如果你要4路LED不同亮度,用TIM1的4个通道最省资源,频率统一反而更好看。
第三个经验:死区时间和刹车功能别浪费。高级定时器的刹车输入(Break)可以在故障时硬件立刻关断PWM输出,做电机保护特别有用。过流信号接到刹车脚,一出事硬件自动封波,比软件响应快得多。
第四个经验:调试时先降频。不确定配置对不对,先把PWM频率降到10Hz,用LED肉眼就能看出闪烁和占空比变化,比示波器还直观。确认逻辑对了再往上调频率。
第五个经验:GPIO模式选错是最隐蔽的坑。普通输出和复用输出在代码上就差一个枚举值,但现象完全不同。养成习惯:凡是外设控制的引脚,一律AF模式,别偷懒用普通输出。
PWM这东西,原理半小时能讲完,但真正在项目里调稳,靠的是对负载特性的理解和对细节的较真。LED和电机只是两个最典型的入口,往深了走,开关电源、逆变器、伺服控制,底层都是这一套占空比的艺术。把这两个场景吃透,后面遇到什么PWM应用,心里都有底。