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翻转手搓到定时器硬件PWM2.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 / ((PSC1) × (ARR1))其中Ftim_clk是定时器的计数时钟。假设STM32F103TIM1挂APB2APB2是72MHz定时器时钟就是72MHz。想要1kHz的PWM可以取PSC71分频到1MHzARR9991000个计数一个周期这样Fpwm 72M/(72×1000) 1kHz。占空比则由CCR决定CCR500时占空比50%CCR900时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是AF1PB0可能是AF2查数据手册的复用表别猜。4.2 有波形但占空比不对查ARR和CCR如果波形频率对但占空比不对八成是CCR设错了或者忘了HAL_TIM_PWM_Start。还有一种情况CCR大于ARR那占空比就是100%一直高电平。我见过有人ARR设999CCR设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应用心里都有底。