☰
STM32定时器时间基准详解:从时钟树到PSC/ARR参数计算
2026/10/3 12:33:05 网站建设 项目流程

1. 从一次“定时器不准”的翻车说起

刚接触 STM32 那会儿,我用 TIM3 做了一个 1ms 的定时中断,用来给数码管做动态扫描。代码写得很顺,PSC=71、ARR=999,心里默算:72MHz 除以 72 得 1MHz,再数 1000 个就是 1ms,完美。烧进去一看,数码管确实在动,但亮度忽明忽暗,按键响应也发飘。拿逻辑分析仪一抓波形,中断周期实际是 1.5ms 左右,差了整整 50%。

后来才发现,我那块板子的外部晶振是 8MHz,但工程里时钟树配的是 HSI 内部 8MHz 直接倍频,实际系统时钟并不是我假设的 72MHz。定时器数的是“时钟节拍”,不是“秒”——它根本不知道什么叫 1ms,它只知道“来一个脉冲我加一”。你喂给它多快的脉冲,它就按多快的节奏数数。这就是标题里那个问题的本质:定时器到底在数什么?答案是:它在数经过预分频之后的时钟脉冲个数,而这个脉冲的源头,是挂在 APB 总线上的那路时钟。

这篇东西我打算把 STM32 定时器的时间基准从源头捋一遍:时钟从哪来、怎么走到定时器、PSC 和 ARR 到底怎么配合、为什么你算出来的值和实测对不上、以及那些年我在捕获、PWM、FOC 这些场景里踩过的坑。适合刚上手 STM32 定时器、或者用了很久但一直“照着例程抄参数”的朋友。看完你至少能做到一件事:拿到任何一个定时需求,能自己把 PSC、ARR 和实际时间三者之间的关系推出来,而不是靠试。

2. 定时器的时间基准到底从哪来

2.1 时钟树:脉冲的“自来水管道”

STM32 的时钟系统像一套自来水管道。源头有几个:HSI(内部高速 RC)、HSE(外部晶振)、PLL(锁相环倍频)。这些源头经过一系列分频、倍频、选择开关,最终送到各个外设。定时器拿到的水,就是从这套管道里分出来的一支。

以最常见的 STM32F103 为例,系统时钟 SYSCLK 最高 72MHz。SYSCLK 经过 AHB 预分频器得到 HCLK(通常也是 72MHz),HCLK 再经过 APB1 和 APB2 预分频器,分别得到 PCLK1(APB1,最高 36MHz)和 PCLK2(APB2,最高 72MHz)。TIM2/3/4/5 挂在 APB1 上,TIM1/8 挂在 APB2 上。

这里有个极其容易被忽略的规则:当 APB 预分频系数为 1 时,定时器时钟等于 PCLK;当 APB 预分频系数大于 1(也就是 2、4、8、16)时,定时器时钟等于PCLK 的 2 倍。这条规则写在参考手册的时钟树图注里,很多人扫一眼就过去了。

为什么这么设计?因为 APB 总线本身跑不了太高频率,但定时器需要更高的计数分辨率。所以硬件做了个“倍频补偿”:APB1 分频后是 36MHz,但挂在上面的 TIM2 实际拿到的是 72MHz。如果你不知道这条规则,按 36MHz 去算 PSC,结果就会差一倍——这正是我开头那次翻车的同类错误。

2.2 一个具体的推算过程

假设你的工程配置是:HSE 8MHz 经 PLL 9 倍频得到 72MHz SYSCLK,AHB 不分频,APB1 分频系数为 2,APB2 不分频。

  • PCLK1 = 72 / 2 = 36MHz,因为分频系数 2 > 1,所以 TIM2/3/4/5 的时钟 = 36 × 2 =72MHz。
  • PCLK2 = 72 / 1 = 72MHz,分频系数为 1,所以 TIM1/8 的时钟 =72MHz。

两条路最后都是 72MHz,看起来一样,但推导逻辑完全不同。如果你哪天把 APB1 分频改成 4,PCLK1 变成 18MHz,TIM2 的时钟就变成 36MHz 了,这时候你原来的 PSC 参数全部失效。所以改时钟树之前,先确认定时器时钟有没有跟着变,这是排查定时异常的第一步。

2.3 内部时钟、外部时钟与触发输入

上面说的是内部时钟(CK_INT),也是绝大多数场景用的。但定时器还能从别的地方取时钟源,这点在捕获和计数场景里很关键:

  • 外部时钟模式 1:从 TIMx_ETR 引脚输入外部脉冲,经过极性选择、分频、滤波后作为计数时钟。超声波测距模块的回波脉宽测量、外部传感器脉冲计数就用这个。
  • 外部时钟模式 2:用 TIMx_CH1 或 CH2 的边沿作为时钟。
  • 内部触发输入(ITRx):一个定时器作为另一个定时器的预分频器,级联使用。比如 TIM1 的更新事件驱动 TIM2 计数,做超长定时。

我做过一个步进电机项目,需要精确统计编码器脉冲数,用的就是外部时钟模式 1,把编码器 A 相接到 ETR 引脚,硬件自动计数,CPU 完全不参与,比中断里手动加一可靠得多。这里要注意 ETR 引脚有滤波配置(ICF 位),信号毛刺多的时候把滤波打开,否则会多计数。

3. PSC 和 ARR:两个参数决定一切

3.1 预分频器 PSC 在干什么

PSC 是 Prescaler,预分频器。它的作用是把输入的时钟脉冲“降速”。公式很简单:

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

注意那个+1。PSC 寄存器写 0 表示 1 分频,写 1 表示 2 分频,写 71 表示 72 分频。为什么是加一而不是直接写分频值?因为硬件计数器是从 0 开始计数的,PSC 寄存器值 0 到 65535 对应 1 到 65536 分频。这个设计在 STM32 里到处都是(ARR 也是),记住“寄存器值 + 1 才是实际分频/计数值”能省掉很多调试时间。

拿 72MHz 举例,我想要 1MHz 的计数时钟,那 PSC + 1 = 72,PSC = 71。这时候计数器每 1 微秒加一。

3.2 自动重装载寄存器 ARR 的角色

ARR 是 Auto-Reload Register,决定计数器数到多少就溢出。计数器从 0 数到 ARR,然后产生更新事件(UEV),同时清零重新开始。所以一个完整周期的计数次数是ARR + 1。

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

还是 72MHz、PSC=71 的例子,想要 1ms 周期:1ms = (ARR+1) × 1us,所以 ARR + 1 = 1000,ARR = 999。

把两个公式合起来看,你会发现 PSC 和 ARR 是乘法关系,理论上可以互相补偿。比如 PSC=71、ARR=999 和 PSC=719、ARR=99 都能得到 1ms。那怎么选?我的经验是:PSC 尽量小,ARR 尽量大。因为 PSC 分频会引入额外的抖动,而且 ARR 大意味着计数分辨率高,做 PWM 占空比调节时步进更细腻。只有在需要超长定时(比如几秒、几十秒)时,才把 PSC 加大,因为 ARR 最大只有 65535(16 位定时器)。

3.3 16 位与 32 位的差异

不是所有定时器都是 16 位。STM32F4 系列的 TIM2 和 TIM5 是 32 位的,ARR 可以到 0xFFFFFFFF。这意味着在 72MHz 下,32 位定时器单次最长定时可以到约 59.6 秒,而 16 位定时器在同样时钟下最长只有约 0.91 秒(65536 / 72MHz)。做长时间间隔测量时,优先选 32 位定时器,能省掉很多软件扩展位数的麻烦。

我见过有人用 TIM3(16 位)做 10 秒定时,靠中断里累加计数实现,结果中断频繁导致 CPU 负载高。其实只要换成 TIM2(32 位),PSC=7199(10kHz 计数),ARR=99999,一次就搞定 10 秒,中断频率降到 0.1Hz,CPU 几乎无感。

4. 从寄存器到代码:一次完整的定时配置

4.1 标准库配置流程

以 TIM3 做 500ms 定时中断为例,系统时钟 72MHz,APB1 分频系数 2(所以 TIM3 时钟 72MHz)。

目标:500ms 周期。选 PSC = 7199,计数时钟 = 72MHz / 7200 = 10kHz,即每 0.1ms 一个计数。ARR + 1 = 500ms / 0.1ms = 5000,ARR = 4999。

void TIM3_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period = 4999; // ARR TIM_TimeBaseStructure.TIM_Prescaler = 7199; // PSC TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }

中断服务函数里记得清标志:

void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 你的业务代码 } }

4.2 HAL 库配置流程

HAL 库把初始化封装得更省事,但时钟配置仍然要你自己在 CubeMX 或 SystemClock_Config 里确认。很多人用 CubeMX 生成代码后直接改 PSC/ARR,却忘了看时钟树,结果还是不准。

TIM_HandleTypeDef htim3; void MX_TIM3_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 7199; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 4999; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_Base_Init(&htim3); sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig); sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig); }

启动中断用HAL_TIM_Base_Start_IT(&htim3);,中断回调函数是HAL_TIM_PeriodElapsedCallback,在里面写业务逻辑,不需要手动清标志(HAL 帮你清了)。

4.3 参数计算速查表

目标周期定时器时钟PSC计数时钟ARR实际周期
1ms72MHz711MHz9991.000ms
10ms72MHz719100kHz99910.000ms
100ms72MHz719910kHz999100.000ms
500ms72MHz719910kHz4999500.000ms
1s72MHz719910kHz99991.000s
1us72MHz072MHz711.000us

这张表建议存下来,调参的时候直接查,比每次按计算器快。注意最后一行 1us 定时,ARR=71 意味着中断频率 1MHz,CPU 基本被中断吃满了,实际项目里不要这么干,微秒级延时用 DWT 或 NOP 循环更合适。

5. 那些让定时器“不准”的坑

5.1 时钟源没配对

最常见的坑,没有之一。表现是定时周期整体偏大或偏小一个固定比例。比如你以为是 72MHz,实际跑的是 8MHz HSI,那所有定时都会慢 9 倍。排查方法:在定时中断里翻转一个 GPIO,用示波器或逻辑分析仪测实际周期,和理论值对比。如果比例是整数倍关系,基本就是时钟源问题。

还有一种隐蔽情况:HSE 起振失败,系统自动切到 HSI,但你的代码里 PSC/ARR 是按 HSE 算的。这时候程序照跑,定时全错。所以启动后要检查 RCC 的时钟切换状态标志,确认 HSE 真的起振了。

5.2 忘记 +1

PSC 和 ARR 都要 +1,这是新手最容易犯的错。PSC=71 是 72 分频,ARR=999 是 1000 次计数。如果你按 71 和 999 直接算,结果会差一点点,短定时看不出来,长定时累积误差就明显了。

5.3 中断里干了太多活

定时器中断频率高的时候,如果中断服务函数里执行了耗时操作(比如浮点运算、串口打印、Flash 操作),会导致中断响应延迟,实际周期被拉长。我见过有人在 1kHz 中断里做 PID 浮点运算加串口发送,结果定时周期抖到 2ms 以上。

解决办法:中断里只做标志位设置或数据搬运,复杂计算放到主循环。如果必须在中断里处理,把中断优先级设高,并且确保处理时间远小于中断周期(一般不超过周期的 10%)。

5.4 自动重装载预装载的影响

ARR 有个预装载功能(ARPE 位)。开启时,你写入的新 ARR 值不会立即生效,而是等到下一个更新事件才加载。关闭时立即生效。做动态调频(比如变频 PWM)时,这个设置很关键。如果边改 ARR 边计数,不开预装载可能出现一个异常长的周期或异常短的周期。我的习惯是:需要运行时改 ARR 的场合,一律开启预装载。

5.5 定时器级联时的时钟传递

用 ITRx 做定时器级联时,主定时器的更新事件作为从定时器的时钟。这时候从定时器的 PSC 仍然起作用,但它的输入时钟变成了主定时器的更新频率。我做过一个项目,TIM1 产生 1kHz 更新事件,TIM2 用它做时钟,PSC=0,ARR=999,得到 1Hz 的慢速定时。这里如果忘了从定时器的时钟源要配成 ITR,它还是按内部 72MHz 跑,结果完全不对。

6. 捕获、PWM 与 FOC 场景下的时间基准

6.1 输入捕获测频率

超声波测距、红外解码、频率计都用输入捕获。原理是:信号边沿触发捕获,把当前计数器值(CNT)存到捕获寄存器(CCR),两次捕获值之差就是脉冲宽度对应的计数个数,再乘以计数周期就是时间。

这里的关键是计数时钟要足够快。假设你要测 1kHz 到 100kHz 的信号频率,计数时钟至少要是被测信号频率的 10 倍以上才能保证精度。72MHz 计数时钟下,测 100kHz 信号,一个周期有 720 个计数,分辨率约 0.14%,够用。但如果 PSC 设大了,计数时钟降到 1MHz,一个周期只有 10 个计数,误差就大了。

捕获还有个坑:计数器溢出。如果被测信号周期大于 ARR 对应的时长,两次捕获之间计数器会溢出,差值算出来是错的。解决办法是把 ARR 设到最大(65535 或 0xFFFFFFFF),或者用溢出中断累加高位。

6.2 PWM 输出与占空比

PWM 模式下,ARR 决定频率,CCR 决定占空比。频率公式和定时一样:f = 定时器时钟 / ((PSC+1) × (ARR+1))。占空比 = CCR / (ARR+1)。

做 LED 调光或者电机调速时,PWM 频率的选择有讲究。LED 调光一般 1kHz 以上,避免肉眼看到闪烁;电机驱动常用 10kHz 到 20kHz,避开人耳听觉范围(否则电机会啸叫);舵机控制是 50Hz,脉宽 0.5ms 到 2.5ms 对应角度。

我调舵机的时候,用 TIM1 的 CH1 输出 PWM,PSC=71(1MHz 计数),ARR=19999(20ms 周期,50Hz),CCR 从 500 到 2500 对应 0.5ms 到 2.5ms。这里 CCR 直接就是微秒数,因为计数时钟是 1MHz,调起来很直观。

6.3 FOC 里的 PWM 与定时器

做 FOC(磁场定向控制)的时候,PWM 定时器不只是输出波形,还要触发 ADC 采样。典型配置是:PWM 中心对齐模式,频率 10kHz 到 20kHz,在计数器过零点或峰值时触发 ADC 注入采样,保证采到的电流是 MOSFET 导通期间的稳定值。

中心对齐模式下,计数器先向上数到 ARR,再向下数到 0,一个完整周期是 2 × ARR 个计数。所以频率公式变成 f = 定时器时钟 / (2 × (PSC+1) × ARR)。这个 2 倍关系很多人第一次做 FOC 时会算错,导致 PWM 频率差一倍。

另外 FOC 需要死区时间,防止上下桥臂直通。死区时间由 BDTR 寄存器配置,单位是定时器时钟周期。比如 72MHz 下要 1us 死区,就写 72。死区太短会烧管子,太长会畸变波形,一般 IGBT 用 2us 到 5us,MOSFET 用 0.5us 到 1us。

6.4 定时器做 USB 设备时的角色

STM32 做 USB 设备时,定时器一般不直接参与 USB 协议,但常用来做 SOF(帧起始)同步或者超时检测。USB 全速设备每 1ms 一个帧,可以用定时器产生 1ms 中断来辅助管理端点缓冲。不过现在 HAL 库的 USB 中间件已经处理了这些,除非你要做特殊时序控制,否则不用自己动定时器。

7. 常见问题速查与排查思路

现象可能原因排查方法
定时周期整体偏大/偏小整数倍时钟源配置错误检查 RCC 配置,确认 HSE 起振,核对 APB 分频与定时器时钟倍频规则
定时周期差一点点PSC/ARR 忘记 +1重新按公式计算
定时周期抖动大中断里耗时操作用 GPIO 翻转测中断执行时间,精简中断代码
捕获值偶尔跳变计数器溢出或信号毛刺加大 ARR,开启输入滤波
PWM 频率不对中心对齐模式忘了 2 倍关系确认计数模式,重新计算
动态改 ARR 出现异常周期未开启预装载设置 ARPE 位
级联定时器不工作从定时器时钟源没配成 ITR检查 SMCR 寄存器 TS 位
定时器完全不计数时钟未使能或 CEN 未置位检查 RCC 使能位和 CR1 寄存器

再补几个我踩过的具体坑:

坑一:CubeMX 里改了时钟树但没重新生成代码。CubeMX 改时钟配置后,必须重新生成代码,否则 SystemClock_Config 还是旧的。我有次在 CubeMX 里把 HSE 从 8MHz 改成 12MHz,忘了重新生成,调了半天定时不对。

坑二:中断优先级配置冲突。定时器中断和串口中断优先级设成一样,串口接收大量数据时定时器中断被延迟,导致定时抖动。给定时器中断设更高的抢占优先级。

坑三:调试器暂停时定时器还在跑。用 Keil 或 ST-Link 调试时,断点停下后定时器可能继续计数,恢复运行时 CNT 已经跑飞。可以在 DBGMCU 寄存器里配置调试时冻结定时器。

坑四:低功耗模式下定时器时钟被关。进 STOP 模式后,APB 时钟停了,普通定时器不工作。要用 RTC 或 LPTIM 做低功耗定时唤醒。

8. 我个人的几条实操建议

调定时器这件事,说到底就是把“时钟从哪来、分频多少、数到多少”这三件事搞清楚。我现在的习惯是,每建一个新工程,先在 SystemClock_Config 里把各条总线的时钟频率注释写清楚,然后在定时器初始化旁边写上 PSC、ARR 的推导过程。这样过两个月回头看代码,不用重新推一遍。

另外,手边常备一个逻辑分析仪或者示波器。定时器这种东西,算得再对也不如测一下来得踏实。一个 GPIO 翻转,一个探头搭上去,实际周期一目了然。我早期靠“感觉”调定时,浪费的时间够买好几个分析仪了。

最后说个容易被忽略的点:不同型号 STM32 的定时器资源差异很大。F103 有 4 个通用定时器加 2 个高级定时器,F4 系列定时器更多,G0/G4 系列又有不同的时钟架构。换芯片的时候,别想当然地照搬参数,先翻一眼参考手册的时钟树章节,确认定时器挂在哪条总线上、有没有倍频规则。这一步花五分钟,能省掉后面五小时的调试。

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

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

立即咨询