STM32 中断嵌套:优先级分组与抢占/子优先级深度解析
👁 3 阅读 · 2026-08-14 · STM32 中断
在 STM32 裸机或 RTOS 开发中,中断嵌套是保障实时性的关键,而优先级分组与抢占/子优先级的配置往往令人困惑。本文从 NVIC 内核结构出发,深入讲解抢占优先级与子优先级的区别,以及如何通过 SCB->AIRCR 完成分组,并给出标准库和 HAL 库的完整配置示例与实战注意事项,助你彻底掌握中断嵌套设计。
## 1. 为什么需要优先级分组?
STM32 的 NVIC(嵌套向量中断控制器)支持中断嵌套,即高优先级中断可以打断低优先级中断的执行。为了管理这种嵌套关系,Cortex-M3/M4 内核引入了**优先级分组**机制,将每个中断的优先级分成两部分:
- **抢占优先级(Preemption Priority)**:决定能否打断其他中断。数值越小,抢占能力越强。
- **子优先级(Sub Priority)**:当两个中断的抢占优先级相同时,子优先级数值越小,先响应(但不能打断彼此)。
注意:**只有抢占优先级不同,才能实现嵌套**;子优先级仅用于排队仲裁。
## 2. 优先级分组寄存器:SCB->AIRCR
优先级分组通过设置应用程序中断及复位控制寄存器(AIRCR)的 PRIGROUP 位域完成。不同值对应不同的抢占/子优先级位宽,以 STM32F1/F4 为例(使用 4 位优先级):
| PRIGROUP 值 | 抢占优先级位数 | 子优先级位数 | 可能的抢占优先级 | 可能的子优先级 |
|------------|--------------|------------|----------------|--------------|
| 0 (NVIC_PriorityGroup_0) | 0 | 4 | 0(不可嵌套) | 0~15 |
| 1 (NVIC_PriorityGroup_1) | 1 | 3 | 0~1 | 0~7 |
| 2 (NVIC_PriorityGroup_2) | 2 | 2 | 0~3 | 0~3 |
| 3 (NVIC_PriorityGroup_3) | 3 | 1 | 0~7 | 0~1 |
| 4 (NVIC_PriorityGroup_4) | 4 | 0 | 0~15 | 0(不支持子优先级) |
配置本质是修改 `SCB->AIRCR` 的 PRIGROUP 位,需使用 `VECTKEY` 解锁。**整个系统只能设置一次分组**,必须在初始化时确定,运行中切勿更改。
## 3. 配置步骤与代码示例
### 3.1 步骤概览
1. 在系统初始化时调用分组函数,设定全局分组策略。
2. 对外设中断的抢占优先级和子优先级赋值。
3. 使能对应外设中断(如 USART1_IRQn)和 NVIC 通道。
4. 编写中断服务函数,并确保高优先级中断能正确嵌套(中断函数内可响应更高抢占优先级的中断)。
### 3.2 标准外设库配置(以 STM32F1 为例)
```c
#include "stm32f10x.h"
void NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 设置分组:第3组,抢占优先级2位,子优先级2位
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置USART1中断:抢占优先级2,子优先级0(抢占优先级数值小于其他中断,可嵌套它们)
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置EXTI0中断:抢占优先级3,子优先级1
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
```
### 3.3 HAL 库配置(以 STM32F4 为例)
```c
#include "stm32f4xx_hal.h"
void HAL_NVIC_Config(void)
{
// 设置分组:第2组,抢占优先级2位,子优先级2位
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 配置USART1中断:抢占优先级2,子优先级0
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
// 配置外部中断0:抢占优先级3,子优先级1
HAL_NVIC_SetPriority(EXTI0_IRQn, 3, 1);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
```
### 3.4 中断服务函数与嵌套演示
```c
// USART1中断服务函数(优先级高,可打断EXTI0的处理)
void USART1_IRQHandler(void)
{
// 假设此时在EXTI0_IRQHandler中被高优先级打断进入这里
// 处理串口事务...
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET) {
// 读取数据
}
}
// EXTI0中断服务函数(优先级低,执行中可能被USART1打断)
void EXTI0_IRQHandler(void)
{
// 长时间处理... 在此时如果USART1中断到来,将暂停本函数,转入USART1_IRQHandler
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 处理外部事件
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
```
## 4. 优先级数值的误区与陷阱
- **数值越小优先级越高**:很多人习惯用“0最大”,在 STM32 中相反。
- **分组后优先级数值范围受限**:例如分组 2 时,抢占优先级只能写 0~3,子优先级只能写 0~3,超出范围会产生不可预测行为。
- **不要在中断服务函数中修改分组**:可能导致已配置的优先级映射错乱,甚至引发 HardFault。
- **RTOS 用户的警告**:FreeRTOS 等内核通常要求 `NVIC_PriorityGroup_4`(即全部位为抢占优先级),并限制中断优先级不高于 `configMAX_SYSCALL_INTERRUPT_PRIORITY`。如果使用 RTOS,分组配置必须与内核要求一致。
- **关闭中断的优先级限制**:`__disable_irq()` 会关闭所有可屏蔽中断,但 NMI 和 HardFault 不受影响,嵌套设计时不建议长期关闭中断。
## 5. 实战建议
1. **先确定实时需求**:例如有 DMA 传输、高速通信、按键消抖等,将最紧急的中断(如系统心跳)设为抢占优先级 0,普通外设设为 2~3。
2. **尽量统一预处理优先级与子优先级位宽**:如果所有中断抢占优先级都不同,子优先级就无效了,但分组仍影响可用数值范围。
3. **通过 `NVIC_GetPriorityGrouping()` 和 `NVIC_GetPriority()` 调试检查实际配置**,避免人为配置错误。
4. **在中断嵌套场景下,注意栈空间**:每次嵌套都会消耗更多栈,需合理设置中断栈大小,防止溢出。
## 6. 总结
STM32 的中断嵌套关键在于正确设置 AIRCR 分组,并理解抢占优先级决定“能否打断”,子优先级决定“谁先执行”。配置时务必遵循“系统分组全局唯一、单次设置”的原则,在标准库与 HAL 库中分别使用 `NVIC_PriorityGroupConfig()` 和 `HAL_NVIC_SetPriorityGrouping()`。结合具体项目的实时性要求,合理分配抢占/子优先级,才能写出健壮且响应及时的中断系统。