资讯动态

STM32 PWM原理与实战:从LED调光到电机调速

发布时间:2026/10/27 1:08:17 来源:尧图企业网站定制
1. 从一个现象说起为什么调亮度不用调电压很多人第一次接触PWM脑子里冒出来的问题都差不多既然要让LED变暗直接给它降电压不就行了要让电机转慢点把供电电压调低不就行了这个思路在逻辑上没毛病但实际工程里几乎没人这么干。原因很简单——调电压这件事在数字电路里又贵又难做而且效果还不好。你想想单片机输出的是数字信号要么高电平要么低电平它本身不具备“输出2.3V”这种精细的模拟能力。如果非要用模拟方式调压你得额外加DAC、加运放、加功率管成本上去了电路复杂度也上去了。更关键的是LED的亮度与电流并不是线性关系电压稍微变一点电流可能变化非常剧烈控制起来很不稳定。电机也一样降低电压会导致扭矩急剧下降低速时甚至转都转不起来。PWM的思路完全换了一个维度我不调电压幅值我调电压“出现的时间比例”。还是那个5V的方波但我让高电平只占整个周期的30%剩下70%的时间是低电平。对于LED来说它的响应频率远高于人眼分辨能力你看到的就是“平均亮度”降低了对于电机来说电感效应会把脉冲电流平滑成连续电流等效于施加了一个更低的平均电压。这就是PWM能调速、调光的核心逻辑。它用时间维度的控制替代了幅值维度的控制完美适配数字系统的天性。1.1 核心概念占空比、频率、周期在展开之前先把三个最基础的参数说清楚后面所有内容都围绕它们转。周期Period一个完整PWM波形从上升到下一次上升的时间长度单位通常是微秒或毫秒。比如周期20ms意味着每20ms重复一次。频率Frequency周期的倒数单位Hz。周期20ms对应50Hz。频率决定了PWM的“刷新速度”。占空比Duty Cycle高电平时间占整个周期的比例通常用百分比表示。占空比60%意味着一个周期里60%的时间是高电平。这三个参数的关系可以用一个很直观的公式表达占空比 高电平时间 / 周期 × 100%假设周期是1ms高电平持续0.3ms那占空比就是30%。如果你把高电平时间改成0.7ms占空比就变成70%。LED会明显更亮电机也会转得更快。1.2 为什么LED和电机对PWM的响应不一样这里有一个很多人忽略的关键点同样是PWMLED和电机需要的频率范围完全不同。LED的闪烁频率只要高于人眼的临界闪烁频率大约60-90Hz人眼就感觉不到闪烁了。所以驱动LED做调光频率选个200Hz到1kHz就很舒服。频率太低会看到闪烁频率太高又没必要还会增加开关损耗。直流电机则不一样。电机的机械惯性很大转子不可能跟着PWM频率来回抖动。如果PWM频率太低比如几十Hz你会听到电机发出“嗡嗡”的啸叫声转动也不平滑。通常有刷直流电机的PWM频率建议在1kHz到20kHz之间超过20kHz就超出人耳听觉范围了电机运行会非常安静。但频率也不是越高越好太高的频率会导致MOS管开关损耗增加驱动电路发热严重。所以你看PWM频率的选择从来不是随便拍脑袋定的它取决于负载的物理特性。这是实操中最容易踩坑的地方之一。2. STM32上PWM到底是怎么产生的搞清楚了PWM的基本原理接下来要解决的问题是在STM32这类单片机上PWM信号到底是怎么“变”出来的很多人用CubeMX配置完就能输出PWM了但问他为什么能输出答不上来。这一章就把这条链路彻底拆开。2.1 定时器PWM的发动机STM32内部产生PWM的核心外设是定时器TIM。你可以把定时器想象成一个不停计数的秒表它从0开始往上数数到一个设定值后归零然后重新开始。这个“设定值”就是自动重装载寄存器ARR的值。举个例子如果ARR设置为999定时器就从0数到999一共数1000个数然后重新从0开始。这1000个计数构成一个完整的PWM周期。在定时器计数的过程中还有一个关键寄存器叫捕获/比较寄存器CCR。定时器会不断把当前计数值CNT和CCR进行比较当CNT CCR时输出高电平当CNT CCR时输出低电平这就是PWM输出的基本机制。占空比的计算公式就变成了占空比 CCR / (ARR 1) × 100%比如ARR999CCR300占空比就是300/100030%。你想调亮度或者调转速本质上就是在运行时修改CCR的值。2.2 频率怎么算一个不能死记的公式PWM频率的公式是PWM频率 定时器时钟频率 / ((ARR 1) × (PSC 1))其中PSC是预分频器Prescaler用来把定时器的输入时钟降频。比如STM32F103的TIM1挂在72MHz的APB2总线上如果PSC71那定时器的实际计数频率就是72MHz/(711)1MHz也就是每1微秒计一个数。假设你要生成一个1kHz的PWM信号周期就是1ms。如果计数频率是1MHz那1ms需要计数1000次所以ARR应该设为999。PSC71ARR999代入公式72,000,000/((9991)×(711))72,000,000/(1000×72)1000Hz。刚好1kHz。这里有个实操中很容易搞错的地方ARR和PSC都是“减一”的关系。因为寄存器是从0开始计数的你要1000个计数ARR就得写999。这个细节在手动计算时经常把人绕进去。2.3 输出通道与GPIO的复用关系定时器算出了PWM波形但信号还得送到具体的引脚上。STM32的定时器通道和GPIO引脚之间是复用映射关系。比如TIM1的通道1可以映射到PA8、PE9等引脚上具体映射到哪个取决于芯片型号和GPIO的复用功能配置。以最常见的STM32F103C8T6为例PA8对应的是TIM1_CH1。你要做的配置步骤是使能TIM1的时钟和GPIOA的时钟把PA8配置为复用推挽输出模式配置TIM1的时基单元ARR、PSC配置TIM1的通道1为PWM模式使能TIM1的通道1输出和定时器本身这五步缺一不可。很多人卡在第一步或者第二步要么时钟没开要么GPIO模式配错了结果波形死活出不来。注意GPIO模式必须选“复用推挽输出”Alternate Function Push-Pull不能选普通的推挽输出。普通推挽输出是由GPIO外设直接控制的而PWM信号来自定时器外设必须通过复用功能才能送到引脚上。2.4 PWM模式1和模式2的区别STM32的定时器支持两种PWM模式很多人配置的时候直接默认选了模式1但从来没想过两者的区别。PWM模式1CNT CCR时输出有效电平CNT CCR时输出无效电平。PWM模式2反过来CNT CCR时输出无效电平CNT CCR时输出有效电平。那什么叫“有效电平”这又涉及到输出极性的配置。你可以把有效电平设为高电平也可以设为低电平。所以实际上有四种组合模式有效极性CNTCCR时输出CNTCCR时输出模式1高高低模式1低低高模式2高低高模式2低高低看起来有点绕但实际用起来很简单如果你用的是常见的低边驱动比如N沟道MOS管拉低LED阴极通常用模式1、高极性。如果你用的是高边驱动或者反相驱动电路可能就需要调整极性。选错了模式你会发现占空比越大反而越暗或者电机反转。3. 从零配置一个PWM输出以PA8驱动LED为例理论说了一堆现在进入实操环节。我以STM32F103C8T6为例从寄存器层面到库函数层面把PA8输出PWM的完整过程走一遍。你照着做基本不会翻车。3.1 硬件连接与注意事项先看硬件。PA8接一个LED串联一个限流电阻。假设LED的正向压降是2V工作电流取10mASTM32的GPIO输出高电平是3.3V那么限流电阻的计算是R (3.3V - 2V) / 10mA 130Ω选一个标准值150Ω或者220Ω都可以电流会略小一点LED稍微暗一些但更安全。接线方式是PA8 → 限流电阻 → LED阳极 → LED阴极 → GND。这样PA8输出高电平时LED亮输出低电平时LED灭。注意STM32的单个GPIO引脚最大输出电流一般是20mA左右驱动普通LED没问题但如果你要驱动大功率LED或者多个LED并联必须加三极管或者MOS管做扩流否则会烧引脚。3.2 时基单元的配置过程时基单元的配置决定了PWM的频率。假设我们要生成1kHz的PWM系统时钟72MHz配置步骤如下首先确定PSC和ARR。为了让计算简单我习惯先把计数频率设为1MHz也就是PSC71。然后1kHz的周期是1ms1MHz计数频率下需要1000个计数所以ARR999。用标准外设库的代码大概长这样TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure);这段代码做了几件事设置ARR为999PSC为71时钟分割为1分频计数模式为向上计数。向上计数是最常用的模式定时器从0数到ARR然后归零。3.3 输出通道的配置细节时基配好之后接下来配置通道1为PWM输出模式TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 300; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure);这里TIM_Pulse就是CCR的值设为300意味着初始占空比是30%。TIM_OCMode_PWM1选了模式1TIM_OCPolarity_High表示有效电平为高。还有一步很容易漏掉使能预装载寄存器。TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE);预装载的作用是你写入新的CCR值时不会立刻生效而是等到下一个周期开始时才更新。这样可以避免PWM波形在周期中间突然变化导致抖动。对于LED调光来说这个抖动可能看不出来但对于电机控制来说波形不连续可能会导致电流冲击。最后使能定时器TIM_Cmd(TIM1, ENABLE);对于高级定时器TIM1、TIM8还需要额外使能主输出TIM_CtrlPWMOutputs(TIM1, ENABLE);这一步是高级定时器特有的通用定时器TIM2-TIM5不需要。很多人用TIM1输出PWM时忘了这一句结果通道配置全对就是没波形。3.4 GPIO复用的正确打开方式GPIO的配置看似简单但细节不少GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);GPIO_Mode_AF_PP就是复用推挽输出。速度选了50MHz实际上PWM频率只有1kHz选2MHz都绰绰有余。但速度配置会影响引脚翻转的上升沿和下降沿时间速度越高边沿越陡EMI也越大。对于低频PWM选低速反而更稳妥。实操心得如果你发现PWM波形出不来按照这个顺序排查——时钟使能了吗GPIO模式对吗高级定时器的主输出使能了吗CCR值是不是设成了0或者大于ARR这四步能解决90%的问题。4. 占空比动态调整从LED呼吸灯到电机调速PWM配好之后只是第一步真正有用的是运行时动态调整占空比。LED呼吸灯和电机调速本质上都是不断修改CCR值的过程但两者的调整策略和注意事项差别很大。4.1 LED呼吸灯的实现逻辑呼吸灯的效果是LED亮度从暗到亮再到暗循环往复。实现思路很简单在一个循环里逐渐增加CCR值到达最大值后再逐渐减小。while(1) { for(int i 0; i 1000; i 10) { TIM_SetCompare1(TIM1, i); delay_ms(10); } for(int i 1000; i 0; i - 10) { TIM_SetCompare1(TIM1, i); delay_ms(10); } }这段代码里CCR从0增加到1000每次加10延时10ms。整个过程大约1秒呼吸效果比较自然。但这里有个问题人眼对亮度的感知不是线性的。占空比从10%增加到20%人眼感觉亮度变化很明显但从80%增加到90%感觉就没那么明显了。如果你想要更自然的呼吸效果应该用伽马校正把线性的CCR值映射到非线性空间。简单做法是查表或者用平方关系近似。注意修改CCR值时如果预装载没使能新的值会立即生效可能导致当前周期波形不完整LED会出现轻微闪烁。所以前面提到的预装载配置一定要打开。4.2 直流电机调速的关键差异电机调速和LED调光在代码层面看起来差不多都是改CCR但实际工程中要考虑的东西多得多。首先是频率选择。前面说过电机PWM频率建议在1kHz到20kHz之间。频率太低电机会啸叫频率太高驱动电路发热。我一般选8kHz或者16kHz这两个频率刚好在人耳听觉范围边缘电机运行比较安静。其次是死区时间。如果你用的是H桥驱动电机正反转上下桥臂的MOS管不能同时导通否则会短路烧管。STM32的高级定时器支持死区插入可以在互补输出之间自动插入一段死区时间。死区时间的大小取决于MOS管的开关速度和驱动电路一般几百纳秒到几微秒。第三是软启动。电机从静止突然全速启动时电流冲击很大可能拉低电源电压导致单片机复位。正确做法是让CCR从0逐渐增加到目标值给电机一个加速过程。这个过程可以在主循环里实现也可以用定时器中断自动完成。// 软启动示例 for(int i 0; i target_duty; i 5) { TIM_SetCompare1(TIM1, i); delay_ms(20); }4.3 占空比与转速的非线性关系很多人以为占空比50%就是半速实际上完全不是。电机的转速和占空比之间的关系受负载、摩擦、反电动势等多种因素影响通常是非线性的。空载时可能30%占空比就能转到接近全速带载时可能70%占空比才勉强启动。如果你需要精确控制转速光靠开环PWM是不够的必须加编码器反馈做闭环控制。PID算法根据编码器测到的实际转速调整PWM占空比这样才能在不同负载下保持转速稳定。实操心得调试电机PWM时先用示波器确认波形频率和占空比是否正确再接电机。我见过有人波形都没看就直接接电机结果频率配成了50Hz电机嗡嗡响就是不转还以为是电机坏了。5. 常见问题与排查技巧实录PWM这个东西配置起来不难但出问题的时候往往让人摸不着头脑。这一章我把这些年踩过的坑和排查思路整理出来希望能帮你少走弯路。5.1 波形出不来按这个顺序查PWM配置完了但引脚上没波形是最常见的问题。排查顺序如下排查步骤检查内容常见错误1定时器时钟使能了吗忘了调用RCC_APB2PeriphClockCmd2GPIO时钟使能了吗只开了定时器时钟忘了GPIO3GPIO模式对吗配成了普通推挽输出而非复用推挽4高级定时器主输出使能了吗TIM1/TIM8忘了TIM_CtrlPWMOutputs5CCR值合理吗CCR0或CCRARR6引脚映射对吗查数据手册确认复用功能这六步基本能覆盖95%以上的“无波形”问题。如果还不行拿示波器直接量引脚确认是单片机没输出还是后级电路把信号吃了。5.2 占空比不对先看预装载有时候波形出来了但占空比和你设的不一样。比如你设CCR300ARR999理论占空比30%但示波器量出来是别的值。这种情况通常是预装载寄存器在作怪。如果你在初始化时先写了CCR然后又改了ARR但预装载没使能CCR的值可能在你改ARR的瞬间就被更新了导致实际占空比偏离预期。解决办法很简单初始化时先配时基再配通道最后统一使能预装载。还有一种可能是时钟频率算错了。比如你以为系统时钟是72MHz实际上因为时钟树配置问题只有36MHz那PWM频率就全错了。用示波器量一下实际频率反推时钟频率就能确认。5.3 LED闪烁或电机抖动查频率和电源LED在PWM调光时出现肉眼可见的闪烁通常是因为PWM频率太低。把频率提高到200Hz以上基本就能解决。如果提高频率后还有闪烁可能是电源纹波太大LED的供电不稳定。电机抖动的原因更多。除了频率太低之外还可能是电源功率不足电机启动时拉低电压驱动电路没有续流二极管电机电感产生的反电动势干扰电路PWM占空比变化太快电机来不及响应我遇到过一次电机抖动查了半天发现是电源线太细电机启动瞬间压降太大导致单片机复位。换了一根粗线就好了。这种问题用示波器看电源波形一目了然。5.4 PWM干扰显示或其他外设怎么办这个问题在热词里也出现了——“pwm背光 干扰 显示”。PWM驱动背光时如果频率和显示刷新率产生拍频屏幕上会出现滚动条纹。解决办法有两个要么把PWM频率提高到远高于显示刷新率要么让PWM频率和显示刷新率保持整数倍关系。如果是PWM信号干扰了其他外设比如ADC采样可以在PWM输出引脚上加RC滤波或者在ADC采样时避开PWM开关瞬间。STM32的定时器支持触发ADC采样的功能可以让ADC在PWM周期的特定时刻采样避开干扰。实操心得PCB布局时PWM走线尽量远离模拟信号线。如果实在避不开中间加一根地线做隔离。这个经验是我在一个电机控制项目里用血泪换来的ADC采样值一直跳最后发现是PWM走线从ADC输入引脚旁边穿过。6. 进阶话题从PWM到更复杂的控制PWM的基础用法掌握之后可以往更深的方向走。这些内容不一定每个项目都用得上但了解一下没坏处。6.1 互补输出与死区时间高级定时器TIM1、TIM8支持互补输出一个通道可以同时输出一对反相的PWM信号专门用于H桥驱动。互补输出之间可以插入死区时间防止上下桥臂直通。死区时间的配置通过BDTR寄存器完成TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime 0x0F; TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure);死区时间的实际长度取决于TIM_DeadTime的值和时钟频率具体对应关系查参考手册的BDTR章节。一般来说几百纳秒的死区时间对大多数MOS管驱动电路都够用了。6.2 PWM输入捕获测量外部信号定时器不仅能输出PWM还能捕获外部PWM信号的频率和占空比。这个功能在测量传感器输出、解码遥控信号时非常有用。输入捕获的原理是当引脚上检测到上升沿或下降沿时定时器自动把当前计数值存入CCR寄存器并触发中断。通过两次捕获的计数值之差就能算出周期和频率。// 输入捕获配置示例 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; TIM_ICInit(TIM2, TIM_ICInitStructure);输入捕获的精度取决于定时器的计数频率。计数频率越高测量分辨率越高但定时器溢出也越快。需要根据被测信号的频率范围来选择合适的PSC和ARR。6.3 用PWM控制DC-DC输出电压这是一个比较巧妙的用法很多DC-DC芯片的反馈引脚FB本来是用来接分压电阻设定输出电压的但如果你在分压电阻网络上叠加一个PWM滤波后的直流电平就能动态调整输出电压。具体做法是PWM信号经过RC低通滤波变成模拟电压通过一个电阻注入到FB节点。调整PWM占空比就改变了注入电流从而改变了FB节点的电压DC-DC芯片会自动调整输出以维持FB电压恒定最终实现输出电压的可编程控制。这种方式的优点是成本低、电路简单缺点是响应速度慢受RC滤波时间常数限制精度也不如专用DAC。适合对动态响应要求不高的场合。注意注入电阻的阻值需要仔细计算太小会严重影响原有分压网络太大会导致调节范围不够。一般取分压电阻的10到100倍比较合适。7. 一些零散但有用的经验最后分享几个我在实际项目中积累的小技巧不一定系统但都挺实用。关于频率选择如果你不确定PWM频率选多少LED调光从1kHz起步电机驱动从8kHz起步舵机控制用50Hz。这几个值覆盖了大多数常见场景。关于占空比分辨率ARR越大占空比可调的档位越多分辨率越高。但ARR太大会导致频率降低。如果你需要高分辨率又要保持频率只能提高定时器时钟频率。STM32的定时器时钟最高可以到72MHz甚至更高足够大多数应用使用。关于GPIO速度PWM引脚的GPIO速度不需要设太高。1kHz的PWM用2MHz速度绰绰有余设成50MHz反而会增加EMI。只有在高频PWM比如100kHz以上时才需要高速配置。关于调试工具示波器是调PWM的必备工具没有示波器至少也要有个逻辑分析仪。靠猜和试错效率太低而且容易把问题归咎于错误的方向。关于代码移植不同STM32系列的定时器寄存器名称可能不同但PWM的工作原理完全一样。F1系列用标准库F4/H7系列用HAL库配置思路一致只是函数名变了。理解了ARR、PSC、CCR这三个核心寄存器的作用换任何芯片都能快速上手。关于PWM故障保护高级定时器的Break功能可以在检测到故障信号时自动关闭PWM输出保护功率电路。这个功能在电机驱动和电源应用中非常重要配置也不复杂建议在涉及功率控制的场合都加上。关于低功耗如果PWM用于电池供电设备注意PWM频率越高开关损耗越大。在满足应用需求的前提下尽量降低频率可以延长电池寿命。另外有些STM32系列支持在低功耗模式下由低功耗定时器产生PWM进一步降低功耗。关于LED驱动芯片的配合如果你用的是专用LED驱动芯片而不是直接驱动LEDPWM信号可能接到芯片的使能脚或FB脚上。这时候要注意芯片的使能响应时间和PWM频率的匹配。有些驱动芯片的使能脚响应很慢PWM频率高了它根本来不及反应输出会变成一条直线而不是调光效果。查芯片数据手册确认最大PWM频率别想当然。

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

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

免费获取报价 →
↑