news 2026/9/7 10:26:09

STM32定时器配置踩坑指南:PSC/ARR计算与时钟源陷阱解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32定时器配置踩坑指南:PSC/ARR计算与时钟源陷阱解析

1. 为什么定时时间总是不对:从一次“翻车”现场说起

先讲个真实经历。有一次我给一块板子做电机控制,定时器打算产生 10kHz 的 PWM,主频 72MHz,PSC 和 ARR 我算得明明白白,公式也背得滚瓜烂熟:频率 = 时钟 / ((PSC+1) * (ARR+1))。PSC 设为 71,ARR 设为 99,算下来正好是 72MHz / 72 / 100 = 10kHz。结果示波器一测,9.99kHz?不对,再仔细一看,实际波形频率是 4.98kHz 左右。我当场就愣住了,公式没错,代码看起来也没错,但输出频率整整少了一半。

后来排查了半天,发现问题根本不在 PSC 和 ARR 上,而是出在时钟源配置。STM32 的定时器时钟不是直接等于主频的,APB1 预分频器在里头做了手脚,尤其是 APB1 分频系数不为 1 的时候,定时器时钟会悄悄变成 APB1 的两倍。这个“隐形翻倍”就是无数人定时器算错的第一个坑。从那以后我养成了一个习惯:写定时器配置前,先花两分钟把时钟树捋一遍,绝不默认“主频多少定时器就是多少”。

这篇文章就是想把我在实际项目中踩过的、帮别人排查过的定时器配置问题系统整理一遍。标题里说的 PSC、ARR 和时钟源这三个地方,几乎覆盖了 90% 的定时器“算错”场景,而且每个坑的背后都有芯片设计逻辑在支撑,不是单纯的“粗心算错”。我会把原理、计算方法和实测验证全部串起来讲,适合正在用标准库或 HAL 库做 STM32 开发、被定时器频率搞到头大的朋友。

2. 时钟源这关过不去,后面全白算:APB1 预分频器的“隐形翻倍”陷阱

2.1 定时器时钟不是你想的那样直接从主频来的

很多新手看 STM32 的框图和时钟树,第一反应就是:系统主频 72MHz,那定时器肯定也是 72MHz 啊,直接套公式不就行了。这个理解对一部分定时器成立,对另一部分则不成立,而这部分恰恰是大家最常用的那批——TIM2、TIM3、TIM4、TIM5 这些挂在 APB1 总线上的通用定时器。

以经典 STM32F103 为例,系统架构是这样的:系统主频 SYSCLK 为 72MHz,AHB 预分频器默认不分频,所以 APB1 总线的时钟是 36MHz(APB1 最高只能跑到 36MHz),APB2 总线时钟是 72MHz(APB2 可以跑到 72MHz)。注意,定时器时钟的规则在这里开始“搞事情”了:当 APB1 预分频系数为 1 时,APB1 上的定时器时钟等于 APB1 时钟;当 APB1 预分频系数大于 1 时,APB1 上的定时器时钟等于 APB1 时钟的两倍。

这个规则意味着,在 F103 这种主频 72MHz 的芯片上,如果代码里把 APB1 分频配成了 2(大多数库函数默认配置就是 2,为了把 72MHz 降到 36MHz 给 APB1 外设用),那 TIM2/3/4/5 这些定时器的输入时钟实际是 36MHz 的两倍,也就是 72MHz。很多人不知道这个翻倍规则,以为定时器时钟是 36MHz,算出来的 PSC 和 ARR 自然就是错的,PWM 频率和定时中断周期全偏,而且往往是偏了一倍左右,极具迷惑性。

有人可能会问:为什么芯片要设计这个翻倍逻辑?其实是为了让定时器在高主频下获得更高的计数精度。APB1 总线为了低功耗把时钟降到了 36MHz,但定时器作为高性能外设,希望拿到跟系统主频一样的 72MHz 来做精细控制,于是芯片在定时器时钟路径上设计了一个倍频器。代价就是开发者在配置时得多想一步:我的定时器到底挂在哪个总线上?这条总线的预分频系数是多少?定时器时钟是否需要翻倍?

2.2 不同系列芯片的时钟树差异,比你想的更容易踩坑

我见过有人把 F103 的习惯直接套到 F407 上,结果定时器频率算出来完全不对。F4 系列的时钟树跟 F1 差异很大,F407 主频可以到 168MHz,APB1 最高 42MHz,APB2 最高 84MHz。同样存在“APB 预分频系数不为 1 时定时器时钟翻倍”的规则,但 APB1 总线频率变了,翻倍后定时器时钟变成 84MHz,而 APB2 上的定时器(TIM1、TIM8 等)翻倍后变成 168MHz。如果你拿着 F103 的 72MHz 经验去算 F407,第一步就把定时器时钟弄错了。

再比如 G0 系列和 L4 系列,时钟树和分频布局又不一样。G0 系列最高主频 64MHz,内部时钟结构经过重新设计,定时器时钟分配和 F1/F4 都有差异。所以跨系列移植定时器代码时,我强烈建议做一件事:打开对应型号的 Reference Manual,翻到时钟树那一页,把定时器时钟路径亲手捋一遍。千万别只看数据手册主频参数就完事,主频不代表定时器时钟。

下面我整理了一张实际项目中最常用配置下的定时器时钟速查表,方便各位对照:

芯片系列系统主频总线定时器时钟典型定时器
STM32F10372MHzAPB1(36MHz, ÷2)72MHz(36×2)TIM2/3/4/5/6/7
STM32F10372MHzAPB2(72MHz, ÷1)72MHzTIM1/8
STM32F407168MHzAPB1(42MHz, ÷4)84MHz(42×2)TIM3/4/5/6/7/12/13/14
STM32F407168MHzAPB2(84MHz, ÷2)168MHz(84×2)TIM1/8/9/10/11
STM32G07064MHzAPB1(64MHz, ÷1)64MHzTIM2/3/4/6/7
STM32G07064MHzAPB2(64MHz, ÷1)64MHzTIM1/17

注意上面 F407 那一行,很反直觉:APB1 总线上外设时钟是 42MHz,但挂在 APB1 上的定时器却能拿到 84MHz。这正好解释了为什么有人用 TIM3 做 1ms 定时,按 42MHz 算怎么都对不上,按 84MHz 一算就通透了。

判断定时器时钟的通用方法:查看你的工程里 RCC 相关配置(标准库是 SystemInit 里的 RCC_CFGR 寄存器设置,HAL 库是 SystemClock_Config 函数里的 APB1/APB2 分频参数),找到 APBx 预分频系数,如果大于 1,这个总线上的定时器时钟就是 APBx 时钟的两倍;如果等于 1,就不翻倍。

2.3 CubeMX 里那栏 Timebase Source 有多少人改错了

在用 CubeMX 配置工程时,有个选项叫 Timebase Source(时基源),默认是 SysTick。很多人不知道这个是干嘛的,我见过有人为了“省一个定时器”把 Timebase Source 改成了 TIM2 之类,结果整个工程跑起来 HAL_Delay 失效,现象还很诡异:初始化卡死、调 HAL_GetTick 一直返回 0。这是因为 Timebase Source 一旦改了,HAL 库的 tick 来源就变了,而你后续如果恰好也在用那个定时器做 PWM 或者编码器计数,必然冲突。

这个选项本身跟 PSC/ARR 的计算没有直接关系,但它能暴露一个很容易忽略的事实:同一个定时器的多个通道和多个功能,底层共享同一个计数器(CNT)和同一个预分频器(PSC)。你用 TIM2 的 CH1 做 PWM,同时又想用 TIM2 做编码器接口模式读取,这在硬件上是不可能的——CNT 只有一个,工作在编码器模式时它就干不了普通计数。类似的,把 Timebase Source 设成某个通用定时器,就等于告诉 HAL:这个定时器的中断和计数资源归系统调度管了,你最好别再拿去干别的。

我个人的习惯是:只要不是极端缺定时器,Timebase Source 永远保持 SysTick。SysTick 是内核自带的 24 位倒计时器,不占用外设定时器资源,专门为操作系统节拍和库函数延时设计的,这才是它最合理的用途。只有一种情况我会考虑改动:产品里需要超低功耗、要跑 RTOS 且任务切换周期需要精确可调时,再研究把 timebase 挪到更合适的定时器上,但那时我会非常小心地检查这个定时器是否与其他外设功能冲突,并在代码注释里明确标注“此定时器已被系统占用”。

3. PSC 和 ARR 的计算:公式背下来没用,单位换算才是第一个坑

3.1 别忘了 PSC 分频之后的计数频率才是 ARR 的地基

假设你现在已经把定时器时钟搞清楚是 72MHz 了,接下来就到了标题说的第二个高频出错点:PSC。PSC 是 16 位的预分频寄存器,作用就是把定时器时钟分频后作为计数器 CNT 的计数脉冲。很多人知道公式里要写“PSC+1”,也知道频率 = 定时器时钟 / ((PSC+1) * (ARR+1)),但一到实际配置就出各种奇怪问题。最常见的一种是:把 PSC 当成最终想要的信号频率了,或者把 PSC 的值直接当作分频系数用。

举个例子,我要产生一个 1kHz 的定时中断,定时器时钟 72MHz。很多人的第一反应是:1kHz 很慢,得大分频。于是 PSC 写 7199,然后套公式 72MHz / 7200 / ARR = 1kHz,算出来 ARR = 10。然后一测,中断频率是 1kHz 吗?是。那这个配置对吗?对了一半。问题在于:当 ARR 只有 10 的时候,计数器的计数周期非常短,更新事件频率很高,但中断里 CNT 的值变化范围只有 0~10,如果你想在中断里做一点需要计数的逻辑,这个 ARR 太小会带来一系列边界问题。当然这个场景下结果是对的,但下面这个场景就错了。

另一种更为隐蔽的错误是:把 PSC 配成了 7200-1 而不是 7200,即 PSC = 7199,然后 ARR 算成了 72MHz / 72000 / 1kHz = 1,结果定时器跑出来的中断频率根本不是 1kHz,而是 36.8kHz 左右。这种错误本质上是因为混淆了“分频系数”和“寄存器值”之间的关系。PSC 寄存器里的值 N 代表“计数 N+1 个时钟脉冲后输出一个脉冲”,也就是分频系数是 N+1,不是 N。这个“差 1”的坑在 ARR 上也有,后面会细讲。

我在实际项目里总结了一个比较稳的计算路径,按照这个路径走基本不会出问题:

  1. 确定定时器时钟频率 f_timer(根据第 2 节的时钟树方法判断)
  2. 确定你需要的计数频率 f_cnt 或者溢出频率 f_overflow
  3. 先选 PSC,原则是让 (PSC+1) 尽量大但不超过 65536,同时保证计数频率不要低到超出你的精度需求
  4. 再算 ARR = f_timer / ((PSC+1) * f_overflow) - 1
  5. 检查 ARR 是否在 0~65535 范围内,如果超出则增大 PSC,如果太小(比如小于 10)则减小 PSC,尽量让 ARR 落在几百到几千的量级,这样计数器计数分辨率相对合理

3.2 一个实际案例:72MHz 下产生 1kHz 中断,PSC 到底该填多少

就拿上面 1kHz 的场景展开算一遍。定时器时钟 72MHz,目标是 1kHz 的定时中断(也就是 CNT 溢出频率 1kHz)。计数器计数一个数需要 1 / 72MHz 秒,约 13.89ns。如果直接让 ARR = 71999,PSC = 0,也能得到 1kHz,但这样 CNT 需要数 72000 个脉冲才溢出一次,ARR 接近 16 位上限 65535,超过就直接出问题。所以必须用 PSC 先降频。

我习惯把 PSC 先固定在一个好算的值。比如 PSC = 7199,分频系数 7200,计数频率变成 72MHz / 7200 = 10kHz。然后 ARR = 10000 / 1000 - 1 = 9。这个配置算出来是完全正确的,实测中断频率就是 1kHz。但注意,这时候 CNT 只从 0 数到 9,10 个计数值约等于 1ms,精度上没有大问题,但如果中断里要做时间戳计算或者输入捕获,分辨率就不够了。

所以我更常用的做法是:让 ARR 保持在一个较大的数值,用 PSC 去粗调时间基准。比如 72MHz 时钟下,PSC = 71(分频 72),计数频率变成 1MHz,也就是计数一个数花 1 微秒。这样 ARR = 999 时,溢出周期正好 1ms。这个方案的好处是:CNT 的值直接对应微秒级的时间,做延时、输入捕获、脉冲计数时不用再做复杂的换算,调试时看 CNT 寄存器就能直观知道过了多少微秒。公式换算就是:溢出周期 = (PSC+1) * (ARR+1) / f_timer = 72 * 1000 / 72MHz = 1ms。

如果你需要的是 PWM 输出且要求频率特别精确,比如 20kHz,那就得让 PSC 尽量小,ARR 也不要太大,让计数分辨率尽量高。例如 PSC = 0,ARR = 3600 - 1 = 3599,72MHz / 3600 = 20kHz,这比 PSC 分频后再数几个数要精确得多。同等条件下,PSC 越小,ARR 越大,输出波形的频率精度越高,但它也会占用更多的 CNT 计数资源。道理不复杂:ARR 大意味着计数器数的数多,周期长,但每个计数的“脚步”是精确的;PSC 大相当于先大步走再去数,步子本身就有量化误差。

3.3 如果 ARR 超过 65535:用“分频优先 + 双定时器级联”的思路

很多人在做低频输出,比如 1Hz 的定时中断或者秒级延时,直接套公式会发现 ARR 需要几十万,超出了 16 位寄存器上限。这时候常见做法是调大 PSC,但 PSC 也只有 16 位,极限情况下 PSC+1 最大 65536,72MHz / 65536 约等于 1098Hz,再配合 ARR 上限 65535,最小溢出频率大概是 0.0167Hz 左右,60 秒一次,理论上还是够用的。但计算下来往往已经没有余量了,ARR 和 PSC 都接近极限值,有些场景下想要 1Hz,就会碰到 PSC 和 ARR 都逼近上限但组合出来差一点的情况,凑不出合适的整数分频。

这时候我建议改用两个定时器级联:第一个定时器输出一个较低频率的触发信号(TRGO),第二个定时器把这个触发信号作为外部时钟或触发源,再做一次分频计数。这种方式等于把分频能力从 16 位 × 16 位扩展到了 32 位级别的等效分频,而且两级的参数都能落在合理的寄存器范围内。代价是占用了两个定时器资源,代码复杂度也高一些。实际项目里,如果只是要秒级的时基,我一般更推荐用 RTC 或者直接以 SysTick 为基础写软件计数器,外设定时器留作更需要精确控制的场景。没必要跟寄存器上限死磕。

4. ARR 重装值差“1”:从 0 到 N 还是从 0 到 N-1,CNT 永远比你想象的少数一位

4.1 ARR=99 到底是多少个计数周期

标题里点的第三个容易算错的地方就是 ARR,这个坑比 PSC 的“差 1”更隐蔽,因为很多人虽然知道公式里有 ARR+1,但在代码里写的时候仍然会习惯性地把目标周期直接填成 ARR。比如前面 10kHz PWM 的例子,72MHz,PSC=71,计数频率 1MHz,想在 CNT 数 100 个脉冲后翻转一次输出,那 ARR 应该写多少?很多人的答案是 99,这是对的,因为 CNT 从 0 开始数,数到 99 一共经历了 100 个计数脉冲,第 101 个脉冲才触发溢出更新。但如果直接填 100,那 CNT 数到 100 溢出,实际计数周期是 101 个脉冲,输出频率变成 1MHz / 101 ≈ 9.9kHz,就差那么一点点。

这类“差 1”错误在低频场景下可能感觉不明显,但在高频 PWM 和需要精确时间基准的场景里立刻原形毕露。例如精确定时 1ms,72MHz,PSC=71,计数频率 1MHz,那 ARR 应该写 999,不是 1000。写 1000 的话,实际溢出周期是 1.001ms。在长时间累加时,1ms 误差会让 10 分钟的定时偏掉 60ms 左右,如果是多通道同步控制或者积分运算,误差会进一步累积。

要想彻底避免这个问题,我的建议是把公式在代码里固定成带“-1”的形式,并在注释里写明单位,不要再靠脑内换算:

// 定时器时钟 72MHz // 目标:PWM 频率 10kHz,占空比 50% // 分频:72MHz / (PSC+1) = 1MHz,即 1us 计数一次 // ARR: 1MHz / 10kHz - 1 = 99 TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_Period = 100 - 1; // 计数器从0数到99,共100个计数周期

HAL 库写法同理,句柄里的 Prescaler 和 Period 字段都存的是寄存器值,也就是分频系数减 1 和计数周期减 1,别被结构体字段名误导直接填目标值。

4.2 重装载寄存器在更新时刻的行为:立刻生效还是下一次生效

这一小节说的不是单纯的计算问题,而是很多人代码逻辑出错的地方:ARR 被修改后,到底什么时候生效?这取决于 TIMx_CR1 寄存器里的 ARPE(Auto-Reload Preload Enable)位。如果 ARPE=0,ARR 寄存器是透明的,你对它写入新值的下一个计数时钟周期就立刻生效,CNT 可能当前正在 500,你改成 300,它马上在 500 处就溢出了,而不是等到 300。如果 ARPE=1,ARR 的新值会被锁存到预装载寄存器,只有等到一次更新事件(溢出或软件触发的更新)发生时,才一次性加载到影子寄存器,生效时间点更可控。

这个差异在动态调整频率或占空比的场景中非常重要。举个例子,你在做软件串口或者呼吸灯,需要在中断服务函数里动态修改 ARR 来改变 PWM 周期。如果 ARPE=0,你写入的新 ARR 可能立即打乱当前计数周期,导致 PWM 波形出现一个异常的短周期或长周期脉冲。如果 ARPE=1,那么直到当前周期正常结束、产生更新事件之后,新 ARR 才会被加载,波形切换就干净利落。

标准库里默认很多时候 ARPE 是关闭的,HAL 库的 TimeBase 初始化默认也是关闭的( TIM_CLOCKDIVISION 和 auto-reload preload 是两回事,要单独设置)。如果你需要动态调速,建议显式打开 ARPE:

// 标准库 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 手动设置 ARPE:在时基初始化后 TIM_ARRPreloadConfig(TIM3, ENABLE); // HAL 库:初始化句柄时设置 htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

我以前做云台电机调速,直接用 PWM 频率控制转速,一开始 ARPE 没开,串口下发新频率时偶尔会听到电机“嗒”一下的抖动,后来发现就是更新事件时刻的 ARR 毛刺造成的。打开预装载功能后,调速曲线立刻平滑了,这次排查给我留下的印象很深:准确计算 PSC/ARR 只是第一步,寄存器生效机制才是决定动态行为是否正确的关键。

4.3 溢出中断里修改 ARR,为什么偶尔会晚一个周期才生效

继续说 ARPE 打开之后的现象。有些人会发现在中断服务函数里改了 ARR,但输出波形的频率变化晚了一个周期甚至更久才出现。原因是:由于预装载寄存器和影子寄存器的切换发生在更新事件(UEV)时,而更新事件发生在 CNT 溢出瞬间,你在更新中断里写入的是下一个周期的预装载值,而不是当前这个已经正在跑的周期。如果当前周期已经开始了,你写的新值只能在下一个周期开始前被锁存,然后下一个周期才生效,等于说从“写入”到“生效”需要等待当前周期结束再经历一个完整周期。

这个“晚一个周期”在很多工程应用里是完全可接受的,甚至更安全,因为它保证了波形的完整性。但如果你做的是高实时性控制,比如在线调整 PWM 频率去匹配电机谐振点,那这个延迟就可能引起短暂的振荡。解决办法是把 ARR 的修改放到更高优先级的中断或者 DMA 里提前更新,或者调整控制策略,把频率切换设计成渐变式,而不是阶跃式。这里不展开,但至少要知道:预装载机制是双向的,它既帮我们消除了毛刺,也带来了延迟。

5. 定时器配置的“隐藏菜单”:编码器模式、输入捕获和同步场景下的计算差异

5.1 编码器模式下的 PSC、ARR 含义完全不同,别拿 PWM 的逻辑套

标题虽然主要讲 PSC、ARR 和时钟源的计算错误,但在实际项目里,定时器在不同工作模式下,这三个参数的含义会发生变化。最常见的就是编码器接口模式。很多人第一次用 TIM 做编码器读取,直接沿用了 PWM 模式的配置思路,结果读出来的计数值完全不正常。

编码器模式下,定时器的 CNT 由外部编码器的 A、B 相脉冲驱动,PSC 依然作为分频器,但它的作用不再是把内部定时器时钟分频后送 CNT,而是对外部编码器信号本身做分频。ARR 在这里的作用是计数的上限/下限边界:当 CNT 计数到 ARR 时,如果编码器继续正转,CNT 会回绕到 0;如果编码器反转,CNT 会从 0 回绕到 ARR。所以 ARR 的值决定了编码器计数范围,也决定了计数溢出方向:在编码器模式下,溢出不一定是“从 ARR 回到 0”,也可能是“从 0 反向到 ARR”,具体取决于计数方向。这个方向性溢出需要靠更新事件和方向标志来综合判断,这比普通定时器的“数到 ARR 就溢出”复杂得多。

配置上的常见错误是:把 ARR 写成 9999 或者 0xFFFF 这类“看起来很大”的数,却不考虑编码器线数。实际上 ARR 的上限应该根据你的机械结构精度需求和 CNT 的位宽来确定。如果电机一圈编码器输出 1000 个脉冲,4 倍频后是 4000 个计数,那 ARR 至少要填 3999,否则计数还没转完一圈就回绕了,位置信息会丢失。反过来,如果 ARR 填得过大,CNT 的数值分辨率没问题,但你在计算角度或者位置时,对溢出事件的处理逻辑要更小心,因为反向回绕可能跨越多个 ARR 周期。

我自己的经验是:在编码器模式下,把 ARR 设置为整整一圈对应的计数值减一,并配合溢出中断做圈数累加,这样 CNT 的值直接就是当前圈内的相对位置,溢出不频繁还需要通过 SR 寄存器的方向位来判断是正向溢出还是反向溢出,逻辑最清晰。举个例子,编码器 1000 线、4 倍频,一圈 4000 个计数值,设 ARR = 3999。CNT 从 0 到 3999 是一整圈,从 3999 反向数回 0 也是完整的一圈。每次进入更新中断,检查 TIMx_CR1 的 DIR 位,如果 DIR=0 说明之前是正转溢出,圈数加 1;如果 DIR=1 说明反转溢出,圈数减 1。这套逻辑配合 CNT 当前值,就能精确还原绝对位置。

5.2 输入捕获测量频率/脉宽时,PSC 决定的是测量精度下限而不是上限

很多做数字测量的人用定时器输入捕获功能测外部方波频率和脉宽。这里的 PSC 配置思路和 PWM 又完全不同。输入捕获模式下,CNT 是自由运行的,靠外部信号的边沿触发捕获寄存器把当前 CNT 值存下来,两次捕获的 CNT 差值 × 单个计数周期 = 信号周期。所以计数频率(即 1 / 单个计数周期)决定了测量分辨率。如果 PSC 分频过大,计数频率太低,两次捕获的差值可能只有几个计数甚至相同,测量精度会急剧下降。

举个例子,你要测量一个 1kHz 的方波,定时器时钟 72MHz,如果 PSC = 7199,计数频率 10kHz,那一个方波周期对应 10 个计数。测量结果是 10 还是 11,取决于捕获瞬间相位,误差可能达到 10%,这对精确测量来说太粗糙了。合理做法是把 PSC 设小甚至设 0,让计数频率接近 72MHz,这样一个 1kHz 方波的周期对应 72000 个计数,测量分辨率在几十纳秒级别,精度提升几个数量级。

所以输入捕获模式下的 PSC 原则是:在 ARR 不溢出的前提下,PSC 尽可能小。如果被测信号频率很低,比如 0.5Hz,周期 2 秒,72MHz × 2 = 1.44 亿计数,ARR 16 位上限根本装不下,这时才需要拆解策略:要么适当增大 PSC 降低计数频率但牺牲分辨率,要么用定时器级联扩展测量范围,要么把测量拆成多段时间窗口拼接。用 PSC 去硬扛大范围测量,是最容易做但精度最差的做法,实际项目里我会先评估分辨率需求再决定。

5.3 多定时器同步启动:为什么都写在最后,但出来不同步

多定时器同步的需求在多电机控制、多路 PWM 相位对齐等场景很常见。需求是:两个或多个定时器在同一条时间线上开始计数,让它们的 PWM 输出严格同相或按预定相位差输出。很多人直接把每个定时器的 PSC、ARR 配置好,然后在代码里顺序调用启动函数,以为这样就行,实测发现两个通道相位对不齐,甚至一个已经开始第二个还没动。

因为定时器从“写配置”到“开始计数”之间的软件延迟不是固定的。函数调用、中断抢占、Flash 取指延迟都会导致两个定时器的启动时刻差出几十到几百纳秒。在一些对相位要求高的场景,这点延迟就是致命误差。

硬件上的标准解法有一个叫做“从模式触发”:选择其中一个定时器作为主定时器,开启它的 TRGO(触发输出),其他定时器配置为从模式,触发源选 ITR(内部触发输入),这样主定时器一旦开始计数,立刻通过硬件触发信号带动从定时器同步启动,几乎零延迟。CubeMX 里在定时器的 Slave Mode 下拉框里选择 Trigger Mode,然后配置 Trigger Source 指向主定时器的触发输出,就能实现硬件级同步。PSC、ARR 的计算逻辑不变,但启动顺序变成一句话:只要主定时器跑起来,其他的就跟着跑,不存在软件启动延迟。

6. 实测验证与排错流程:从示波器读数到中断标志位,逐步定位“算错”的环节

6.1 一套我常用的验证流程,照着做能省半天排查时间

先声明一个问题:很多人验证定时器配置,只用眼睛看板子上的 LED 闪烁频率或者用耳朵听蜂鸣器音调,这种验证方式精度太低,根本判断不了“差 1”级别的误差。有条件的话,我推荐用逻辑分析仪或示波器测 PWM 输出脚的实际频率,如果你的定时器配置了引脚输出和中断,直接测量引脚的波形是最直接、最高精度的验证方法。

我的标准验证流程是这样的:

  1. 先用数字万用表的频率挡确认 PWM 输出频率的大致范围。万用表频率挡精度有限,但能快速粗查有没有量级级错误。
  2. 再用逻辑分析仪或者示波器测量精确频率,同时观察波形占空比。如果是带记忆的示波器,能看频率计读数到小数点后两三位。
  3. 如果输出频率和设计目标偏差较大,先从时钟树重新检查定时器时钟频率,确认 APB 预分频系数和定时器翻倍逻辑。
  4. 然后打印或调试查看 PSC 和 ARR 的实际寄存器值,看代码最终写入的值是否符合预期。CubeMX 生成的代码可以在这里检查:htim3.Init.Prescalerhtim3.Init.Period
  5. 再检查 RCC 配置中时基时钟是否使能。比如用了 TIM3 却没在 RCC 里打开 APB1 的 TIM3 时钟,寄存器写进去也白写,而 CubeMX 生成代码一般自动处理好,但手动移植时特别容易漏掉这一条。
  6. 最后检查中断是否真的进入了。如果配置了更新中断但频率始终不对,可能中断没触发,也可能是中断标志位没清除。HAL 库里 HAL_TIM_IRQHandler 会自动清理,标准库则需要手动清除 TIM_ClearITPendingBit(TIMx, TIM_IT_Update)。

6.2 排查案例:一个“绝对算对了”的配置,为何频率还是偏低一半

这里我想还原一个真实排查过程,因为这种问题太典型了。有朋友用 F103,主频 72MHz,想用 TIM2 输出 1kHz 方波。他的配置是:PSC = 7199,ARR = 9,理论值 72MHz / 7200 / 10 = 1kHz。结果示波器一测,只有 500Hz,整整齐齐的一半。

他一开始怀疑公式不对,后来怀疑是不是寄存器没写进去,折腾了一个多小时。我让他打印 RCC 配置看 APB1 预分频系数,发现 SystemInit 配置里 APB1 分频是 2,APB1 总线 36MHz,按照翻倍规则,TIM2 时钟是 72MHz。到这里看起来没问题,公式也能对上。但等下,他用的定时器时钟真的翻倍了吗?不一定。F103 的 TIM2 属于 APB1 总线上的定时器,但还要注意另一个细节:定时器时钟翻倍只对通用定时器有效,而对基本定时器(TIM6、TIM7)没有影响。他用的 TIM2 属于通用定时器,规则适用,那为什么是 500Hz?

继续追查发现,他的初始化代码在 SystemInit 之后、RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE) 之前,先执行了一步错误的操作:把 APB1 的预分频改成了 4。可能原因是从别的例程复制而来的残留代码,或者 CubeMX 重新生成后被手动覆盖了。APB1 分频从 2 改成 4 之后,APB1 时钟变成 18MHz,翻倍后 TIM2 时钟变成 36MHz,比原来的 72MHz 少了一半。频率自然从 1kHz 变成 500Hz。

这个案例说明什么呢?定时器时钟的计算是链式的,任何一个环节的分频系数搞错,后面的 PSC/ARR 再对也没有用。排查时不能只看定时器配置本身,要从 RCC 时钟配置一路查到具体定时器时钟,任何一个环节都不能“默认是对的”。

6.3 常见错误汇总与自查清单

结合这些年的项目经验,我来一个定时器配置错误汇总表,方便大家对照排查:

错误类型具体表现根因分析
APB 预分频理解错误PWM 频率偏差 1/2 或 1/4没有考虑 APB 分频系数大于 1 时定时器时钟翻倍
PSC 差 1频率略高于目标值PSC 寄存器值代表分频系数减一
ARR 差 1频率略低于目标值ARR 寄存器值代表计数周期减一
PSC/ARR 混淆比例频率偏差很离谱把 PSC 当成了想要的时间长度,或把 ARR 当成分频系数
ARPE 关闭时动态调 ARR波形毛刺新值立即加载到影子寄存器,破坏当前周期
编码器模式下 ARR 过大位置回绕逻辑混乱ARR 没有对应一圈的实际计数值
输入捕获时 PSC 过大测量分辨率差计数频率太低,周期量化误差大
从模式触发器未配置多定时器启动不同步软件顺序启动有延迟,没用硬件 TRGO/ITR 同步

自查的时候,我建议把公式写在一张便签上贴在显示器旁边,每次配置定时器先按这个顺序过一遍:定时器时钟 → PSC → ARR → ARPE → 中断使能 → 启动。尤其是定时器时钟这一步,没有确认清楚之前不要动 PSC 和 ARR。

7. 一点个人建议:把 PSC 和 ARR 的计算从“心算”变成“函数”

文章写到这里,两个多小时了,但我还想分享一个能长期提升效率的做法:与其每次在主程序里心算 PSC 和 ARR,不如封装一个统一的计算函数。我在自己的工程里习惯抽象出类似这样的结构:

// 根据目标频率 Hz 自动计算 PSC 和 ARR // 传入:timer_clock(定时器时钟频率)、target_freq(目标频率) // 传出:psc、arr 指针 uint8_t TIM_CalcPscArr(uint32_t timer_clock, uint32_t target_freq, uint16_t *psc, uint16_t *arr) { // 先假定 PSC 为 0,即计数器直接以 timer_clock 计数 // 如果 ARR 超出 16 位范围,则逐步增大 PSC uint32_t psc_val = 0; uint32_t arr_val = 0; while (psc_val <= 65535) { arr_val = timer_clock / ((psc_val + 1) * target_freq); if (arr_val >= 1 && arr_val <= 65535) { *psc = (uint16_t)psc_val; *arr = (uint16_t)(arr_val - 1); return 0; } psc_val++; } return 1; // 目标频率太低,超出单个定时器能力 }

有了这类函数之后,我在配置定时器时很少再手算,只需要保证传入的 timer_clock 是从第 2 节时钟树流程确认过的数值,就不会再犯低级的“差 1”错误。当然,这个函数只是粗略的分频方案,如果你的场景有特殊的精度或同步要求,还是要根据前几节的原理手动挑选 PSC 和 ARR 的组合,尤其是输入捕获和编码器模式,不能一概而论。

说实话,PSC、ARR、时钟源这三个词单独看都不难,但放在一起就是 STM32 开发里最容易出错的地方。我见过太多人卡在定时器频率不对的问题上,最后发现不是公式没背熟,而是某个前缀配置或某个“差 1”细节没注意到。希望这篇文章能帮大家把这几个坑一次性填平,少走几步弯路。下次如果还有人跟你说“我定时器算得可准了”,你可以笑着问他一句:那你 APB1 预分频系数是多少,定时器时钟翻倍了没有?

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

CMSIS-DSP源码深度审计:FFT/FIR优化与工业落地实战

我得先说明一个感受&#xff1a;做Cortex-M嵌入式开发的工程师&#xff0c;几乎没有人没听说过CMSIS-DSP&#xff0c;但真正打开过这个库源码、一行一行读过实现的人&#xff0c;少之又少。大多数人停留在"调API"的层面。我之所以花整块时间去做一次源码级的梳理&…

作者头像 李华