3. 中断响应流程:中断采样、压栈、跳转、执行ISR、出栈与返回

好,咱们今天聊点硬核的。中断响应流程,说白了就是CPU被“打断”之后,怎么把手头的活儿放下,去处理紧急事件,再回来接着干。这个过程,我习惯把它拆成五个环节:采样、压栈、跳转、执行ISR、出栈与返回。每一个环节都有坑,踩过你就知道了。

3.1 中断采样——CPU是怎么知道有中断的?

CPU不是时刻盯着中断引脚看的。它有自己的节奏,每个指令周期末尾,会去“瞄一眼”中断请求线。这个动作就叫采样。

举个例子,ARM Cortex-M系列,它在每个指令的末尾采样IRQ线。如果检测到有效电平(通常是低电平或上升沿),就会在下一个指令周期进入中断处理流程。

关键点:采样只在指令边界发生。所以中断不会在指令执行中间被响应,这保证了指令的原子性。

我在项目中遇到过一个问题:一个外部中断信号持续时间太短,刚好落在CPU的采样窗口之外。结果呢?中断丢了。后来我加了一个硬件展宽电路,把脉冲拉长到至少3个CPU时钟周期,问题才解决。

避坑指南:我曾经因为没仔细看数据手册的中断采样时序,导致一个产品在强干扰环境下频繁丢失中断。后来发现,采样窗口和信号抖动叠加,刚好错过了有效电平。建议你在设计时,给中断信号留足余量,至少保持2-3个CPU时钟周期。

3.2 压栈——CPU在“记笔记”

CPU决定响应中断后,第一件事不是跳转,而是压栈。它要把当前正在做的事情“记下来”,等处理完中断再回来接着干。

压栈的内容通常包括:

  • 程序计数器(PC)——中断返回后该执行哪条指令
  • 状态寄存器(PSR/xPSR)——当前的标志位、条件码
  • 通用寄存器——R0-R3、R12等(不同架构有差异)
  • 链接寄存器(LR)——有些架构用它保存返回地址

以Cortex-M3为例,硬件自动压栈8个寄存器:R0-R3、R12、LR、PC、xPSR。这个过程是硬件完成的,不需要你写代码干预。你想想看,如果让软件来压栈,那中断延迟得多大?

注意:硬件自动压栈的寄存器数量,直接影响中断延迟。Cortex-M系列之所以在实时控制领域这么火,跟它硬件压栈只花12个时钟周期有很大关系。

我习惯在项目初期就查清楚MCU的压栈开销。有一次选型时,发现某款国产MCU的硬件压栈需要40多个时钟周期,比Cortex-M多了三倍。做高速数据采集时,这个差距就非常明显了。

3.3 跳转——找到ISR的入口

压栈完成后,CPU需要找到中断服务函数(ISR)的入口地址。这个地址存在哪里?存在中断向量表中。

中断向量表,说白了就是一个地址数组。每个中断源对应一个表项,里面存着该中断的ISR入口地址。CPU拿到中断号后,直接查表跳转。

中断号 向量表偏移 ISR入口地址
0 0x0000_0000 栈顶指针(MSP初始值)
1 0x0000_0004 Reset_Handler
2 0x0000_0008 NMI_Handler
... ... ...
16 0x0000_0040 EXTI0_IRQHandler

这里有个坑:向量表可以重定位。很多MCU允许你把向量表放到RAM里,运行时动态修改。我做过一个Bootloader项目,就是利用向量表重定位,在运行时切换中断处理逻辑。

警告:千万不要在中断向量表里填错地址!我曾经见过一个同事,把ISR函数名拼写错了,结果编译器生成了一个空函数。中断来了,CPU跳进去,啥也没干就返回了。这个bug查了整整两天。

3.4 执行ISR——真正干活的地方

跳转成功后,CPU开始执行你的ISR代码。这里要注意几点:

  • ISR要短小精悍——中断里不要做复杂运算,不要调用printf,不要等待
  • 注意中断嵌套——高优先级中断可以打断低优先级ISR
  • 清除中断标志——不清理的话,退出后又会立即触发

我个人的习惯是:ISR里只做三件事——读取数据、设置标志、清除中断。剩下的处理,全部放到主循环或任务里去做。你想想看,如果在ISR里做浮点运算,那中断延迟会变得不可控。

经验之谈:我曾经在一个电机控制项目里,把PID计算放在了ISR中。结果中断执行时间太长,导致低优先级的中断丢失了。后来我把PID计算挪到任务里,ISR只负责采集电流和转速,问题就解决了。

3.5 出栈与返回——从哪里来回哪里去

ISR执行完毕后,CPU要“恢复现场”。这个过程叫出栈,就是把之前压栈的寄存器值弹回来。

出栈的顺序和压栈相反:先弹xPSR,再弹PC,最后弹通用寄存器。CPU执行一条特殊的返回指令(如Cortex-M的BX LR),把PC恢复到中断前的值。

这里有个细节:有些架构在返回时会自动重新使能中断。比如Cortex-M,执行BX LR时,硬件会自动恢复PRIMASK寄存器,把全局中断使能位还原。

核心要点:出栈和返回必须原子完成。如果在出栈过程中又被中断打断,那栈里的数据就乱套了。硬件设计保证了这一点,你不需要担心。

嗯,这里要注意:如果你的ISR里修改了某些全局变量,而这些变量又被主循环使用,那就要考虑原子操作或关中断保护。否则,可能会出现“部分更新”的问题。

3.6 完整流程的时间线

我把整个中断响应流程画成时间线,方便你理解:

  1. T0:中断信号到达CPU引脚
  2. T1:当前指令执行完毕,CPU采样到中断请求
  3. T2-T3:硬件自动压栈(约12个时钟周期)
  4. T4:查向量表,跳转到ISR入口
  5. T5-Tn:执行ISR代码
  6. Tn+1:出栈(约12个时钟周期)
  7. Tn+2:返回被中断的程序,继续执行

从T0到T4的时间,就是中断延迟。这个值越小,系统的实时性越好。我选型时,一定会看数据手册里的中断延迟参数。

再次提醒:中断延迟不是固定值。它受当前指令类型、总线负载、中断优先级等因素影响。设计时,一定要留出余量,别卡着极限值算。

好了,中断响应流程就讲到这里。下一章咱们聊聊中断优先级和嵌套管理,那又是一个容易出坑的地方。