第二章 中断控制器(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 里最绕的地方。我尽量用大白话讲清楚。
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 的核心就三件事:
- 寄存器操作:ISER 使能、ICER 禁能、IP 设优先级。
- 优先级分组:用 PRIGROUP 决定抢占和子优先级的位数分配。
- 优先级配置:数字越小优先级越高,抢占优先级决定能否打断。
最后送大家一句话:中断优先级设计,宁可留空,不要填满。给未来的维护留点余地,也给自己留条后路。好了,这一章就到这儿,下一章咱们聊聊中断的响应流程和现场保护。