07-中断系统.md 8.9 KB

07 中断系统(HAL)

目标:理解优先级分组与嵌套,实现 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 的统一规则)

详细工作原理

NVIC 寄存器组

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。

中断入栈序列 (Exception Entry)

当 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,开始执行中断服务函数。

异常返回 (Exception Return)

中断服务函数末尾执行 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 完成后)

PRIGROUP 位域分割

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)为最低优先级。

中断优先级分组

中断排队与嵌套

中断排队 vs 中断嵌套

中断排队与嵌套

中断排队:多个中断等待 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

NVIC 配置

CubeMX → NVIC Settings 中:

  1. 使能要用到的中断
  2. 设置抢占优先级 (Preemption Priority) 和子优先级 (Sub Priority)
  3. HAL_NVIC_SetPriority(IRQn, PreemptPriority, SubPriority) 或在 CubeMX 中图形化设置

EXTI 外部中断

CubeMX 配置

  • PB0 → GPIO_EXTI0
  • GPIO 参数:Pull-up
  • NVIC → EXTI line0 interrupt → Enable
  • GPIO mode:External Interrupt Mode with Rising/Falling edge detection

关键函数

函数 说明
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);
    }
}

UART 中断接收

已在 04-UART串口.md 详细说明,核心模式:

HAL_UART_Receive_IT() 启动中断接收
         ↓
硬件接收 1 字节 → 触发中断
         ↓
HAL_UART_RxCpltCallback() 被调用
         ↓
用户在回调中重新调用 HAL_UART_Receive_IT()

常见坑点

  • ⚠️ 中断服务函数写在 stm32f1xx_it.c,不要直接修改
  • ⚠️ 用户只重写回调函数(__weak 弱函数),不要改 ISR
  • ⚠️ 回调函数中不要使用 HAL_Delay()(它依赖 SysTick 中断,同优先级可能导致死锁)
  • ⚠️ 中断函数应短小、快速执行,耗时操作放主循环