3. 定时器中断配置:更新中断的完整流程
定时器的更新中断,说白了就是定时器计数值溢出时触发的那一下。很多初学者觉得这玩意儿简单,不就是开个中断嘛。但我在项目里见过不少人栽在这上面——要么中断进不去,要么进去就死机。今天咱们就把这个流程彻底捋清楚。
3.1 更新中断的触发时机
先搞清楚一件事:什么时候会产生更新中断?
- 计数器溢出:从 ARR 值回滚到 0 的那一瞬间
- 软件触发:你手动设置 UG 位(更新生成位)
- 从模式触发:比如编码器模式下,计数方向改变
我一般只用溢出触发,最干净,也最好排查。你想想看,如果中断莫名其妙频繁触发,八成是 UG 位被哪里误写了。
核心概念:更新中断 = 计数器从 ARR 回到 0 时,硬件自动置位 UIF 标志位。这个标志位如果不手动清,中断会一直触发。
3.2 配置流程:三步走
我个人习惯把配置拆成三步,这样调试时思路清晰:
- 定时器基础配置:时钟源、预分频、自动重装值
- 中断使能:打开更新中断允许位
- 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。你可以在启动文件里查到所有中断向量名。
函数体里要做三件事:
- 检查中断标志位:确认是不是更新中断触发的
- 执行你的业务逻辑:比如翻转 LED、更新变量
- 清除中断标志位:不清除的话,中断会一直触发
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 完整配置流程图
下面这张图是我自己画的一个配置流程,每次写定时器中断代码前我都会瞄一眼:
3.6 调试时怎么验证中断是否工作
代码写完了,怎么知道中断有没有跑起来?我一般用三种方法:
- 方法一:断点法。在 ISR 里打个断点,运行后看能不能停进去。注意:断点法会影响实时性,但验证功能足够了。
- 方法二:GPIO 翻转法。在 ISR 里翻转一个 GPIO,用示波器或者逻辑分析仪看波形。频率对不对,一眼就知道。
- 方法三:变量计数法。定义一个全局变量,每次进中断加 1,主循环里打印出来。适合没有示波器的场景。
我的调试习惯:先用 GPIO 翻转法确认中断频率正确,再用断点法检查业务逻辑。这样效率最高,不会在错误的方向上浪费时间。
3.7 常见故障与定位
最后列几个我实际遇到过的故障,你碰到了可以直接对号入座:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 中断完全不触发 | 时钟没使能 / NVIC 没配 / 中断标志没清 | 检查 RCC 寄存器、NVIC->ISER 寄存器 |
| 中断频率不对 | 预分频或自动重装算错了 | 用 GPIO 翻转法测实际频率 |
| 中断只触发一次 | ISR 里忘了清标志位 | 检查 TIM_SR 寄存器的 UIF 位 |
| 中断响应延迟大 | 优先级设得太低 / 其他中断占太久 | 调高抢占优先级,或优化其他 ISR |
| 程序跑飞进 HardFault | ISR 里操作了不允许的操作(如 printf) | ISR 里只做轻量操作,复杂逻辑放主循环 |