目标:理解优先级分组与嵌套,实现 EXTI 外部中断和 UART 中断接收
NVIC 优先级分组(由 SCB->AIRCR 寄存器的 PRIGROUP 位决定)。STM32 把中断优先级分为抢占优先级和子优先级两部分,但两者的位数比例可以调整:
| 分组 | 抢占优先级位数 | 子优先级位数 | 可配置优先级数量 |
|---|---|---|---|
NVIC_PRIORITYGROUP_0 |
0 | 4 | 抢占: 1种, 子: 16级 |
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种, 子: 1级 |
数值越小,优先级越高!("越小越优先"是 ARM Cortex 的统一规则)
Cortex-M3 的 NVIC 通过一组系统控制寄存器管理中断的使能、挂起、清除和优先级:
| 寄存器组 | 名称 | 功能 |
|---|---|---|
| ISER[0~2] | Interrupt Set-Enable Register | 写 1 使能中断,读返回当前使能状态 |
| ICER[0~2] | Interrupt Clear-Enable Register | 写 1 禁能中断 |
| ISPR[0~2] | Interrupt Set-Pending Register | 写 1 将中断置为挂起状态(软件触发中断) |
| ICPR[0~2] | Interrupt Clear-Pending Register | 写 1 清除挂起状态 |
| IP[0~79] | Interrupt Priority Register | 每个中断 8 位,STM32 只使用高 4 位 |
每个寄存器组有 3 个 32 位寄存器(ISER0~2 等),覆盖 STM32 最多 80 个中断。IP 寄存器的字节地址按中断号连续排列,每个中断占用 1 字节(8 位),但 STM32 仅使用 [7:4] 高 4 位存储优先级值,低 4 位读回为 0。
当 Cortex-M3 响应中断时,硬件自动完成以下操作(无需软件干预):
主栈指针 (MSP) 当前指向
│
▼
┌──────────┐ ← SP 递减(满递减栈)
│ xPSR │ ← 程序状态寄存器(含条件标志、中断状态)
├──────────┤
│ PC │ ← 返回地址(被中断指令的下一条)
├──────────┤
│ LR │ ← 返回后使用的栈指针
├──────────┤
│ R12 │ ← 通用寄存器
├──────────┤
│ R3 │
├──────────┤
│ R2 │
├──────────┤
│ R1 │
├──────────┤
│ R0 │ ← 入栈完成,SP 指向栈顶
└──────────┘
入栈的 8 个寄存器(xPSR、PC、LR、R12、R3~R0)共 32 字节,由硬件在 12 个总线周期内自动完成。入栈后,硬件从向量表(位于 Flash 起始地址)取出对应的中断向量(即 ISR 入口地址),加载到 PC,开始执行中断服务函数。
中断服务函数末尾执行 BX LR 指令时,LR 寄存器中保存的是一个特殊值——EXC_RETURN,而非普通返回地址:
| EXC_RETURN | 返回模式 |
|---|---|
0xFFFFFFF1 |
返回 Handler 模式(使用 MSP) |
0xFFFFFFF9 |
返回 Thread 模式(使用 MSP) |
0xFFFFFFFD |
返回 Thread 模式(使用 PSP) |
硬件检测到 LR 的值是 EXC_RETURN(高 4 位全 1)时,自动执行出栈操作,恢复 R0~R3、R12、LR、PC、xPSR,恢复被中断的代码执行。
Tail-Chaining(尾链):
当中断 A 完成后,如果还有一个中断 B 正在挂起等待,硬件跳过中断 A 的出栈和中断 B 的入栈,直接执行中断 B 的 ISR。这省去了 12 个周期的入栈和 12 个周期的出栈,共节省 24 个总线周期。
无尾链: ISR_A → 出栈(12周期) → 入栈(12周期) → ISR_B
有尾链: ISR_A ───────────────→ ISR_B (省 24 周期)
Late-Arrival(迟到中断):
中断 A 的入栈过程中,一个更高优先级的中断 B 到达。硬件不打断当前的入栈操作,但入栈完成后,直接从向量表取中断 B 的向量(而非中断 A 的向量),优先执行中断 B。
入栈开始 → 中断 B 到达(更高优先级)
│
入栈完成 → 取中断 B 向量 → 执行 ISR_B
→ 执行 ISR_A(B 完成后)
SCB->AIRCR 寄存器的 PRIGROUP[9:8] 位决定优先级的分割方式。每个中断的优先级值存储在 NVIC_IPRx 寄存器的高 4 位 [7:4],低 4 位 [3:0] 硬件忽略(始终读回 0):
NVIC_IPRx 寄存器(8 位,每个中断一个):
[7:4] ← 有效优先级值 [3:0] ← 恒为 0
┌──┬──┬──┬──┐┌──┬──┬──┬──┐
│B7│B6│B5│B4││ 0│ 0│ 0│ 0│
└──┴──┴──┴──┘└──┴──┴──┴──┘
PRIGROUP=4 (Group 4):全部 4 位 = 抢占优先级
PRIGROUP=3 (Group 3):高 3 位 = 抢占,低 1 位 = 子
PRIGROUP=2 (Group 2):高 2 位 = 抢占,低 2 位 = 子
PRIGROUP=1 (Group 1):高 1 位 = 抢占,低 3 位 = 子
PRIGROUP=0 (Group 0):0 位 = 抢占,低 4 位 = 子
"数值越小优先级越高" 的原理:ARM Cortex-M 的优先级比较器使用二进制数值比较,0 最小,优先级最高。因此配置 NVIC_IPRx 时,写入 0 为最高优先级,写入 240(即 0xF0)为最低优先级。
中断排队:多个中断等待 CPU 执行,NVIC 根据优先级决定谁先执行。
中断嵌套:一个中断正在执行时,另一个更高优先级的中断到来,暂停当前中断去执行更高的中断。
判定规则:
抢占优先级不同 → 高抢占优先级可以嵌套低抢占优先级的中断
抢占优先级相同 → 不嵌套,按子优先级排队
子优先级也相同 → 按中断向量号(硬件固定),号小的先执行
优先级分组 Group 2(2位抢占 + 2位子优先级)。五个中断按到达时间排列:
| 中断 | 抢占优先级 | 子优先级 | 到达顺序 |
|---|---|---|---|
| A | 1 | 0 | ① |
| B | 0 | 1 | ② |
| C | 0 | 2 | ③ |
| D | 2 | 0 | ④ |
| E | 3 | 0 | ⑤ |
执行流(数值越小优先级越高):
① A(抢占1) 开始执行
└─ ② B(抢占0) 到来 → 0 > 1 → **嵌套!暂停 A,B 开始**
└─ ③ C(抢占0, 子2) 到来 → 同组不嵌套 → 排队(子2 < B的子1,C在B之后)
└─ B 完成 → C 开始执行
└─ C 完成 → **回到 A**(A 被抢占后恢复)
└─ ④ D(抢占2) 到来 → 2 < 1 → 排队(D在A后面)
└─ A 完成 → D 开始 → ⑤ E(抢占3) 在 D 后面排队
最终执行顺序:A → B → C → A(恢复) → D → E
CubeMX → NVIC Settings 中:
HAL_NVIC_SetPriority(IRQn, PreemptPriority, SubPriority) 或在 CubeMX 中图形化设置| 函数 | 说明 |
|---|---|
HAL_GPIO_EXTI_IRQHandler(PIN) |
ISR 中调用,检测边沿并调回调 |
HAL_GPIO_EXTI_Callback(PIN) |
用户重写此回调(弱函数) |
/* stm32f1xx_it.c — 中断服务函数 */
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
/* main.c — 回调函数 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
已在 04-UART串口.md 详细说明,核心模式:
HAL_UART_Receive_IT() 启动中断接收
↓
硬件接收 1 字节 → 触发中断
↓
HAL_UART_RxCpltCallback() 被调用
↓
用户在回调中重新调用 HAL_UART_Receive_IT()
stm32f1xx_it.c,不要直接修改__weak 弱函数),不要改 ISRHAL_Delay()(它依赖 SysTick 中断,同优先级可能导致死锁)