☰
STM32F103高级定时器PWM配置实战:TIM1/TIM8刹车与MOE详解
2026/10/17 10:02:02 网站建设 项目流程

搞PWM的时候,大多数人第一步想到的肯定是TIM2、TIM3这样的通用定时器,因为它们简单直观:ARR设个值,CCR给个占空比,引脚上就有波形了。可一旦换成STM32F103里的高级定时器(TIM1、TIM8),同一套思路往往就翻车——波形不出来、输出引脚一直是空闲电平、引脚发热,甚至还会误触发蜂鸣器或继电器。这篇内容就是围绕stm32F103高级定时器使用pwm功能时的配置来讲的,我会把自己压箱底的那套排查逻辑和寄存器配置流程全写出来。不是那种复制粘贴的“初始化代码+一句注释”,而是告诉你每一步为什么要这么写,哪些位必须置1,哪些寄存器的顺序不能乱,以及那些元器件手册里不会写明、但实测一定会遇到的坑。适合刚接触高级定时器的朋友,也适合已经被TIM1的刹车逻辑折磨过一晚上、想彻底搞明白原理的人。

1. 高级定时器不是“多几个通道”的普通定时器

1.1 它真正多出来的,是一套“刹车”逻辑

我最早接触高级定时器的时候,也觉得TIM1和TIM3应该差不多,无非通道多一点。但看完参考手册的寄存器列表就明白了,高级定时器多了一整个BDTR寄存器,也就是“刹车和死区控制”。这玩意在通用定时器上根本不存在,但恰恰是它决定了PWM信号能不能从引脚上顺利发出去。BDTR里的最高位就是MOE,主输出使能。MOE等于1的时候,PWM输出才真正有效;如果MOE等于0,不管你把CCR设成多少、定时器有没有启动,引脚都会保持空闲电平。这个特性本身是为了配合电机驱动器做的安全设计:一旦刹车输入有效或发生故障,硬件立刻把输出封锁掉,防止炸管子。但也正是这个机制,害得无数人把高级定时器当普通定时器用的时候,莫名其妙没有输出。我见过一些工程,GPIO、RCC、ARR、CCR全配对了,波形就是不出来,最后发现是漏掉了TIM_CtrlPWMOutputs或者直接写BDTR使能MOE这一步。

1.2 正弦波也好、方波也好,先想清楚“谁在控制引脚”

普通定时器的PWM,输出引脚电平基本由比较结果决定,高级定时器则在中间插入了“主输出控制”环节。我习惯画一条链路:计数器CNT和比较寄存器CCR比较,产生OCxREF信号,然后OCxREF经过极性选择、死区控制、空闲电平控制,最后才到达引脚。路上任何一个环节被封锁,引脚就纹丝不动。具体到寄存器上,这一步由CCER和BDTR协同完成。CCER决定通道输出使能和极性,BDTR里的MOE决定主输出有没有真正放行,DTG位段决定死区时间,BKE和AOE决定刹车后是锁死还是自动恢复。这条逻辑链不是靠记住某个函数就能掌握的,而是要能对着参考手册的框图走一遍,调试才不会像无头苍蝇一样乱猜。通用定时器的输出链路短,出问题很容易看出来;高级定时器中间环节多,一条链路走不通,波形就出不来。所以这篇文章的中心思想很简单:如果你想用高级定时器做PWM,就不要拿通用定时器的思路硬套,先把“哪里会切断输出”搞清楚。

2. 算清PWM频率和占空比,再考虑怎么写代码

2.1 定时器时钟从哪来:先过系统时钟与APB2预分频这一关

STM32F103上,TIM1和TIM8挂在APB2总线上。很多教程默认系统主频是72MHz,APB2预分频为1,此时定时器时钟就是72MHz。这个结论看着简单,实际工程里经常出问题:有人用了库函数初始化时钟,把APB2预分频器设成2或者更低,高级定时器时钟就掉下来了,PWM频率也跟着变化。我建议每一位做定时器的人先把时钟树图纸翻出来对照一遍,画一条从外部晶振到APB2预分频器、再到定时器时钟的路径,确认每个环节的分频比。在F103上,APB2定时器的时钟通常要么等于系统主频,要么等于系统主频的一半,取决于APB2预分频设置。拿到这个数之后,把它代入PWM频率公式f = f_timer / ((PSC+1) * (ARR+1)),才能算出你真正想要的周期。如果连定时器时钟都没搞明白,后面所有数字都是瞎蒙,波形要么差0.1%无所谓,要么差得离谱甚至无法触发外设。

2.2 一个实用的参数组合:20kHz载波频率的两个算例

假设系统主频72MHz,高级定时器时钟72MHz,目标PWM频率20kHz。先按最简单的情况来:PSC=0,那么ARR+1 = 72MHz / 20kHz = 3600,所以ARR=3599。此时占空比调节步长是1/3600,也就是0.028%左右,非常细,驱动电机、控制LED亮度都够用。如果不想ARR这么长,那可以抬高PSC,比如PSC=71,则ARR+1 = 72MHz / ((71+1)*20kHz) = 50,ARR=49。这样频率依然是20kHz,但占空比调节步长一下子变成了2%。用在电调或调光上,2%的步长会让人觉得亮度变化“一跳一跳”的。两种组合都是20kHz,但实际体验差异巨大。这就是为什么我从来不只是给别人一个“ARR=3599,PSC=0”的万能答案,而是要告诉他们这个数字背后的分辨率概念:ARR越长,占空比调节越细腻,但ARR也受到16位计数器上限的约束,最大65535,所以最低PWM频率不是无限低的。

2.3 别把分辨率算成占空比精度

我再多说一个容易犯的错:有人把PSC + ARR的组合等同于占空比精度,其实不是。占空比精度由CCR的步进决定,具体是1/(ARR+1),而ARR本身是由频率倒推出来的。换句话说,频率定了,ARR基本就定了一大半,你不能既想要低载波频率、又想要千分之一的占空比步长,除非换更高分辨率的定时器。高级定时器的计数器只有16位,这一物理限制绕不过去。有些应用真的需要极慢PWM且极细腻的占空比调节,那就得考虑低主频方向或者用软件扩展,但那是另一个话题了。就stm32F103高级定时器使用pwm功能时的配置而言,通常20kHz到30kHz的载波搭配ARR在3000到4000之间,是比较舒服的区间,兼顾了调节精度和计数负载。如果PWM频率到100kHz以上,ARR只剩719,占空比步长就变成0.14%,对很多开环控制也凑合,但闭环就得掂量一下了。

3. 寄存器版最小配置:从时钟到引脚一次走通

3.1 开时钟、配引脚:先让复用功能“不打架”

我不太喜欢上来就贴一堆标准外设库的初始化代码,因为库函数封装太厚了,出问题反而不好定位。这里我用寄存器方式走一遍最小配置,并且把所有关键点讲清楚。第一步是两个时钟使能位:开启GPIO所属端口时钟,以及开启TIM1或TIM8的时钟。如果TIM1的某个通道要用到默认引脚PA8、PA9、PA10、PA11等,这些引脚都属于GPIOA,就要确保GPIOA的时钟打开了。如果做的是端口重映射,比如把TIM1的部分通道映射到其他引脚,那还要打开AFIO时钟并配置重映射寄存器。多数只使用默认引脚的简单项目可以不用AFIO,但如果你发现引脚功能死活用不了,回头查一下是不是重映射或者复用时钟没开,排错时这一条价值很大。然后是GPIO模式:高级定时器PWM输出引脚必须配置为复用推挽输出,也就是AF_PP,一般还要配合50MHz的输出速度。谁要是把引脚误配成了模拟输入或普通推挽输出,波形一样出不来,或者输出非常奇怪。

3.2 建周期、定占空比:PSC、ARR、CCR1的写入顺序

最小配置可以用下面这段伪代码来表达,注释里我把每个寄存器的作用写清楚:

// 1. 打开时钟:TIM1和GPIOA RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_TIM1EN; // 2. PA8配置为复用推挽输出,50MHz // 注意PA8在GPIOA的CRH寄存器第0个字段,也就是bit0~bit3 GPIOA->CRH &= ~(0x0FUL << (0 * 4)); GPIOA->CRH |= 0x0BUL << (0 * 4); // CNF=10(复用推挽), MODE=11(50MHz) // 3. 时基配置:20kHz,PSC=0,ARR=3599 TIM1->CR1 = 0; TIM1->PSC = 0; TIM1->ARR = 3599; TIM1->CNT = 0; // 4. 通道1配置为PWM模式1,开启预装载 TIM1->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // 5. 设置占空比:CCR1/3600,这里设为50% TIM1->CCR1 = 1800; // 6. 使能通道1输出,高电平有效 TIM1->CCER |= TIM_CCER_CC1E; // 7. 关键一步:主输出使能 TIM1->BDTR |= TIM_BDTR_MOE; // 8. 启动计数器 TIM1->CR1 |= TIM_CR1_CEN;

有一个容易被忽略的细节是预装载位。CCMR1里的OC1PE一旦置1,那么CCR1和ARR的值不会立刻生效,而是等到更新事件才一次性更新,这样可以在运行中改变频率和占空比而不产生毛刺。这个机制在步进电机、舵机控制里非常重要,因为你不能接受波形中间突然出现一个畸形脉冲。如果你的系统不关心动态修改参数,只上电时给一个固定占空比,那预装载位不是强制要求的,但我强烈建议一开始就养成开预装载的习惯,反正没有坏处。另外,写入顺序上有一点要提醒:使能通道输出(CCER)和开主输出(BDTR.MOE)最好在配置完CCMR之后再写,否则引脚可能先出现一个错误的空闲电平,虽然最终波形会恢复正常,但某些接在引脚上的负载可能已经被这个短暂异常信号触动了。

3.3 启动序列:先开主输出,再使能计数器

这段代码的第8步是最后启动计数器,但实际调试的时候,我更推荐一个更保守的启动顺序:先把BDTR里的MOE置1,再写CR1的CEN。为什么?因为高级定时器的输出行为在计数器没启动时会处于空闲状态,如果你先启动了计数器再打开MOE,硬件在极短时间里可能先产生一个没有主输出控制的中间状态。多数情况下无伤大雅,但在控制IGBT、MOSFET的半桥电路里,这种毫秒级的毛刺足以炸管。我的习惯是把它做成一个固定序列:“时基配置→通道配置→占空比写入→CCER输出使能→BDTR主输出使能→CR1启动”,顺序严谨,每次都不变。这样无论逻辑上还是时序上,都不会出现输出未受控的空窗期。如果你用的是标准外设库,老版本的库函数里有一个TIM_CtrlPWMOutputs,它本质上就是操作BDTR里的MOE位,别漏掉就行。如果你用的是HAL库,HAL_TIM_PWM_Start内部也会处理MOE,但前提是你先初始化好了时基和通道,没有初始化到位,HAL也救不了你。

4. 引脚上没波形?第一件事查“主输出使能”

4.1 排查顺序:与其乱改寄存器,不如从头走一遍

高级定时器PWM不出波形,这是最高频的问题,没有之一。我调试过很多次这类问题,也接受过不少网友的提问,总结下来,最有效的做法不是直接从中间某个寄存器开始猜,而是从时钟到引脚完整过一遍。第一步,用调试器或串口打印确认RCC寄存器里的TIM1时钟和GPIO时钟确实勾上了。第二步,确认GPIO的CRH或者CRL里,引脚模式是不是复用推挽,有没有被其他外设配置覆盖。第三步,看TIM1的CR1寄存器,CEN位是否置1,CNT是否在跑。第四步,看CCMR1的OC1M位段是不是PWM模式,是不是011(模式2)或110(模式1),很多库函数示例写的是模式2,如果你抄的是模式2也没问题,但要知道两者输出极性相反。第五步,看CCER的CC1E,如果用的是互补通道还要看CC1NE。第六步,看BDTR寄存器的MOE位。这一步我放在最后,并不是因为它不重要,而是因为它最容易被忽视,所以我反而会建议你最开始就直接看这一位,一旦MOE为0,后面几项全都不用查了,输出肯定没有。这个排查思路适用于TIM1,也适用于TIM8,因为两者的高级功能结构基本一致。

4.2 刹车状态一直为忙,多半是复位电压或配置遗漏

有时候你会发现MOE位即使写了1,读取后它又变回0,或者波形出来一瞬间又没了。这种诡异现象,十有八九是刹车输入在捣乱。F103高级定时器的刹车输入可以来自引脚,也可以来自内部比较器输出,一旦刹车有效,硬件会自动把MOE清掉,输出被强制屏蔽。很多人的刹车引脚悬空或者被PCB上其他信号干扰,比如电机启停瞬间的噪声,结果PWM刚输出就立刻被砍掉了。解决方法是先看BDTR里的BKE位和BKP位。如果你压根不需要硬件刹车功能,最简单的做法是把BKE设为0,这样外部刹车引脚的变化不会影响MOE。如果你确实需要刹车,那就要确认刹车极性配置对不对,还要注意刹车信号的电平保持时间。另外,AOE位决定了刹车解除后输出是否自动恢复:AOE=0时刹车事件后需要软件重新写MOE;AOE=1时,下一个更新事件会自动恢复。这个细节在做安全保护时特别重要,因为有些应用期望刹车后“锁死”,有些应用期望故障消失后自动恢复,两者用到的配置完全不同。

4.3 MOE、空闲电平和极性组合,容易把人绕晕

还有一个不太容易想明白的问题:为什么MOE位明明是1,引脚上还是高电平或低电平不变?这就要理解“空闲状态”这个设计了。BDTR寄存器里有控制空闲电平的配置,对应CCER里的CC1P极性位,也和PWM模式有关。比如PWM模式1下,OCxREF高电平表示比较值大于计数值,低电平表示小于。如果你把极性配反了,示波器上看到的占空比正好是“反的”,比如你期望20%占空比,实际看到80%。这不是高级定时器特有的问题,但高级定时器的空闲电平逻辑会让它显得更复杂。我调试时最常用的技巧是把CCR设成中间值,比如ARR=3599时把CCR设为1800,然后看输出是高是低、占空比是不是50%,如果不用占空比计都能正常,那基本可以排除输出链路问题,剩下就是计算和逻辑层面的事了。反之如果波形完全没有,则重点查MOE和刹车。

5. 高级定时器的真功夫:互补输出、死区和刹车控制

5.1 从IO配置到CCER,互补通道不是“多接一根线”这么简单

说到互补输出,这是高级定时器区别于通用定时器的“招牌功能”。TIM1和TIM8不仅能输出一路PWM,还能同时输出它的反相信号,并且两个信号之间可以插入死区。做半桥或全桥驱动时,上下桥臂的开关管不能同时导通,否则电源直接短路,靠的就是这一对互补信号加死区来保证安全性。配置互补输出时,要在CCER里同时打开CC1E和CC1NE,前者使能主通道,后者使能互补通道。引脚需要选两路,比如TIM1_CH1和TIM1_CH1N都配置成复用推挽。很多人的误区是:只开了CC1NE却忘了开CC1E。虽然在部分情况下硬件也能输出互补信号,但逻辑不符合预期。更稳妥的做法是两条通道一起开,再检查BDTR里的死区时间对不对。调试时用示波器的双通道同时测量CH1和CH1N,放大波形看它们的交叠区,如果发现两个信号有一瞬间同时为高,那就说明死区时间设小了,必须加大。

5.2 死区时间如何根据DTG计算,以及什么时候需要它

死区时间的计算是高级定时器配置里让人最头疼的部分,因为DTG位段不是简单的线性关系。F103参考手册给出的规则是分段的,如果用tDTS表示定时器时钟周期,在72MHz主频下tDTS = 1/72MHz ≈ 13.89ns,那么当DTG的bit7为0时,死区时间等于DTG[6:0] × tDTS,范围大约是0到1.7us左右。如果bit7为1,又有第二个公式,需要把DTG的低几位乘上2、4、8甚至16再乘以tDTS,覆盖更长的死区范围。比如想要接近1us的死区,可以用DTG=0x48,此时bit7为0,DTG[6:0]=0x48=72,死区时间就是72 × 13.89ns ≈ 1us。如果你需要一个完全不依赖计算、只求安全的值,那可以把DTG直接调到略大于手册建议的最小值,然后用示波器实测确认。关于死区时间要不要设,我的建议是:只要你的PWM信号要去驱动半桥、全桥、电机驱动芯片或任何功率开关,就一定要设。死区越小,输出效率越高,但风险也越大;死区越大,越安全,但波形会有一段双方都关闭的时间。工程上先从保守值开始,比如1us到3us,实测发热和波形之后再逐步收紧。没有示波器就别瞎调死区,因为你根本看不到两个信号是否交叠,风险不可控。

5.3 用刹车输入做硬件保护,别把希望全押在中断里

刹车输入是高级定时器里最有价值的安全功能,很多人却把它当成一个无用的附加功能。它的价值在于纯硬件响应:只要刹车信号有效,主输出立即被封锁,不需要CPU断点、不需要中断服务程序、不需要花时间执行指令。对于电机驱动、逆变器这类一旦故障就会瞬间损坏硬件的场景,这种硬件级保护比任何软件逻辑都可靠。实际配置时,BKE位使能刹车输入,BKP位选择有效电平,然后可以选择是否打开自动恢复AOE。之后把需要接入的故障信号送到对应的刹车引脚,电平一跳变,输出立刻被硬件封锁。这里有个细节:刹车事件产生后,即便你没开中断,也可以通过读取BDTR寄存器里的状态位来知道发生过刹车。调试时如果你想确认刹车功能是否正常,可以手动把刹车引脚拉到有效电平,看看MOE会不会被清零,如果能被强制清零,说明刹车链路工作正常。我通常会在初始化代码里增加一个短暂的自检:先手动触发一次刹车,延时几十毫秒后再恢复,确保保护链路在上电时就是可用的。这个习惯帮我在实际现场项目里挽回了很多次可能的炸管事故。

6. 我踩过几个月的坑,一次说完

6.1 占空比到了99%之后,“看起来”什么都没发生

早些年在做一个模拟项目X的时候,我需要用高级定时器输出占空比可调的PWM去控制一个比例阀。一切配置正常,占空比从0调到99%都很顺手,但调到“100%”准备做满量程标定时,输出突然变成了常高电平,但阀门却没有到达期望的位置。我一开始以为是占空比计算不对,后来才意识到,在边沿对齐模式下,CCR最大值最多只能等于ARR,此时的比较输出在绝大多数周期内都是高,但严格说起来它并不是真正的100%,因为比较事件还是会在边界产生一个极窄的低脉冲。对于比例阀、电机驱动这类惯性负载,这个极窄脉冲可能无感,但对于采样要求苛刻的传感器或通信电路,这个毛刺会引入干扰。解决办法很直接:程序里做饱和处理,把CCR限制在ARR以内,并且不要迷信“CCR=ARR就等于100%”这种说法。如果你真的需要完全恒高电平,那就不应该依赖PWM通道,而是直接把引脚拉到有效电平。

6.2 死在互补PWM中心对齐模式下的小问题

还有一次在客户现场,我用TIM8做三相逆变器的PWM,中心对齐模式,互补输出加死区。现场反馈说电机低频时震动明显,我拿示波器一看,波形的上升沿和下降沿总有非常对称的毛刺,频率正好是载波的两倍。排查很久才发现是我把占空比的预期值算错了半天:中心对齐模式下,计数器是从0向上数到ARR,再向下数到0,整个PWM周期包含两次比较事件,所以真正意义上的PWM频率其实变成了定时器溢出频率的两倍,而且比较时刻在波形两侧都有。这个和边沿对齐不一样。如果驱动芯片对这个时序有特定要求,像我这样直接照搬边沿对齐的代码就是埋雷。那次之后我做了一套自定义的宏,把边沿对齐和中心对齐分开封装,频率、占空比、死区各算各的,再也没出过这种低级错误。中心对齐模式本身不是坏东西,它让波形对称性更好,也方便跟电流采样点对齐,但前提是你真的理解它的工作方式。

6.3 经验检查表:写在纸上也比出问题时拍脑袋强

下面这份检查表是我后来固定使用的,每次新板子调试高级定时器PWM前,我都会照着过一遍,也建议你存在工程注释里,方便下次排错:

  • [ ] RCC时钟:TIM1/TIM8和所用GPIO口时钟是否都开启了
  • [ ] GPIO模式:输出引脚是否复用推挽,速度设置是否合理
  • [ ] 时基参数:PSC、ARR、CNT是否按计算值写入,有没有溢出风险
  • [ ] 比较通道:CCMR中的OC1M选的是模式1还是模式2,极性是否符合预期
  • [ ] 通道使能:CCER中对应主通道和互补通道是否都打开
  • [ ] 主输出使能:BDTR中的MOE是否置1,有没有被刹车意外清零
  • [ ] 刹车配置:BKE不用时是否关闭,用的时候极性和触发电平是否匹配
  • [ ] 死区时间:DTG值是否按tDTS换算过,实测波形有没有交叠
  • [ ] 启动顺序:先配置完所有寄存器和输出链路,最后启动计数器

这上面每一项我都出过问题,没有哪一项是多余的。尤其是MOE和刹车这两项,只要有一项没查,示波器上就极有可能是平的,或者波形闪一下就没了。你可能会觉得这个检查表太基础,但恰恰是这些基础项,决定了整个高级定时器PWM功能是否可靠。

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

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

立即咨询