3. 定时器中断配置:更新中断的完整流程

定时器的更新中断,说白了就是定时器计数值溢出时触发的那一下。很多初学者觉得这玩意儿简单,不就是开个中断嘛。但我在项目里见过不少人栽在这上面——要么中断进不去,要么进去就死机。今天咱们就把这个流程彻底捋清楚。

3.1 更新中断的触发时机

先搞清楚一件事:什么时候会产生更新中断?

  • 计数器溢出:从 ARR 值回滚到 0 的那一瞬间
  • 软件触发:你手动设置 UG 位(更新生成位)
  • 从模式触发:比如编码器模式下,计数方向改变

我一般只用溢出触发,最干净,也最好排查。你想想看,如果中断莫名其妙频繁触发,八成是 UG 位被哪里误写了。

核心概念:更新中断 = 计数器从 ARR 回到 0 时,硬件自动置位 UIF 标志位。这个标志位如果不手动清,中断会一直触发。

3.2 配置流程:三步走

我个人习惯把配置拆成三步,这样调试时思路清晰:

  1. 定时器基础配置:时钟源、预分频、自动重装值
  2. 中断使能:打开更新中断允许位
  3. NVIC 配置:设置优先级、使能中断通道

顺序不能乱。我曾经有一次先配了 NVIC,再开定时器时钟,结果中断死活不响应——因为 NVIC 使能时定时器还没准备好,中断标志被吃了。

3.2.1 定时器基础配置

这段代码我闭着眼都能写出来,但每次还是会检查一遍:

// 假设使用 TIM3,时钟 72MHz
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

TIM_TimeBaseStructure.TIM_Prescaler = 7199;      // 72MHz / (7199+1) = 10kHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 9999;          // 10kHz / (9999+1) = 1Hz
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;  // 高级定时器才用

TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

我的习惯:预分频和自动重装值我习惯分开算。先算预分频得到想要的计数频率,再算自动重装得到中断周期。这样万一频率不对,一眼就能看出是哪个参数算错了。

3.2.2 使能更新中断

这一步很多人会漏掉。定时器本身的中断源有好几个,你得明确告诉它:我要用更新中断。

TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);

就这么一行。但注意,TIM_IT_Update 是宏定义,别写成 TIM_IT_CC1 或者别的。我见过有人把捕获比较中断和更新中断搞混,结果定时器跑得飞起,中断就是不来。

3.2.3 NVIC 优先级设置

NVIC 这块是新手最容易懵的地方。STM32 的优先级分组有 5 种模式,你得先决定用哪种:

分组 抢占优先级位数 子优先级位数 典型应用场景
NVIC_PriorityGroup_0 0 4 所有中断同级,按子优先级排队
NVIC_PriorityGroup_1 1 3 2级抢占,8级子优先级
NVIC_PriorityGroup_2 2 2 4级抢占,4级子优先级(最常用)
NVIC_PriorityGroup_3 3 1 8级抢占,2级子优先级
NVIC_PriorityGroup_4 4 0 16级抢占,无子优先级

我个人习惯用 Group_2。4 级抢占够用,4 级子优先级也能处理一些嵌套场景。配置代码长这样:

NVIC_InitTypeDef NVIC_InitStructure;

// 优先级分组只需在系统初始化时设置一次
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

注意:NVIC_PriorityGroupConfig 整个项目只能调用一次!如果你在多个地方调用,后调用的会覆盖前面的设置。我建议放在 main 函数最开头,或者系统初始化函数里。

3.3 中断服务函数怎么写

中断服务函数的名字是固定的,不能自己乱起。TIM3 的中断服务函数叫 TIM3_IRQHandler。你可以在启动文件里查到所有中断向量名。

函数体里要做三件事:

  1. 检查中断标志位:确认是不是更新中断触发的
  2. 执行你的业务逻辑:比如翻转 LED、更新变量
  3. 清除中断标志位:不清除的话,中断会一直触发
void TIM3_IRQHandler(void)
{
    // 1. 检查更新中断标志位
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        // 2. 业务逻辑:翻转 LED
        GPIO_ToggleBits(GPIOB, GPIO_Pin_0);

        // 3. 清除更新中断标志位
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

这里有个坑:TIM_GetITStatus 和 TIM_GetFlagStatus 有什么区别?

  • TIM_GetITStatus:检查中断标志位 且 中断使能位。更安全,推荐使用。
  • TIM_GetFlagStatus:只检查标志位,不管中断有没有使能。一般用于查询模式。

我建议一律用 TIM_GetITStatus。曾经有个同事用 GetFlagStatus 做判断,结果中断没使能时标志位被意外置位,程序跑飞了。

3.4 中断优先级设置的坑

优先级设置看似简单,但实际项目中踩过的坑不少:

  • 抢占优先级相同的情况:如果两个中断抢占优先级一样,它们不会互相打断。必须等一个处理完,另一个才能进来。
  • 子优先级只在同抢占优先级时生效:抢占优先级不同时,子优先级没有意义。
  • 数值越小优先级越高:0 是最高优先级,15 是最低。这个和直觉相反,我刚开始也老搞反。

经验之谈:如果你的系统里有多个中断,建议把定时器中断的抢占优先级设得比主循环任务高,但比紧急硬件中断(比如 DMA 错误、硬件错误)低。这样既能保证定时精度,又不会耽误紧急情况处理。

3.5 完整配置流程图

下面这张图是我自己画的一个配置流程,每次写定时器中断代码前我都会瞄一眼:

定时器更新中断配置流程图 定时器更新中断配置流程 1. 使能定时器时钟 2. 配置预分频 + 自动重装值 3. TIM_ITConfig 使能更新中断 4. 设置优先级 + 使能 NVIC 通道 5. 使能定时器(TIM_Cmd) 6. 编写中断服务函数 关键检查点 • 时钟是否已使能? • 中断标志位是否清除? • 优先级分组是否只设一次? • ISR 名字是否与向量表一致?

3.6 调试时怎么验证中断是否工作

代码写完了,怎么知道中断有没有跑起来?我一般用三种方法:

  • 方法一:断点法。在 ISR 里打个断点,运行后看能不能停进去。注意:断点法会影响实时性,但验证功能足够了。
  • 方法二:GPIO 翻转法。在 ISR 里翻转一个 GPIO,用示波器或者逻辑分析仪看波形。频率对不对,一眼就知道。
  • 方法三:变量计数法。定义一个全局变量,每次进中断加 1,主循环里打印出来。适合没有示波器的场景。

我的调试习惯:先用 GPIO 翻转法确认中断频率正确,再用断点法检查业务逻辑。这样效率最高,不会在错误的方向上浪费时间。

3.7 常见故障与定位

最后列几个我实际遇到过的故障,你碰到了可以直接对号入座:

现象 可能原因 排查方法
中断完全不触发 时钟没使能 / NVIC 没配 / 中断标志没清 检查 RCC 寄存器、NVIC->ISER 寄存器
中断频率不对 预分频或自动重装算错了 用 GPIO 翻转法测实际频率
中断只触发一次 ISR 里忘了清标志位 检查 TIM_SR 寄存器的 UIF 位
中断响应延迟大 优先级设得太低 / 其他中断占太久 调高抢占优先级,或优化其他 ISR
程序跑飞进 HardFault ISR 里操作了不允许的操作(如 printf) ISR 里只做轻量操作,复杂逻辑放主循环