1. 时钟源怎么算:APB 分频系数才是最大“暗坑”
1.1 定时器时钟为什么不是 72MHz:APB 分频器的“翻倍”逻辑
先说一个我见过无数次的场景:有人用 STM32F103 写定时器,主频 72MHz,想做一个 1ms 中断,于是直接套公式72MHz / 1000 = 72000,然后PSC = 7199, ARR = 9,结果一测中断间隔是 2ms,怎么调都不对。问题不在 PSC 和 ARR,而在最容易被忽略的时钟源。
大多数 STM32 的定时器时钟并不直接等于芯片主频,它挂在 APB1 或 APB2 总线上,中间隔了两层分频器。以经典的 STM32F103 为例:SYSCLK(最高 72MHz)先经过 AHB 预分频得到 HCLK,HCLK 再分频得到 APB1 时钟(PCLK1)和 APB2 时钟(PCLK2)。问题在于,当 APBx 的预分频系数不等于 1 时,挂在该总线上的定时器时钟会强制翻倍,也就是说定时器的实际输入时钟等于PCLKx × 2。
很多人只盯着“APB1 最大 36MHz”这个限制,以为 TIM2 到 TIM4 的时钟就是 36MHz,结果所有时间参数都按 36MHz 算,实际却跑在 72MHz,中断频率直接翻倍。反过来,如果用了标准库的默认配置,APB1 预分频系数是 2,PCLK1 = 36MHz,但 TIM2 的时钟确实是 72MHz。这个“翻倍规则”写在了参考手册的时钟树图里,可惜很多教程一句话带过,导致它成了定时器算错的第一大来源。
要彻底搞懂,只需要记住一张图的关系:SYSCLK → AHB 分频 → APBx 分频 → 定时器时钟。只有当 APBx 分频系数为 1 时,定时器时钟才等于 PCLKx;否则定时器时钟等于PCLKx × 2。F1 系列里 APB2 上挂的是 TIM1 和 TIM8,APB1 上挂的是 TIM2/3/4/5/6/7,所以不同总线上的定时器,时钟源可能完全不同。
1.2 用 1ms 定时中断实测一个时钟源案例
与其背手册,不如直接实测。下面这段代码是我常用的“时钟源验证法”:用 TIM2 产生 1ms 中断,在中断里翻转一个 GPIO,然后用示波器或逻辑分析仪看波形,周期对不对一目了然。
// 以 STM32F103 标准库为例,假设 SystemInit 已经配置好 72MHz 主频 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); TIM_TimeBaseInitTypeDef tim; // 关键:TIM2 挂在 APB1 上,APB1 预分频 = 2,所以 TIM2CLK = 36MHz x 2 = 72MHz // PSC = 71 -> 72MHz / (71+1) = 1MHz // ARR = 999 -> 1MHz / (999+1) = 1kHz,即 1ms 中断 tim.TIM_Prescaler = 71; tim.TIM_Period = 999; tim.TIM_ClockDivision = 0; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &tim); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); // NVIC 配置略 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); GPIOA->ODR ^= GPIO_Pin_0; // 翻转,实测周期应为 2ms,高电平 1ms } }如果你把TIM_Prescaler改成 35,TIM_Period改成 999,得到的还是约 1ms,因为36MHz / 36 × 1000 = 1kHz。但如果你误以为 TIM2CLK 是 36MHz,用PSC = 35, ARR = 999,计算结果其实和 72MHz 下PSC = 71是一样的。很多“好像定时挺准”的感觉就是这么来的——参数不同,结果碰巧相同,反而让人更难发现时钟源搞错了。
我建议所有人在写定时器之前,先用这个 GPIO 翻转法确认一遍时钟源,成本不到五分钟,却能省下后面排查时间问题的几个小时。
1.3 不同系列的时钟树差异:F1、F4、H7 各玩各的
如果你只在 F1 上跑过,换到 F4 或 H7 时还沿用“APB1 分频不等于 1 则定时器时钟翻倍”的规律,大概率会再踩一次坑。这个规律的大方向在 F4 上也成立,但 F4 的 APB1 最高是 42MHz(在 168MHz 主频下),APB2 最高 84MHz,预分频系数为 4 时定时器时钟同样翻倍到 84MHz,细节依然要看具体时钟树。
到了 H7 系列,情况更复杂。H7 的主频更高,总线分为 AHB1、AHB2、AHB3,APB1/APB2 的分频和内核电压、电源域都有关系,定时器时钟有独立的 RCC 时钟源选择位,甚至有些定时器可以选 PLL2 或 CSI 作为时钟源。这时候再靠“经验公式”就不好使了,必须打开芯片对应的参考手册时钟树图,逐级确认。
一个实用建议:工程里尽量定义一个宏或常量来表示定时器时钟频率,比如#define TIM2_CLK 72000000UL,这样换芯片时只要改一处,不用满工程找魔法数字。我见过很多工程把 72000000 直接写进公式里,换平台时漏改一个就出大问题。
2. PSC 和 ARR:你算错的关键在“+1”和 16 位边界
2.1 溢出时间公式:为什么 PSC 和 ARR 都要加 1
定时器溢出时间的基础公式是:Tout = (PSC + 1) × (ARR + 1) / TIMxCLK。这里最容易被忽略的就是两个+1。注意这里的 PSC 和 ARR 都是寄存器值,它们从 0 开始计数,所以实际参与分频的系数必须加 1。拿 72MHz 时钟举例:PSC = 71, ARR = 999时,实际分频系数是 72,自动重载值是 1000,算出的是72 × 1000 / 72MHz = 1ms。
如果你忘了加 1,直接用71 × 999 / 72MHz,得到约 0.98625ms,误差约 1.4%。单独看一个定时器好像影响不大,但用在 PWM 频率、捕获测量、多定时器同步时,误差会被放大,而且这种误差是“有规律但不好查”的那种,最折磨人。
更隐蔽的是,有些人习惯把公式记成“PSC + 1就是分频系数”,但在 CubeMX 的界面上填数字时又开始犯迷糊。CubeMX 的 TIM 配置页面里,PSC 和 ARR 填的就是寄存器值,也就是说如果你想分频 72 倍,就要填 71。而 Stall 库里如果你调用__HAL_TIM_SET_PRESCALER(&htim, 71),它设置的也是寄存器值。这一点务必养成肌肉记忆。
2.2 PSC 的“0”不等于关闭分频器
不少新手会问:PSC 填 0 是不是就不分频了?从结果上看,PSC = 0 表示预分频系数为 1,定时器时钟直接给计数器,确实是不降频。但从寄存器语义上讲,没有“关闭预分频器”这个操作,预分频器永远在工作,只是分频系数是 1 而已。
这里还有一个容易忽略的细节:STM32 的预分频器是缓冲型的。也就是说,运行中修改 PSC 寄存器的值,并不会立刻生效,要等到当前计数周期结束、更新事件产生后,新的预分频值才会被装入。调试 PWM 动态调速时,经常有人发现改了 PSC 没反应,其实是没触发更新事件,或者没等更新。此时可以用软件产生更新事件:先设置TIM_PSC,再调用TIM_GenerateEvent(TIMx, TIM_EventSource_Update)强制重新装载。
另外要注意,PSC 和 ARR 都是 16 位寄存器,最大只能写到 65535。如果你试图写入更大的值,标准库或 HAL 库通常会做截断处理,但结果不是你想要的。比如想写 100000,结果实际写入的是 34464(100000 对 65536 取模),定时时间完全乱套。这种问题在代码里极难发现,因为寄存器写进去的值不报错,但行为完全不对。
2.3 超过 16 位量程怎么办:大 PSC 与级联/32 位定时器
当你需要的定时周期很长,ARR 已经到 65535 还不够时,第一个想法是继续加大 PSC。比如 72MHz 时钟下,PSC = 65535, ARR = 65535的溢出周期是65536 × 65536 / 72MHz ≈ 59.65s,做秒级定时完全够用。
但这里有个精度陷阱:PSC 越大,计数器的每个计数周期越长。以PSC = 65535为例,计数器每跳一次就代表 65536 个时钟周期,约 0.91ms。这时你实际上已经无法通过 ARR 做到毫秒以下级别的精细控制,因为 ARR 的调节粒度被 PSC 拉大了。比如想从 59.65s 调到 59.64s,ARR 减小 1,相当于时间缩短约 0.91ms,能调,但已经无法平滑微调。
所以正确思路是:先按目标时间确定 PSC,让计数频率尽量是一个“整”数,再选 ARR。比如目标 1 秒,72MHz 下可以选PSC = 7199(计数频率 10kHz),ARR = 9999,这样每秒进一次中断,误差取决于晶振本身,而不是寄存器取舍。反过来,如果选PSC = 65535,即使 ARR 取整到 65535,时间也不是正好 60 秒,只是约 59.65 秒。
如果你既想要长周期,又想要高精度,F1 系列里 TIM2 和 TIM5 是 32 位定时器,ARR 可以写到 4294967295。比如 72MHz 下,PSC = 71, ARR = 71999999,溢出周期是 1000 秒级别,同时计数粒度还是 1μs,这才是长定时的最佳方案。再不够就做定时器级联,把上一个定时器的溢出事件作为下一个的时钟源,但寄存器配置复杂度明显上升,非必要不建议一上来就玩级联。
3. 从 PWM 频率反推参数:“四舍五入”最容易翻车
3.1 从目标频率反推 PSC/ARR 的思考过程
PWM 频率的公式和溢出时间公式一模一样:PWM_Freq = TIMxCLK / ((PSC + 1) × (ARR + 1))。假设 TIMxCLK = 72MHz,想要 20kHz 的 PWM,如果PSC = 0,那么ARR + 1 = 72MHz / 20kHz = 3600,即ARR = 3599。此时占空比调节分辨率是 3600 级,大约 11.8 bit,够用了。
但如果你想要 1kHz 的 PWM 保持同样分辨率,还是PSC = 0,ARR = 71999,超出了 16 位上限。这时只能加大 PSC,比如PSC = 71,计数频率变成 1MHz,ARR = 999,占空比分辨率降到 1000 级。也就是说,PWM 频率和占空比分辨率是一对矛盾:频率越低,要么牺牲分辨率(加大 PSC),要么动用 32 位定时器。
我见过不少人在这一步“四舍五入”翻车。比如目标频率是 1000Hz,有人算出来ARR + 1 = 72000,然后直接把 ARR 填 72000,发现 16 位寄存器根本装不下,写进去被截断成 6464,实际频率完全不对。如果寄存器值是 16 位,填 72000 这个动作本身就是 bug,而不是“近似”的问题。
正确的做法是:先确认目标频率和定时器时钟是否匹配。如果算出来的ARR + 1大于 65536,要么提高 PSC 降低计数频率,要么换 32 位定时器,要么调整目标频率。在实际产品里,电机 PWM 常用 20kHz 是因为高于人耳听觉范围且驱动频率合适;LED 调光常用几 kHz 到几十 kHz 是为了避免频闪。定频率之前,先搞清楚你的负载需要什么频率,再定参数。
3.2 100% 占空比异常:一个真实的“算错”现场
热搜词里有一条“stm32定时器输出pwm时100%占空比异常”,这个问题我踩过,也帮别人排查过,非常典型。先交代背景:用 PWM1 模式,有效电平是高电平,ARR = 999,CCR 控制占空比。正常情况下 CCR 从 0 到 999 变化,占空比从 0% 到 100%。很多人想让 PWM 输出 100% 占空比,就把 CCR 写成 1000,也就是ARR + 1。
问题来了:ARR 的计数值范围是 0 到 999,当计数器计到 999 后溢出回 0。如果你把 CCR 设为 1000,计数器永远到不了 1000,电平比较事件不会发生,输出的实际行为取决于极性配置。在一些配置下,结果是输出一直保持高电平,这看起来像“100% 占空比”,但如果你在运行中动态把 CCR 从 1000 调回 500,会发现波形切换异常,或者某一段时间内输出不可控。
要稳定输出 100% 占空比,直接把 CCR 设为 ARR 即可。比如ARR = 999, CCR = 999,计数器计数到 999 时比较匹配,输出保持高电平,到了溢出更新事件阶段仍然保持有效电平,行为是稳定的。反过来,0% 占空比则把 CCR 设为 0,但如果有效电平是高电平,CCR = 0 时计数器从 0 开始就等于匹配,行为也比较特殊,建议实际用示波器确认一次。
这类问题的本质是边界条件,而边界条件恰恰是“手算参数”最难覆盖的部分。所以我一直建议:PWM 的 CCR 动态调整范围要控制在 0 到 ARR 之间,不要把 ARR 以外的值写进去。
3.3 用 CubeMX 做交叉验证,但别被界面误导
CubeMX 能帮你省很多事,但前提是你知道它到底帮你做了什么。在 CubeMX 的时钟树页面上,你可以直观地看到 APB1、APB2 分频系数和最终定时器时钟频率,这比对着参考手册算省力很多。但注意,TIM 配置页面的 PSC 和 ARR 输入框填的依然是寄存器值,不是实际分频系数,也不是目标时间值。
这导致一个很有意思的现象:CubeMX 的“计算辅助”给了它对应的公式参考,但它不会带你避开“16 位溢出”和“PSC 选多大合适”的坑。比如你填PSC = 0, ARR = 71999,它可能不提示你 ARR 超限,生成代码后实际运行就出问题。
所以我的建议是:先用 CubeMX 看时钟树确认 TIMxCLK,再自己手算一套 PSC/ARR,最后在 CubeMX 里验证。如果你在 CubeMX 里算出的结果和手算结果不一样,多半是时钟树理解错了,优先检查 APBx 分频系数,再看定时器挂在哪条总线上。CubeMX 是“交叉验证工具”,不是“替代思考工具”。
4. 常见问题与排查技巧实录
4.1 中断频率变成一半或两倍:先查 APB 分频与倍频
如果你的定时器中断频率和预期差了一倍,大概率问题在时钟树而不是 PSC/ARR。最典型的情况是:标准库工程里SystemInit已经把主频配成 72MHz,但 APB1 预分频是 2,PCLK1 = 36MHz,而你以为 TIM2 时钟就是 36MHz,于是按 36MHz 算参数。结果实际时钟是 72MHz,中断频率是预期的两倍。
反过来,如果你从某个例程里复制了一段代码,例程配置的 APB1 预分频是 1(PCLK1 = 72MHz),而你的工程里改成了 2,那么定时器时钟同样会变化。这种“代码能跑但时间不对”的故障最难查,因为编译不报错,逻辑也没问题。
排查方法很简单:在中断里翻转 GPIO,用示波器数一下实际频率。如果频率刚好是预期的 2 倍或 1/2,基本可以锁定时钟树配置问题。再用调试器看RCC->CFGR寄存器的PPRE1、PPRE2位,确认 APB 分频系数,就能准确定位。
4.2 定时时间“慢半拍”的排查清单
我把自己调试定时器常遇到的坑汇总成了一张清单,碰到问题按顺序查,大部分都能解决:
| 检查项 | 具体做法 |
|---|---|
| 时钟源 | 确认 TIMx 挂在 APB1 还是 APB2,确认 APBx 分频系数是否为 1,是否触发定时器时钟翻倍 |
| PSC/ARR 是否 +1 | 用公式Tout = (PSC+1)x(ARR+1)/TIMxCLK反算一遍 |
| 16 位溢出 | 检查 PSC、ARR、CCR 是否超过 65535,PWM 的 CCR 是否超过 ARR |
| 预分频缓冲生效 | 运行中改 PSC 是否触发更新事件,必要时手动TIM_GenerateEvent |
| 中断/NVIC | 检查是否开启对应中断、NVIC 优先级是否配置、中断标志是否清除 |
| 定时器被占用 | 确认同一个定时器没有被 GPIO 复用、DMA、编码器模式等同时占用 |
这条清单里的每一项我都实际遇到过。例如运行中修改 PSC 没生效,就是因为更新事件没有触发;CCR 超过 ARR 导致占空比异常,是因为边界条件没考虑;定时器被复用,是因为初始化 GPIO 时把定时器引脚和普通输出引脚冲突了。这些坑,单独看都不难,但在一个复杂工程里同时出现时,会让人排查到怀疑人生。
4.3 我的三板斧:手算、实测、日志验证
最后分享我自己做定时器相关功能的固定流程:
第一步,手算。不管用不用 CubeMX,我都会先在草稿纸上把时钟树理清楚:主频是多少,AHB 分频多少,APB1/APB2 分频多少,TIMxCLK 是多少,然后代入 PSC/ARR 公式。这个过程不用很久,但能逼着自己把逻辑想清楚。
第二步,实测。写一个最小测试工程,用定时器中断翻转 GPIO,示波器测波形。波形对了再往下写业务逻辑,波形不对先排查参数。这一步能解决 90% 的“我觉得我算对了”问题。不要嫌麻烦,示波器比人脑可靠。
第三步,日志验证。如果是在调试电机控制或通信协议,定时器参数会影响业务逻辑,我会在关键事件打时间戳,用串口打印出来,对比实际时间间隔和预期是否一致。比如 PID 控制周期预期是 1ms,实际跑出来 1.5ms,问题可能出在中断响应延时或阻塞操作,而不是定时器本身。
这套流程帮我少走了很多弯路。我见过不少人一上来就写完整业务代码,最后定时不准,排查了半天才发现是最初的定时器时钟源就算错了。基础参数的验证,越早做成本越低。