资讯动态

PWM原理与STM32实战:从LED调光到电机调速的完整指南

发布时间:2026/10/8 16:00:19 来源:尧图企业网站定制
1. 从一颗LED的呼吸效果说起PWM到底在干什么很多人第一次接触PWM都是在调LED亮度的时候。代码里写一个TIM_SetCompare1(TIM3, 500)灯就暗一点改成900灯就亮一点。看起来很简单但如果你停下来问自己一句为什么改变一个比较值灯就会变亮变暗这个问题如果答不上来那后面调电机、调舵机、调加热丝基本就是照抄代码出了问题也不知道从哪查。PWM的全称是Pulse Width Modulation中文叫脉冲宽度调制。它的本质其实就一句话用一串频率固定的方波通过改变高电平占整个周期的比例来等效地控制输出功率。注意这里的关键词是等效因为PWM并没有真的去改变电压大小它改变的是通电时间占比。拿LED举例。假设你用3.3V的GPIO去驱动一颗LED串了一个限流电阻。如果你一直输出高电平LED就是最亮一直输出低电平LED就灭。那如果我想让它半亮呢直接输出1.65V普通GPIO做不到就算用DAC做到了效率也很低。PWM的思路是我让它在1毫秒内前0.5毫秒输出高后0.5毫秒输出低然后不断重复。因为LED和人的眼睛都有惯性人眼存在视觉暂留效应它看到的不是一闪一闪而是平均亮度——也就是50%的亮度。这就是PWM最朴素的理解它不是调电压而是调开的时间比例。这个比例有个专门的名字叫占空比Duty Cycle公式是占空比 高电平时间 / 整个周期时间 × 100%比如周期是1ms高电平0.3ms占空比就是30%。周期是20ms高电平1.5ms占空比就是7.5%。周期不变只改高电平时间平均功率就跟着变。这就是PWM能调速调光调温的根本原因。这里有个特别容易被忽略的点PWM的频率必须足够高高到负载反应不过来。对LED来说频率低于50Hz人眼就能看出闪烁所以一般用几百Hz到几kHz。对直流电机来说频率太低电机会听到嗡嗡声、转动不连续太高又会让驱动MOS管发热增加所以常用1kHz到20kHz之间。对加热丝这种热惯性极大的负载频率甚至可以低到1Hz都没问题因为热量根本来不及散掉。所以你看PWM不是一个玄学它是一个非常物理的东西用时间换功率。理解了这一点后面所有的配置、寄存器、库函数都只是实现手段而已。2. 占空比、频率、分辨率三个参数决定PWM好不好用真正开始写代码之前必须把PWM的三个核心参数搞清楚否则你会遇到灯调不亮电机有异响舵机抖个不停这些典型问题。这三个参数就是频率、占空比、分辨率。它们互相牵制不是随便设的。2.1 频率决定了快慢也决定了适用场景频率就是PWM波形一秒钟重复多少次单位Hz。它决定了这个PWM信号适合驱动什么。应用场景常用PWM频率原因LED调光200Hz ~ 5kHz太低会闪太高开关损耗大直流电机调速1kHz ~ 20kHz太低有啸叫、转动不平滑太高驱动发热舵机控制50Hz周期20ms标准舵机协议固定脉宽0.5~2.5ms步进电机细分20kHz以上需要高于人耳听觉范围避免噪音开关电源几十kHz ~ 几百kHz频率越高电感电容可以越小这张表不是死规定但它反映了一个规律频率的选择本质是在负载能不能接受和开关损耗能不能承受之间找平衡。频率低开关次数少损耗小但负载会感觉到断续频率高负载感觉平滑但每次开关都要消耗能量驱动器件会热。2.2 占空比决定了输出多少但它有边界占空比从0%到100%0%就是一直低电平100%就是一直高电平。中间的值就是部分导通。但实际使用中有两个边界要注意0%和100%不是所有定时器都能干净输出。有些定时器的PWM模式在比较值等于自动重装载值时输出电平会有一个周期的毛刺。如果你用PWM控制MOS管这个毛刺可能导致误导通。占空比不能无限细分。这就引出了分辨率的问题。2.3 分辨率决定了能调多细分辨率指的是占空比能分成多少档。它由定时器的计数位数决定。比如一个16位定时器自动重装载寄存器ARR设为65535那占空比就能分成65536档分辨率就是1/65536约0.0015%。如果ARR只设成99那就只有100档分辨率1%。这里有个非常重要的公式必须记住PWM频率 定时器时钟 / ((ARR 1) × (PSC 1))其中ARR是自动重装载值PSC是预分频值。而分辨率就是ARR1的倒数。关键矛盾来了在定时器时钟固定的情况下频率和分辨率是此消彼长的。你想要频率高ARR就得小分辨率就低你想要分辨率高ARR就得大频率就低。举个例子。STM32F103的TIM3挂在72MHz总线上。如果我想输出1kHz的PWM同时想要1000档分辨率先定ARR 9991000档那么 1000 72,000,000 / ((9991) × (PSC1))算出来 PSC1 72PSC 71这样频率是1kHz分辨率是1/1000。如果我想把分辨率提高到10000档ARR9999那PSC17.2不是整数只能取7或8频率就会变成1.028kHz或900Hz不再是精确的1kHz。这就是为什么很多教程里说PWM频率和分辨率要折中。在实际项目里先确定负载需要的频率再在满足频率的前提下把ARR尽量设大以获取更高的分辨率。这个顺序不能反。3. 从寄存器到库函数STM32上配置一路PWM的完整链路理论说完了现在进入实操。我以STM32F103的TIM3通道1对应PA6引脚为例把配置PWM的完整链路走一遍。之所以选TIM3是因为它挂在APB1总线上时钟是72MHz比较有代表性。如果你用的是其他型号比如GD32、HC32F460逻辑是一样的只是寄存器和库函数名字不同。3.1 第一步搞清楚时钟从哪来很多人配置PWM失败第一步就错了——没开时钟。STM32的外设默认是不工作的你必须手动使能它的时钟。TIM3挂在APB1上GPIOA挂在APB2上所以RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);如果你用的是HAL库那就是__HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE();这一步看起来简单但它是灯不亮最常见的原因之一。我见过太多人代码逻辑全对就是忘了开时钟查了半天。3.2 第二步把引脚配成复用推挽输出PA6要输出PWM不能配成普通的推挽输出必须配成复用推挽输出AF_PP。为什么因为普通输出模式下引脚的电平由GPIO的ODR寄存器控制而复用模式下引脚的控制权交给了片上外设这里是TIM3。如果你配错了TIM3内部再怎么计数引脚也不会有反应。GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);这里顺便说一句GPIO的8种工作模式很多人搞不清。简单记输入四种浮空、上拉、下拉、模拟输出四种推挽、开漏、复用推挽、复用开漏。PWM输出用复用推挽I2C用复用开漏ADC采样用模拟输入按键检测用上拉或下拉输入。选错了不会烧芯片但功能一定不对。3.3 第三步配置时基单元算出PSC和ARR时基单元决定了PWM的频率。假设我要输出1kHz、分辨率1000档的PWMTIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure);验证一下72MHz / (72 × 1000) 1kHz。对的。这里有个细节TIM_Period写的是999不是1000。因为计数器是从0数到ARR一共ARR1个数。写1000的话实际是1001档频率会变成999Hz。这种差一错误在嵌入式里非常常见配置定时器、串口波特率、ADC采样时间时都要注意。3.4 第四步配置PWM输出通道TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // CCR初始占空比50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);这里出现了PWM模式1和模式2的区别这是热词里经常被问到的。简单说PWM模式1计数器小于CCR时输出有效电平大于等于CCR时输出无效电平。PWM模式2反过来计数器小于CCR时输出无效电平大于等于CCR时输出有效电平。有效电平是高还是低由TIM_OCPolarity决定。所以模式1高极性就是计数值小于CCR时输出高。模式2高极性就是计数值小于CCR时输出低。两者波形正好相反。实际用哪个都行看你驱动电路的需要。比如驱动一个低边MOS管你可能希望默认输出低那就用模式2。3.5 第五步使能预装载和定时器TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE);TIM_ARRPreloadConfig和TIM_OC1PreloadConfig这两个预装载功能很重要。它们的作用是你写入的新ARR或CCR值不会立即生效而是等到下一次更新事件时才生效。这样可以避免在计数过程中改值导致波形出现毛刺。如果你在做电机调速占空比是动态变化的不开预装载就可能出现电机抖动。到这里PA6上就应该有1kHz、50%占空比的方波了。用示波器一测就能看到。如果没有按这个顺序查时钟开了吗引脚模式对吗定时器使能了吗CCR值是不是大于ARR了4. 为什么LED能调光电机能调速负载视角下的PWM配置跑通了但很多人还是有个疑问同样是PWM为什么LED是变暗电机是变慢加热丝是变热这背后其实是不同负载对PWM的响应方式不同。理解了这个你才能针对不同负载选对频率和驱动电路。4.1 LED靠视觉暂留和平均电流LED是二极管它的亮度基本正比于流过它的平均电流。PWM让LED在亮和灭之间快速切换只要频率高于人眼闪烁融合频率大约60Hz以上人眼看到的就是平均亮度。占空比50%平均电流就是最大电流的一半亮度也就大约一半。但这里有个坑LED的亮度并不严格正比于占空比。因为人眼对亮度的感知是对数关系占空比从0%到10%的变化看起来比从90%到100%的变化明显得多。所以如果你做呼吸灯用线性占空比变化会感觉暗的时候变化快亮的时候变化慢。想要视觉上均匀得做伽马校正或者用查表法。另外驱动大功率LED时不能直接用GPIO。GPIO一般只能输出20mA左右驱动1W的LED根本不够。这时候要用专门的LED驱动芯片比如恒流驱动IC。这类芯片通常有FB反馈脚和CS电流采样脚通过调整采样电阻或PWM调光脚来控制亮度。PWM调光脚的本质还是我们说的占空比控制只不过芯片内部帮你做了恒流。4.2 直流电机靠平均电压和机械惯性直流电机的转速近似正比于电枢电压。PWM通过快速开关让电机两端得到平均电压。比如24V供电50%占空比电机等效得到12V转速大约就是额定转速的一半。但电机和LED不一样它是感性负载而且有机械惯性。这带来两个问题第一电机是电感电流不能突变。PWM关断瞬间电机线圈会产生反向电动势如果不加续流二极管这个高压可能击穿驱动管。所以驱动直流电机必须用H桥或MOS管加续流二极管。第二频率选择很讲究。频率太低比如100Hz电机会听到明显的啸叫转动也不平滑频率太高比如50kHzMOS管的开关损耗会显著增加驱动板发热。一般1kHz到20kHz是甜区。我个人的经验是小电机用8kHz左右大电机用16kHz左右既避开听觉敏感区又不至于让驱动太热。还有一点电机启动瞬间电流很大可能是额定电流的5到10倍。如果你用PWM直接软启动从0%慢慢加到目标占空比可以有效减小冲击电流。这个技巧在电池供电的设备里特别有用。4.3 舵机PWM的异类靠脉宽而不是占空比舵机虽然也用PWM控制但它的逻辑和调光调速完全不同。舵机看的是高电平持续时间不是占空比。标准舵机要求周期20ms50Hz高电平0.5ms对应0度1.5ms对应90度2.5ms对应180度。所以舵机的PWM频率是固定的50Hz你不能随便改。改了就失控。而且舵机的分辨率取决于你能把脉宽细分到什么程度。20ms周期下如果ARR1999972MHz/72/2000050Hz那每1个计数就是1微秒0.5ms到2.5ms就是500到2500个计数分辨率足够。这就是为什么同一个定时器可以同时输出不同频率的PWM——只要它们在不同的通道且ARR相同频率就相同如果ARR不同那就不行。所以如果你要同时控制舵机和调光最好用两个不同的定时器。5. 那些年踩过的PWM坑从灯不亮到电机啸叫PWM配置本身不难难的是出了问题怎么查。下面这几个坑是我在实际项目中真实遇到过的每一个都花了不少时间。5.1 灯不亮先查时钟再查引脚最后查CCR有一次我用STM32F4的TIM1输出PWM代码检查了十遍灯就是不亮。后来发现是TIM1挂在APB2上我开了APB1的时钟。F1和F4的定时器总线分配不一样F1的TIM1在APB2TIM2~TIM7在APB1F4的TIM1和TIM8在APB2其他在APB1。这个细节不看手册真的会错。还有一个更隐蔽的高级定时器TIM1和TIM8需要额外使能主输出。普通定时器配置完就能输出但TIM1/TIM8有个刹车寄存器BDTR里面的MOE位必须置1否则引脚就是没输出。这个坑我第一次用TIM1时踩了整整一个下午。TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须加这一句5.2 电机啸叫频率选错了或者占空比太低电机啸叫一般有两个原因。一是PWM频率落在人耳敏感区2kHz~5kHz这时候把频率调到8kHz以上或1kHz以下就能改善。二是占空比太低电机处于断续导通状态电流不连续也会产生噪音。这时候要么提高占空比要么提高PWM频率让电流连续。我遇到过一次特别诡异的啸叫频率怎么调都有。最后发现是电源去耦没做好PWM开关时电源纹波太大耦合到了电机上。在驱动板电源脚并一个1000uF电解加一个0.1uF陶瓷电容问题就消失了。所以PWM问题不一定是PWM本身的错电源完整性也很关键。5.3 占空比改不动预装载没开或者改错了寄存器动态调占空比时如果你直接写CCR寄存器而没开预装载可能会出现改了值但波形没变或者波形跳变的情况。正确做法是开预装载然后用库函数TIM_SetCompare1来改。这个函数内部会处理预装载逻辑。还有一种情况你改的是CCR1但输出用的是通道2。这种低级错误在复制粘贴代码时特别容易发生。查的时候用示波器看波形比盯着代码看有效得多。5.4 波形有毛刺地线没接好或者探头补偿没调有时候示波器上看到的毛刺不是电路的问题是测量的问题。示波器探头的地线夹子如果太长会引入电感高频PWM下就会看到振铃。把地线夹子换成短的弹簧地针毛刺往往就消失了。这个经验对做开关电源和高速数字电路的人特别重要。6. 进阶玩法PWM还能这么用PWM不只是调光调速它在嵌入式里还有很多骚操作。这些用法在热词里也能看到影子比如PWM监控硬盘温度PWM故障保护。6.1 用PWM做DAC低通滤波出模拟电压如果你需要输出一个模拟电压但单片机没有DAC可以用PWM加RC低通滤波来实现。原理是PWM的平均值就是占空比乘以高电平电压。比如3.3V、50%占空比平均就是1.65V。用一个电阻和电容把高频滤掉就得到比较干净的直流电压。这个方法的精度取决于PWM分辨率和滤波效果。16位定时器理论上能做到16位DAC但实际上受电源纹波和滤波电路限制能做到10~12位就不错了。响应速度也慢因为滤波电容越大纹波越小但建立时间越长。适合输出变化缓慢的参考电压不适合输出音频。6.2 用PWM控制温度加热丝的惯性就是天然滤波器控制加热丝这种大惯性负载PWM频率可以很低甚至1Hz。因为加热丝热惯性大几秒钟的通断根本不会让温度明显波动。这时候用PWM比用继电器更平滑寿命也更长。但要注意加热丝是纯阻性负载但冷态电阻比热态小很多。刚上电时电流很大选MOS管和电源时要留足余量。我一般按热态电流的2倍来选。6.3 PWM故障保护刹车输入和死区时间高级定时器TIM1/TIM8有刹车输入功能。当刹车引脚检测到低电平时PWM输出会立即被强制到安全状态由刹车极性决定。这个功能在电机驱动里非常重要可以在过流、过温时快速关断PWM保护功率管。死区时间也是电机驱动必须考虑的。H桥的上下管如果同时导通就会短路。死区时间就是在上下管切换时插入的一小段都不导通的时间避免直通。STM32的高级定时器可以硬件插入死区不需要软件干预。6.4 用PWM测频率和占空比输入捕获PWM不只是输出还可以输入。用定时器的输入捕获功能可以测量外部PWM信号的频率和占空比。原理是捕获上升沿和下降沿的时间戳两个上升沿之间是周期上升沿到下降沿是高电平时间。这个功能在做遥控信号解码、转速测量时很有用。7. 不同平台上的PWM配置差异STM32、GD32、HC32F460虽然PWM原理一样但不同厂商的单片机在配置细节上有差异。这里简单对比一下方便你换平台时快速上手。平台库风格时钟使能高级定时器特殊要求备注STM32HAL/标准库/LL__HAL_RCC_TIMx_CLK_ENABLETIM1/TIM8需使能MOE资料最多生态最好GD32类似STM32类似类似寄存器基本兼容时钟树略有不同HC32F460华大自家库PWC_Fcg2PeriphClockCmd需配置端口复用功能时钟和外设使能方式不同以HC32F460为例它的PWM配置流程和STM32类似但有几个坑它的端口复用功能需要单独配置不像STM32那样在GPIO初始化里就指定了AF。而且它的定时器时钟源选择更灵活配置错了频率会差很多。我第一次用HC32F460时就是没注意端口复用PWM死活出不来。换平台时最靠谱的方法是先找一个官方例程把例程跑通再在例程基础上改。不要凭STM32的经验直接写因为时钟树、外设使能、引脚复用这些底层细节每家都不一样。8. 写在最后PWM是嵌入式的万能工具PWM这个东西入门只要十分钟但真正用好需要理解负载、理解频率、理解驱动电路。我见过太多人代码能跑但一问为什么这个频率就答不上来。这种能跑就行的心态在做简单项目时没问题但一旦遇到电机发热、LED闪烁、舵机抖动就抓瞎了。我的建议是每次配置PWM都先把频率、占空比、分辨率这三个参数算一遍写下来。然后想清楚这个负载需要什么频率驱动电路能不能承受这个频率。养成这个习惯你调PWM就不会再靠试了。还有一点示波器是调PWM最好的朋友。没有示波器你就是在盲调。一个入门级的示波器几百块钱能帮你省下无数个抓头发的夜晚。我刚开始学的时候就是靠一个二手示波器把PWM、串口、I2C这些波形一个个看明白的。看波形比看代码直观一百倍。最后分享一个小技巧如果你不确定PWM配置对不对先把频率设低一点比如10Hz然后接一个LED。10Hz下LED会明显闪烁你能直接看到占空比的变化。确认逻辑对了再把频率提到正常值。这个方法特别适合初学者比直接上示波器还直观。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑