2. 中断控制器:NVIC 架构、优先级分组、使能与屏蔽
中断控制器,说白了就是 CPU 的“门卫”。它决定哪个中断能进来,哪个得排队,哪个优先级更高。在 Cortex-M 系列里,这个门卫叫 NVIC(嵌套向量中断控制器)。
我刚开始接触 NVIC 时,觉得它不就是个中断管理器嘛,有啥好研究的?直到有一次产品在现场频繁死机,查了三天才发现是优先级配置出了问题。嗯,从那以后我再也不敢小看这个“门卫”了。
2.1 NVIC 架构概览
NVIC 是 Cortex-M 内核的一部分,不是外设。它直接集成在 CPU 内部,所以响应速度极快。我个人习惯把 NVIC 理解成三个核心模块:
- 中断挂起寄存器:记录哪些中断正在等待处理
- 中断优先级寄存器:存储每个中断的优先级数值
- 中断使能寄存器:控制每个中断的开关
NVIC 最多支持 240 个外部中断,但具体数量取决于芯片厂商。比如 STM32F103 只有 60 个,而某些高端型号会更多。你想想看,这么多中断要管理,没有一套清晰的架构怎么行?
核心要点:NVIC 的响应速度是硬件级别的,从中断触发到进入中断服务函数,通常只需要 12 个时钟周期。这是软件轮询永远比不了的。
下面这张图展示了 NVIC 在系统中的位置和内部结构:
2.2 中断优先级分组
这是 NVIC 里最容易搞混的地方。优先级分组决定了你拿多少位来表示“抢占优先级”,多少位来表示“子优先级”。
Cortex-M 使用 8 位优先级寄存器,但实际只用高 4 位(有些芯片用高 3 位)。这 4 位怎么分配?靠的是 SCB->AIRCR 寄存器里的 PRIGROUP 字段。
| PRIGROUP 值 | 抢占优先级位数 | 子优先级位数 | 抢占优先级等级数 | 子优先级等级数 |
|---|---|---|---|---|
| 0 | 4 | 0 | 16 | 1 |
| 1 | 3 | 1 | 8 | 2 |
| 2 | 2 | 2 | 4 | 4 |
| 3 | 1 | 3 | 2 | 8 |
| 4 | 0 | 4 | 1 | 16 |
我的经验:项目初期就定好分组策略,中途别改。我曾经在一个项目里,前期用分组 2(2+2),后期改成分组 0(4+0),结果所有中断优先级都得重新算,差点没把我整崩溃。
分组配置代码示例:
// 设置优先级分组为 3 位抢占 + 1 位子优先级
// 即 PRIGROUP = 4
#define AIRCR_VECTKEY_MASK 0x05FA0000UL
#define AIRCR_PRIGROUP_MASK 0x00000700UL
#define AIRCR_PRIGROUP_4_4 0x00000400UL // 4位抢占, 4位子优先级
SCB->AIRCR = AIRCR_VECTKEY_MASK | AIRCR_PRIGROUP_4_4;
配置完成后,每个中断的优先级数值就按这个分组来解析。比如你给某个中断赋优先级值 0x30,在分组 2(2+2)下,高 2 位是抢占优先级(3),低 2 位是子优先级(0)。
2.3 中断使能与屏蔽
使能和屏蔽听起来差不多,但其实是两码事。我刚开始也搞混过,后来吃了亏才分清楚。
- 中断使能:让某个中断源可以被 NVIC 响应。用
NVIC_EnableIRQ()实现。 - 中断屏蔽:临时关闭所有中断或某组中断。用
__disable_irq()或 BASEPRI 寄存器实现。
使能单个中断的代码:
// 使能 USART1 中断
NVIC_EnableIRQ(USART1_IRQn);
// 设置优先级
NVIC_SetPriority(USART1_IRQn, 0x20); // 优先级值越小,优先级越高
屏蔽中断有三种方式,我列个表对比一下:
| 方式 | 作用范围 | 使用场景 | 注意事项 |
|---|---|---|---|
__disable_irq() |
全局所有中断 | 临界区保护 | 执行时间要极短,否则丢中断 |
__set_BASEPRI() |
低于某优先级的中断 | 只屏蔽低优先级中断 | 高优先级中断仍可响应 |
NVIC_DisableIRQ() |
单个中断 | 动态开关某个外设中断 | 注意中断挂起位可能残留 |
避坑指南:我曾经在临界区里用了 __disable_irq(),但忘了在退出前重新使能。结果系统跑了几分钟后所有中断都停了,看门狗超时复位。后来我改用 BASEPRI 屏蔽低优先级中断,高优先级的中断(比如系统滴答定时器)还能正常工作,系统稳定性好多了。
使用 BASEPRI 的示例:
// 屏蔽优先级低于 0x80 的所有中断
// 优先级值大于等于 0x80 的中断被屏蔽
__set_BASEPRI(0x80);
// 临界区代码
// ...
// 恢复中断
__set_BASEPRI(0);
这里有个细节:BASEPRI 的值越大,屏蔽的中断越多。0x80 表示屏蔽优先级值 ≥0x80 的中断,优先级值 0x00~0x7F 的中断不受影响。你想想看,这比直接关全局中断要精细多了。
2.4 中断挂起与清除
中断来了但 CPU 正在处理更高优先级的中断怎么办?NVIC 会把中断挂起,等 CPU 空闲了再处理。挂起位在 NVIC->ISPR 寄存器里。
有时候我们需要手动清除挂起位,比如在调试时:
// 清除 USART1 中断的挂起状态
NVIC_ClearPendingIRQ(USART1_IRQn);
// 检查某个中断是否处于挂起状态
uint32_t pending = NVIC_GetPendingIRQ(USART1_IRQn);
重要提醒:在中断服务函数退出前,一定要清除外设的中断标志位,否则 NVIC 会认为中断还在请求,导致反复进入 ISR。这是新手最容易犯的错误之一。
NVIC 的架构设计其实很精巧。它把优先级分组、使能、屏蔽、挂起这些功能都集成在一起,用硬件实现了中断的自动管理。我们做嵌入式开发的,只要理解清楚这几个概念,就能把中断用得得心应手。
嗯,中断控制器的内容就这些。记住一句话:优先级分组定好就别改,使能和屏蔽要分清,挂起位记得清。做到这三点,中断这块基本不会出大问题。