4. 中断响应流程:CPU保存上下文、跳转ISR、中断返回

好,咱们今天聊聊中断响应流程。说白了,就是CPU接到中断信号后,到底干了些什么事。

我个人习惯把中断响应分成三步:保存上下文 → 跳转ISR → 中断返回。这三步走完,一个完整的中断生命周期才算结束。你想想看,CPU正在跑主程序,突然被叫去处理中断,总得把手头的活儿先放一放吧?放之前得记下来干到哪了,处理完再回来接着干。这就是保存上下文和恢复上下文的意义。

4.1 保存上下文:CPU的“备忘录”

什么是上下文?说白了就是CPU当前的工作状态。包括程序计数器(PC)、通用寄存器、状态寄存器(如ARM的CPSR)等等。这些信息必须完整保存,否则中断返回后程序就乱套了。

我记得有一次调试一个STM32的项目,中断里改了R0寄存器,但没保存。结果中断返回后主程序算出来的结果全错了。嗯,从那以后我每次写中断服务函数,第一件事就是检查寄存器有没有压栈。

保存上下文的典型做法:

  • 硬件自动保存:CPU硬件会自动把PC、状态寄存器等关键信息压入栈中。比如ARM Cortex-M系列,中断一进来,硬件自动压栈8个寄存器(R0-R3、R12、LR、PC、xPSR)。
  • 软件手动保存:有些架构(比如早期的8051)需要程序员在ISR开头手动PUSH寄存器。我刚开始做嵌入式时,就吃过这个亏——忘了PUSH ACC,结果中断里改了累加器,主程序直接崩了。

关键点:保存上下文是中断响应的第一步,也是最容易出问题的一步。尤其是嵌套中断场景,每一层中断都要保存自己的上下文,栈空间得算清楚。

4.2 跳转ISR:找到中断服务函数的入口

上下文保存好了,接下来CPU就要去找中断服务函数(ISR)的入口地址。这个跳转过程,不同架构的实现方式不太一样。

常见的做法有两种:

  • 向量表跳转:CPU从固定的向量表地址读取ISR入口。比如ARM Cortex-M的向量表放在0x00000000起始位置,每个中断源对应一个32位的入口地址。中断一来,CPU直接查表跳转。
  • 软件查表:有些低端MCU没有硬件向量表,需要软件在中断入口处通过查询中断标志位,再跳转到对应的处理函数。我早期做PIC单片机时就遇到过这种情况,每个中断都要写一大段if-else判断,效率确实不高。

这里有个细节要注意:中断向量表的地址。有些芯片支持重定位向量表(比如STM32的SCB->VTOR寄存器),你可以把向量表搬到RAM里动态修改。我在一个需要动态注册中断的项目里用过这个功能,确实灵活,但调试时也踩过坑——向量表地址没对齐,导致中断跳飞了。

小技巧:写ISR时,尽量让函数名和中断源对应。比如TIM2_IRQHandler、USART1_IRQHandler。这样查向量表时一目了然,也方便后期维护。

4.3 中断返回:从哪里来回哪里去

ISR执行完了,CPU得回到被中断的地方继续干活。中断返回的核心动作就是恢复上下文,把之前保存的寄存器值弹回来,然后跳回原来的PC地址。

中断返回的指令因架构而异:

  • ARM:使用BX LRPOP {PC}指令。注意LR在中断模式下存的是返回地址,不是普通的链接寄存器。
  • x86:使用IRET指令,它会同时恢复CS、EIP和EFLAGS。
  • RISC-V:使用MRET指令(机器模式)或SRET(监管模式)。

我曾经在一个RISC-V的项目里,因为中断返回指令写错了(用了RET而不是MRET),导致CPU一直卡在中断里出不来。查了两天才发现是特权级没切换回来。嗯,这种低级错误,犯过一次就再也不会忘了。

避坑指南:中断返回时,一定要确保栈指针恢复到中断前的状态。如果ISR里压栈和出栈不匹配,返回地址就会错乱。我曾经见过一个同事,在ISR里PUSH了3个寄存器,但只POP了2个,结果程序跑飞了。调试了一下午才发现。

4.4 完整的中断响应流程示例

咱们以ARM Cortex-M3为例,看看一个完整的中断响应流程长什么样:

; 假设主程序正在执行,突然来了一个中断
; 1. 硬件自动保存上下文(8个寄存器压栈)
;    - R0, R1, R2, R3
;    - R12
;    - LR (当前为EXC_RETURN)
;    - PC (返回地址)
;    - xPSR

; 2. 硬件从向量表读取ISR入口地址
;    例如:向量表基址0x00000000 + 中断号*4

; 3. 跳转到ISR执行
IRQ_Handler:
    PUSH {R4-R11}       ; 保存剩余的通用寄存器(软件保存)
    ; ... 处理中断逻辑 ...
    POP {R4-R11}        ; 恢复通用寄存器

; 4. 中断返回
    BX LR               ; LR中存的是EXC_RETURN,硬件自动恢复上下文

你看,整个流程其实很清晰。硬件帮你做了大部分脏活累活,但软件部分(比如保存R4-R11)还是得自己操心。

4.5 中断响应的时间开销

中断响应时间,说白了就是从中断信号到达CPU引脚,到CPU开始执行ISR第一条指令的时间。这个时间直接影响系统的实时性。

阶段 时间开销 说明
中断延迟 3-12个时钟周期 CPU识别中断、完成当前指令、进入异常模式
压栈时间 8-16个时钟周期 硬件自动压栈,取决于寄存器数量和总线宽度
查向量表 2-4个时钟周期 从内存读取ISR入口地址
跳转ISR 1-3个时钟周期 流水线刷新、指令预取

我在做电机控制项目时,对中断响应时间特别敏感。当时用的Cortex-M4跑168MHz,中断响应大概在12个时钟周期左右(约70ns)。但如果你用了RTOS,中断响应时间还得加上OS的中断切换开销,那就得另算了。

总结一下:中断响应流程看似简单,但每个环节都可能成为性能瓶颈。保存上下文要快、跳转ISR要准、中断返回要稳。做嵌入式开发,这三步必须烂熟于心。