☰
STM32定时器与PWM原理详解:从寄存器配置到常见坑
2026/10/24 6:56:48 网站建设 项目流程

前阵子帮一位刚入门的朋友排查一个问题:定时器配置得看起来没问题,启动函数也调了,但引脚上就是测不到PWM波形。我让他查一下芯片型号,然后提醒去看高级定时器里那个主输出使能位,问题三秒解决。这种"配了但没输出"的场景,在STM32定时器开发里太常见了。

这篇想跟你把STM32定时器与PWM原理从头到尾捋一遍。定时器是STM32里最常用的外设之一,PWM又是它最具代表性的应用形态。很多人用过PWM,但遇到频率偏差、占空比异常、波形不存在时,主要靠猜。我会从定时器家族的差异讲起,拆开时基单元的PSC、ARR、CNT三兄弟,再说明PWM的生成机制,最后给出从CubeMX到寄存器直写的完整配置流程,以及我实际踩过的一些坑。适合正在学嵌入式,或者准备用STM32做电机控制、电源、灯效驱动的朋友参考。

1. 定时器家族:先搞清自己手里有哪些资源

STM32的定时器不是"一个"外设,而是一整个家族。如果不先搞清楚手里的型号有哪些定时器,后面的很多问题都会让人摸不着头脑。

1.1 基本、通用、高级定时器的核心定位

以常见的F1和F4系列为例,定时器大致分成三类:

  • 基本定时器:一般叫TIM6、TIM7。它们只有时基单元,没有外部的输入输出引脚,也就是说不能直接产生PWM波形。它的主要工作是内部定时、产生更新中断,以及触发DAC转换。不少新手想在TIM6上输出PWM,翻遍引脚表找不到对应通道,就是因为没分清类型。

  • 通用定时器:一般叫TIM2、TIM3、TIM4、TIM5。这是最常用的一类。它们有时基单元,还有4个独立通道,支持PWM输出、输入捕获、输出比较,某些还可以做编码器接口。日常做LED呼吸灯、舵机控制、灯带调光,基本都是用它们。

  • 高级定时器:一般叫TIM1、TIM8。在通用定时器的基础上,多出死区发生器、互补输出、刹车输入等功能。这些是为电机控制、逆变器、开关电源这类场景准备的。比如电机驱动里上下桥臂的互补PWM,就必须靠高级定时器来做,不然外部电路要额外处理防直通问题,麻烦得多。

定时器类型时基单元输出通道PWM输出互补输出与死区典型用途
基本定时器有无不支持无定时中断、触发DAC
通用定时器有4路支持无LED控制、舵机、频率测量
高级定时器有4路(含互补)支持支持电机控制、逆变电源

1.2 定时器位数与时钟来源的隐藏差异

除了类型,还要注意位数。同一个系列里,不同定时器的计数寄存器位数可能不一样。部分型号的TIM2和TIM5是32位,ARR最大能到40亿级别;而TIM3、TIM4通常是16位,ARR最大65535。这个差异在低频场景下会直接决定你能不能一次配出目标周期。

另一个容易忽略的是定时器时钟来源。定时器并不一定直接吃系统主频。在F1里,APB1总线上挂着通用定时器和基本定时器,如果APB1分频器不为1,定时器时钟会自动变为APB1频率的2倍。比如系统72MHz,APB1分频2得到36MHz,但TIM2的时钟不是36MHz而是72MHz。很多人按36MHz去算PSC和ARR,结果频率偏差一倍,半天找不到原因。这个细节后面还会再提。

建议:动手前先看参考手册里"时钟树"章节,确认当前用到的定时器输入时钟到底是多少MHz,这比猜可靠得多。

2. 时基单元三兄弟:PSC、ARR、CNT怎么配合

不管什么类型的定时器,核心都是时基单元。它由三个关键寄存器组成:CNT计数器、PSC预分频器、ARR自动重装载寄存器。

2.1 预分频器PSC:为什么实际分频系数是PSC+1

预分频器的作用是把定时器输入时钟放慢,让计数器以一个更低的速度工作。很多人不理解为什么写PSC=71得到的是72分频,而不是71分频。因为预分频器的实现方式是"计数到PSC就清零并输出一个脉冲",也就是说它内部从0数到PSC,整整经历了PSC+1个输入时钟周期才输出一次。所以:

分频系数 = PSC + 1

举个例子,定时器输入时钟72MHz,写入PSC=71,计数器CNT得到的工作频率就是:

72MHz / (71+1) = 1MHz

也就是每隔1us,CNT加1。这个1us就是我们后续计算所有时间的基础。如果你想要计数器每0.5us跳动一次,那就得让分频后是2MHz,推回去就是PSC+1 = 36,PSC=35。

2.2 自动重装载寄存器ARR:溢出边界的意义

CNT从0开始,每来一个计数脉冲就加1,一直加到ARR,然后下一个脉冲到来时溢出归零,同时产生更新事件。注意关键点:不是数到ARR就结束,而是"CNT等于ARR后再来一个脉冲才产生更新",所以一个完整的计数周期是ARR+1个脉冲。

继续用上面的例子,计数频率1MHz,每个脉冲1us。如果ARR=999,那么CNT从0到999,数了1000次,溢出周期就是:

1000 × 1us = 1ms

也就是1kHz的更新频率。很多人想在定时器中断里做1ms节拍,就这么配。但写着写着会把ARR填成1000,想着"1000个1us就是1ms",实际却变成1001us,还带一个计数周期的误差。这个+1的细节在PWM频率计算里同样关键。

溢出频率公式可以统一写成:

溢出频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))

如果你做的是1ms节拍这种场景,直接用这个公式反推参数,比手动心算边界条件更不容易出错。

2.3 更新事件、中断和重复计数

CNT溢出时会产生更新事件UEV,同时更新影子寄存器。如果使能了更新中断,还会在溢出时进入中断回调。默认情况下,每个溢出周期都产生一次更新中断。

有些应用希望中断频率比PWM频率低一些,比如PWM频率做到20kHz,但只想每1ms进一次中断做控制计算。直接开更新中断意味着20kHz的中断频率,CPU负载很重。这时可以用高级定时器里的重复计数寄存器RCR,设置溢出N次之后才真正产生一次更新事件,操作灵活很多。普通通用定时器没有这个功能,需要降低中断频率就只能把ARR整体调大,或者另开一个定时器来控制节奏。

2.4 16位定时器的时间上限

16位定时器的ARR最大只有65535。假设计数频率1MHz,最大溢出周期是65.535ms。如果需要1秒级甚至分钟级的定时,单靠一个16位定时器直接配ARR是配不出来的。常见做法有三个:

  • 改用32位定时器,比如部分型号的TIM2;
  • 在更新中断里用软件计数器累计,中断里每进一次就加一次软件计数,比如累加到1000就代表1秒;
  • 用更低频的计数时钟,比如从外部引脚引入低频时钟源。

第三个方法在普通定时用途里用得不多,前两个足够覆盖大多场景。

3. PWM到底是怎么出来的:输出比较机制拆解

理解了时基单元,PWM就不再神秘。PWM的本质就是靠计数器CNT和一个比较寄存器CCR的持续比较,在比较结果翻转时改变引脚电平。

3.1 比较寄存器CCR 与输出引脚状态

每个PWM通道都有一个捕获/比较寄存器CCR。CNT不停累加,硬件持续把CNT和CCR做比较。在PWM模式1且向上计数时,规则是:

  • CNT < CCR:通道输出有效电平,默认是高电平;
  • CNT >= CCR:通道输出无效电平,默认是低电平。

于是每个溢出周期内,从0到CCR是有效,从CCR到ARR是无效,引脚上就出现一个方波。改变CCR就改变了高电平持续时间,也就是占空比。改变PSC或ARR就改变周期,也就是频率。

这里有个容易被忽略的"影子寄存器"概念。直接写CCR不会立即改变比较行为,除非关闭预装载功能。通常我们会开启CCR预装载,那么新值会在更新事件(溢出)时一次性生效,从而避免在波形周期中间出现毛刺。这个特性对电机和电源控制非常重要。

3.2 PWM模式1与模式2的差异

PWM模式1和模式2,可以简单理解为逻辑相反。

  • 模式1(向上计数):CNT < CCR时输出有效;
  • 模式2(向上计数):CNT >= CCR时输出有效。

如果固定用默认高电平有效来看,模式2输出的波形相位刚好和模式1相反。实际项目中如果发现波形对调了,检查两处:一个是CCMR里的OC1M位选的模式1还是模式2,另一个是CCER里的CC1P极性位。极性位CC1P可以再把输出整体翻转一次。总结一下就是:

模式决定比较逻辑,极性决定最终输出是否翻转。

很多初学者只注意到模式,忽略了极性位,导致高有效和低有效搞反,在控制舵机这种对脉宽有明确要求的场合会出问题。

3.3 频率和占空比的完整计算公式

PWM频率计算公式:

PWM频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))

占空比精确值是:

占空比 = CCR / (ARR + 1)

这个公式里又出现一次+1。很多人习惯写成CCR/ARR,当ARR足够大时误差很小,但较真起来,CCR=ARR时并不是100%占空比,而是99.9%。比如ARR=999, CCR=999,占空比是999/1000=99.9%。在绝大多数控制场景里这点差别无所谓,但如果做高精度电源,还是要按精确公式来。

举一个实际例子。目标是20kHz、50%占空比的PWM,定时器时钟72MHz。有两种配法:

方案A:PSC=71,得到计数频率1MHz;ARR=49,那么PWM频率 = 1MHz/50 = 20kHz。CCR取25,占空比 = 25/50 = 50%。这个方案计数精度低,50%对应CCR在25附近,每一步变化就是2%,如果要做精细调光就太粗了。

方案B:PSC=0,计数频率保持72MHz;ARR=3599,那么PWM频率 = 72MHz/3600 = 20kHz。CCR取1800,占空比 = 1800/3600 = 50%。这个方案里CCR每变化1,只改变约0.028%的占空比,调节精度高得多。

配置方案PSCARRCCR频率占空比调节精度
方案A71492520kHz约2%
方案B03599180020kHz约0.028%

选参数时,要在ARR位数和调节精度之间做权衡。方案B精度好,但ARR也不能无限大,毕竟16位定时器上限65535。如果你用的是16位定时器,又想从1Hz到20kHz大范围变频,就经常需要动态切换PSC或者接受频率分档变化。

3.4 高级定时器的死区与互补输出

高级定时器比通用定时器多出来的核心能力是互补输出和死区。互补输出的意思是同一组通道,比如CH1和CH1N,两个引脚输出逻辑相反的电平,正好用来驱动半桥电路里的上下两个功率管。

死区时间则是防止上下管同时导通的保险:在切换电平的瞬间,硬件会强制插入一段两边都关闭的时间。比如设置死区2us,那么下管关断之后要等2us,上管才会导通。死区太短有直通风险,太长会增加开关损耗,实际调节需要根据功率管的开关参数来确定。

死区时间由高级定时器的DTG寄存器配置,不同数值区间对应不同的计算方式,细节比较多。这里只要记住:做电机控制或电源逆变,互补输出和死区是不可替代的功能。如果你只是驱动一个LED或者舵机,用通用定时器就完全够了,别为了高级功能多交学费。

4. 实操配置:CubeMX配置和寄存器直写两条路

理论讲完,落到写代码。实际项目里两种方式都存在:CubeMX图形化配置加HAL库,以及纯寄存器直写。下面分别说明。

4.1 五分钟在CubeMX里生成可用工程

用CubeMX配置PWM,核心步骤可以概括为:

  • 选择一个定时器,比如TIM3,将Channel1设置为PWM Generation CH1;
  • 在Parameter Settings里配置Prescaler、Counter Period、Pulse,以及PWM Mode;
  • 确认引脚在复用功能状态,检查是否有冲突;
  • 生成代码后在主函数里调用启动函数。

生成代码后,典型的主函数代码是这样:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

这句调用必须执行,否则前面全部白配。之后需要改占空比时,使用HAL库提供的方式:

__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);

duty就是新的CCR值。这个值应保持在0到ARR之间。有的HAL库例子会直接写CCR,那也可以,但推荐用宏定义的方式,代码更清晰。

需要说明一个高级定时器的差异。使用TIM1或TIM8时,HAL_TIM_PWM_Start内部会把高级定时器的主输出使能位MOE一并打开,所以你在CubeMX里正常用HAL库,不会遇到"高级定时器没有输出"的问题。但如果你用寄存器方式直写,忘了置MOE,PWM就会一直沉默。这个坑后面还会细说。

4.2 寄存器直写:一次把关键位看明白

寄存器方式的套路并不复杂,关键是每一步知道自己在做什么。以F4系列、通用定时器某个通道输出PWM为例,核心流程如下:

先使能定时器和GPIO时钟,然后配置GPIO为复用功能,并设置复用映射,把引脚接到定时器的输出通道。接着配时基:

TIM3->PSC = 71; TIM3->ARR = 999; TIM3->CCR1 = 500;

配置PWM模式和通道输出使能:

TIM3->CCMR1 |= (0x6 << 4); // OC1M置110,选择PWM模式1 TIM3->CCMR1 |= TIM_CCMR1_OC1PE; // 开启输出比较预装载 TIM3->CCER |= TIM_CCER_CC1E; // 使能CH1输出

最后启动定时器:

TIM3->CR1 |= TIM_CR1_CEN;

如果是高级定时器TIM1或TIM8,还要多加一句,否则没波形:

TIM1->BDTR |= TIM_BDTR_MOE;

MOE全称Main Output Enable,也就是主输出使能。如果不置位,高级定时器的输出通道在外部看来是禁用的。这几乎是寄存器开发里最常见的一个低级失误,但造成的现象很迷惑,因为代码检查了好多遍都看不出问题。

还要提一下预装载。CCMR1里的OC1PE位如果置1,CCR1写入的新值会等到溢出时才生效;如果置0,写入立即生效。高精度控制通常开启预装载,避免波形畸变。

4.3 运行中动态改占空比与频率

PWM参数往往不是配置一次就完事,比如呼吸灯、调速、闭环控制都要求运行时改占空比。改占空比最简单:

__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, new_ccr);

改频率则意味着改ARR。但注意,只改ARR会导致占空比也跟着变。比如原来ARR=999, CCR=500,占空比50%;你把ARR改成1999,CCR还是500,占空比就掉到25%了。所以变频同时要按目标占空比同步改CCR。有些应用场景对占空比精度要求高,改频率时需要两个参数联动:

__HAL_TIM_SET_AUTORELOAD(&htim3, new_arr); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, new_arr * target_duty / 100);

另外,PSC也可以在运行时修改,但修改PSC一般会先触发一次更新事件,某些芯片上行为有差异,不如操作ARR那么直观,所以变频优先改ARR,避不开PSC时再查手册确认时序。

5. 常见问题与排查实录

这块内容全是实际操作中积累的,优先级很高。

5.1 PWM完全没有输出

我碰到最多的就是PWM没有一点波形。排查思路可以按这个顺序来:

  • 是否调用了HAL_TIM_PWM_Start。CubeMX生成代码后忘了启动函数,这种低级出错率远超想象;
  • 高级定时器的MOE是否使能。寄存器开发时尤其容易漏;
  • GPIO是否真的配置成了复用功能。F1和F4的GPIO配置方式不同,F1有独立的复用重映射寄存器,F4则是把引脚功能写进AFR寄存器,搞混了引脚自然没信号;
  • 引脚是否被其他外设占用。CubeMX的多引脚视图里如果出现红色冲突,就要先解决冲突;
  • 定时器时钟是否开启。查RCC对应总线上的使能位。

一个典型的排查方法是:先用代码把该引脚掰成普通推挽输出,拉高拉低,看示波器上电平是否变化。能变化就证明GPIO和引脚没问题,问题在定时器侧;不能变化就查GPIO配置和硬件连接。

5.2 频率偏差:从计算错误到晶振翻车

频率不对理论上大家都会套公式,但实际坑很多。

最大的坑是时钟源本身不对。某次调试时,外部晶振虚焊,系统没检测到HSE就自动切回了内部HSI,所有频率全偏。你按72MHz算参数,实际只有8MHz或16MHz,PWM频率偏差几倍都有可能。判断方法很直接:查看RCC时钟相关寄存器,确认外部晶振标志是否置位。像这种硬件底层的坑,只看应用代码永远看不出来。

第二个常见坑是PSC和ARR忘记加1。少写一个+1,频率误差在低频时尤其明显,比如1kHz目标变成999Hz或1001Hz。当你明显感觉频率不对但又不至于离谱时,优先检查这两个+1。

第三个坑是被APB预分频误导。前面提到的F1系列里TIMxCLK可能是APB1的两倍。如果直接用APB1频率计算,所有结果差一倍。参考手册或CubeMX的时钟配置图都能帮你确认实际时钟数值。

5.3 占空比异常:高电平、反相、突变

占空比不对主要有几类现象。

  • 输出一直为高或接近100%。最常见原因是CCR值超过ARR,或者CCR写成了ARR+1甚至更大。这时候CNT永远小于CCR,输出始终有效。反过来,如果CCR写0,输出几乎全程无效;
  • 波形反相。检查PWM模式1/2和极性位,前文讲过的两处配置别漏;
  • 波形中间出现毛刺或抖动。多半是CCR预装载没开,运行中直接改CCR导致的。解决方式是开启OC1PE,让新值在溢出时统一生效;
  • 多个通道互相干扰。检查每个通道的CCER使能位是否独立,以及是否错误复用了同一个引脚。

有些人的项目里更新中断和PWM共用同一个定时器,中断回调里执行了耗时的浮点运算,占空比更新出现明显延迟。这不算PWM本身的故障,但会影响实时性。更合理的做法是控制计算放到主循环或优先级更高的中断里,PWM的CCR只留最终结果。

5.4 没有示波器怎么调PWM

示波器是最直接的调试工具,但不是每个人都随时有。我分享几个替代手段。

  • 逻辑分析仪:多路、低价,测频率和占空比完全够用,只是看不到模拟噪声细节;
  • LED亮度观察:把LED串联电阻接在PWM输出上,肉眼观察亮度变化。调占空比时亮度跟着变,说明PWM基本正常。如果想让肉眼看得清楚,把PWM频率降到1kHz以下,LED闪烁会更明显;
  • 用ADC采集自己的PWM输出:设置RC低通滤波后接ADC,模拟一个简易DAC,ADC读数约等于占空比乘以参考电压。这个方法能定量验证占空比变化,只是精度有限;
  • 用定时器的输入捕获功能,把同一个引脚或另一个引脚回接到通道上,软件里读捕获值算占空比。这算是嵌入式工程师的自我验尸法,能不动外部设备就完成基本验证。

我个人的习惯是,调试PWM先看频率再看占空比。频率不对优先查时钟树和PSC、ARR;占空比不对优先查CCR边界、模式和极性。如果完全不输出,先检查启动函数、MOE和GPIO复用。这三个顺序用熟之后,大部分问题都能在十分钟内定位。

再分享一个小技巧:遇到"看起来一切正常但波形差一点"的时候,把ARR故意设成一个整数,比如9999,PWM频率设置音频范围以外的低频,然后用逻辑分析仪或示波器数波形,这样更容易直观判断误差来自哪里。

定时器和PWM这块,表面看就是配置几个寄存器,实际水挺深。我做电机控制项目时,后来养成了固定习惯:每次写配置前先在纸上写下目标频率、目标占空比、定时器输入时钟,再反推PSC、ARR、CCR三个数值,最后才落到代码里。这套流程帮我避开了大量低级错误。如果你在项目中也遇到过类似困惑,不妨试试同样的习惯,把数学关系先算清楚,再看波形时心里就有底了。

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

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

立即咨询