3、中断响应流程:CPU响应中断的硬件流程、现场保存与恢复、中断向量表解析
中断响应,说白了就是CPU被“打断”后,怎么放下手里的活,去处理紧急事件,再回来接着干。这个流程如果没搞透,你写的中断服务程序再漂亮,延迟也会高得离谱。我见过不少工程师,中断里一上来就做复杂运算,结果现场保存都没做完,下一个中断又来了——系统直接崩掉。
这一节,我们就把CPU响应中断的硬件流程、现场保存与恢复、以及中断向量表这三个核心点拆开揉碎。你想想看,搞懂这些,你才能精准控制中断延迟。
3.1 CPU响应中断的硬件流程
CPU不是软件主动去“检查”有没有中断的。硬件有一套固定的、不可跳过的流程。我习惯把这个流程分成5个原子步骤:
- 中断请求:外设或内部模块拉高中断请求线(比如IRQ引脚)。
- 中断判优:CPU在当前指令执行完后,检查中断标志位。如果有多个中断,硬件优先级仲裁器决定谁先上。
- 中断响应:CPU发出中断应答信号(INTA),通知外设“我准备处理你了”。
- 硬件现场保存:CPU自动将当前程序计数器(PC)和处理器状态字(PSR)压入堆栈。这一步是硬件完成的,不需要你写代码。
- 跳转中断服务程序:CPU从中断向量表中取出对应的入口地址,跳转执行。
关键点:第4步“硬件现场保存”只保存了PC和PSR。通用寄存器、局部变量等,需要你在中断服务程序开头手动保存。这是很多新手踩坑的地方。
我曾经在一个项目中,用了一个低端ARM Cortex-M0芯片。硬件自动保存了R0-R3、R12、LR、PC、xPSR,但我的中断里用到了R4-R11。没手动保存,结果中断返回后,主程序跑飞了。嗯,从那以后,我写中断的第一行代码永远是PUSH指令。
3.2 现场保存与恢复
现场保存,就是中断服务程序(ISR)的“入场登记”和“离场签退”。你想想看,CPU正在算一个关键数据,突然被拉去处理中断。如果中断里把寄存器改得乱七八糟,回来时主程序的数据就全毁了。
3.2.1 硬件自动保存 vs 软件手动保存
| 保存类型 | 保存内容 | 触发时机 | 是否可配置 |
|---|---|---|---|
| 硬件自动保存 | PC、PSR(或xPSR)、部分核心寄存器 | CPU响应中断时自动完成 | 通常不可配置 |
| 软件手动保存 | 通用寄存器、浮点寄存器、局部变量 | ISR入口处由程序员编写PUSH指令 | 完全由你控制 |
我个人习惯,在ISR入口处先保存所有可能用到的寄存器,哪怕这个中断只用到了R0。为什么?因为编译器优化可能会改变寄存器使用情况。你少保存一个,就可能埋下一个“偶发崩溃”的雷。
避坑指南:我曾经在一个FreeRTOS项目中,中断里调用了printf函数。printf内部用了大量寄存器,而我只保存了R0-R3。结果中断返回后,任务堆栈被破坏,系统随机死机。排查了三天才发现是现场保存不完整。从那以后,我坚持在ISR入口处使用“全保存”策略。
3.2.2 现场恢复的注意事项
现场恢复就是中断返回前的POP操作。这里有一个很容易忽略的点:恢复顺序必须与保存顺序完全相反。堆栈是后进先出的,你如果顺序搞反了,寄存器值就全乱了。
; 正确的保存与恢复示例(ARM Cortex-M汇编风格)
ISR_Handler:
PUSH {R4-R11, LR} ; 保存通用寄存器和链接寄存器
; ... 中断处理代码 ...
POP {R4-R11, PC} ; 恢复寄存器,并返回主程序
注意上面代码的最后一行:POP指令把LR的值弹给了PC。这就是中断返回的硬件机制——把之前保存的返回地址恢复到PC,CPU就知道该从哪继续执行了。
警告:不要在ISR中随意修改堆栈指针(SP)。如果你在中断里调用了函数,函数返回后SP必须和进入时一致。否则现场恢复时,POP出来的数据全是错的。
3.3 中断向量表解析
中断向量表,就是一张“中断处理函数地址清单”。CPU收到中断后,根据中断号去这张表里查,找到对应的ISR入口地址,然后跳过去。
3.3.1 向量表的结构
这张表通常放在Flash或RAM的起始地址。以ARM Cortex-M为例,向量表的前16个条目是系统异常,从第16个开始才是外部中断。
| 偏移地址 | 中断号 | 类型 | 描述 |
|---|---|---|---|
| 0x0000_0000 | - | 栈顶地址 | 主堆栈指针初始值 |
| 0x0000_0004 | 1 | 复位 | Reset_Handler |
| 0x0000_0008 | 2 | NMI | 不可屏蔽中断 |
| 0x0000_000C | 3 | 硬错误 | HardFault_Handler |
| ... | ... | ... | ... |
| 0x0000_0040 | 16 | 外部中断0 | EXTI0_IRQHandler |
| 0x0000_0044 | 17 | 外部中断1 | EXTI1_IRQHandler |
你想想看,如果这张表里的地址写错了,CPU跳转到一个非法地址,系统直接HardFault。我调试时最怕遇到这种情况——程序跑飞了,但不知道飞哪去了。
3.3.2 向量表的配置与重定位
有些芯片允许你把向量表重定位到RAM中。为什么这么做?因为运行时可以动态修改中断处理函数。比如,你在Bootloader阶段用一个简单的ISR,等App加载完成后,再换成功能完整的ISR。
我建议,除非你有动态修改中断处理函数的需求,否则不要轻易重定位向量表。重定位需要额外配置VTOR寄存器(Vector Table Offset Register),一旦配置错误,所有中断都会失效。
实战经验:我在一个OTA升级项目中,需要把向量表从Flash拷贝到RAM。拷贝完成后,必须用__DSB()和__ISB()指令做内存屏障和指令同步。否则CPU可能还在读旧的Flash向量表,导致中断响应错乱。
3.4 中断响应流程总览
为了让你更直观地理解整个流程,我画了一张图。这张图把硬件流程、现场保存、向量表查询串在了一起。
这张图里,从“中断请求”到“跳转ISR”一共6步。其中第4步和第5步是硬件自动完成的,你无法干预。你能优化的,是第6步之后——ISR内部的执行效率。
3.5 中断延迟的关键影响因素
搞懂了流程,我们就能精准定位中断延迟的瓶颈了。我个人把中断延迟拆成三部分:
- 硬件延迟:从中断请求到CPU开始执行ISR第一条指令的时间。这部分由芯片设计决定,你改不了。但你可以通过选择更高性能的芯片来缩短它。
- 现场保存延迟:硬件自动保存和软件手动保存的时间。这部分你可以优化——比如减少手动保存的寄存器数量。
- 向量表查询延迟:CPU查表跳转的时间。如果向量表在RAM中且被频繁访问,可能因为Cache miss增加延迟。
优化建议:我习惯把最紧急的中断(比如定时器中断)放在向量表的前面位置。因为有些芯片的向量表查询是从头开始顺序匹配的,中断号越小,查询越快。虽然这个优化效果不大,但积少成多。