☰
STM32 PWM生成实战:频率占空比计算与HAL库配置指南
2026/10/13 16:24:34 网站建设 项目流程

很多朋友第一次接触PWM都是从那颗经典的呼吸灯开始的——LED渐渐变亮又变暗,看着挺神奇。但等你自己真正要在STM32上生成PWM信号的时候,问题马上就来:频率选多少?占空比怎么算?为什么波形出不来?为什么电机一堵转板子就烧?这些坑我在项目里几乎全踩过一遍。这篇东西就按我实际调PWM的顺序,从原理讲到HAL库配置,再到不同应用场景的参数选择,尽量把"生成PWM信号"这件事讲透。适合正在用STM32做LED调光、电机驱动,或者刚开始接触PWM的嵌入式开发者和电子爱好者。

1. 别把PWM想简单了:它不只是电平翻转

1.1 从"开关灯"理解PWM的等效输出

PWM的全称是Pulse Width Modulation,脉冲宽度调制。说白了就是在一段固定周期内,控制高电平持续时间和低电平持续时间的比例。你可能觉得这不就是让IO口高低翻转嘛,但工程上真正的价值在于:当翻转频率远高于负载的响应速度时,负载感受到的是一个连续可调的平均值。

我经常用一个类比你肯定秒懂:门外太阳光太强,你想让房间暗一点。如果慢慢拧调光旋钮让灯变暗,那是模拟调压;如果装一个百叶窗,快速地把叶片打开又合上,只要切换频率足够高,人眼感知到的就是"介于全开和全关之间的一个稳定亮度"。PWM就是那个百叶窗。LED并不关心电压是不是阶梯变化,它只知道"亮了多久、灭了多久"的平均效果;电机也是一样,负载的机械惯性会把高频脉动过滤掉,最后体现在转速上。

1.2 为什么必须要硬件定时器,不能靠延时函数

新手最容易踩的坑,是想用delay函数在main循环里翻转IO来"生成PWM"。比如这样写:拉高、延时、拉低、延时、循环。这个方法在呼吸灯试验里确实能亮能暗,但放到项目里就是灾难:CPU百分之百被占用,什么别的代码都跑不了;延时的长短受中断影响,波形抖动得一塌糊涂;占空比稍微调细一点,主循环就卡得跟幻灯片一样。

这也是为什么单片机里都有专门的定时器外设。定时器硬件不依赖CPU,自己就能按照预分频值、自动重载值、比较值协同工作。你只需要配一次参数,PWM就自己跑起来了,CPU只在需要调整参数时写一下寄存器就能改变输出。做工程和做试验的区别就在这里:试验能亮就行,工程必须让CPU腾出来干正事。

1.3 硬件PWM的三个关键角色:PSC、ARR、CCR

以STM32的通用定时器为例,生成PWM的核心就是配置这三个寄存器:

  • PSC(Prescaler,预分频器):对定时器输入时钟做分频,降低计数器步进速度;
  • ARR(Auto-Reload Register,自动重载值):计数器计数的最终值,配合预分频器决定PWM周期;
  • CCR(Capture/Compare Register,捕获比较寄存器):计数器计到这个值时触发输出翻转,具体触发成高还是低,由PWM模式和极性共同决定。

有些教程管ARR叫"Period"或者"重载值",管CCR叫"Pulse"或者"比较值",指的都是同一个东西。你只要记住:计数器的时钟决定时间精度,ARR决定周期长短,CCR决定占空比大小。这三个参数就是后面所有计算的基础,建议刚开始学的时候在纸上把这三个值算一遍再填进CubeMX,比直接填数值理解深刻得多。

2. 生成PWM之前,先把频率、占空比、分辨率算明白

2.1 占空比计算公式:先搞懂再调参

占空比的定义很简单:一个周期内高电平的时间占总周期的比例。

Duty = Ton / (Ton + Toff) × 100%

在STM32的向上计数模式下,ARR+1对应整个周期,CCR对应模式1下高电平持续的长度,所以占空比计算公式也可以写成:

Duty = CCR / (ARR + 1) × 100%

举例来说,ARR=999,CCR=500,占空比就是50%;CCR=700,占空比就是70%。网上搜的那堆"pwm占空比计算公式"基本都是这个意思,别把它想复杂了。

需要注意,占空比的分子是CCR本身,不是"周期减去CCR"。我第一次调舵机时就是搞反了,结果占空比越调越小,舵机反而往反方向转,查了半天才发现是这种低级错误。这种基础公式最好自己动手推一遍,不要死记。

2.2 频率计算:PSC和ARR怎么配才能得到目标频率

PWM频率的计算公式是:

f_PWM = TIM_CLK / ((PSC + 1) × (ARR + 1))

其中TIM_CLK是定时器输入时钟。公式里的两个+1都容易漏,因为PSC和ARR从0开始计数。我第一次配置时钟树时漏了+1,算出来的频率就差一点,用示波器一量才发现不对。

举个真实例子:STM32G070CBT6,内部高速时钟16MHz,我想产生1kHz的呼吸灯PWM。

取PSC=15,分频后的定时器时钟是16MHz / (15+1) = 1MHz; 取ARR=999,PWM频率就是1MHz / (999+1) = 1kHz。

在这个配置下,占空比每变化1就相当于变化0.1%,分辨率是千分之一。如果你需要更细腻的亮度过渡,可以反着配:PSC=0、ARR=15999,同样输出1kHz(16MHz/16000),但分辨率提高到了1/16000。代价是计数器数值变大,处理比较和中断时计数频率也会跟着升高。成本和收益要自己权衡。

2.3 同一个频率,在不同负载上感受完全不同

为什么"选频率"会成为搜索热词?因为频率不是拍脑袋定的,它取决于负载的物理响应速度。

  • LED和人眼:人眼对几十赫兹的闪烁已经不太敏感,几百赫兹以上更是无感,所以呼吸灯用500Hz到1kHz完全够;
  • 电机和人耳:电机转动时电磁力和机械结构会发出声音,声音频率恰好等于PWM频率。如果PWM频率落在人耳最敏感的1kHz到8kHz区间,你就会一直听到"滋——"的尖叫。所以直流电机调速常用16kHz到20kHz,因为大多数人听不到这个频段;
  • 舵机内部:舵机内部有一个标准的RC接收电路,以50Hz作为基准周期、以0.5ms到2.5ms的脉宽对应角度。这时PWM频率由舵机协议决定,必须用50Hz,不是你随便选个20kHz就能让它转的。

"频率越高越好"是新手很容易踩的误区。MOS管和驱动电路每一次开关都有损耗,频率翻倍,开关损耗基本也翻倍;过高频率还会引入电磁干扰和驱动不足的问题。后面第5章我会按具体场景给出参考值。

3. STM32 HAL实战:从CubeMX配置到示波器出波

3.1 CubeMX里到底要配哪些地方

就拿STM32G070CBT6配PB6输出PWM这个案例来说。PB6在STM32G070上默认复用就是TIM3_CH1,对应搜索里"stm32g070cbt6 pwm输出程序 hal库函数"那些朋友,多数就是在这个芯片上卡住了,其实配置思路和F030系列是完全一样的。

打开CubeMX后做四件事:

  1. 时钟树:给定时器选一个时钟源。如果只是验证PWM,用内部HSI 16MHz就够了,不用外接晶振。先确定APB定时器时钟那一路是多少,后面计算才准;
  2. TIM3:把Clock Source勾选为Internal Clock,然后在Parameter Settings里填PSC和ARR。以刚才的1kHz为例,Prescaler填15,Counter Period填999,Counter Mode选Up;
  3. 通道:在TIM3的PWM Generation Channel 1里,Mode选PWM mode 1,Pulse填初始值比如499,OC Polarity默认High不动;
  4. GPIO:在Pinout页把PB6选为TIM3_CH1复用功能,CubeMX会自动把引脚配置成复用输出。

这里有个提醒:F030/G070这类芯片的GPIO复用功能不是随便映射的,PB6能复用成TIM3_CH1,而PB7可能是TIM3_CH2,要查数据手册或用CubeMX的pinout提示。如果你把引脚配置成普通推挽输出,再用软件翻转,那就是前面说的软件PWM,性能和可靠性都差一个量级。

3.2 代码骨架:启动PWM和动态调参

CubeMX生成工程后,PWM其实已经初始化好了,你在用户代码里要做的事并不多。启动一路PWM就一行:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

这一句的作用是让定时器真正输出PWM波形。很多人的误区是CubeMX配完就以为波形出来了,实际上不调用Start,通道输出一直是静止的某个电平。

运行过程中改占空比用宏:

/* 把占空比改为700/1000 = 70% */ __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 700);

改频率则改ARR:

/* 把ARR从999改成499,PWM频率变为2kHz */ __HAL_TIM_SET_AUTORELOAD(&htim3, 499); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 349); // 注意同步调整比较值

为什么要同步调整CCR?因为ARR一旦变化,CCR不跟着按比例调整,占空比会突然跳变。比如ARR从999改成499,CCR保持700会超过ARR,输出直接变成常高或常低,整个PWM就废了。改频率这种事,计算完一定看一眼参数范围再写。

3.3 实测中我踩过的四个坑

第一个坑:Pulse填0导致什么都看不到。CubeMX里Pulse是初始比较值,你填0,模式1下计数器一开始就大于比较值,输出一直为低。这种"没有波形"最容易让人怀疑是时钟问题或引脚问题,实际只是初始值没填对。

第二个坑:GPIO复用没配对。PB6选成TIM3_CH1后,还要确保没有其他外设抢占同一引脚。比如你同时把PB6配给了另一个通信协议的片选,两个外设打架,波形就是出不来或时好时坏。

第三个坑:极性搞反。OC Polarity设成High,比较匹配前是高电平、匹配后是低电平;设成Low就正好反过来。如果驱动电路是低电平点亮LED,极性问题会让亮度变化方向和代码完全相反,你会看到"占空比加大反而变暗",非常迷惑。

第四个坑:在呼吸灯里用HAL_Delay阻塞延时。每个亮度等级延时20ms,占空比从0到1000循环一圈要20多秒,期间CPU全被延时卡死。真正做呼吸灯一般用定时器中断更新占空比,或者把亮度变化放在主循环里配合系统的tick调度,中间不要阻塞。

4. PWM模式1/模式2、单脉冲、中心对齐:配置细节里的门道

4.1 PWM模式1和模式2到底差在哪

CubeMX里PWM Mode有两个选项:PWM mode 1和PWM mode 2。它们的波形差别在于:

  • 向上计数模式:计数器从0计到ARR。模式1下,计数到CCR之前输出有效电平(一般算高),到达CCR后翻转;模式2则相反,计数到CCR之前输出无效电平,过了CCR才是有效电平。
  • 向下计数模式:逻辑正好反过来。

所以模式1和模式2本质上就是"高电平在前"还是"高电平在后"的差别。选择上没有绝对对错,完全看外部电路的逻辑:如果你的驱动电路要求高电平使能,用模式1配合High极性;如果要求低电平使能,可以用模式2配合Low极性,也可以模式1配Low极性。关键是搞清自己用的是哪种极性,别配反了。

4.2 单脉冲模式能不能"连续发射PWM波"

搜索词里有"单脉冲模式 连续发射pwm波",这个问题很典型。STM32定时器的单脉冲模式(One Pulse Mode,OPM)是一次性的:计数器收到触发信号后启动,按预设的周期和比较值输出一个脉冲,然后计数器停止,等待下一次触发。它本质就是"受外来事件触发的单次PWM"。

所以答案是:单脉冲模式不能连续自发地输出PWM波。如果你需要连续PWM,直接关闭OPM,用普通PWM模式就行;如果你需要"来一个触发信号就输出一个精确宽度的脉冲",那OPM是正解。它的典型用途包括:超声波驱动、精确短脉冲输出,或者做一次性的脉宽校准。

4.3 中心对齐模式:电机控制里值得关注的选择

前面说的都是边沿对齐模式,也就是计数器从0加到ARR再瞬间回0。中心对齐模式(Center-Aligned)下,计数器先从0加到ARR,再从ARR减到0,输出脉冲的上升沿和下降沿都相对于计数值中点对称。

很多做电机控制的老工程师喜欢用中心对齐,因为它的有效开关频率是边沿对齐的两倍,且输出波形谐波更小,电磁噪声更柔。代价是配置复杂一点,更新比较值还要注意计数方向。如果你是新手,刚开始用边沿对齐完全够用;等你做无刷电机驱动时发现电流噪声偏大,再切到中心对齐试试,往往有惊喜。

5. 应用场景决定参数:LED、直流电机、舵机、无刷电机各用多少频率

5.1 呼吸灯和RGB调光:频率主要照顾眼睛

呼吸灯基本就是给LED的亮度做正弦或线性渐变。频率选500Hz到1kHz完全够,人眼无感,LED也不会有残影发虚。至于亮度曲线,不要用线性占空比直接变化,因为人眼对亮度的感知是对数的,线性变化会让人觉得"前半段很亮,后半段几乎不变"。可以用查找表放一组指数或正弦曲线,呼吸效果会自然很多。

RGB调光稍微讲究一点。三种颜色的LED要同时工作,PWM频率建议1kHz以上,最好2kHz到5kHz,否则在快速移动物体上可能出现分色伪影。另外三路PWM的初始相位最好对齐,这样合成颜色时不容易闪。用STM32多个定时器通道分别控制R、G、B是常规做法,如果通道不够,就用同一个定时器的不同通道,它们在时间上天然同步,比分散在不同定时器里更好。

5.2 直流电机和舵机:频率是给负载选的

直流电机调速的PWM频率,我一般从16kHz起步。再低一点比如10kHz会有人觉得能听到高频啸叫;高到20kHz以上MOS开关损耗和驱动发热又开始上升。这里要根据具体驱动芯片和电机的电感参数去微调,最好在示波器上看电流波形再定。

有朋友拿着MG310P20这类集成驱动板,在7.4V电压下问该用多少PWM频率。我的实用建议是:先确认驱动板输入逻辑电平和你的MCU匹配,再从16kHz开始调。如果电机啸叫明显就往下调几千赫兹,比如14kHz或12kHz;如果驱动板发热厉害就往回调。弄清自己板子上的开关器件参数,比纠结一个固定值更有意义。

舵机是另一个极端。标准模拟舵机内部按50Hz周期、0.5ms到2.5ms脉宽判断角度,也就是说周期固定20ms,脉宽在0.5ms(0°附近)到2.5ms(180°附近)之间线性映射。生成方法就是ARR设成20ms对应计数值,CCR设成目标脉宽对应计数值。数字舵机虽然接收频率可以更高,但兼容起见先用50Hz不会错。

常见PWM应用参数参考:

应用推荐PWM频率备注
呼吸灯500Hz ~ 1kHz人眼无感,占空比按指数曲线变化
RGB调光1kHz ~ 5kHz防止分色伪影,尽量三通道同步
直流电机调速16kHz ~ 20kHz避开人耳频段,兼顾开关损耗
模拟舵机50Hz周期20ms,脉宽0.5ms ~ 2.5ms
BLDC六步换向16kHz ~ 20kHz需要互补PWM加死区
单相PWM整流10kHz ~ 20kHz由电流纹波和EMI综合决定

5.3 BLDC六步换向:多路PWM和死区

"六步换向pwm波形"这个搜索词说明有人被无刷电机驱动卡住了。六步换向本质是在三相桥臂上,按转子位置切换导通组合,每个60°电角度区间内,上下桥臂按特定规律导通。常见的调制策略有上桥PWM下桥恒通、上桥恒通下桥PWM,以及PWM_ON_PWM这类改进型。

硬件上通常需要三路互补PWM输出加死区。STM32的TIM1/TIM8高级定时器就是为这个设计的:每路都能输出互补的CHx和CHxN,还有硬件死区寄存器。死区时间一般从几百纳秒到几微秒,具体看MOS管的关断延迟。设小了上下桥臂直通,直接烧管子;设大了波形畸变和发热增加。调试六步换向时建议先在低速下空载运行,用示波器同时看三对桥臂,确认换向逻辑和PWM调制都符合预期,再接负载。

5.4 从电机到电源:PWM整流器是另一个"物种"

如果你是在做单相PWM整流器、单相全桥PWM整流这类电源方向,那PWM的频率选择逻辑又不一样了。这种场景里,PWM开关频率直接决定电感电流纹波和EMI,常见在10kHz到20kHz级别。Simulink做双闭环仿真时,PWM模块通常就是拿三角载波和调制波做比较,载波频率就是开关频率。

电流内环的PI参数和电压外环的带宽都要基于开关频率设计,开关频率定多少,直接决定电感选多大、谐波能不能过标准。这个话题可以单独写一篇,这里只提醒一点:电源类应用一定要把开关频率和器件损耗放在一起算,不是越高越好。你在仿真里把频率调高,波形漂亮了,但实际硬件里的MOS管会告诉你什么叫热崩溃。

6. PWM故障保护:为什么你的板子一堵转就会烧

6.1 软件保护为什么往往来不及

我见过很多开发板烧MOS,十有八九是电机堵转。转子卡死,电流从正常运行时的几百毫安飙升到几十安培,MOS管的结温在几十毫秒内就能突破极限。这个时候如果靠主循环里循环判断ADC值再关闭PWM,黄花菜都凉了。就算你用了中断,ADC采样启动到转换完成,再到CPU读寄存器和关输出,中间少说也有几十到上百微秒,在这种故障条件下足够把器件烧穿。

6.2 用高级定时器的刹车功能做硬件级保护

STM32高级定时器有个特性经常被忽略:刹车输入(BKIN)和主输出使能(MOE)。你可以把比较器输出或电机驱动芯片的故障信号引到BKIN引脚,当硬件电平触发刹车时,定时器会在一个时钟周期内强行关闭所有PWM输出,完全不走CPU。这个响应速度是软件中断比不了的。

HAL库里有专门的配置函数,关键配置项如下:

TIM_BreakDeadTimeConfigTypeDef sConfig = {0}; sConfig.OffStateRunMode = TIM_OSSR_DISABLE; sConfig.OffStateIDLEMode = TIM_OSSI_DISABLE; sConfig.BreakState = TIM_BREAK_ENABLE; sConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH; sConfig.BreakFilter = 0; HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sConfig);

具体成员名在不同系列HAL库里略有差异,以你用的头文件为准。BreakPolarity要和你故障信号的有效电平一致,弄反了会一上电就锁死输出。配置好后,故障信号一来,PWM立刻全灭,直到你清除触发条件并重新使能MOE。注意MOE被刹车后不会自己恢复,你需要在代码里显式调用恢复函数,这也是安全设计的一部分。

6.3 软件兜底和逐波限流

硬件刹车做第一层,软件层还是要做第二层。ADC那一路用来做过流预警,一旦电流超过阈值,除了关PWM,还要置一个故障标志、停止换向逻辑、通过串口上报状态。更讲究的做法是逐波限流:用比较器在每个PWM周期内实时限制电流峰值,超过阈值就在本周期内关断该路PWM,下一个周期再重新评估。

这样做的好处是,电机堵转时电流波形是"削顶"的,器件温度可控,不会立刻烧毁。逐波限流和软件关闭是不同的保护深度,做无刷驱动时,优先考虑前面那一层。不要指望一套软件逻辑包打天下,硬件保护永远要比软件快。

7. 跳出STM32:CCU6、FlexIO这类专用外设怎么玩PWM

7.1 英飞凌CCU6:电机控制的老牌选手

搜索词里出现"ccu6 pwm"的,多半在接触英飞凌的XC2000或者AURIX系列。CCU6的全称是捕获比较单元6,设计目标就是电机控制。它内部有T12和T13两个定时器组,T12可以产生三路带硬件死区的互补PWM,正好对应三相逆变器的三对桥臂;T13单独产生一路PWM,常常用来做PFC或额外的一路开关。

CCU6的调制模式里也有类似"模式0/模式1"的概念,理解了STM32的PWM模式1/2的逻辑之后,再看CCU6的调制模式文档就不会发怵。它的硬件死区、故障封锁脚(CTRAP)和刹车机制,本质上和STM32的TIM1/8是同一套思路:可靠性靠硬件,不靠CPU。

7.2 FlexIO(FXIO):灵活IO也能当PWM用

"fxio pwm"这个热词,很多情况下指的是NXP一些MCU上的FlexIO功能。FlexIO本来是个柔性串行外设,用来模拟UART、SPI、I2C这类通信协议,但它的定时器模块也可以配置成PWM输出。好处是引脚映射极其灵活,输出通道多,还能和DMA配合,做LED矩阵、单总线灯条这类需要多路脉宽信号的场景很方便。

用FlexIO生成PWM的坑在于配置项特别多,每个定时器引脚的模式都要从文档里一个个查。建议先跑官方的例程,确认能用再改参数,不要上来就自己从零配。和CCU6以及高级定时器对比起来,FlexIO更适合中等分辨率、多通道、IO需要灵活迁移的场景,而不是高可靠性电机驱动。

PWM外设怎么选,重点看这几个维度:

PWM外设常见平台典型特点
CCU6英飞凌XC2000/AURIX三路PWM加一路PFC、硬件死区、CTRAP故障封锁
FlexIO/FXIONXP及部分FPGA/MCUIO灵活、DMA联动、适合多路LED/灯条
TIM1/TIM8高级定时器STM32互补输出、硬件死区、BKIN刹车
通用定时器STM32单路或多路PWM,无互补,适合LED/简单驱动

7.3 PWM外设选型,重点看这几项

不管是CCU6、FlexIO还是STM32定时器,选型时抓住五个维度:通道数量够不够,有没有互补输出和死区,有没有硬件故障输入,定时器时钟和计数器位数能不能支持你的频率与分辨率,以及能不能和DMA联动批量更新多个通道。

最后一条很多人忽略:在需要同时更新三路占空比的无刷电机里,用DMA批量写比较寄存器,比逐路调用宏要高效得多,且时序更一致。说到底,生成PWM这件事,外设帮我们省下的每一微秒 CPU 时间,最后都会变成系统稳定性和流畅度上的回报。

说了这么多原理、配置和平台差异,最后讲个我自己的习惯:调PWM一定要养成看波形的习惯,示波器也好,逻辑分析仪也好。没有示波器的时候,我见过太多人靠猜调试——LED亮度不对就改占空比,电机啸叫就改频率,改来改去不知道哪一步起了作用。引脚上接个探头,频率、占空比、上升沿毛刺一眼看清,很多"玄学"问题当场破案。PWM生成并不难,难的是你愿意在动手之前想清楚它在你的系统里担任什么角色——是给眼睛看的,给耳朵听的,还是给电机和电源干重活的。想清楚这个,参数和配置自然就有方向了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询