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 分组规则配置流程

配置中断优先级分组必须在系统启动早期、使能任何中断之前完成。一旦分组确定,整个系统的中断优先级含义即被锁定。配置步骤如下:

  1. 读取当前 AIRCR 寄存器: 通过 __get_PRIGROUP() 或直接读取 SCB->AIRCR。
  2. 清除 PRIGROUP 位域: 将 AIRCR 的 bits[10:8] 清零。
  3. 写入新的分组值: 设置 PRIGROUP 为所需值(0~4),同时必须写入 VECTKEY 字段(0x5FA)以防止误写。
  4. 验证配置: 回读 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 配置下,三个中断的嵌套与仲裁流程:

中断优先级分组嵌套示例 - Group 2 Group 2 中断嵌套与仲裁流程(2位抢占 + 2位子优先级) 时间 主程序(Main Loop) IRQ1 (抢占=0, 子=1) t1: IRQ1 到达 IRQ2 (抢占=1, 子=0) t2: IRQ2 到达 IRQ3 (抢占=0, 子=2) t3: IRQ3 到达 嵌套行为分析: • IRQ1(抢占=0)执行中,IRQ2(抢占=1)到达 → 抢占优先级更高(数值更小),IRQ2 打断 IRQ1。 • IRQ2 执行中,IRQ3(抢占=0)到达 → 抢占优先级相同,但 IRQ3 子优先级=2 > IRQ1 子优先级=1,IRQ3 不能打断 IRQ2,等待。 • IRQ2 完成后,硬件仲裁 IRQ1 和 IRQ3:子优先级 1 < 2,IRQ1 优先执行。

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 与应用程序的分组配置不一致。