☰
STM32定时器到底在数什么?一文讲透PSC、ARR与时钟链路
2026/10/6 1:03:02 网站建设 项目流程

定时器是STM32里存在感最高、但常被忽略的一类外设。很多人用定时器做延时、PWM、输入捕获、超声波测距,代码抄得飞快,一问“定时器到底在数什么,时间基准从哪里来”,却说不清楚。我也经历过这个阶段:PSC填了个数、ARR填了个数,中断倒是会进,可换块芯片或改了个时钟源,时间就全乱了。这篇文章就把这根链路彻底讲透——定时器数的是脉冲,脉冲来自时钟,时钟与配置有关。读完你就能自己算出“1毫秒”是怎么来的,也排查得了典型的定时器故障。适合刚入门STM32的人、做毕业设计时被定时器折磨的人,以及玩过51但刚切换过来的同学。

1. 定时器到底在“数”什么——先认识时基单元

1.1 定时器的本质是“脉冲计数器”,不是“秒表”

STM32的定时器,英文名叫Timer,中文翻译成“定时器”,这个翻译容易误导人。很多人以为它内部有个“秒表”,是直接数时间的。真相不是这样——它本质上是一个计数器,数的是脉冲个数。如果它数的是固定频率的脉冲,那每个脉冲就代表固定的时间长度,数够多少个脉冲,就等于过了多少时间,这才有“定时”的效果。

打个比方,你站在厂门口数工人进厂的人数,你本人就是一台计数器。如果工人每分钟进10个,你数到100个,就知道过了10分钟。定时器也一样,它内部有个寄存器叫CNT,它会根据某个时钟信号“+1”。这个时钟信号每跳一次边沿,CNT就加一。关键点在于:这个时钟信号的频率,决定了CNT每加一次需要多久。

很多人卡在这里,是因为只记住了“PSC分频”“ARR自动重装”这些词,却没理解它们是在“改造脉冲”,而不是“直接改时间”。理解成“先决定每个脉冲多长时间,再决定数多少个脉冲”,整个定时器就顺了。

1.2 计数脉冲从哪来:内部时钟才是时间基准的主力

定时器的计数脉冲来源其实有好几个,典型的有四种:

  • 内部时钟CK_INT:来自定时器所在总线(APB1/APB2)的时钟,这是最常用的来源。几乎所有的定时中断、PWM、延时,都是靠它。
  • 外部时钟模式1:来自定时器的外部引脚TIx,例如TIM2的CH1引脚PA0接一个外部方波信号,定时器直接对这个信号的边沿计数。
  • 外部时钟模式2:来自ETR引脚,需要配置触发检测。
  • 内部触发输入ITRx:用一个定时器的输出触发另一个定时器,实现定时器级联。

在绝大多数应用里,我们用的是第一种。内部时钟CK_INT就是定时器的时间基准。而CK_INT是从哪条链路来的,就是下文要重点讲的时钟树问题。这也是标题里“时间基准从哪里来”的核心答案:定时器本身不产生时间,它的时间基准完全来自系统时钟树的分频、倍频结果。

1.3 关键认知:定时器数的是“边沿”,不是“秒”

再说一个常见误区。很多人看寄存器手册,里面写着“Timer clock = 72 MHz”,就认为定时器每秒跑72M次。实际上,计数器的时钟频率就是它“+1”的频率吗?不完全对。

有些定时器内部还有额外的分频,有些计数器并非每来一个时钟脉冲就加一,比如编码器模式、外部时钟的滤波与分频。但即便在最基础的内部时钟模式下,定时器接收到的时钟通常是经过时钟树预分频之后的信号。

这里要强调一个概念:一个周期为T的时钟信号,每经过一个上升沿或下降沿,CNT就加1。如果这个时钟信号每秒变化72M次,那两次计数之间的间隔就是 1/72MHz ≈ 13.89纳秒。这个间隔就是定时器的最小计时单位。如果你把计数器配成每1微秒加一次,那它每秒加100万次,数到1000就是1毫秒。

所以,定期器“数”的从来不是秒数,而是若干个边沿。只要把“每个边沿代表多长时间”搞清楚,所有定时计算都只是乘法。

2. 时间基准从哪里来:时钟树与定时器时钟链路

2.1 从晶振到定时器总线:一条完整的时钟链路

我们以最经典的STM32F103系列为例,走一遍时钟链路。一颗8 MHz的外部晶振接到OSC_IN/OSC_OUT引脚,经过锁相环PLL倍频(F103典型配置为×9),得到72 MHz的SYSCLK。SYSCLK再经过AHB预分频器(通常为1/1),得到72 MHz的HCLK。之后HCLK经过APB1预分频器(通常为2分频),得到36 MHz的APB1外设时钟。按照STM32的惯例,APB1上的定时器时钟会自动翻倍,也就是说TIM2、TIM3、TIM4、TIM5、TIM6、TIM7的时钟是72 MHz。

同理,APB2预分频器一般配置为1/1,所以APB2上的外设时钟就是72 MHz。挂在APB2上的TIM1、TIM8,它们的定时器时钟也是72 MHz。这就是为什么很多人用CubeMX看到TIM3的时钟显示是72 MHz,而APB1总线时钟只有36 MHz——翻倍机制在起作用。

这条链路可以用一个简单的式子表示: HSE(8 MHz) → PLL×9 → SYSCLK(72 MHz) → AHB:1 → APB1:2 → TIM3时钟 = APB1外设时钟×2 = 72 MHz。

如果换成HSE为25 MHz的板子,PLL配置成其他倍数,定时器时钟也会跟着变。很多初学者把“代码在正点原子板上能用,到手头的板子就不对”的原因,正是两块板的晶振频率不同,导致整条时间基准都变了。

2.2 为什么APB分频为1时定时器时钟不变,分频不为1时反而翻倍?

这是一个非常容易混淆的细节。STM32的参考手册里写着:如果APB预分频器为1,定时器时钟频率等于APB外设时钟频率;如果APB预分频器不为1,定时器时钟频率是APB外设时钟频率的两倍。

我把这句话用人话翻译一下:APB1平时是2分频,此时APB1外设只能跑36 MHz,但定时器需要更快的时钟,所以硬件自动给你×2,给定时器补回72 MHz。如果APB1设置为1分频,说明APB1外设本身就是72 MHz,那定时器也不需要翻倍了,直接用72 MHz即可。

这个“硬件自动补”的设计,是给开发者节省功耗的:外设部分以低频运行,只有定时器这种需要精准计时的模块才用高频时钟。但也因为有了这个机制,很多人看数据手册时绕不过弯,我最初也在这里踩过坑——以为TIM3的时钟就是36 MHz,结果定时时间全部多了一倍。判断定时器时钟究竟是多少,一定不要光看APB总线频率,要去看定时器时钟那一栏,CubeMX的时钟配置页面里会写得清清楚楚。

2.3 实际操作:用CubeMX确认你的定时器时钟

在CubeMX里打开STM32F103C8T6工程,进入Clock Configuration页面。你可以看到左侧“APB1 peripheral clocks”显示36 MHz,而下面“APB1 timer clocks”显示72 MHz。如果你把APB1 Prescaler改成1/1,两栏都会变成72 MHz。这个页面就是最直观的时间基准入口。

配置TIM3时,我建议先把这个页面拍下来,把“APB1 timer clocks”这个数记为TCLK。后续PSC和ARR的计算全靠它,如果这个数错了,后面全部白算。用CubeMX的好处是,它生成代码时会在HAL_TIM_Base_Init里自动把时钟配置好,你只需要在__HAL_RCC_TIM3_CLK_ENABLE()后使用即可,不需要手动访问RCC寄存器。

2.4 顺带一提:TIM3为何如此常用

热词里频繁出现“stm32f103 cubemx 定时器3”,不是巧合。TIM3是一个通用定时器,向上/向下计数都支持,有4个捕获/比较通道,可以输出PWM、做输入捕获、编码器模式,还能触发ADC。它挂在APB1上,时钟72 MHz,位置和功能都比较均衡。很多毕业设计的“超声波测距”“PWM调速”“舵机控制”,底层都是TIM3在跑。如果你是从C8T6入手,TIM3基本就是你的“瑞士军刀”。

3. 把“数数”变成“时间”:PSC、CNT与ARR的配合

3.1 预分频器PSC:先给脉冲降频,再喂给计数器

预分频器PSC(Prescaler)是定时器里的第一道关卡。它的作用很直白:把输入的定时器时钟频率分成更低频率的计数脉冲。为什么需要它?因为72 MHz太高了,如果直接用CNT去数72 MHz的脉冲,计数器从0加到65535只要不到1毫秒,定时太长就没法覆盖。通过预分频器,把计数脉冲降到合适的频率,然后再数。

PSC是一个16位的寄存器,写入的值会加1后作为分频系数。比如PSC=71,实际分频系数是72,计数频率从72 MHz变成1 MHz,每个计数脉冲周期1微秒。这里要注意,PSC寄存器的值和实际分频系数的关系是“差1”的,很多人写PSC=1000以为分了1000倍,实际分了1001倍,在经常计数的场景下会积累出明显误差。

3.2 计数器CNT与自动重装载寄存器ARR:数到什么时候“归零”

计数器CNT是一个随每个计数脉冲递增的寄存器。它从0开始,每来一个计数脉冲加1。自动重装载寄存器ARR里存着一个上限值,当CNT数到ARR,也就是CNT将要溢出时,硬件会自动把CNT清零,同时产生一个更新事件(Update Event),并置位UIF标志。如果开启了更新中断,此时就会进入中断回调。

把PSC和ARR结合起来看:PSC决定“每个计数脉冲间隔多久”,ARR决定“总共数多少个脉冲”。两者相乘,才是定时周期。公式非常简洁:

定时周期T = (PSC+1) × (ARR+1) / TCLK

这个公式是定时器时基计算的核心。所有“1毫秒”“1秒”都是拿这个公式倒推出来的。

3.3 一个完整的计算实例:算出“1毫秒”定时中断

假定TCLK = 72 MHz,需求是产生1 ms的定时中断。先设PSC=71,得到计数频率1 MHz。再算ARR:T = (71+1) × (ARR+1) / 72 MHz = 1 ms。即 (ARR+1) / 1 MHz = 0.001 s,所以ARR+1=1000,ARR=999。

也就是说,CNT从0数到999,刚好数了1000次,每次数1微秒,总耗时1毫秒。此时CNT从999变到1000时发生溢出,自动归零并触发更新中断。这个计算里我不推荐“心算”ARR,而是每次都拿计算器推一下,尤其是改过TCLK之后。

再举一个反例:如果PSC配错了,比如PSC=70,那计数频率就是72 MHz/71≈1.014 MHz,每周期约0.985微秒,定时1秒大概偏0.015秒。如果PSC=72,计数频率1 MHz不是正好71对应1.000 MHz吗?错,是PSC=71对应72分频,PSC=72对应73分频。这个“差1”的坑,我在实际项目中踩过不止一次。

3.4 别忘了计数器位宽:16位和32位的区别

STM32的定时器分为16位和32位两类。TIM3是16位定时器,PSC和ARR都是16位,最大只能写到65535。如果你要产生很长的定时周期,例如PSC=65535、ARR=65535、TCLK=72 MHz,算出来周期大约为:(65536×65536)/72MHz ≈ 59.65秒。如果超过了这个范围,16位定时器就无能为力了。

TIM2和TIM5在F103里是32位计数器,ARR最大可以到4294967295,能定几个小时甚至更长。但32位定时器占用的寄存器更多,中断回调里处理的数据类型也要用32位,否则容易溢出。这个在选择定时器时就要想清楚:如果是秒级以下的短周期任务,16位完全够用;如果要做长时间计时、低频计数,优先用32位定时器。

另外,更新事件并不只是用来进中断。它还会触发DAC、ADC采样,可以作为DMA请求,也能触发其他定时器级联。这就是为什么很多高级用法总要先配一个“时基定时器”——你要先用PSC和ARR把节奏定好,后面的事才有时间轴可依。

4. 时基应用的三个典型场景拆解

4.1 场景一:用定时器做1ms系统心跳

嵌入式系统经常需要一个周期固定的“心跳”,比如1 ms中断一次来扫描按键、刷新显示、做软件定时器累加。用TIM3实现很直接。在CubeMX里选择TIM3,设置Clock Source为Internal Clock,PSC填71,ARR填999,使能Update Interrupt。生成代码后,在main()里调用HAL_TIM_Base_Start_IT(&htim3),然后在回调函数里处理。

回调函数名字是固定规律:void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)。它不属于任何特定定时器,所以进去之后要先判断if (htim->Instance == TIM3),再执行自己的逻辑。这一点新手很容易忽略,以为TIM3和TIM4的回调是分开的,结果两个定时器共用同一个回调,里面没做区分,逻辑就乱了。

我通常在小工程里维护一个全局的volatile uint32_t sys_tick,每次TIM3中断加一。所有需要延时的模块都去读这个值,而不是直接HAL_Delay()阻塞,这样整个系统的实时性会好很多。

4.2 场景二:输入捕获测频率——定时器“反向”用

定时器不仅能自己产生脉冲,还能拿外部信号“喂”给它。输入捕获的经典做法是:把被测量的方波信号接到定时器的某个捕获通道引脚上,配置为上升沿捕获。信号每来一个上升沿,硬件就会把此时CNT的值锁存到捕获寄存器CCRx里。

两次连续捕获之间的CNT差值,就是信号一个周期内,计数器增加的次数。把这个差值乘以计数脉冲的周期,就是信号的周期。再取倒数就得到频率。举个例子:TIM3的计数频率为1 MHz,输入信号第一次捕获时CCR=1000,第二次捕获时CCR=4000,差值3000,那么信号周期=3000微秒,频率约333.33 Hz。

热词“stm32定时器捕获测频率”指的就是这个玩法。STM32还支持PWM输入模式,本质是用两个通道同时捕获上一个上升沿和下一个上升沿,直接算出高电平时间。测超声波模块的Echo脉宽,就是靠这种捕获实现的。这里有个技巧:捕获模式下的CNT溢出要处理好,如果两次捕获间隔太长,中间CNT溢出过多次,差值就会算错,这时要在更新中断里统计溢出次数。

4.3 场景三:从时基角度理解PWM波形

PWM和定时器的关系,其实就是“计数比较”的关系。CNT从0数到ARR,周而复始;CCR里存着一个阈值,硬件每个计数周期都把CNT和CCR比较,CNT小于CCR时输出一个电平,CNT大于等于CCR时输出另一个电平。这样,CNT的周期决定了PWM的周期,CCR的值决定了高电平的占比,也就是占空比。

PWM频率的计算公式为:f_PWM = TCLK / ((PSC+1) × (ARR+1))。这个式子展开看,其实就是时基公式的倒数。改频率用PSC和ARR,改占空比用CCR,二者互不影响。这就是为什么电机驱动里经常用TIM3输出20 kHz左右的PWM,TIM4改占空比调速度,分工非常明确。

热词里还有“foc pwm波形和定时器”,FOC控制对PWM的频率和占空比精度要求都很高,特别是中心对齐模式,需要定时器支持中央对齐计数。正规地说,STM32的TIM1和TIM8是高级控制定时器,带互补输出、死区控制,专门给电机FOC这类场景用。如果你只是要一个呼吸灯,TIM3就够了;如果要驱动三相逆变桥,那就得上TIM1/TIM8。二者定位不同,根源还是各自的时基单元能力和带外设资源不同。

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

5.1 定时时间到了,却进不了中断

这个问题出现的频率极高。配置了定时器,中断也开了,但函数就是进不去。排查顺序建议这样:

第一,检查RCC里是否使能了TIM3的时钟。CubeMX生成的代码一般不会有问题,但手写工程很容易漏掉__HAL_RCC_TIM3_CLK_ENABLE()。第二,检查NVIC里是否勾选了TIM3全局中断。第三,检查代码里是否调用了HAL_TIM_Base_Start_IT(),这个函数不仅要启动定时器,还会把更新中断打开,漏了就白配。第四,检查回调函数名有没有拼错,HAL库要求完全匹配,少一个下划线或多一个字母都会导致中断无效。

实战中我还遇到过一种隐蔽情况:中断回调里执行时间太长,比如在里面跑了一个HAL_Delay(5),导致下个中断还没处理完又来了,UIF标志一直置位,看起来像“卡死”。定时中断里面尽量不要做耗时操作,哪怕打印串口都要控制,最好只置标志位,让主循环去处理。

5.2 定时时间总是两倍或一半

定时时间翻倍,基本可以锁定是时钟配置问题。最常见的就是我之前提到的APB1分频理解错误。比如你以为TIM3时钟是36 MHz,按TCLK=36 MHz去配PSC,但实际定时器时钟是72 MHz,那么实际周期就是你算的一半,也就是定时器节奏变快了一倍。反之,如果你按72 MHz算,但某个工程下定时器时钟只有36 MHz,周期就是两倍。

怎么定位?在CubeMX的Clock Configuration页面里看实际值,或者读RCC_CFGR寄存器里的PPRE1位段。还有一个土办法:写一个翻转LED的程序,把周期设为1秒,拿手机秒表或者逻辑分析仪测一下。实测周期是2秒,说明定时器时钟比预期慢一半;实测周期0.5秒,说明快一倍。这比纯看寄存器直观多了。

5.3 换了个外部晶振,定时全乱

很多开发板上HSE是8 MHz,但也有12 MHz、25 MHz的外部晶振。程序里如果默认按8 MHz配置倍频系数,在12 MHz晶振板上会导致SYSCLK跑飞,定时器时钟当然也跟着乱。更隐蔽的是外部晶振起振失败:芯片会默默从HSE切回HSI。HSI是内部RC振荡器,频率一般为8 MHz左右,但不同芯片差异较大。如果从72 MHz降到8 MHz,定时器节奏会慢接近9倍,表现就是LED闪得非常慢。

遇到这种“定时突然全乱”的板子,我建议第一步就怀疑时钟源。看程序里是否读取过RCC_GetFlagStatus(RCC_FLAG_HSERDY),或者用示波器量一下MCO引脚输出的SYSCLK信号,可以快速判断到底跑在什么频率上。这也是为什么我总说,定时器排查很大程度是时钟排查。

5.4 SysTick滴答定时器和通用定时器怎么分工

热词里也有“滴答定时器”。SysTick是一个集成在Cortex-M内核里的简单定时器,它的时钟一般来自内核时钟HCLK。很多人分不清它和TIM3的关系:SysTick主要负责操作系统(比如RTOS)的周期性时基,HAL_Delay也是用它在实现的。它只有24位,不占外设资源,但不能做PWM、输入捕获这些功能。通用定时器则功能完整,资源有限且多功能,适合真正需要计时的场景。

实际工程里我习惯让TIM3专门做“软实时”的调度时基,SysTick留给HAL库自己用。如果你在裸机上需要µs级延时,用SysTick配合DWT会更准,但那是另一个话题了。

5.5 从51定时器迁移过来的思维转变

51单片机的定时器,很多人是照着“机器周期”理解的:12 MHz晶振,机器周期1微秒,TH0/TL0计数。到了STM32,这种固定对应关系不存在了,因为定时器时钟是可以通过PLL倍频出来的,PSC又给你一道自定义分频,配置一下可以精确到纳秒级。

最大的思维转变是:51里通常“定时器溢出时间是固定的”,而STM32里你必须自己定义“每个计数脉冲的含义”。51的TH0/TL0只有16位,很多人养成了“溢出一次重新塞初值”的习惯;STM32有自动重装载寄存器,不需要在中断里手动给CNT赋值,溢出后自动归零,省了一条赋值语句,也少了一个容易出错的环节。

如果你是从51过来的,我的建议是:忘掉“每次重装初值”的套路,CLR也好、赋值也好,都让硬件来。你只需要维护好PSC、ARR两个数,程序会干净很多。

6. 个人调试心得:先问“时钟对不对”,再问“代码对不对”

写了这么多,最想说的一句经验就是:定时器出问题,九成出在时钟链路上,不是出在定时器本身。我调试定时器相关功能时,第一件事永远是打开CubeMX的时钟配置页面,把TCLK确认一遍;第二件事才是看PSC和ARR的计算;第三件事才是看中断配置。这个顺序帮我省了很多无用的调试时间。

再分享一个小工具:手头没有逻辑分析仪时,我会把GPIO翻转和定时器中断绑在一起,在TIM3中断里翻转PA5,再把PA5接到另一个定时器的输入捕获通道上,自测自量。这样能确认系统实际跑的频率,也可以验证整个时基链路是否正确。这个方法不用额外设备,只用STM32自带的定时器互测,很实用。

最后再提醒一个细节,也是最容易踩的坑:如果修改了PSC或ARR的值,最好在修改前关闭定时器,修改后再重新启动。有些库函数在运行时改ARR会出现“缓冲前值”的问题,导致第一个周期异常。配置好参数再启动,能够避免很多莫名其妙的现象。定时器的世界并不复杂,把“时钟→分频→计数→重装载”这条链记在心里,剩下的都是简单的算术题。

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

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

立即咨询