4. 定时器定时计算:预分频器与自动重装载值的计算,定时时间的精确计算与误差分析
各位同学,咱们今天聊点实在的。定时器这东西,说白了就是STM32里的一个计数器,你给它一个时钟源,它就开始数数。数到某个值,它就翻个跟头,告诉你“时间到了”。
那问题来了——怎么让它精确地数出1毫秒、1秒,甚至1分钟?这里面的门道,全在预分频器(PSC)和自动重装载值(ARR)这两个寄存器上。
4.1 定时器的时钟源与计数基础
先搞清楚一件事:定时器不是自己凭空计时的,它需要一个时钟源。在STM32里,定时器的时钟通常来自APB总线。以我常用的STM32F103为例,APB1上的定时器时钟是36MHz,APB2上是72MHz。
嗯,这里有个坑——很多人以为APB1的定时器时钟就是36MHz,其实不一定。如果APB1的预分频系数不是1,那么定时器时钟会翻倍。我刚开始做项目时就吃过这个亏,算出来的时间总是不对,后来查手册才发现这个细节。
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。
步骤:
- 先确定PSC。我希望计数器的步长是1μs,所以PSC+1 = 72,000,000 / 1,000,000 = 72,PSC = 71。
- 再算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 定时计算的知识结构图
下面这张图把定时器定时的整个计算流程串起来了,大家可以对照着看:
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 实际调试中的误差排查思路
如果你发现定时器跑出来的时间不对,别急着怀疑芯片坏了。按这个顺序排查:
- 先确认时钟源——用示波器测一下MCO引脚输出的时钟频率,或者用串口打印RCC时钟配置。
- 再检查PSC和ARR的配置——很多人把PSC和ARR的值写反了,或者忘了减1。
- 然后看中断服务函数——如果中断里代码太多,会导致下一次中断被延迟响应。
- 最后用示波器实测——在中断里翻转一个GPIO,用示波器看波形周期。
我记得有一次,一个同事说他的定时器跑出来是2ms,但配置的是1ms。我让他把代码发给我一看,发现他在中断里调了一个printf函数,光这个函数就执行了将近1ms。嗯,这种问题其实很常见,大家注意就好。
重要提醒:不要在定时中断里做耗时操作,比如printf、延时、复杂的浮点运算。如果非要做,考虑用DMA或者把任务放到主循环里处理。中断服务函数的原则是“快进快出”。