news 2026/10/19 1:46:03

STM32F103定时器寄存器深度解析:从计数原理到PWM与输入捕获实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103定时器寄存器深度解析:从计数原理到PWM与输入捕获实战

1. 为什么第八章值得单独拎出来讲

搞STM32F103的人,前面几章大概率已经把GPIO、时钟树、中断向量表这些啃得差不多了。到了第八章,很多人会突然卡住——因为这一章开始碰的是定时器。不是那种“配个延时”的简单活儿,而是从基本定时器到通用定时器、高级定时器,从计数模式到PWM输出、输入捕获、编码器接口,一整套东西全砸过来。

我见过太多人学到这一章就开始抄代码,能跑就行,问他为什么这么配,答不上来。结果换个需求,比如要输出一个占空比可调、频率可变的PWM,或者要测一个外部脉冲的宽度,立马抓瞎。所以第八章我打算用“白话”的方式,把寄存器层面的逻辑掰开揉碎讲清楚。你不需要死记硬背每个位的定义,但得明白为什么这个位要这么设,以及不这么设会出什么问题。

这一章的核心就一句话:STM32F103的定时器本质上是一个带预分频器的计数器,所有花里胡哨的功能都是围绕这个计数器衍生出来的。你把这句话吃透了,后面什么PWM、输入捕获、输出比较,全都是同一个底层逻辑的不同用法。适合谁看?适合已经能点灯、能配中断,但一看到TIMx_CR1、TIMx_CCMR1这些寄存器就头大的嵌入式初学者,也适合工作几年但一直用库函数、想回头补寄存器底子的人。

2. 定时器的整体架构与核心思路拆解

2.1 三个定时器家族的分工逻辑

STM32F103的定时器不是一种,而是分了三档:基本定时器(TIM6/TIM7)、通用定时器(TIM2~TIM5)、高级定时器(TIM1/TIM8)。很多人一开始不理解为什么要分这么细,其实从寄存器角度看,就是功能模块的堆叠。

基本定时器最简单,只有一个16位计数器、一个预分频器、一个自动重装载寄存器,主要用来做时基,比如给DAC触发或者做简单的定时中断。它没有外部引脚,不能输出PWM,也不能捕获外部信号。你可以把它理解成一个“纯内部计时器”。

通用定时器在基本定时器的基础上,加了四个独立通道,每个通道可以配置成输出比较、PWM输出、输入捕获、输出比较单脉冲模式。还多了编码器接口、霍尔传感器接口。TIM2~TIM5都是这一类,其中TIM2和TIM5是32位计数器,TIM3和TIM4是16位。注意,32位和16位在寄存器操作上有个坑:32位的TIM2/TIM5在读写CNT寄存器时,需要分两次读或者用特定顺序,否则可能读到不一致的值。这个后面会细说。

高级定时器TIM1和TIM8在通用定时器基础上,又加了互补输出、死区插入、刹车输入、重复计数器。这些功能是给电机控制、数字电源这类场景准备的。比如你驱动一个三相无刷电机,需要三对互补PWM,还要防止上下桥臂直通,死区时间就是干这个的。重复计数器RCR则允许你设置每N个计数周期才产生一次更新事件,这在做高频PWM时能减轻CPU中断负担。

从寄存器映射上看,基本定时器只有CR1、CR2、DIER、SR、EGR、CNT、PSC、ARR这几个。通用定时器在此基础上多了CCMR1/2、CCER、CCR1~4、SMCR。高级定时器再多BDTR、RCR、CCR5/6(用于互补通道的额外比较)。所以你学的时候,先把基本定时器的寄存器吃透,后面每加一个功能,就是多操作几个寄存器而已。

2.2 预分频器与自动重装载的配合逻辑

这是第八章最核心的一个计算,也是很多人配错的地方。STM32F103的定时器时钟源通常来自APB1或APB2。注意一个细节:当APB预分频系数为1时,定时器时钟等于APB时钟;当APB预分频系数大于1时,定时器时钟等于APB时钟的2倍。这个规则在参考手册的时钟树章节有写,但很多人忽略。

假设系统时钟72MHz,APB1预分频系数为2,那么APB1时钟是36MHz,但TIM2~TIM7的时钟是72MHz。APB2预分频系数为1,那么TIM1和TIM8的时钟就是72MHz。这个点如果不搞清楚,你算出来的定时时间会差一倍。

定时器的计数频率公式是:

计数频率 = 定时器时钟 / (PSC + 1)

其中PSC是预分频寄存器的值,实际分频系数是PSC+1。比如PSC=71,那么分频系数是72,72MHz/72=1MHz,也就是计数器每1微秒加一。

然后定时周期公式是:

定时周期 = (ARR + 1) / 计数频率

ARR是自动重装载寄存器的值。比如ARR=999,计数频率1MHz,那么定时周期就是1000微秒,也就是1毫秒。这里注意,ARR+1是因为计数器从0计到ARR,一共ARR+1个计数。

所以完整的定时时间公式是:

定时时间 = (ARR + 1) * (PSC + 1) / 定时器时钟频率

这个公式你最好记在脑子里,因为后面配PWM频率、输入捕获超时,全都要用。我见过有人把PSC和ARR搞反,结果定时时间差了十万八千里。还有个坑:PSC和ARR寄存器都有影子寄存器,你写入的值不会立即生效,而是在下一个更新事件时才被加载到实际寄存器。如果你在运行中修改PSC或ARR,需要手动产生一个更新事件(置位EGR的UG位)才能立即生效。这个在动态调频调占空比时特别重要。

2.3 计数模式的选择与适用场景

STM32F103的通用定时器支持三种计数模式:向上计数、向下计数、中央对齐(向上/向下交替)。基本定时器只支持向上计数。

向上计数是最常用的,计数器从0加到ARR,溢出后回到0,产生更新事件。向下计数是从ARR减到0,下溢后回到ARR,产生更新事件。中央对齐模式则是先向上到ARR-1,再向下到0,如此反复。中央对齐模式主要用于电机控制中的PWM生成,因为它的输出波形是对称的,能减少谐波。

在寄存器层面,计数模式由CR1的CMS位和DIR位控制。CMS[1:0]选择中央对齐模式,DIR选择向上或向下。注意,在中央对齐模式下,DIR位是只读的,由硬件自动切换。如果你试图写DIR,会被忽略。

这里有个实操心得:如果你只是做普通的定时中断,用向上计数就够了,别没事儿选中央对齐,因为中央对齐的更新事件频率是向上计数的两倍,中断里处理的东西会翻倍。我早期做的一个项目,用中央对齐模式做1ms定时,结果中断频率变成2kHz,CPU负载直接上去了,后来改成向上计数才降下来。

3. 核心寄存器逐位拆解与实操要点

3.1 CR1控制寄存器的关键位

CR1是定时器最核心的控制寄存器,位定义如下(只挑最关键的几个):

  • CEN(位0):计数器使能。置1开始计数,清0停止。这个位在初始化时最后置1,避免配置过程中计数器乱跑。
  • UDIS(位1):更新禁止。置1时禁止更新事件产生,但影子寄存器仍然会被加载。这个位用得少,一般保持0。
  • URS(位2):更新请求源。置1时只有计数器溢出/下溢才产生更新中断,而软件置位UG不产生中断。这个在需要手动更新影子寄存器但不想触发中断时很有用。
  • OPM(位3):单脉冲模式。置1时,计数器在下一个更新事件后自动停止。做单次延时很方便。
  • DIR(位4):计数方向。0向上,1向下。中央对齐模式下只读。
  • CMS(位6:5):中央对齐模式选择。00边沿对齐,01/10/11分别对应不同的中央对齐模式。
  • ARPE(位7):自动重装载预装载使能。置1时ARR有影子寄存器,更新事件时才加载;置0时ARR写入立即生效。这个位很关键,做PWM动态调频时,如果ARPE=0,你改ARR会立即影响当前周期,可能导致波形畸变;如果ARPE=1,则在下个周期生效,波形平滑。
  • CKD(位9:8):时钟分频。用于给输入捕获做数字滤波,一般设00。

实操中,我通常这样配:CR1 = 0x0000,然后根据需要置CEN、ARPE、CMS。注意,不要一上来就置CEN,先把PSC、ARR、CCMR、CCER都配好,最后再置CEN。

3.2 CCMR1/CCMR2通道模式寄存器

这两个寄存器控制四个通道的模式,每个通道占8位。以CCMR1为例,低8位控制通道1,高8位控制通道2。关键位:

  • CCxS(位1:0):通道方向。00输出,01输入(IC1映射到TI1),10输入(IC1映射到TI2),11输入(IC1映射到TRC)。做PWM输出时设00。
  • OCxM(位6:4):输出比较模式。110是PWM模式1,111是PWM模式2。PWM模式1是计数器小于CCR时输出有效电平,模式2相反。
  • OCxPE(位3):输出比较预装载使能。置1时CCR有影子寄存器,更新事件时加载。做PWM时建议置1,避免改占空比时出现毛刺。
  • OCxFE(位2):输出比较快速使能。一般不用。

这里有个坑:CCMR1的位定义在输出模式和输入模式下是不同的。比如位3在输出模式下是OCxPE,在输入模式下是ICxPSC。所以配置前一定要先确定通道方向,再按对应模式写位。我见过有人把输入捕获配成输出模式,结果死活捕获不到,查了半天才发现CCxS没设对。

3.3 CCER捕获/比较使能寄存器

CCER控制每个通道的使能和极性:

  • CCxE(位0/4/8/12):通道使能。输出模式下必须置1才能输出到引脚。
  • CCxP(位1/5/9/13):极性。0高电平有效,1低电平有效。做PWM时,如果你希望占空比表示高电平时间,就设0;如果驱动的是低电平有效的LED,就设1。
  • CCxNE(位2/6/10/14):互补输出使能。只有高级定时器有。
  • CCxNP(位3/7/11/15):互补输出极性。

注意,CCxE和CCxNE不能同时为0,否则输出引脚会处于高阻态。高级定时器还要配BDTR的MOE位(主输出使能),否则即使CCxE置1,引脚也不输出。这个MOE位是高级定时器独有的坑,很多人配完TIM1的PWM发现没波形,就是忘了置MOE。

3.4 预分频器与自动重装载寄存器的写入顺序

PSC和ARR都是16位寄存器,但写入时要注意:先写PSC,再写ARR,最后产生更新事件。因为PSC的影子寄存器在更新事件时加载,如果你先写ARR再写PSC,而ARR又比较小,可能在PSC生效前就产生溢出,导致第一个周期时间不对。

标准流程是:

TIMx->PSC = psc_value; TIMx->ARR = arr_value; TIMx->EGR |= TIM_EGR_UG; // 产生更新事件,加载影子寄存器 TIMx->SR &= ~TIM_SR_UIF; // 清除更新标志,避免立即进中断

注意最后一步清标志,因为置UG会同时置位UIF,如果你开了更新中断,会立刻进一次中断。这个在初始化时如果不处理,会导致第一次中断提前到来。

4. PWM输出与输入捕获的完整实操流程

4.1 输出可调频率和占空比的PWM

假设用TIM3的通道1(PA6)输出PWM,要求频率1kHz,占空比50%,系统时钟72MHz,APB1预分频系数为2(所以TIM3时钟72MHz)。

计算过程:

  • 目标频率1kHz,周期1ms。
  • 取计数频率1MHz,则PSC = 72MHz/1MHz - 1 = 71。
  • 周期1ms,计数1000次,ARR = 1000 - 1 = 999。
  • 占空比50%,CCR1 = (999+1) * 50% = 500。

配置步骤:

  1. 使能TIM3时钟:RCC->APB1ENR |= RCC_APB1ENR_TIM3EN。
  2. 配置PA6为复用推挽输出:GPIOA->CRL,对应位设0xB(复用推挽,50MHz)。
  3. 配TIM3:
    TIM3->PSC = 71; TIM3->ARR = 999; TIM3->CCR1 = 500; TIM3->CCMR1 = (6<<4) | (1<<3); // PWM模式1,预装载使能 TIM3->CCER = (1<<0); // 通道1使能,高电平有效 TIM3->CR1 = (1<<7) | (1<<0); // ARPE使能,CEN使能 TIM3->EGR = (1<<0); // 产生更新事件 TIM3->SR &= ~(1<<0); // 清更新标志

实测下来,PA6会输出1kHz、50%占空比的方波。如果你想动态改占空比,直接写TIM3->CCR1即可,因为OCxPE=1,新值会在下个更新事件生效,波形不会畸变。如果想动态改频率,改ARR和PSC,但注意PSC改完也要产生更新事件才生效。

注意:改ARR时如果ARPE=0,会立即生效,可能导致当前周期被截断。所以做动态调频时,ARPE一定要置1。

4.2 输入捕获测脉冲宽度

用TIM4的通道1(PB6)捕获一个外部脉冲的高电平宽度。思路是:上升沿捕获时记录CNT到CCR1,同时把捕获极性改为下降沿;下降沿捕获时再记录CNT,两次差值就是高电平时间。

配置步骤:

  1. 使能TIM4时钟,配置PB6为浮空输入或上拉输入。
  2. 配TIM4:
    TIM4->PSC = 71; // 计数频率1MHz,每微秒加一 TIM4->ARR = 0xFFFF; // 最大计数 TIM4->CCMR1 = (1<<0); // 通道1输入,IC1映射到TI1 TIM4->CCER = (1<<0); // 通道1使能,上升沿捕获 TIM4->DIER = (1<<1); // 使能通道1捕获中断 TIM4->CR1 = (1<<0); // 使能计数器
  3. 中断服务函数里:
    if (TIM4->SR & (1<<1)) { if (捕获的是上升沿) { rise = TIM4->CCR1; TIM4->CCER |= (1<<1); // 改为下降沿捕获 } else { fall = TIM4->CCR1; width = fall - rise; // 单位微秒 TIM4->CCER &= ~(1<<1); // 改回上升沿 } TIM4->SR &= ~(1<<1); }

这里有个坑:如果脉冲宽度超过ARR,计数器会溢出,差值计算就错了。解决办法是开溢出中断,在溢出中断里加一个溢出计数,最终宽度 = 溢出次数 * (ARR+1) + fall - rise。或者把PSC调大,让计数频率降低,但精度会下降。我一般根据预估的最大脉宽来设PSC,留20%余量。

4.3 高级定时器的互补PWM与死区

用TIM1的通道1和互补通道1(PA8和PB13)输出一对互补PWM,频率10kHz,死区1微秒。

计算:

  • 10kHz周期100微秒,取计数频率1MHz,PSC=71,ARR=99。
  • 死区1微秒,死区时间 = DTG[7:0] * Tdtg,其中Tdtg = 1/72MHz ≈ 13.89ns。1us / 13.89ns ≈ 72。查表,DTG=0x48左右可以产生约1us死区(具体查参考手册的死区时间表)。

配置:

TIM1->PSC = 71; TIM1->ARR = 99; TIM1->CCR1 = 50; // 50%占空比 TIM1->CCMR1 = (6<<4) | (1<<3); TIM1->CCER = (1<<0) | (1<<2); // CC1E和CC1NE都使能 TIM1->BDTR = (0x48<<0) | (1<<15); // 死区时间,MOE使能 TIM1->CR1 = (1<<7) | (1<<0); TIM1->EGR = (1<<0); TIM1->SR &= ~(1<<0);

注意BDTR的MOE位(位15)必须置1,否则输出无效。这个位在刹车事件后会被硬件清零,所以如果用了刹车功能,刹车解除后要重新置MOE。

5. 常见问题与排查技巧实录

5.1 定时器不计数或计数不对

最常见的原因有三个:时钟没使能、CEN没置1、PSC/ARR算错。排查时先读RCC的使能寄存器确认时钟开了,再读CR1的CEN位,最后用调试器看CNT是否在变。如果CNT在变但周期不对,检查PSC和ARR的值,注意APB预分频系数对定时器时钟的影响。

还有一个隐蔽的坑:如果你在调试模式下暂停了CPU,定时器可能还在计数。STM32的DBGMCU模块可以配置调试时冻结定时器,在DBGMCU_APB1FZ或DBGMCU_APB2FZ里置对应位即可。我早期调试PWM时,一暂停就看到波形乱跳,后来才知道要冻结。

5.2 PWM输出没有波形或波形异常

先查GPIO配置,必须是复用推挽输出,不能是普通推挽。再查CCER的CCxE位,高级定时器还要查BDTR的MOE位。如果波形频率不对,查PSC和ARR。如果占空比不对,查CCR和OCxM模式。如果波形有毛刺,查ARPE和OCxPE是否使能。

有个特殊情况:当CCR=0时,PWM模式1输出一直是低电平;当CCR>ARR时,输出一直是高电平。这是正常行为,不是bug。如果你想要0%和100%占空比,直接设CCR=0或CCR=ARR+1即可。

5.3 输入捕获捕获不到或捕获值不对

先确认GPIO是输入模式,且外部信号确实有跳变。再查CCMR1的CCxS位是否设成了输入。然后查CCER的CCxE和CCxP,极性设反了会捕获到相反的边沿。如果捕获值偶尔不对,可能是信号抖动,可以在CCMR1里设ICxPSC做数字滤波,或者在硬件上加RC滤波。

还有一个坑:输入捕获和输出比较共用CCR寄存器,如果你同时用同一个通道做输入和输出,会冲突。所以规划功能时,一个通道只做一件事。

5.4 更新中断进得太频繁或太少

更新中断频率 = 计数频率 / (ARR+1)。如果进得太频繁,增大PSC或ARR。如果太少,减小。注意中央对齐模式的更新频率是边沿对齐的两倍。另外,如果开了URS位,软件置UG不会触发中断,但硬件溢出会。这个在需要手动更新影子寄存器时很有用。

5.5 高级定时器刹车功能误触发

刹车输入引脚如果悬空,可能被噪声触发,导致MOE被清零,输出关闭。解决办法是在BDTR里配置刹车极性,或者使能刹车输入滤波,或者干脆不用刹车时把刹车引脚配置成普通GPIO。我有个项目就是刹车引脚没处理,电机一转就停,查了两天才发现是刹车误触发。

6. 寄存器操作中的几个实战心得

第一个心得:初始化时先写PSC和ARR,再写CCMR和CCER,最后写CR1和EGR。这个顺序能避免计数器在配置过程中乱跑。我见过有人先置CEN再配PSC,结果第一个周期完全不对。

第二个心得:动态改PWM参数时,利用影子寄存器。ARPE=1和OCxPE=1时,你写ARR和CCR不会立即生效,而是在下个更新事件生效。这样波形是连续的,不会出现半个周期。如果你需要立即生效,就置UG位,但要注意会触发更新中断。

第三个心得:32位定时器TIM2/TIM5读CNT要小心。因为32位寄存器在16位总线上要分两次读,如果两次读之间发生了进位,读出来的值就错了。正确做法是先读高16位,再读低16位,再读一次高16位,如果两次高16位相同,则值有效。或者用DMA读取。这个坑在高速计数时特别明显。

第四个心得:定时器中断里尽量少做事。定时器中断频率可能很高,如果里面做浮点运算或者复杂逻辑,CPU负载会很大。我一般只在中断里置标志位,主循环里处理。如果非要在中断里处理,用DMA或者硬件自动操作,比如用DMA把CCR值批量更新,实现任意波形输出。

第五个心得:调试时善用DBGMCU冻结定时器。在调试器里暂停CPU时,如果不冻结定时器,CNT会继续跑,你看到的寄存器值和你暂停时的实际值不一致。在DBGMCU_APB1FZ里置对应位,就能在调试时冻结TIM2~TIM7,APB2FZ里冻结TIM1和TIM8。

7. 从寄存器到实际项目的映射思路

学完第八章,你手里就有了一个非常灵活的定时器工具。实际项目中,我通常这样用:

  • 做系统时基:用TIM6或TIM7做1ms中断,给任务调度器提供心跳。基本定时器没有引脚,不占用GPIO,最合适。
  • 做PWM调光:用TIM3或TIM4的通道输出PWM,驱动LED或者电机。频率选1kHz~20kHz,看负载特性。
  • 做脉冲测量:用TIM2或TIM5的输入捕获,测超声波回波、红外遥控信号。32位计数器不用担心中间溢出。
  • 做电机控制:用TIM1或TIM8的高级定时器,输出三对互补PWM,配死区和刹车。这是高级定时器的主场。
  • 做编码器接口:用TIM2~TIM5的编码器模式,直接读正交编码器的计数,不用外部中断。这个功能在寄存器层面就是配SMCR的SMS位为001或010,然后读CNT即可。

最后再分享一个小技巧:如果你需要多个定时器同步,可以用一个定时器做主,输出TRGO信号,另一个定时器配成从模式,用ITR0~ITR3接收。这样两个定时器的计数器是同步的,做多路PWM相位控制时特别有用。具体配SMCR的TS位和SMS位,主定时器配CR2的MMS位。这个在第八章的末尾有提到,但很多人没注意,实际用起来能省不少事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 1:43:40

Context-Mode:统一管理运行环境与请求上下文,告别散落判断

团队最近接了一个跨平台的项目&#xff0c;需求不算复杂&#xff0c;但前后端加在一起&#xff0c;逻辑跑起来之后问题不少。数据查不到、权限对不上、页面行为一会儿一个样&#xff0c;排查到最后发现大部分问题的根子&#xff0c;都出在“运行环境上下文”这一层。当时我把这…

作者头像 李华
网站建设 2026/10/19 1:43:23

不装客户端:网盘直链解析在网页版完成,下载交给 IDM

不装客户端&#xff1a;网盘直链解析在网页版完成&#xff0c;下载交给 IDM 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动云…

作者头像 李华
网站建设 2026/10/19 1:42:37

ComfyUI-LTXVideo 帧插值实战:快速治好 AI 视频的跳帧卡顿

ComfyUI-LTXVideo 帧插值实战&#xff1a;快速治好 AI 视频的跳帧卡顿 【免费下载链接】ComfyUI-LTXVideo LTX-Video Support for ComfyUI 项目地址: https://gitcode.com/GitHub_Trending/co/ComfyUI-LTXVideo 想象这样一个瞬间&#xff1a;你满怀期待地把刚生成的视频…

作者头像 李华