中断控制器: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 内部经历了以下步骤:

  1. 中断请求到达:外设拉高中断信号线。
  2. 使能检查:NVIC 检查对应中断的使能位,如果被禁止,请求被忽略。
  3. 挂起标志置位:使能后,NVIC 将挂起寄存器对应位置 1。
  4. 优先级比较:NVIC 比较当前中断的抢占优先级与正在执行的中断优先级。如果更高,触发嵌套;否则等待。
  5. 向量表查表:NVIC 根据中断号从向量表中取出服务函数地址。
  6. 压栈与跳转:硬件自动压栈(xPSR、PC、LR、R0-R3、R12),然后跳转到服务函数。
  7. 执行与返回:服务函数执行完毕后,硬件自动出栈,恢复现场。
NVIC 中断处理流程 外设中断请求 使能位检查 挂起标志置位 抢占优先级比较 是否高于当前中断? 是 向量表查表 + 跳转 执行中断服务函数 否 等待当前中断完成

实际项目中的优先级分配策略

我总结了一套优先级分配原则,在多个项目里验证过,分享给你:

  • 最高抢占优先级(0):留给系统心跳定时器、看门狗、紧急故障处理。这些中断一旦错过,系统可能崩溃。
  • 次高优先级(1-3):留给实时性要求高的外设,比如 DMA 传输完成、高速 ADC 采样、电机控制 PWM 同步。
  • 中等优先级(4-7):留给通信接口,比如 UART、SPI、I2C。这些中断对实时性有要求,但允许微秒级延迟。
  • 低优先级(8-15):留给按键扫描、LED 刷新、传感器轮询等非关键任务。

核心原则:中断优先级不是越高越好。给每个中断分配「够用」的优先级,留出余量给未来可能新增的中断。我见过有人把所有中断都设成优先级 0,结果一个中断卡住,整个系统瘫痪。

常见陷阱与避坑指南

嗯,这里要注意几个容易翻车的地方:

  • 陷阱一:优先级分组在运行时修改。一旦系统跑起来,所有中断的优先级数值含义就固定了。中途改分组,会导致已配置的优先级数值被重新解释,中断行为完全乱套。我建议在系统初始化第一行就设置好分组,之后不再改动。
  • 陷阱二:忽略子优先级的非嵌套特性。同抢占优先级的中断不会互相打断,如果你需要 A 打断 B,必须给 A 更高的抢占优先级,光靠子优先级没用。
  • 陷阱三:中断服务函数执行时间过长。高优先级中断的服务函数如果执行太久,会阻塞所有低优先级中断。我曾经在电机控制项目里,把 FOC 计算放在中断里,结果串口中断被饿死,调试信息都打印不出来。

我曾经犯过的错:在一个多传感器采集项目里,我把所有传感器中断都设成了抢占优先级 2,子优先级按传感器重要性分配。结果发现高频率的传感器中断频繁触发,低频率的传感器中断几乎得不到执行。后来把高频率传感器中断的抢占优先级降到 3,问题解决。记住:频率高的中断,优先级要适当降低,否则它会霸占 CPU。

小结