2. 中断恢复的基石:保存与恢复现场(压栈出栈)、中断返回指令(RETI/IRET)

中断恢复这事儿,说白了就是“怎么出去,怎么回来”。

你想想看,CPU 正忙着算你的主程序,突然一个中断来了。它得放下手里的活,跳去执行中断服务程序。但问题来了——跳过去之前,CPU 当前的状态怎么办?寄存器里的值、程序跑到的位置、各种标志位……这些要是丢了,回来之后程序就乱套了。

所以,中断恢复的基石就两个动作:保存现场和恢复现场。而实现这两个动作的,就是压栈、出栈,以及那条关键的中断返回指令。

2.1 保存现场:把“当前状态”存起来

保存现场,就是在进入中断服务程序之前,把 CPU 当前的关键寄存器内容压入栈中。

我个人习惯把栈想象成一个“临时储物柜”。你进去办事之前,先把身上的贵重物品存进去,拿个号牌。办完事出来,凭号牌取回东西,原封不动。

核心原则:中断服务程序执行期间,不能破坏主程序的任何状态。你动了哪个寄存器,就得在退出前把它恢复原样。

具体要保存哪些东西?这取决于 CPU 架构。我列个常见的清单:

保存项 说明 谁来做
程序计数器(PC) 中断发生时的下一条指令地址 硬件自动压栈
状态寄存器(PSW/FLAGS) 进位、零标志、中断使能位等 硬件自动压栈(部分架构)
通用寄存器(R0-R7等) 中断服务程序中会用到的寄存器 软件手动压栈
其他特殊寄存器 如堆栈指针、链接寄存器等 视架构而定

嗯,这里要注意:硬件自动保存的部分,你不需要操心。但软件手动保存的部分,你必须自己写代码。我在项目中遇到过有人忘了保存某个寄存器,结果中断返回后主程序里的循环变量莫名其妙变了,查了两天才找到原因。

2.2 压栈与出栈:栈的操作艺术

压栈(PUSH)和出栈(POP)是保存与恢复现场的具体手段。栈是一种后进先出(LIFO)的数据结构,正好符合中断嵌套的需求——后发生的中断先处理,先恢复。

来看一个典型的保存现场代码片段:

; 中断服务程序入口
ISR_Handler:
    ; 保存现场:压栈
    PUSH    R0          ; 保存通用寄存器
    PUSH    R1
    PUSH    R2
    PUSH    PSW         ; 保存状态寄存器(如果硬件没自动保存)
    
    ; 这里开始执行中断服务程序的具体逻辑
    ; ... 你的业务代码 ...
    
    ; 恢复现场:出栈(注意顺序与压栈相反)
    POP     PSW
    POP     R2
    POP     R1
    POP     R0
    
    ; 中断返回
    RETI

我的习惯:写中断服务程序时,我总会在压栈和出栈的代码块前后加上注释标记。比如 ; === SAVE CONTEXT === 和 ; === RESTORE CONTEXT ===。这样代码审查时一眼就能看出有没有漏掉恢复操作。

出栈的顺序为什么必须和压栈相反?因为栈是后进先出。你最后压进去的东西,最先弹出来。如果你搞反了顺序,寄存器的值就全乱了。

2.3 中断返回指令:RETI 与 IRET

保存和恢复现场做完之后,最后一步就是执行中断返回指令。这条指令的作用是:

  1. 从栈顶弹出之前保存的程序计数器(PC)值
  2. 从栈顶弹出之前保存的状态寄存器(PSW/FLAGS)值
  3. 重新使能中断(如果硬件在进入中断时自动关闭了中断)
  4. 跳回主程序被中断的位置继续执行

不同架构的指令名称不一样:

架构 指令 特点
8051 RETI 恢复 PC,重新使能中断
x86 IRET 恢复 PC、CS、FLAGS,支持任务切换
ARM SUBS PC, LR, #4 通过修改 PC 和恢复 CPSR 实现返回
RISC-V MRET 恢复 PC 和机器状态寄存器

我曾经踩过的坑:在 8051 项目里,有人用 RET 代替了 RETI。RET 只恢复 PC,不会重新使能中断。结果中断只触发了一次,之后就再也进不来了。系统看起来还在跑,但实际上已经“聋”了。记住:中断服务程序必须用 RETI(或对应架构的中断返回指令),不能用 RET。

2.4 中断嵌套与现场保护

中断嵌套,就是正在处理一个中断的时候,又来了一个更高优先级的中断。这时候现场保护就变得更复杂了。

我画了一张图,帮你理解这个过程:

中断嵌套与现场保护流程 中断嵌套与现场保护流程 主程序执行 低优先级中断 保存现场(压栈) 高优先级中断 保存现场(压栈) 高优先级处理 执行中断服务程序 高优先级恢复 恢复现场(出栈) 低优先级继续 执行中断服务程序 低优先级恢复 恢复现场(出栈) 主程序继续执行 关键点: • 每个中断进入时都要保存现场(压栈),退出时恢复现场(出栈) • 高优先级中断可以打断低优先级中断,但恢复时先恢复高优先级 • 栈的后进先出特性天然支持嵌套——后发生的中断先恢复

从这张图你能看到:

  • 主程序执行时,低优先级中断来了,保存现场,进去处理
  • 处理到一半,高优先级中断来了,再次保存现场,进去处理
  • 高优先级处理完,恢复现场,回到低优先级继续
  • 低优先级处理完,恢复现场,回到主程序

这就是栈的魅力——每一层嵌套都对应一次压栈,退出时依次出栈。只要栈空间够用,嵌套多少层都没问题。

关于栈空间:我建议你在设计阶段就估算好最大中断嵌套深度。每个中断服务程序压栈的字节数 × 最大嵌套层数,再加上主程序本身的栈使用量,就是你需要的最小栈空间。我曾经在一个资源受限的 MCU 上吃过亏,栈溢出导致系统随机崩溃,后来加了栈边界检查才定位到问题。

2.5 常见陷阱与避坑指南

做中断恢复这么多年,我总结了一些常见的坑:

  • 漏恢复寄存器:压栈了 5 个寄存器,出栈只做了 4 个。栈指针偏移,返回地址出错,系统直接跑飞。
  • 出栈顺序错误:先压 R0 再压 R1,出栈时先弹 R0 再弹 R1?不对,应该先弹 R1 再弹 R0。顺序反了,寄存器的值就乱了。
  • 在中断里调用函数:函数内部可能也会压栈出栈,如果你没处理好,会破坏中断的现场。我建议中断服务程序尽量精简,或者确保调用的函数不会修改你已保存的寄存器。
  • 中断返回指令用错:前面说过了,RET 和 RETI 的区别,用错了系统就“聋”了。

我曾经遇到的一个真实案例:某产品在量产阶段偶发死机,概率大约千分之一。查了两个月,最后发现是中断服务程序里调用了 printf 函数。printf 内部用了大量栈空间,而且修改了多个寄存器但没有全部保存。中断返回时,主程序的某个关键变量被改写了,导致逻辑判断出错。从那以后,我定了一条铁律:中断服务程序里绝对不允许调用标准库的 IO 函数。

好了,中断恢复的基石就是这些。保存现场、恢复现场、中断返回指令,这三个东西搞明白了,中断机制你就掌握了八成。剩下的就是具体架构的细节差异,但原理是相通的。