2. 中断控制器:NVIC 架构、优先级分组、使能与屏蔽

中断控制器,说白了就是 CPU 的“门卫”。它决定哪个中断能进来,哪个得排队,哪个优先级更高。在 Cortex-M 系列里,这个门卫叫 NVIC(嵌套向量中断控制器)。

我刚开始接触 NVIC 时,觉得它不就是个中断管理器嘛,有啥好研究的?直到有一次产品在现场频繁死机,查了三天才发现是优先级配置出了问题。嗯,从那以后我再也不敢小看这个“门卫”了。

2.1 NVIC 架构概览

NVIC 是 Cortex-M 内核的一部分,不是外设。它直接集成在 CPU 内部,所以响应速度极快。我个人习惯把 NVIC 理解成三个核心模块:

  • 中断挂起寄存器:记录哪些中断正在等待处理
  • 中断优先级寄存器:存储每个中断的优先级数值
  • 中断使能寄存器:控制每个中断的开关

NVIC 最多支持 240 个外部中断,但具体数量取决于芯片厂商。比如 STM32F103 只有 60 个,而某些高端型号会更多。你想想看,这么多中断要管理,没有一套清晰的架构怎么行?

核心要点:NVIC 的响应速度是硬件级别的,从中断触发到进入中断服务函数,通常只需要 12 个时钟周期。这是软件轮询永远比不了的。

下面这张图展示了 NVIC 在系统中的位置和内部结构:

NVIC 架构与中断处理流程 Cortex-M 内核 CPU 执行主程序 NVIC 控制器 中断优先级管理 挂起 / 使能 / 屏蔽 外部中断源 GPIO / 定时器 / UART DMA / ADC / 外部引脚 最多 240 个 中断向量表 存储 ISR 入口地址 优先级分组寄存器 抢占优先级 / 子优先级 关键路径说明: 1. 外设产生中断请求 → NVIC 接收并检查优先级 2. NVIC 判断是否抢占当前任务 → 挂起或立即响应 3. 从向量表加载 ISR 地址 → CPU 跳转执行中断服务函数 4. 执行完毕后返回断点,恢复现场

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 的架构设计其实很精巧。它把优先级分组、使能、屏蔽、挂起这些功能都集成在一起,用硬件实现了中断的自动管理。我们做嵌入式开发的,只要理解清楚这几个概念,就能把中断用得得心应手。

嗯,中断控制器的内容就这些。记住一句话:优先级分组定好就别改,使能和屏蔽要分清,挂起位记得清。做到这三点,中断这块基本不会出大问题。