4、中断嵌套与抢占:可嵌套中断控制器(NVIC)、抢占优先级与子优先级

中断嵌套,说白了就是高优先级的中断能把低优先级的中断“踢开”,自己先执行。等它处理完了,再回来继续执行刚才被打断的那个。这个机制在实时系统里太重要了。我刚开始做嵌入式时,就吃过没处理好嵌套的亏——一个关键的中断被另一个不紧急的中断堵住了,整个系统响应直接崩掉。

ARM Cortex-M 系列内核里,负责管理这一切的硬件叫 NVIC(Nested Vectored Interrupt Controller,可嵌套中断控制器)。它不光能处理中断嵌套,还支持中断向量化,也就是说每个中断源都有自己独立的入口地址,不用像老式单片机那样靠软件查询中断标志位。

4.1 NVIC 的核心能力

NVIC 到底能干什么?我列几个关键点:

  • 支持最多 240 个中断源(具体数量看芯片型号,比如 STM32F103 只有 60 个)
  • 每个中断可独立使能/除能,还能设置挂起和清除挂起
  • 中断优先级可编程,支持 8~256 级(取决于实现)
  • 自动硬件压栈和出栈,中断切换时 CPU 自动保存 R0-R3、R12、LR、PC、xPSR
  • 咬尾中断(Tail-Chaining):如果两个中断连续发生,省去多余的出栈/入栈
  • 晚到中断(Late-Arriving):更高优先级中断在压栈阶段到达,直接替换当前要处理的中断

嗯,这里要注意:NVIC 的嵌套是硬件自动完成的,不需要你写额外的代码。但优先级怎么分配,完全由你决定。

4.2 抢占优先级与子优先级

ARM Cortex-M 的优先级系统分两层:抢占优先级(Preempt Priority)和子优先级(Sub Priority)。这个设计我一开始也觉得绕,后来发现它其实很灵活。

抢占优先级决定一个中断能否打断另一个正在执行的中断。子优先级呢?当两个中断的抢占优先级相同时,子优先级高的先执行,但不会发生嵌套。

举个例子:

中断源 抢占优先级 子优先级
定时器 A 0(最高) 0
UART 接收 1 0
GPIO 外部中断 1 1
DMA 完成 2 0

在这个配置下:

  • 定时器 A 可以打断 UART、GPIO、DMA 的中断服务函数
  • UART 和 GPIO 抢占优先级相同,所以它们之间不会互相打断
  • 如果 UART 和 GPIO 同时挂起,UART 先执行(子优先级更高)
  • DMA 优先级最低,谁都能打断它

我个人习惯把最紧急的中断(比如系统心跳、故障检测)放在抢占优先级 0,其他按实时性要求依次排开。

4.3 优先级分组:AIRCR 寄存器

抢占优先级和子优先级的位数分配,由 系统控制块(SCB) 中的 AIRCR(Application Interrupt and Reset Control Register)寄存器的 PRIGROUP 位段决定。Cortex-M3/M4 支持 8 种分组方式:

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(无嵌套)

实际项目中,我建议用分组 3(4 位抢占 + 4 位子优先级),也就是 16 级抢占优先级,每级下面有 16 个子优先级。这个粒度对大多数应用都够用了。

设置代码很简单,以 STM32 为例:

// 设置优先级分组:4位抢占优先级,4位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);

// 配置具体中断的优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
⚠️ 重要提醒: 优先级分组必须在系统初始化时设置一次,之后不要再改。一旦改了分组,所有已配置的中断优先级含义都会变,很容易导致优先级反转或意外嵌套。我曾经在一个项目里因为不同模块各自调了分组函数,结果调试了两天才发现是这里的问题。

4.4 中断嵌套的硬件行为

当一个高优先级中断发生时,CPU 正在执行低优先级中断的服务函数,硬件会怎么做?

  1. CPU 检测到更高优先级中断请求
  2. 当前指令执行完后,硬件自动压栈(保存现场)
  3. 从向量表中取出高优先级中断的入口地址
  4. 跳转执行高优先级中断服务函数
  5. 高优先级中断执行完毕,执行出栈指令(恢复现场)
  6. 继续执行刚才被打断的低优先级中断

整个过程是硬件自动完成的,不需要你手动保存寄存器。但要注意:中断服务函数要尽量短,否则嵌套层数多了,栈空间会急剧消耗。

💡 经验之谈: 我一般把中断服务函数控制在 10~20 行以内。如果要做复杂处理,就在中断里只设置一个标志位,把实际工作放到主循环或任务里去做。这样能有效减少嵌套深度,也降低了栈溢出的风险。

4.5 中断嵌套的优先级逻辑图

下面这张图展示了中断嵌套的完整流程。你可以看到不同优先级的中断如何互相打断,以及硬件如何管理压栈和出栈。

中断嵌套与抢占优先级逻辑图 时间→ 主程序(优先级最低) 低优先级中断 高优先级中断 继续主程序 硬件自动压栈 硬件自动压栈 硬件出栈恢复 嵌套过程说明 1. 主程序执行时,低优先级中断到达 → 压栈 → 执行低优先级 ISR 2. 低优先级 ISR 执行中,高优先级中断到达 → 再次压栈 → 执行高优先级 ISR 3. 高优先级 ISR 完成 → 出栈 → 继续低优先级 ISR → 出栈 → 继续主程序 优先级等级与嵌套关系 抢占优先级 0(最高) 抢占优先级 1 抢占优先级 2(最低) 高优先级可打断低优先级,同级之间不嵌套(子优先级决定谁先执行)

4.6 避坑指南:我踩过的几个坑

中断嵌套看起来简单,实际项目中容易出问题。我分享几个亲身经历:

  • 栈溢出:嵌套层数多了,每个中断都要压栈 8 个寄存器(32 字节),再加上局部变量。我曾经在一个项目中嵌套了 4 层,结果栈空间不够,程序跑飞。后来我把每个中断任务的栈深度都算了一遍,才解决。
  • 优先级反转:低优先级中断占用了高优先级中断需要的资源,导致高优先级被阻塞。解决办法是让中断服务函数里不要调用可能被阻塞的 API。
  • 忘记设置优先级分组:默认分组可能不是你想要的。比如 STM32 上电默认是分组 0(7 位抢占 + 1 位子优先级),如果你没改就直接配优先级,效果可能完全不对。
  • 在中断里调用 printf:printf 通常不可重入,而且执行时间很长。在中断里调用它,轻则影响实时性,重则死锁。我一般用环形缓冲区 + 标志位的方式,把打印任务放到主循环里做。
📌 核心要点:
  • NVIC 硬件自动管理中断嵌套,你只需要配置好优先级
  • 抢占优先级决定能否打断,子优先级决定同级的执行顺序
  • 优先级分组在系统初始化时设置一次,之后不要修改
  • 中断服务函数要短,避免深层嵌套和不可重入调用