1. 为什么你按公式算的定时器参数,上机总是差那么一点
做嵌入式这些年,定时器是 STM32 里用得最频繁的外设,但也是翻车率最高的外设。PSC(预分频器)和 ARR(自动重装载值)的计算公式,几乎每个教程都会写:定时频率 = 时钟源频率 / ((PSC + 1) * (ARR + 1))。不少朋友照着公式算完,下载到板子上却发现现象完全不对——要么中断来得太快,要么 LED 闪烁频率肉眼可见地偏了,甚至干脆没反应。
问题通常不出在公式本身,而在于三个极易被忽略的地方:PSC 的取值边界、ARR 的计数模式差异、时钟源到底是从哪条总线来的。今天我就把这三个坑逐个拆开讲,每个都会给出原理分析、计算过程和一个可以直接抄的验证方法。这不仅是给刚入门的新手避坑,很多做了两年以上的开发者也未必能把时钟树里那几级分频关系说清楚。
先明确一个前提:下面所有讨论基于 STM32F1 和 STM32F4 系列,但结论对 GD32、AT32 等国产替代芯片同样生效,因为它们的外设框架高度兼容。文中会用到标准外设库和 HAL 库两种写法,涉及寄存器时会给出参考手册的章节位置,方便你直接查阅。
2. 时钟树里的“隐形乘法器”:PSC 计算的源头就不对
定时器的时钟并不直接等于系统主频。在绝大多数 STM32 芯片上,定时器时钟从 APB1 或 APB2 总线来,而这两条总线的前级还有一个 AHB 预分频器。关键点来了:当 APB 预分频系数不等于 1 时,定时器时钟会被强制倍频,倍频系数是 2。这是 STM32 参考手册里写得明明白白、但经常被跳读的一句话。
2.1 APB 分频与定时器倍频的联动关系
以 STM32F103 为例,系统主频 72MHz,AHB 不分频,APB1 最高只能跑到 36MHz,所以 APB1 预分频器要设成 /2。此时挂在 APB1 上的 TIM2/3/4/5/6/7,它们的时钟输入不是 36MHz,而是72MHz。
为什么?因为芯片内部给定时器设计了一个倍频器:当 APB 分频系数 > 1 时,定时器时钟 = APB 时钟 × 2。这套设计的目的是让定时器能在较高的时钟频率下工作,而不受 APB 总线低速的限制。
但很多人只记住了“APB1 是 36MHz”,于是计算 PSC 和 ARR 时直接把这个错误的值代入。算出来的结果差了一倍,现象就是中断频率或 PWM 频率全部偏慢。
再举一个 F4 的例子:STM32F407 主频 168MHz,APB1 最高 42MHz,所以 APB1 预分频为 /4。这时 APB1 上的定时器时钟就是 42MHz × 2 = 84MHz;APB2 最高 84MHz,预分频为 /2,APB2 上的高级定时器 TIM1/TIM8 和通用定时器 TIM9~TIM11 的时钟就是 84MHz × 2 = 168MHz。
看明白了吗?不同总线上的定时器,即使全片共用一个主频,实际时钟也可能完全不同。我见过一份代码,TIM2 用 72MHz 算、TIM1 也用 72MHz 算,结果前者对了后者翻倍错乱。
2.2 利用 SystemClock_Config 反推定时器时钟
如果你用的是 CubeMX 生成的工程,有一个很笨但很有效的办法:打开SystemClock_Config()函数,逐行看 RCC 的配置:
static void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }对照时钟树,可以一步步推:
- HSE = 8MHz,PLL 倍频 9 → SYSCLK = 72MHz
- AHB 不分频 → HCLK = 72MHz
- APB1 分频 /2 → PCLK1 = 36MHz,定时器时钟 = 36 × 2 = 72MHz
- APB2 分频 /1 → PCLK2 = 72MHz,定时器时钟 = 72MHz
所以对于 F103,绝大多数定时器最终时钟都是 72MHz。对于 F407 这类更高主频的芯片,一定要按上面 F4 的例子重新推导一遍,不要沿用 F1 的结论。
2.3 CubeMX 里怎么看实际时钟数值
用 CubeMX 打开工程,在“Clock Configuration”页面里,把鼠标悬停在 APB1 Timer Clocks 或 APB2 Timer Clocks 的字段上,IDE 会直接显示该总线上定时器的时钟频率(比如 72 MHz 或 84 MHz)。这里有两点要提醒:
- 必须确认自己选的是“Timer Clocks”而不是总线的“APB1 peripheral clocks”,两者数值差一倍是常态。
- 国产芯片用 CubeMX 或各家自研的图形化配置工具时,同样界面里显示的就是定时器实际输入时钟,可直接用来计算。
3. PSC 和 ARR 之间那条被忽略的“隐藏规则”
PSC 和 ARR 的配置看似只是两个整数,但它们之间存在一条几乎没有人专门讲、却直接影响计数精度的规则:PSC 的改变会立即生效,而 ARR 的改变在重复计数模式下需要等到下一个更新事件才会有保障地生效。这条规则决定了你在运行中动态调整参数时,是“这次就变”还是“下次才变”。
3.1 向上计数模式下 PSC 的生效时机
STM32 的预分频器有一个缓冲寄存器。你写 PSC 时,真正起作用的是它的影子寄存器,影子寄存器在什么时刻装载新值,取决于计数器的当前状态。参考手册里有一段原话翻译过来是:预分频器被写入后,要到下一个更新事件才会装载进影子寄存器。在向上计数模式下,更新事件由计数器溢出产生。
这意味着什么呢?假设你正在跑一个 1kHz 的中断,在中断回调里把 PSC 改小,希望立刻加速到 2kHz。但实际效果是:当前这个计数周期还按旧的 PSC 走,下一个周期才用新值。如果你要求“从下一拍开始精确变化”,就必须靠软件先触发一次更新事件(UG 位),把影子寄存器强制刷新:
TIM2->PSC = 71; // 新预分频值 TIM2->EGR = TIM_EGR_UG; // 强制产生更新事件,装载影子寄存器3.2 ARR 的缓存机制与 PWM 输出毛刺
自动重装载寄存器同样有影子寄存器。但更有意思的是,如果关闭了 ARPE 位(Auto-Reload Preload Enable),新写入的 ARR 值会直接生效;如果开着 ARPE,就必须等更新事件。很多人在做 PWM 调速时,一边开着 ARPE 一边直接改 ARR,期望当前周期立刻变脉宽,结果波形却变了半个周期甚至一个周期。
不要小看这一个 PWM 周期的延迟。在电机控制里,电流环的 PWM 更新周期通常是 10kHz,一个周期的延迟看起来不多,但叠加了 PID 运算延迟和采样延迟后,相位裕度会被吃掉好几度,高速运转时容易引发电流振荡。正确的做法是:在需要逐周期更新占空比的应用里,关闭 ARPE 或配合更新事件触发 DMA 写入 ARR。
3.3 边界情况:PSC = 0 与 PSC = 65535
PSC 是 16 位寄存器,取值范围 0~65535,对应分频系数 1~65536。两个极端都有实际应用,但易错点不同:
- PSC = 0:分频系数为 1,定时器时钟直接送给计数器。此时如果 ARR 也设得很大,计数器溢出周期极长,配合 32 位定时器(TIM2/TIM5)可以做长时间测量。但要注意,PSC=0 时功耗相对较高,计数器的翻转频率就是定时器时钟本身。
- PSC = 65535:分频系数为 65536,定时器时钟被压得非常低。在低频 PWM 输出(比如 1Hz 左右的慢闪)时很常用,但前提是 PSC + 1 的乘法不溢出。曾经有人把 PSC 设成 65535、ARR 设成 65535,期望得到极低的频率,结果算下来发现(65536 × 65536)超出了定时器的计数范围,时序完全错乱。这种场景应该用
PSC 大 + ARR 小或PSC 小 + ARR 大的组合,而不是两个都顶到上限。
4. 时钟源选错,所有计算全部白搭
算 PSC 和 ARR 之前,有一件事必须先确认:这个定时器用的时钟源是内部时钟、外部时钟模式 1,还是外部时钟模式 2。默认情况下定时器使用内部时钟(CK_INT),也就是上面说的 APB 总线倍频后的时钟。但很多高级应用会在初始化时把时钟源切到外部引脚,如果忘记了这茬,后面按内部时钟算的参数全部失效。
4.1 外部时钟模式的本质:计数器的“时钟输入”变了
外部时钟模式 1(TIM_TS_Input_TI1 或 TI2)允许把某个引脚的边沿当作计数器的时钟。此时定时器不再按固定的内部频率计数,而是每个外部脉冲计一次。PSC 在这里依然有效——它对输入的外部脉冲做分频。ARR 则决定了多少个外部脉冲算一轮。
这个模式下最大的误用场景是测频率。假设你要测量一个 PWM 信号的频率,常规思路是把被测信号接到定时器的捕获通道,开启捕获中断,在中断里读取 CCR 寄存器求周期。但如果这时候有人把定时器配置成了外部时钟模式,CNT 的“时间基准”就不再是芯片主频,而是被测信号本身。此时 CCR 读到的不再是周期对应的计数值,而是完全无法解释的随机数——因为你在拿一个“以被测信号为时钟”的计数器去测同一个信号。
4.2 外部时钟模式 2:和模式 1 的区别到底在哪
外部时钟模式 2 只适用于 TIM1 和 TIM8 等少数定时器,它使用 ETR 引脚作为时钟输入。与模式 1 相比,模式 2 不需要经过触发选择器,信号路径更短,理论上抖动更小、频率上限更高。
但这两种模式的共同点是对输入信号的最小脉冲宽度有要求。参考手册明确规定:外部时钟输入的高电平和低电平各自至少要保持 2 个定时器时钟周期,否则可能漏计数。也就是说,虽然你能用外部时钟接到很高的频率,但定时器本身的主频必须足够高,才能保证采样到每一个边沿。
实测过用 TIM1 的外部时钟模式 2 去数一个约 5MHz 的脉冲信号,定时器时钟 72MHz,结果丢数严重。后来把分辨率降下来、信号改成 1MHz,才恢复正常计数。所以外部时钟不是想接多快就有多快,定时器主频至少要是被测信号频率的 4 倍以上,最好留 10 倍余量。
4.3 时钟失联时的典型表现
时钟源配置错,最常见的现象有这几种:
- 定时器中断不触发,但程序不卡死(时钟源是空的,计数器根本没有时钟来源)
- 定时器“偶尔动一下”,间隔毫无规律(外部时钟引脚悬空,受到干扰触发计数)
- PWM 完全没有输出,但代码逻辑看起来一切正常(定时器时钟源被设成了外部时钟,而外部引脚没有信号输入)
排查这些现象的思路比较简单:先看 RCC 对应定时器的时钟有没有使能,再看 TIMx_SMCR 寄存器里的 SMS 位和 TS 位是不是被改成了外部模式。如果是用 HAL 库,就检查HAL_TIM_Base_Init里的ClockDivision和结构体是否残留了上一次配置的字段。
有一种比较隐蔽的情况是使用HAL_TIM_PWM_Start之前调用了编码器接口初始化函数,编码器模式本质上就是一种外部时钟模式(计数器跟随 A/B 相脉冲计数)。初始化完编码器没有复位定时器时钟源,导致后开的 PWM 通道计数器跑在编码器脉冲上。这个坑在平衡车和机器人项目里特别常见,排查时建议直接读一下TIMx->SMCR寄存器,确认 SMS = 0b000(内部时钟模式)。
5. 进入不可预期的时基精度:几十微秒的误差积累是怎么发生的
很多人算定时器参数时,只算“理论值”,却忽略了误差的来源不只是整数取整,还有中断响应延迟、NVIC 抢占优先级配置,以及编译器的优化行为。在要求毫秒级以上的应用中这些误差无关痛痒,但一旦涉及微秒级延时或高频 PWM,就会出现一连串让人摸不着头脑的怪现象。
5.1 中断响应延迟不是固定的
假设 TIM2 配置为 1ms 中断一次,主循环里有一个 500μs 的任务。中断到来后,CPU 需要压栈、跳转到中断服务函数、执行 HAL 库的处理逻辑、再弹栈。这些操作消耗的时间不是固定的,取决于当时 CPU 流水线状态和中断优先级。
我用逻辑分析仪抓过 STM32F103 的 TIM2 中断响应时间,从硬件置位中断标志到用户回调函数的第一条语句开始执行,大概在 1.5μs 到 3.5μs 之间波动。这个波动在做精确时间戳时直接导致相邻两次中断的实际间隔相差约 2μs。如果应用对时间一致性要求较高,不能依赖“每次进中断都正好在溢出点上”这个假设,而应把定时器的值读取放到中断服务函数最开头:
void TIM2_IRQHandler(void) { uint32_t snapshot = TIM2->CNT; // 尽量早地捕获计数值 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 处理业务逻辑 } }5.2 预装载与立即装载混用引发的“毛刺”
有些库函数在初始化时会默认开启 ARPE。如果你要让 PWM 占空比精确跳变到某个新值,但 ARPE 开着,又没等更新事件,就会发生“影子寄存器装载到一半”的尴尬。不对,严格来说 ARR 是 16 位整套装载,不会出现一半,但会出现“装载时机的下一秒不确定性”:你写 ARR 的操作可能发生在计数器已越过新 ARR 值的位置,于是这个周期不溢出,拖到下一个周期。
这种现象在示波器上表现为:控制指令发出后,PWM 周期偶尔会突然拉长一个周期。看起来像偶发故障,实际就是 ARPE 和写入时序碰撞的结果。解决方法是两种:
- 在更新事件触发的中断里修改 ARR,因为此时计数器刚好归零,写入后整个周期是完整的。
- 使用 DMA 搬运 ARR 值,由更新事件触发 DMA 传输,把新值在计数器还没走到比较点之前加载完毕。
5.3 时钟源与休眠模式的配合问题
低功耗项目中常遇到一个诡异现象:进入 STOP 模式后,定时器中断唤不醒芯片。查到最后往往不是定时器的问题,而是定时器的时钟源依赖 HSE 或 PLL,而 STOP 模式下这些时钟被关闭了。若要让定时器在低功耗下继续运行,得使用 LSI 或 LSE 作为时钟源的 RTC 或低功耗定时器(LPTIM),普通定时器在 STOP 模式下无法工作。
FSM 状态机设计时,好的做法是保留一个 LPTIM 作为唤醒源,而不是把一个高速定时器硬塞进 STOP 模式里。这样既能保证唤醒精度,又不用反复折腾时钟切换。
6. 从报警到精准计时:一次完整的调试
用一个真实遇到的案例收束全文。之前一个基于 STM32F103 的工装项目,要求输出 100Hz 的 PWM 信号,占空比可通过串口动态调整。代码写完后上电,示波器一看:输出频率只有 52Hz 左右,而且修改占空比后,波形要过一个周期才变化,偶尔还会出现一个明显拉长的周期。
当时的第一反应是 PSC 和 ARR 算错了,于是按公式重算了一遍:72MHz / ((PSC+1)(ARR+1)) = 100Hz,取 PSC=71、ARR=9999。理论值完全正确,但现象对不上。随后把示波器探头换到 TIM3 的另外一路通道上,发现频率一样不对,这就排除了单个定时器硬件损坏的可能。
接着,用调试器读 TIM3->PSC 和 TIM3->ARR,发现数值和设置一致。问题不在寄存器数值,而在时钟。用调试器查看 RCC->CFGR,发现 APB1 预分频器被设成了 /1,而不是 CubeMX 里显示的 /2。翻看代码,原来是早期某次测试时,有人手写了一句 RCC_CFGR |= ... 把 APB1 分频改了,之后的代码又没重新初始化时钟。此时 APB1 实际是 72MHz,定时器时钟也是 72MHz,但 PSC 和 ARR 是按 APB1=36MHz、定时器时钟=72MHz 算的……等等,如果定时器时钟还是 72MHz,为什么输出是 52Hz 而不是 100Hz?
问题很快被定位到另一处:重复计数器的影子装载时序。由于占空比调整用的是 PWM 模式 1,并在中断里直接改 CCR 值,而 ARPE=1,所以每次改占空比都有一个周期的延迟;这解释了“过一周期才变化”的现象。但 52Hz 依然解释不通。
最后打开逻辑分析仪,长时间抓取 PWM 脚波形,发现相邻周期呈现 5ms 和 15ms 交替的规律。这说明定时器有时候按 100Hz 跑,有时候却按 66.7Hz 跑——两种周期交替出现。此时再回头看代码,发现中断服务函数里除了改 CCR,还顺手把 ARR 重新写了一遍,而 ARPE=1 导致 ARR 写入到影子寄存器的时机不确定,偶尔与计数器的当前位置产生冲突,造成计数周期被拉长。
修复方法很简单:把 ARR 的更新放到更新事件中断里,且写入前先清除更新标志。这样时序上保证每次修改都发生在计数器归零的那一刻,不会再撞车。
这个案例看起来名字叫“定时器参数算错”,但最终问题一大半在时钟配置和影子寄存器的使用时序上。很多人遇到类似现象就急着改 PSC 和 ARR,来回试几个值,侥幸试对了就收工,却从头到尾没搞清楚真正的原因。
我后来在实际项目里养成一个习惯:定时器相关的调试,一律先在固定中断频率下单独验证时钟源对不对,再验证 PSC/ARR 组合对不对,最后才调占空比或捕获逻辑。一步一个脚印,比什么都靠猜可靠得多。