4. 中断优先级分组:抢占优先级与子优先级的位域划分、分组规则配置
在 ARM Cortex-M 系列处理器(如 STM32、NXP LPC 等)中,中断优先级并非一个简单的 0~255 数值,而是通过 优先级分组寄存器(PRIGROUP) 将优先级寄存器中的位域划分为两个逻辑部分:抢占优先级(Preempt Priority) 和 子优先级(Subpriority)。这种分组机制直接决定了中断嵌套的行为和响应顺序,是驱动开发中必须精确配置的核心参数。
核心概念: 抢占优先级决定一个中断能否打断另一个正在执行的中断(嵌套);子优先级决定当多个中断具有相同抢占优先级时,硬件优先响应哪一个。分组规则通过 SCB->AIRCR 寄存器的 PRIGROUP 位域设置。
4.1 优先级寄存器的位域结构
Cortex-M3/M4/M7 使用 8 位优先级寄存器(实际使用的位数由芯片厂商决定,例如 STM32 仅使用高 4 位)。这 8 位(或实际有效位)通过 PRIGROUP 划分为两组:
- MSB 侧(高位): 抢占优先级位域,决定中断的抢占能力。
- LSB 侧(低位): 子优先级位域,决定同组抢占优先级下的仲裁顺序。
分组规则由 PRIGROUP 值 决定,该值占用 SCB->AIRCR 寄存器的 bits[10:8]。下表列出了标准的分组配置:
| PRIGROUP 值 | 抢占优先级位数 | 子优先级位数 | 抢占优先级等级数 | 子优先级等级数 | 典型应用场景 |
|---|---|---|---|---|---|
| 0 (NVIC_PriorityGroup_0) | 0 | 4 | 1 | 16 | 无嵌套,仅按子优先级仲裁 |
| 1 (NVIC_PriorityGroup_1) | 1 | 3 | 2 | 8 | 两级嵌套,简单实时系统 |
| 2 (NVIC_PriorityGroup_2) | 2 | 2 | 4 | 4 | 平衡嵌套与仲裁,通用嵌入式 |
| 3 (NVIC_PriorityGroup_3) | 3 | 1 | 8 | 2 | 多级嵌套,强实时控制 |
| 4 (NVIC_PriorityGroup_4) | 4 | 0 | 16 | 1 | 完全抢占,无子优先级 |
实践提示: STM32 HAL 库使用
HAL_NVIC_SetPriorityGrouping() 函数配置分组。大多数 RTOS(如 FreeRTOS)要求使用 Group 4(4 位抢占,0 位子优先级),以确保所有中断优先级仅由抢占优先级决定,避免子优先级干扰调度器逻辑。
4.2 分组规则配置流程
配置中断优先级分组必须在系统启动早期、使能任何中断之前完成。一旦分组确定,整个系统的中断优先级含义即被锁定。配置步骤如下:
- 读取当前 AIRCR 寄存器: 通过
__get_PRIGROUP()或直接读取 SCB->AIRCR。 - 清除 PRIGROUP 位域: 将 AIRCR 的 bits[10:8] 清零。
- 写入新的分组值: 设置 PRIGROUP 为所需值(0~4),同时必须写入 VECTKEY 字段(0x5FA)以防止误写。
- 验证配置: 回读 AIRCR 确认分组生效。
/* 标准库方式:设置分组为 Group 2(2位抢占 + 2位子优先级) */
#define NVIC_AIRCR_PRIGROUP_MASK ((uint32_t)0x00000700)
#define NVIC_AIRCR_VECTKEY ((uint32_t)0x05FA0000)
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
uint32_t reg_value;
reg_value = SCB->AIRCR; /* 读取当前值 */
reg_value &= ~(NVIC_AIRCR_VECTKEY | NVIC_AIRCR_PRIGROUP_MASK); /* 清除 VECTKEY 和分组位 */
reg_value |= NVIC_AIRCR_VECTKEY | (PriorityGroup << 8); /* 写入 VECTKEY 和新分组 */
SCB->AIRCR = reg_value; /* 写入寄存器 */
}
/* 调用示例:配置为 Group 2 */
NVIC_SetPriorityGrouping(2);
配置陷阱: 写入 AIRCR 时必须保持 VECTKEY 字段为 0x5FA,否则写入被忽略。此外,不要在中断服务函数(ISR)中修改分组,因为分组改变会立即影响所有已配置中断的优先级含义,导致不可预测的嵌套行为。
4.3 位域划分与优先级数值计算
理解位域划分后,才能正确计算中断优先级数值。假设芯片使用 4 位优先级(实际有效位为 bits[7:4]),分组为 Group 2(2 位抢占 + 2 位子优先级):
- 抢占优先级位域: bits[7:6](2 位),取值范围 0~3。
- 子优先级位域: bits[5:4](2 位),取值范围 0~3。
- 优先级寄存器值: (抢占优先级 << 2) | 子优先级。
例如,设置抢占优先级为 1、子优先级为 2,则优先级寄存器值为 (1 << 2) | 2 = 6。数值越小,优先级越高。
/* 手动计算优先级值 */
#define PRIORITY_GROUP_2_PREEMPT_BITS 2
#define PRIORITY_GROUP_2_SUB_BITS 2
uint8_t CalculatePriority(uint8_t preempt_prio, uint8_t sub_prio)
{
/* 确保值在有效范围内 */
if (preempt_prio >= (1 << PRIORITY_GROUP_2_PREEMPT_BITS)) return 0xFF;
if (sub_prio >= (1 << PRIORITY_GROUP_2_SUB_BITS)) return 0xFF;
/* 左移子优先级位数,然后合并 */
return (preempt_prio << PRIORITY_GROUP_2_SUB_BITS) | sub_prio;
}
/* 使用 HAL 库直接设置 */
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 2); /* 抢占=1,子优先级=2 */
4.4 分组对中断嵌套行为的影响
分组规则直接决定了中断嵌套的深度和规则。以下 SVG 图展示了 Group 2 配置下,三个中断的嵌套与仲裁流程:
4.5 分组配置的工程决策
选择分组规则需要权衡嵌套深度与仲裁粒度。以下表格总结了不同场景下的推荐分组:
| 系统类型 | 推荐分组 | 理由 |
|---|---|---|
| 裸机简单轮询 | Group 0 | 无需嵌套,仅需区分中断优先级顺序 |
| 裸机多级实时控制 | Group 3 或 Group 4 | 需要明确的高、中、低抢占等级,子优先级仅用于同等级仲裁 |
| FreeRTOS / RT-Thread | Group 4 | RTOS 通常将 PendSV 和 SysTick 配置为最低抢占优先级,子优先级必须为 0 以保证调度正确 |
| 混合中断 + 任务同步 | Group 2 | 平衡嵌套深度(4 级)和子优先级仲裁(4 级),适用于大多数外设驱动 |
关键原则: 一旦项目启动,不要更改分组。更改分组会重新解释所有已配置中断的优先级数值,导致中断响应顺序完全错乱。在驱动初始化代码中,建议将分组配置放在
SystemInit() 或 HAL_Init() 之后的第一行。
4.6 常见配置错误与调试方法
- 错误 1:分组配置后未验证。 写入 AIRCR 后立即回读,确认 PRIGROUP 位域与预期一致。
- 错误 2:中断优先级数值超出有效位范围。 例如在 4 位有效位系统中设置优先级为 0xF0,实际只取高 4 位,导致优先级意外降低。
- 错误 3:RTOS 中使用了非 Group 4 分组。 导致 PendSV 中断无法被正确抢占,系统调度异常。
- 错误 4:在 ISR 中修改分组。 分组修改后,当前正在执行的中断优先级含义立即改变,可能导致无限递归或死锁。
/* 调试方法:读取当前分组并打印 */
uint32_t GetCurrentPriorityGroup(void)
{
uint32_t aircr = SCB->AIRCR;
return ((aircr >> 8) & 0x07);
}
/* 在调试串口输出分组值 */
printf("Current PRIGROUP: %lu\n", GetCurrentPriorityGroup());
调试技巧: 使用调试器的寄存器窗口直接观察 SCB->AIRCR 的 bits[10:8]。如果发现分组值异常,检查是否有代码意外修改了 AIRCR,或者 Bootloader 与应用程序的分组配置不一致。