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
保存和恢复现场做完之后,最后一步就是执行中断返回指令。这条指令的作用是:
- 从栈顶弹出之前保存的程序计数器(PC)值
- 从栈顶弹出之前保存的状态寄存器(PSW/FLAGS)值
- 重新使能中断(如果硬件在进入中断时自动关闭了中断)
- 跳回主程序被中断的位置继续执行
不同架构的指令名称不一样:
| 架构 | 指令 | 特点 |
|---|---|---|
| 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 函数。
好了,中断恢复的基石就是这些。保存现场、恢复现场、中断返回指令,这三个东西搞明白了,中断机制你就掌握了八成。剩下的就是具体架构的细节差异,但原理是相通的。