4.1 定时器原理与寄存器配置 第 3 章结尾留了一个尾巴:busydelay 靠 CPU 空转凑时间,又慢又不准。这一节让硬件定时器接管时间——它是个自动数节拍的计数器,数满了还能主动打断 CPU。掌握 PSC 与 ARR 的换算数学、更新中断的配置链和看门狗的守时机制,你就拥有了嵌入式系统里最可靠的时间基准。 计数器的数学:两步算出任意周期 定时器的骨架是一条三级流水:输入时钟先过预分频器 PSC,得到滴答频率;计数器 CNT 每个滴答加一,加到自动重装值 ARR 就溢出归零,同时产生更新事件。溢出频率(即中断周期)的公式: 16 位上限是这条公式的紧约束:PSC 和 ARR 都是 16 位(最大 65535),单级分频的周期上限是 65536×65536/72MHz ≈ 59.
第 3 章结尾留了一个尾巴:busy_delay 靠 CPU 空转凑时间,又慢又不准。这一节让硬件定时器接管时间——它是个自动数节拍的计数器,数满了还能主动打断 CPU。掌握 PSC 与 ARR 的换算数学、更新中断的配置链和看门狗的守时机制,你就拥有了嵌入式系统里最可靠的时间基准。
定时器的骨架是一条三级流水:输入时钟先过预分频器 PSC,得到滴答频率;计数器 CNT 每个滴答加一,加到自动重装值 ARR 就溢出归零,同时产生更新事件。溢出频率(即中断周期)的公式:
溢出频率 = 定时器输入频率 / [(PSC+1) × (ARR+1)] 已知 STM32F103 定时器时钟 72 MHz,想要 10 ms 周期(100 Hz): 第 1 步选 PSC:72 MHz / (PSC+1) = 滴答频率,取 PSC=7199 → 滴答 10 kHz(每滴答 0.1 ms) 第 2 步算 ARR:100 Hz = 10 kHz / (ARR+1) → ARR+1 = 100 → ARR = 99 注意两个 +1:寄存器值比实际分频/计数少一,因为硬件从 0 开始数。
16 位上限是这条公式的紧约束:PSC 和 ARR 都是 16 位(最大 65535),单级分频的周期上限是 65536×65536/72MHz ≈ 59.6 秒,一般够用;要更长周期就上 STM32 的 32 位定时器(TIM2/TIM5 在部分型号)或在中断里软累计。反向的教训是参数不唯一:同样是 10 ms,PSC=71、ARR=99999 会溢出吗?会——ARR 超 16 位,写入被截断成 0x863F,周期完全错乱。拆分原则:PSC 尽量大、让 ARR 落在好算的整数附近,两步都核对是否越过 65535。
把数学落成代码。从时钟使能到 ISR 清标志,完整链条六步:
#include "stm32f10x.h" volatile uint32_t g_ticks_10ms = 0; /* 硬件/ISR 会改写的变量必须 volatile */ void tim3_init_10ms(void) { RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; /* TIM3 挂在 APB1:使能位在这边 */ TIM3->PSC = 7200 - 1; /* 72 MHz / 7200 = 10 kHz 滴答 */ TIM3->ARR = 100 - 1; /* 100 滴答 = 10 ms 溢出一次 */ TIM3->DIER |= TIM_DIER_UIE; /* 允许更新中断 */ NVIC_SetPriority(TIM3_IRQn, 2); /* 抢占优先级 2:低于按键中断 */ NVIC_EnableIRQ(TIM3_IRQn); TIM3->CR1 |= TIM_CR1_CEN; /* 计数器启动 */ } void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { /* 确认是更新中断 */ TIM3->SR = ~TIM_SR_UIF; /* 写 0 清标志(SR 多数位写 0 清除)*/ g_ticks_10ms++; /* 节拍累计:全系统的时间基准 */ } }
这个 10 ms 节拍立刻能兑现第 3 章的承诺——把按键状态机消抖喂起来:
int main(void) { led_pb5_init(); /* 第 3 章的 LED 初始化 */ key_pa0_init(); /* 第 3 章的按键初始化 */ tim3_init_10ms(); /* 本节的节拍发生器 */ while (1) { /* 每 10 ms 扫描一次按键;消抖状态机见 3.2 节 */ static uint32_t last = 0; if (g_ticks_10ms != last) { last = g_ticks_10ms; if (key_scan_tick()) { GPIOB->ODR ^= (1UL << 5); /* 确认一次按压,翻转 LED */ } } /* 主循环还可以干别的事:节拍由中断累计,这里永不阻塞 */ } }
对比 3.2 节的 busy_delay 版本,变化是本质性的:CPU 的时间不再被延时吞掉,主循环随时能插入新任务;闪烁周期精确到定时器精度(晶振级),与代码执行时间无关;新增功能只是往主循环里加代码,不用重新算延时。"中断给节拍、主循环跑逻辑"是裸机程序的黄金架构,第 6 章的 RTOS 不过是把这套模式推到极致。
TIM3_IRQHandler 里只有两行,这不是偷懒,是纪律。中断服务函数的执行时间决定系统最坏响应延迟:ISR 跑 5 ms,意味着任何更低优先级的中断至少晚 5 ms。所以 ISR 里只做三类事:清标志、置标志或累计计数、往缓冲区/队列塞数据。凡是耗时的(串口打印、复杂计算、延时等待)一律搬到主循环或任务里做。这条纪律在第 6 章 RTOS 里会升级成硬规则——FreeRTOS 的许多 API 根本不允许在 ISR 里调用非 FromISR 版本。
另一个高频翻车点是标志清除方式。TIM 的 SR 寄存器多数位是"写 0 清除",而 EXTI 的 PR 是"写 1 清除"——两个约定相反,写反了轻则标志清不掉(中断风暴),重则误清别人的标志。拿不准时查参考手册的寄存器表格,那一栏"clear by writing 0/1"永远值得多看一眼。
时钟数学有个讨厌的特性:主频一改,所有 PSC/ARR 全部重算。从 72 MHz 的 F103 换到 168 MHz 的 F4,或 ESP32 的 80 MHz 外设定时器时钟,同样的 10 ms 周期参数完全不同。与其死记"72 MHz 时 PSC=7199",不如记住推导路径:先定滴答粒度(取一个整得好算的值,如 10 us),再除出 PSC,最后用目标周期除以粒度得到 ARR。72 MHz 想要 10 us 粒度:PSC = 72 − 1;100 ms 周期:ARR = 10000 − 1。换 168 MHz?PSC = 168 − 1,ARR 不变。把粒度固定的好处是所有周期换算只动 ARR 一处,参数表立即可读。
定时器管"按点干活",看门狗管"没按点干活就重启"。独立看门狗 IWDG 有专属的低速时钟(40 kHz 的 LSI),主时钟崩了它照转;它从装载值往下数,数到零就复位芯片,程序必须定期"喂狗"重装计数。这让"程序跑飞"从无限滞留变成一次可预期的重启:
void iwdg_init_500ms(void) { IWDG->KR = 0xCCCC; /* 启动看门狗,LSI 40 kHz 自动开启 */ IWDG->KR = 0x5555; /* 解除寄存器写保护 */ IWDG->PR = 2; /* 分频 8:滴答 = 40 kHz / 8 = 5 kHz */ IWDG->RLR = 2500 - 1; /* 2500 滴答 = 500 ms:超时即复位 */ IWDG->KR = 0xAAAA; /* 喂狗,重装计数 */ } /* 主循环每圈喂一次;喂狗间隔必须稳定小于 500 ms, 否则说明某处卡死——这正是我们想要的行为。 */ void feed_dog(void) { IWDG->KR = 0xAAAA; }
喂狗周期的选择体现工程智慧:太短,正常代码偶尔慢一点(OTA 擦写 Flash 时 CPU 停顿几百毫秒很正常)就被误杀重启;太长,真卡死时恢复太慢。常见做法是"正常耗时 × 3 到 5 倍",并且在已知的耗时操作(擦 Flash、深睡唤醒)前临时喂一次。看门狗是嵌入式可靠性的底线配置,量产固件不开看门狗等于裸奔。
同一套计数硬件还能反向使用——测量外部事件的时长。把输入捕获通道对准一个脉冲信号,上升沿记下 CNT 值、下降沿再记一次,两次差值乘以滴答时长就是脉宽;超声波测距模块的回波高电平正是这么量的。定时器还能级联:一个定时器的溢出事件当下一个的输入时钟,把 16 位扩成 32 位。这些用法换了寄存器名字,内核仍是"PSC 分频、CNT 计数"这套数学——学透本节,STM 十几个定时器的手册对你来说只是同一篇文章的不同排版本。
下一节让计数器走出中断世界:PWM 输出路径上,比较器每滴答和 CCR 比一次,方波从引脚自动流淌——呼吸灯的舞台就搭好了。