4. 定时器定时计算:预分频器与自动重装载值的计算,定时时间的精确计算与误差分析

各位同学,咱们今天聊点实在的。定时器这东西,说白了就是STM32里的一个计数器,你给它一个时钟源,它就开始数数。数到某个值,它就翻个跟头,告诉你“时间到了”。

那问题来了——怎么让它精确地数出1毫秒、1秒,甚至1分钟?这里面的门道,全在预分频器(PSC)和自动重装载值(ARR)这两个寄存器上。

4.1 定时器的时钟源与计数基础

先搞清楚一件事:定时器不是自己凭空计时的,它需要一个时钟源。在STM32里,定时器的时钟通常来自APB总线。以我常用的STM32F103为例,APB1上的定时器时钟是36MHz,APB2上是72MHz。

嗯,这里有个坑——很多人以为APB1的定时器时钟就是36MHz,其实不一定。如果APB1的预分频系数不是1,那么定时器时钟会翻倍。我刚开始做项目时就吃过这个亏,算出来的时间总是不对,后来查手册才发现这个细节。

我的习惯:每次配置定时器前,先确认一下RCC时钟树,用RCC_ClocksTypeDef结构体把系统时钟、APB1、APB2的时钟都读出来,打印到串口看一眼。这一步能省掉后面80%的调试时间。

4.2 预分频器(PSC)的作用与计算

预分频器,英文叫Prescaler。它的作用很简单——把输入的时钟频率降下来。你想想看,如果直接用72MHz去计数,计数器每13.9纳秒就跳一次,很多应用根本用不上这么高的精度,反而浪费资源。

PSC是一个16位的寄存器,取值范围是0~65535。注意,实际的分频系数是PSC+1。为什么?因为当PSC=0时,表示不分频,也就是1分频。

计算公式:

定时器实际时钟频率 = 定时器输入时钟 / (PSC + 1)

举个例子:

输入时钟 = 72MHz
PSC = 71
实际时钟频率 = 72,000,000 / (71 + 1) = 1,000,000 Hz = 1MHz

也就是说,计数器每1微秒跳一次。这个精度在很多场合已经够用了。

关键点:PSC的值决定了计数器的“步长”。步长越小,精度越高,但ARR需要更大才能达到同样的定时时长。步长越大,定时范围越广,但精度会下降。这是一个权衡。

4.3 自动重装载值(ARR)的计算

ARR决定了计数器要数到多少才触发一次事件。它也是一个16位寄存器,取值范围0~65535。

定时时间的计算公式:

定时时间 = (ARR + 1) × (PSC + 1) / 定时器输入时钟

注意,这里ARR也要加1。因为计数器是从0开始数的,数到ARR一共是ARR+1个脉冲。

咱们来算一个实际需求:

需求:用TIM2产生一个1ms的定时中断。TIM2挂在APB1上,时钟为72MHz。

步骤:

  1. 先确定PSC。我希望计数器的步长是1μs,所以PSC+1 = 72,000,000 / 1,000,000 = 72,PSC = 71。
  2. 再算ARR。1ms = 1000μs,所以ARR+1 = 1000,ARR = 999。

代码实现:

// 定时器配置:1ms中断
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseStructure.TIM_Prescaler = 71;        // PSC = 71
TIM_TimeBaseStructure.TIM_Period = 999;           // ARR = 999
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);

我曾经踩过的坑:把PSC和ARR的值搞反了。有一次我写了一个PSC=999,ARR=71的配置,结果定时器跑得飞快,中断响应不过来,整个系统卡死了。后来用逻辑分析仪一看,中断周期只有72μs,根本不是我要的1ms。所以,PSC决定步长,ARR决定步数,这两个概念一定要分清楚。

4.4 定时时间的精确计算与误差分析

理论计算是一回事,实际跑起来是另一回事。误差从哪来?我总结了几点:

4.4.1 时钟源本身的误差

STM32的内部RC振荡器精度不高,典型误差在1%~3%之间。如果你用的是HSE(外部晶振),精度会好很多,一般在±50ppm以内。

我的建议:凡是涉及精确计时的项目,一律用外部晶振。我在一个电机控制项目里吃过亏,用内部RC振荡器做PWM,结果电机转速飘得厉害,换了HSE之后问题就解决了。

4.4.2 整数分频带来的量化误差

这是最容易被忽略的误差来源。PSC和ARR都是整数,但实际需要的分频系数往往不是整数。

举个例子:

输入时钟72MHz,想要一个10ms的定时。

理想情况:需要720,000个时钟周期。

但ARR最大只能到65535,所以必须用PSC分频。

如果PSC=71,步长1μs,那么ARR需要9999(因为10ms = 10,000μs)。

看起来没问题,对吧?

但如果输入时钟是72MHz,PSC=71,实际步长是:

72,000,000 / 72 = 1,000,000 Hz = 1μs

完美,没有误差。

但换个场景:输入时钟是36MHz,想要1ms定时。

如果PSC=35,步长 = 36,000,000 / 36 = 1,000,000 Hz = 1μs,ARR=999,也没问题。

但如果PSC=39呢?步长 = 36,000,000 / 40 = 900,000 Hz ≈ 1.111μs。ARR=899时,定时时间 = 900 × 1.111μs = 999.9μs,误差0.01%。

你看,只要PSC+1能整除输入时钟,就没有量化误差。否则,误差就来了。

实用技巧:优先选择PSC+1能整除输入时钟的值。如果实在无法整除,尽量让PSC+1接近输入时钟的整数因子,这样误差可以控制在可接受范围内。

4.4.3 软件延迟带来的误差

这个误差很多人不注意。中断响应时间、指令执行时间,都会影响实际的定时精度。

我做过一个测试:在TIM中断里只做一个IO翻转,用示波器测量,发现实际周期比理论值多了约0.5μs。这就是中断入栈、出栈、跳转带来的开销。

如果中断服务函数里还有复杂的计算,误差会更大。

我的做法:在要求高精度的场合,我会在计算ARR时预留几个计数周期,用来补偿中断开销。具体补偿多少,用示波器或逻辑分析仪实测一下,心里就有数了。

4.5 定时计算的知识结构图

下面这张图把定时器定时的整个计算流程串起来了,大家可以对照着看:

定时器定时计算知识结构图 定时器输入时钟 预分频器 PSC 计数器 CNT 自动重装载 ARR 更新事件/中断 核心公式 定时时间 = (ARR+1) × (PSC+1) ────────────── 输入时钟频率 步长 = (PSC+1) / 时钟频率 步数 = ARR + 1 误差来源: 1. 时钟源精度 2. 整数分频量化误差 3. 中断响应延迟 4. 软件执行开销 注意事项 PSC和ARR都是16位寄存器,取值范围0~65535;实际分频系数为PSC+1,计数周期数为ARR+1

4.6 常见定时场景的参数速查表

为了方便大家快速配置,我整理了一个常用定时参数的速查表。假设输入时钟为72MHz:

目标定时时间 PSC值 ARR值 实际步长 理论误差
1μs 0 71 13.9ns 0%
10μs 7 89 111.1ns 0.01%
100μs 71 99 1μs 0%
1ms 71 999 1μs 0%
10ms 71 9999 1μs 0%
100ms 7199 999 100μs 0%
1s 7199 9999 100μs 0%

小技巧:从表里可以看出,PSC=71和PSC=7199这两个值特别“干净”,因为它们能整除72MHz。在实际项目中,我经常优先选用这两个PSC值,然后通过调整ARR来改变定时时长。这样既保证了精度,又方便后期修改。

4.7 实际调试中的误差排查思路

如果你发现定时器跑出来的时间不对,别急着怀疑芯片坏了。按这个顺序排查:

  1. 先确认时钟源——用示波器测一下MCO引脚输出的时钟频率,或者用串口打印RCC时钟配置。
  2. 再检查PSC和ARR的配置——很多人把PSC和ARR的值写反了,或者忘了减1。
  3. 然后看中断服务函数——如果中断里代码太多,会导致下一次中断被延迟响应。
  4. 最后用示波器实测——在中断里翻转一个GPIO,用示波器看波形周期。

我记得有一次,一个同事说他的定时器跑出来是2ms,但配置的是1ms。我让他把代码发给我一看,发现他在中断里调了一个printf函数,光这个函数就执行了将近1ms。嗯,这种问题其实很常见,大家注意就好。

重要提醒:不要在定时中断里做耗时操作,比如printf、延时、复杂的浮点运算。如果非要做,考虑用DMA或者把任务放到主循环里处理。中断服务函数的原则是“快进快出”。