中断控制器:NVIC 工作原理与优先级分组
NVIC,全称嵌套向量中断控制器,是 Cortex-M 内核里负责管理中断的核心部件。说白了,它就是中断系统的「交通警察」——哪个中断能进来、哪个中断先执行、哪个中断能打断谁,全由它说了算。
我个人习惯把 NVIC 理解成一个三层结构:中断使能控制、优先级裁决、向量表跳转。这三层配合好了,系统才能稳定跑起来。我在项目中遇到过不少因为 NVIC 配置不当导致的诡异问题,比如中断响应延迟突然变大、低优先级任务被饿死,追根溯源都是优先级分组没搞对。
NVIC 的核心能力
NVIC 支持最多 240 个中断源(具体看芯片型号),每个中断都有独立的使能位、挂起位和优先级寄存器。它最厉害的地方是:硬件自动完成中断嵌套和优先级比较,不需要软件干预。
- 中断使能/禁止:通过 NVIC_ISER 和 NVIC_ICER 寄存器控制,每个位对应一个中断。
- 中断挂起/清除:通过 NVIC_ISPR 和 NVIC_ICPR 寄存器,可以软件触发或清除中断请求。
- 中断优先级:每个中断有 8 位优先级寄存器,但实际使用的位数由芯片决定(常见 4 位或 3 位)。
- 向量表:存放中断服务函数的入口地址,NVIC 根据中断号自动查表跳转。
关键点:NVIC 的优先级比较是硬件行为,一旦当前中断的优先级高于新来的中断,新中断就会被阻塞。这个「硬件自动裁决」机制是实时性的根本保障。
中断优先级分组:抢占优先级与子优先级
优先级分组是 NVIC 里最容易让人迷糊的地方。Cortex-M 把优先级寄存器的高几位定义为抢占优先级,低几位定义为子优先级。分组方式由 SCB->AIRCR 寄存器的 PRIGROUP 位决定。
你想想看,为什么要有分组?因为实际项目中,有些中断必须能打断其他中断(比如定时器中断打断串口中断),有些中断之间只需要排队(比如两个同级别的外设中断)。分组就是为了区分这两种场景。
| PRIGROUP 值 | 抢占优先级位数 | 子优先级位数 | 优先级总数 |
|---|---|---|---|
| 0 | 7 | 1 | 128 级抢占 × 2 级子优先级 |
| 1 | 6 | 2 | 64 级抢占 × 4 级子优先级 |
| 2 | 5 | 3 | 32 级抢占 × 8 级子优先级 |
| 3 | 4 | 4 | 16 级抢占 × 16 级子优先级 |
| 4 | 3 | 5 | 8 级抢占 × 32 级子优先级 |
| 5 | 2 | 6 | 4 级抢占 × 64 级子优先级 |
| 6 | 1 | 7 | 2 级抢占 × 128 级子优先级 |
| 7 | 0 | 8 | 1 级抢占 × 256 级子优先级 |
我的建议:绝大多数嵌入式项目用 PRIGROUP = 3(4 位抢占 + 4 位子优先级)就够了。16 级抢占优先级足够区分关键中断和普通中断,16 级子优先级也能处理同组内的排队需求。别搞太复杂,否则调试时自己都记不住。
抢占优先级 vs 子优先级:到底怎么用?
这两个概念的区别,我直接说人话:
- 抢占优先级:决定一个中断能不能打断另一个正在执行的中断。数值越小,优先级越高。
- 子优先级:当两个中断的抢占优先级相同时,子优先级决定谁先执行。但子优先级不能产生嵌套——同抢占优先级的中断之间不会互相打断。
举个例子:假设 PRIGROUP = 3,中断 A 的优先级 = 0x10(抢占 1,子 0),中断 B 的优先级 = 0x11(抢占 1,子 1)。如果 A 正在执行,B 来了,B 会等 A 执行完再执行——因为抢占优先级相同,不嵌套。但如果中断 C 的优先级 = 0x00(抢占 0,子 0),它来了就能直接打断 A 或 B。
我曾经踩过的坑:有一次把定时器中断和外部中断都设成了抢占优先级 0,以为它们都能快速响应。结果定时器中断执行时间稍长,外部中断被堵住了,导致系统丢了一个关键信号。后来把外部中断的抢占优先级改成 0,定时器改成 1,问题解决。记住:真正需要快速响应的中断,才给最高抢占优先级。
优先级分组配置实战
配置优先级分组,实际上就是写 SCB->AIRCR 寄存器。标准库和 HAL 库都有封装好的函数,但底层原理你得清楚。
// 设置优先级分组:4位抢占 + 4位子优先级
// 对应 PRIGROUP = 3
#define AIRCR_PRIGROUP_POS 8
#define AIRCR_PRIGROUP_MASK 0x7
#define SCB_AIRCR 0xE000ED0C
void NVIC_SetPriorityGrouping(uint32_t group) {
uint32_t reg_val;
reg_val = *((volatile uint32_t *)SCB_AIRCR);
reg_val &= ~(AIRCR_PRIGROUP_MASK << AIRCR_PRIGROUP_POS);
reg_val |= (group << AIRCR_PRIGROUP_POS);
reg_val |= 0x5FA << 16; // 解锁密钥
*((volatile uint32_t *)SCB_AIRCR) = reg_val;
}
// 使用示例
NVIC_SetPriorityGrouping(3); // 16级抢占 × 16级子优先级
注意:写 AIRCR 寄存器时必须带上 0x5FA 密钥,否则写操作会被忽略。这是硬件保护机制,防止意外修改优先级分组。我见过有人直接赋值导致配置无效,中断优先级乱套的案例。
NVIC 中断处理流程
一个中断从触发到执行,NVIC 内部经历了以下步骤:
- 中断请求到达:外设拉高中断信号线。
- 使能检查:NVIC 检查对应中断的使能位,如果被禁止,请求被忽略。
- 挂起标志置位:使能后,NVIC 将挂起寄存器对应位置 1。
- 优先级比较:NVIC 比较当前中断的抢占优先级与正在执行的中断优先级。如果更高,触发嵌套;否则等待。
- 向量表查表:NVIC 根据中断号从向量表中取出服务函数地址。
- 压栈与跳转:硬件自动压栈(xPSR、PC、LR、R0-R3、R12),然后跳转到服务函数。
- 执行与返回:服务函数执行完毕后,硬件自动出栈,恢复现场。
实际项目中的优先级分配策略
我总结了一套优先级分配原则,在多个项目里验证过,分享给你:
- 最高抢占优先级(0):留给系统心跳定时器、看门狗、紧急故障处理。这些中断一旦错过,系统可能崩溃。
- 次高优先级(1-3):留给实时性要求高的外设,比如 DMA 传输完成、高速 ADC 采样、电机控制 PWM 同步。
- 中等优先级(4-7):留给通信接口,比如 UART、SPI、I2C。这些中断对实时性有要求,但允许微秒级延迟。
- 低优先级(8-15):留给按键扫描、LED 刷新、传感器轮询等非关键任务。
核心原则:中断优先级不是越高越好。给每个中断分配「够用」的优先级,留出余量给未来可能新增的中断。我见过有人把所有中断都设成优先级 0,结果一个中断卡住,整个系统瘫痪。
常见陷阱与避坑指南
嗯,这里要注意几个容易翻车的地方:
- 陷阱一:优先级分组在运行时修改。一旦系统跑起来,所有中断的优先级数值含义就固定了。中途改分组,会导致已配置的优先级数值被重新解释,中断行为完全乱套。我建议在系统初始化第一行就设置好分组,之后不再改动。
- 陷阱二:忽略子优先级的非嵌套特性。同抢占优先级的中断不会互相打断,如果你需要 A 打断 B,必须给 A 更高的抢占优先级,光靠子优先级没用。
- 陷阱三:中断服务函数执行时间过长。高优先级中断的服务函数如果执行太久,会阻塞所有低优先级中断。我曾经在电机控制项目里,把 FOC 计算放在中断里,结果串口中断被饿死,调试信息都打印不出来。
我曾经犯过的错:在一个多传感器采集项目里,我把所有传感器中断都设成了抢占优先级 2,子优先级按传感器重要性分配。结果发现高频率的传感器中断频繁触发,低频率的传感器中断几乎得不到执行。后来把高频率传感器中断的抢占优先级降到 3,问题解决。记住:频率高的中断,优先级要适当降低,否则它会霸占 CPU。