第二章 中断控制器(NVIC)详解

好,咱们接着聊中断系统。上一章我讲了中断的宏观概念,这一章咱们深入到底层——NVIC,也就是嵌套向量中断控制器。说白了,它就是 Cortex-M 内核里负责管理中断的那个“大管家”。

我记得刚接触 STM32 那会儿,看到 NVIC 那一堆寄存器,头都大了。后来做项目做多了才发现,其实核心就三件事:哪个中断能用、优先级怎么排、中断来了怎么响应。今天咱们就把这三件事彻底讲透。

一、NVIC 寄存器结构

NVIC 的寄存器,说白了就是一堆 32 位的控制位。我挑几个最常用的给你说说:

寄存器 功能 我常用的场景
ISER[0..7] 中断使能寄存器 写1使能,写0无效
ICER[0..7] 中断清除寄存器 写1禁能,写0无效
ISPR[0..7] 中断挂起寄存器 软件触发中断用
ICPR[0..7] 中断清除挂起寄存器 清除挂起状态
IP[0..239] 中断优先级寄存器 每个中断8位,但只用高4位

这里有个坑,我当年踩过——ISER 和 ICER 都是写 1 有效。你写 0 进去,啥事都不会发生。我曾经在调试时对着 ISER 写了个 0,心想“我明明使能了呀”,结果中断死活不进来。后来查手册才发现,使能中断必须写 1,不是写 0。

注意:NVIC 只管理内核之外的异常(比如外设中断)。像 HardFault、SVC 这些内核异常,归 SCB(系统控制块)管,别搞混了。

二、中断优先级分组

优先级分组,这是 NVIC 里最绕的地方。我尽量用大白话讲清楚。

Cortex-M3/M4 支持最多 256 级优先级(8位),但大多数芯片只用了高 4 位,也就是 16 级。这 4 位怎么分配?靠的是 SCB->AIRCR 寄存器里的 PRIGROUP 位。

说白了,优先级分组就是决定:这 4 位里,多少位给抢占优先级,多少位给子优先级

分组 抢占优先级位数 子优先级位数 实际效果
NVIC_PriorityGroup_0 0 位 4 位 只有子优先级,没有抢占
NVIC_PriorityGroup_1 1 位 3 位 2级抢占,8级子优先级
NVIC_PriorityGroup_2 2 位 2 位 4级抢占,4级子优先级
NVIC_PriorityGroup_3 3 位 1 位 8级抢占,2级子优先级
NVIC_PriorityGroup_4 4 位 0 位 16级抢占,没有子优先级

我个人习惯用 Group_2,也就是 4 级抢占 + 4 级子优先级。为什么?因为大多数嵌入式项目,中断优先级分 4 层就够用了——紧急的、重要的、一般的、不着急的。再细分下去,反而容易把自己绕晕。

核心原则:抢占优先级决定能否打断正在执行的中断。子优先级只在同抢占优先级时才有用,决定谁先执行。

三、抢占优先级与子优先级配置

好,理论说完了,咱们上代码。这是我在实际项目中用的配置函数:

// 设置优先级分组为 Group_2:4级抢占 + 4级子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 配置 USART1 中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级 0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 配置 TIM2 中断
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  // 抢占优先级 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;         // 子优先级 1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

你看,USART1 的抢占优先级是 1,TIM2 是 2。数字越小优先级越高。所以 USART1 可以打断 TIM2 的中断。这就是抢占优先级的实际效果。

那子优先级什么时候用?举个例子:假如你有两个中断,抢占优先级都是 2,但子优先级分别是 0 和 1。当它们同时发生时,子优先级 0 的先执行。仅此而已,子优先级不能打断同抢占优先级的中断。

避坑指南:我曾经在一个项目里,把两个外设的中断都设成了抢占优先级 0、子优先级 0。结果它们同时触发时,系统直接卡死了。后来查出来是中断嵌套把自己给锁死了。所以我的建议是——同抢占优先级的中断,子优先级尽量别设成一样,留点余量。

四、实际项目中的优先级设计思路

说了这么多,到底怎么分配优先级?我分享一个我常用的套路:

  • 抢占优先级 0:系统时钟中断(SysTick)、看门狗。这些是命根子,谁都不能打断。
  • 抢占优先级 1:实时性要求高的外设,比如 DMA、高速通信(SPI、UART)。
  • 抢占优先级 2:普通外设,比如按键中断、ADC 转换完成。
  • 抢占优先级 3:不紧急的任务,比如温度传感器读取、LED 闪烁。

你想想看,如果温度传感器读取中断把 DMA 传输给打断了,那数据可能就丢了。所以优先级分配,说白了就是给每个中断排个队——谁更重要,谁先上。

嗯,这里还要注意一点:中断服务函数要短小精悍。我见过有人把 500 行的处理逻辑直接写在中断里,结果其他中断等得花儿都谢了。正确的做法是:中断里只做标志位或数据拷贝,具体处理放到主循环或任务里。

五、总结一下

NVIC 的核心就三件事:

  1. 寄存器操作:ISER 使能、ICER 禁能、IP 设优先级。
  2. 优先级分组:用 PRIGROUP 决定抢占和子优先级的位数分配。
  3. 优先级配置:数字越小优先级越高,抢占优先级决定能否打断。

最后送大家一句话:中断优先级设计,宁可留空,不要填满。给未来的维护留点余地,也给自己留条后路。好了,这一章就到这儿,下一章咱们聊聊中断的响应流程和现场保护。