
1. 从一颗LED说起为什么半亮这件事没那么简单很多人第一次接触PWM都是从让LED变暗一点这个需求开始的。你手里有STM32或者51单片机GPIO口接了一颗LED写一句GPIO_SetBits灯就亮写GPIO_ResetBits灯就灭。这时候你想让它半亮第一反应可能是——那我输出一个1.65V的电压不就行了问题是数字GPIO只有高和低两种状态它不会给你输出半个高电平。就算你用DAC硬凑出一个中间电压LED的亮度-电压曲线也不是线性的稍微偏一点亮度就跳变而且效率极低大部分能量都变成热量耗在限流电阻或者调整管上了。PWMPulse Width Modulation脉冲宽度调制解决的正是这个问题我不改变电压幅值只改变高电平占整个周期的时间比例。灯还是被5V或3.3V驱动但它在每个周期里只亮一部分时间。只要这个周期足够短人眼就分辨不出闪烁看到的就是一个稳定的平均亮度。这就是PWM调速、PWM调光最朴素也最核心的直觉。但这里有个特别容易被忽略的点PWM本质上不是输出模拟量而是用时间换幅度。它输出的永远是数字方波只是这个方波经过负载LED、电机、加热丝的惯性或者人眼的视觉暂留之后等效成了一个中间值。理解这一点后面所有关于频率选择、占空比计算、电机为什么能转的问题都能顺理成章地推下去。这篇内容我打算把PWM从点亮一颗LED一路讲到让直流电机转起来中间会穿插STM32上的实际配置、频率和占空比怎么算、为什么电机不能随便用低频PWM、以及我在调试中踩过的几个真实的坑。不管你是刚学单片机的新手还是已经用过HAL_TIM_PWM_Start但没深究过原理的老手应该都能从中找到点有用的东西。2. 占空比、频率、周期三个参数决定一切2.1 占空比到底在描述什么占空比Duty Cycle的定义很直白一个周期内高电平时间除以整个周期时间通常用百分比表示。50%占空比就是高电平占一半25%就是高电平占四分之一。对于LED占空比直接对应平均亮度对于直流电机占空比直接对应平均电压进而对应转速。这里要强调一个新手常犯的误区占空比描述的是平均效果不是瞬时状态。在50%占空比下LED在周期前半段是全亮的后半段是全灭的它从来没有真正处于半亮的物理状态。你如果拿示波器去看看到的就是一个干净的方波。之所以看起来是半亮是因为人眼的积分效应。同理电机在PWM驱动下电流是断续的但因为电机绕组的电感会续流电流被平滑成了接近直流的波形所以电机转起来是平稳的而不是一顿一顿的。2.2 频率选错了一切白搭频率或者说周期是PWM里最容易被忽视、但最容易出问题的参数。频率太低LED会肉眼可见地闪电机能听到啸叫频率太高驱动电路的开关损耗上去了MOS管发热甚至有些驱动器根本跟不上。我一般会按负载类型来选频率下面这张表是我自己总结的常用区间实测下来比较稳负载类型推荐PWM频率原因LED调光1kHz ~ 5kHz高于人眼闪烁融合频率又不会让驱动芯片开关损耗过大直流有刷电机10kHz ~ 20kHz高于人耳听觉上限约20kHz避免啸叫舵机50Hz舵机内部协议规定周期20ms步进电机细分20kHz以上需要平滑的电流波形加热丝/大功率负载100Hz ~ 1kHz热惯性大低频足够减少开关损耗拿LED举例如果你用100Hz去调光虽然理论上人眼融合频率是24Hz左右但100Hz在快速移动或者余光观察时还是能察觉到闪烁尤其是用手机摄像头拍的时候会看到明显的条纹。我一般直接上1kHz起步稳妥。2.3 周期、频率、计数值的换算关系在STM32上配置PWM绕不开这三个量的换算。以通用定时器为例PWM频率由下面这个公式决定PWM频率 定时器时钟 / ((Prescaler 1) * (ARR 1))其中Prescaler是预分频系数ARR是自动重装载寄存器的值也就是计数上限。举个实际例子STM32F103的TIM1挂在72MHz的APB2总线上我想输出1kHz的PWM那么1000 72000000 / ((PSC1) * (ARR1))如果我取PSC71那么(PSC1)7272000000/72 1000000再除以(ARR1)要等于1000所以ARR11000ARR999。这样占空比的分辨率就是1/1000也就是0.1%一档对LED调光来说完全够用。提示ARR越大占空比分辨率越高但频率会降低。这两个是此消彼长的关系配置时要先确定频率再在满足频率的前提下尽量把ARR取大以获得更细的调节粒度。3. STM32上把PWM跑起来从PA8到示波器上的方波3.1 为什么PA8经常被拿来当PWM输出如果你翻STM32F103的引脚定义会发现PA8有个复用功能叫TIM1_CH1。TIM1是高级定时器功能比通用定时器更全支持互补输出、死区插入、刹车输入这些电机控制才用得上的特性。很多人第一次学PWM就用PA8因为教程多、例程全。但要注意TIM1是高级定时器光配置好通道还不够必须额外使能主输出MOE否则引脚上什么波形都没有。这个坑我在下面会专门讲。选引脚的时候有个基本原则先查数据手册的复用功能表确认这个引脚确实能映射到你用的定时器通道。不是随便一个GPIO都能输出PWM的只有带定时器通道复用功能的引脚才行。像PA8对应TIM1_CH1PA0对应TIM2_CH1PB6对应TIM4_CH1这些是固定的硬件连接改不了。3.2 用HAL库配置PWM的完整流程我以STM32CubeMX HAL库为例把配置流程拆开讲。这套流程在F1、F4、H7上大同小异理解了就能迁移。第一步在CubeMX里把PA8配置成TIM1_CH1的复用推挽输出。注意是复用推挽不是普通推挽普通推挽输出的是GPIO电平复用推挽输出的才是定时器产生的PWM波形。第二步配置TIM1。Clock Source选Internal ClockPrescaler填71Counter Period也就是ARR填999这样得到1kHz。然后找到Channel1Mode选PWM Generation CH1Pulse填500对应50%占空比Polarity选High高电平有效。第三步生成代码后在main函数里调用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);如果是TIM1这种高级定时器光这一句可能还不够需要额外加一句__HAL_TIM_MOE_ENABLE(htim1);MOE是Main Output Enable高级定时器的输出总开关。没有它通道配置得再对引脚上也是死的。我第一次用TIM1的时候就被这个卡了半个下午示波器上一直是低电平查了半天才发现是MOE没开。3.3 动态改占空比别直接写CCR运行中要调亮度或者调转速就得改占空比。占空比对应的是捕获比较寄存器CCR的值。改CCR有两种写法__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, new_value);或者直接操作寄存器TIM1-CCR1 new_value;两种都行但要注意new_value不能超过ARR的值否则占空比会变成100%或者出现异常。我一般会写个限幅函数把输入值钳在0到ARR之间避免上层逻辑传进来一个越界值把波形搞乱。还有一个细节如果你在PWM运行过程中改CCR新的占空比会在下一个周期生效不会在当前周期中途突变。这个特性对电机调速很重要它保证了波形的完整性不会产生毛刺。4. 从LED到电机负载变了PWM的玩法也变了4.1 LED和电机的本质区别在哪LED是个二极管正向导通后电流和电压近似指数关系而且它没有惯性你给它断电它立刻就灭。电机不一样电机是感性负载绕组有电感断电之后电感会产生反向电动势试图维持电流。这个差异决定了LED可以直接用GPIO或者小驱动芯片推电机必须用专门的驱动电路H桥或者MOS管。还有一个关键区别是电流。LED的工作电流一般几毫安到几十毫安GPIO直接推小功率LED没问题。但直流电机启动电流可能是额定电流的5到10倍几百毫安甚至几安培GPIO那点驱动能力根本不够硬接上去要么电机不转要么把单片机引脚烧了。4.2 电机为什么能转起来平均电压决定转速直流有刷电机的转速近似正比于电枢两端的平均电压。PWM通过快速开关把电源电压斩波成一个方波电机绕组因为电感效应把电流平滑了等效感受到的就是一个平均电压。占空比50%平均电压就是电源电压的一半转速大约就是全速的一半空载情况下。这里有个反直觉的点电机在PWM驱动下电流是连续的但电压是断续的。你如果拿示波器同时看电压和电流会看到电压是方波电流是带纹波的近似直流。这是因为电机绕组的电感在PWM关断期间通过续流二极管继续维持电流。所以选驱动电路的时候续流二极管是必须的没有它关断瞬间的反向电动势会把MOS管击穿。4.3 驱动电路怎么选从L298N到MOS管小功率场合L298N这种集成H桥模块很常见接线简单能正反转但它的导通压降大约2V效率低大电流下发热严重。稍微讲究一点用DRV8833、TB6612这类MOS工艺的驱动芯片压降小很多。再往上就是自己搭H桥用N沟道MOS管加半桥驱动芯片效率最高但设计复杂。我个人的经验是玩具级项目用TB6612就够了工业级或者大电流场合老老实实上MOS管加专用驱动。L298N现在基本可以淘汰了发热和压降都太吃亏。5. 那些年我在PWM上踩过的坑5.1 高级定时器的MOE一个下午的教训前面提过TIM1、TIM8这类高级定时器必须使能MOE才有输出。这个坑的隐蔽之处在于代码编译通过HAL_TIM_PWM_Start也返回HAL_OK但引脚上就是没波形。你会怀疑引脚配置错了、时钟没开、通道选错了唯独想不到是MOE。后来我养成了一个习惯用高级定时器输出PWM第一件事就是确认MOE。5.2 频率太低导致电机啸叫有次做一个直流电机调速我随手用了1kHz的PWM结果电机发出刺耳的啸叫像蚊子一样。原因是1kHz落在人耳听觉范围内电机绕组的磁致伸缩效应把电信号变成了声波。改成16kHz之后啸叫完全消失。这个经验告诉我电机PWM频率一定要高于20kHz除非你的应用对开关损耗特别敏感。5.3 占空比突变导致电机抖动还有一次我在调速时直接让占空比从10%跳到90%电机猛地一顿齿轮箱都发出了异响。后来改成渐变每次增加1%间隔10ms就平滑多了。电机调速要做斜坡处理不能突变这跟变频器里的加减速时间是一个道理。5.4 忘记共地导致波形异常这个坑比较低级但很常见单片机的地和驱动电路的地没有连在一起结果PWM信号参考电平不对驱动芯片收到的波形乱七八糟。任何多电源系统共地是第一步别嫌麻烦。6. 几个容易被问到的细节问题6.1 PWM模式1和模式2到底差在哪STM32的PWM输出有两种模式。模式1是计数值小于CCR时输出有效电平模式2是计数值小于CCR时输出无效电平。说白了就是极性相反。模式1配合高极性就是占空比越大高电平越多模式2配合高极性就是占空比越大低电平越多。实际用哪个取决于你的驱动电路是高电平有效还是低电平有效。大多数场合用模式1就够了除非你的驱动芯片是低电平使能。6.2 死区时间是干什么的死区时间只在互补输出或者H桥驱动里才有意义。H桥的上下两个MOS管如果同时导通电源直接短路瞬间烧管。死区时间就是在上下管切换时插入一段两个都关断的空白期保证不会直通。STM32的高级定时器内置了死区发生器配置几个寄存器就能用。做电机驱动死区时间是保命的东西不能省。6.3 怎么用示波器验证PWM对不对调PWM的时候示波器是最靠谱的工具。看三个东西频率对不对、占空比对不对、幅值对不对。如果频率不对查PSC和ARR如果占空比不对查CCR如果幅值不对查引脚配置和供电。没有示波器的话用逻辑分析仪也行几十块钱的那种就够看PWM了。7. 写在最后的一点个人体会PWM这个东西原理简单到一句话就能说完但真正用好它需要对负载特性、驱动电路、定时器硬件都有理解。我见过太多人卡在配置都对但就是没输出上最后发现是MOE没开或者引脚复用没配对。也见过电机啸叫、驱动发热、占空比跳变这些实际问题每一个背后都是对PWM某个细节的理解不到位。我的建议是先把LED调光跑通用示波器把波形看清楚再上电机。LED是纯阻性负载没有续流、没有死区、没有啸叫这些问题是理解PWM最好的起点。等你能闭着眼睛算出任意频率下的PSC和ARR能一眼看出占空比和CCR的对应关系再去碰电机会顺很多。另外别迷信例程。例程能跑通不代表你理解了换个定时器、换个引脚、换个频率可能就出问题。真正掌握PWM的标志是你能根据需求反推出所有参数并且在出问题时知道从哪个环节开始查。