4. 中断嵌套场景分析:高优先级中断抢占低优先级中断的流程,现场保护与恢复机制
中断嵌套,说白了就是高优先级的中断把正在执行的低优先级中断给“打断”了。很多初学者觉得这很简单,不就是优先级高的先跑嘛。但实际调试起来,现场保护和恢复这块,坑特别多。我早期做SPI从机响应时,就因为这个嵌套恢复顺序搞错,导致数据连续错位,查了整整两天。
这一节,咱们把中断嵌套的完整流程掰开揉碎,从硬件压栈到软件恢复,一步步看清楚。
4.1 中断嵌套发生的条件
不是所有中断都能嵌套。要发生嵌套,必须同时满足三个条件:
- CPU 或 NVIC 支持嵌套:Cortex-M 系列默认支持,但有些老架构需要软件开启。
- 高优先级中断的抢占优先级 > 当前正在执行的中断:注意,是抢占优先级,不是子优先级。
- 全局中断使能(如 PRIMASK 未置位):如果你在低优先级中断里关了总中断,那谁也进不来。
核心概念:抢占优先级决定“能不能打断”,子优先级决定“同时挂起时谁先执行”。嵌套只跟抢占优先级有关。
4.2 嵌套流程:从硬件到软件
我习惯把嵌套流程分成三个阶段:硬件自动处理、高优先级 ISR 执行、硬件自动恢复。咱们用一张流程图看清楚。
4.3 现场保护与恢复的底层细节
现场保护,说白了就是“把当前干到一半的事记下来,等会儿回来接着干”。Cortex-M 的硬件自动完成了大部分工作,但软件部分如果没处理好,恢复时就会出问题。
4.3.1 硬件自动保护的内容
当中断被响应时,硬件会自动把以下寄存器压入当前栈(MSP 或 PSP):
| 寄存器 | 说明 | 压栈顺序 |
|---|---|---|
| R0 - R3 | 函数参数和临时变量 | 先压 |
| R12 | 内部通用寄存器 | 第5个 |
| LR (R14) | 返回地址(被替换为 EXC_RETURN) | 第6个 |
| PC (R15) | 被中断时的程序计数器 | 第7个 |
| xPSR | 程序状态寄存器 | 最后 |
个人经验:我建议你在调试嵌套问题时,先检查栈顶的 xPSR 值。如果 xPSR 的 bit[9](ICI/IT 位)异常,说明中断发生在多周期指令中间,恢复时可能跳错位置。这种情况在 SPI 高速传输时特别容易出现。
4.3.2 软件需要保护的内容
硬件只保护了“基本现场”。如果你的 ISR 里用了 R4-R11、浮点寄存器、或者全局变量,那就得软件自己来。
- R4-R11:Cortex-M 的 AAPCS 规定,这些寄存器由被调用者(ISR)保存。编译器会自动在 ISR 入口压栈、出口出栈。
- 全局变量/外设寄存器:如果高优先级 ISR 修改了低优先级 ISR 正在用的全局变量,恢复后就会读到错误值。这就是经典的“共享数据破坏”问题。
- SPI 外设状态:比如低优先级 ISR 刚读了 SPI DR 但还没处理完,高优先级 ISR 又去操作同一个 SPI 外设,那数据就乱套了。
我曾经踩过的坑:有一次 SPI 从机接收,低优先级 ISR 里用了一个全局 buffer 指针,高优先级 ISR 也往同一个 buffer 写数据。结果低优先级恢复后,指针位置已经变了,数据全部错位。后来我加了一个“嵌套计数器”,高优先级 ISR 检测到嵌套时,改用临时 buffer,才彻底解决。
4.4 嵌套恢复的 EXC_RETURN 机制
中断返回时,CPU 不是靠“记住”之前的状态,而是靠 LR 里存放的 EXC_RETURN 值。这个值告诉硬件:
- 返回哪个模式(线程模式 / Handler 模式)
- 使用哪个栈指针(MSP / PSP)
- 是否使用浮点单元
常见的 EXC_RETURN 值:
| EXC_RETURN 值 | 含义 |
|---|---|
| 0xFFFFFFF1 | 返回 Handler 模式,使用 MSP |
| 0xFFFFFFF9 | 返回线程模式,使用 MSP |
| 0xFFFFFFFD | 返回线程模式,使用 PSP |
嵌套恢复时,高优先级 ISR 执行 BX LR(LR 里是 0xFFFFFFF1),硬件自动出栈,恢复低优先级 ISR 的现场。然后低优先级 ISR 继续执行,直到它自己执行 BX LR(LR 里是 0xFFFFFFF9 或 0xFFFFFFFD),再恢复主程序的现场。
关键点:嵌套恢复的顺序是“后进先出”。高优先级 ISR 先出栈,低优先级 ISR 后出栈。这个顺序由硬件自动保证,软件不需要干预。但如果你在 ISR 里手动修改了 LR,那就可能破坏这个机制。
4.5 嵌套场景下的 SPI 调试要点
SPI 通信对中断嵌套特别敏感。我总结了几条实战经验:
- SPI ISR 里不要开长时间循环:如果低优先级 SPI ISR 里做了大量数据处理,高优先级中断长时间进不来,可能导致 SPI FIFO 溢出。
- 共享 SPI 外设时加互斥:如果两个中断都要操作同一个 SPI,建议用信号量或关中断保护。我习惯在 SPI 操作前后加
__disable_irq()和__enable_irq(),但注意关中断时间不能太长。 - 检查栈空间是否足够:嵌套会消耗更多栈空间。每个嵌套层大约需要 64 字节(8 个寄存器 × 8 字节)。如果栈溢出,恢复时 PC 会指向随机地址,直接 HardFault。
- 用调试器观察 LR 值:在 ISR 入口处设置断点,查看 LR 的值。如果 LR 不是 EXC_RETURN 格式,说明中断响应异常。
调试小技巧:我常用一个“嵌套深度”变量,在 ISR 入口加 1,出口减 1。如果深度超过预期值,就触发断言。这样能快速发现意外的嵌套或递归中断。
4.6 一个典型的嵌套故障案例
最后分享一个真实案例。有一次,SPI 从机接收数据时,偶尔会出现一个字节的偏移。我查了三天,最后发现是这么回事:
- 低优先级 SPI ISR 正在读取 SPI DR 寄存器
- 高优先级定时器中断触发,嵌套进来
- 定时器 ISR 里操作了同一个 SPI 外设的配置寄存器
- 低优先级恢复后,SPI 的状态机已经变了,读到的数据错位
解决方案很简单:定时器 ISR 里不要碰 SPI 外设,改用软件标志,由主循环去处理。嗯,有时候最简单的方案反而最有效。