4. ARMv7异常与中断处理:异常向量表、异常优先级、中断响应流程、FIQ与IRQ的区别
异常和中断,是嵌入式系统里最核心的机制之一。说白了,就是CPU在正常执行流里,突然被“拽”去处理一件更紧急的事。ARMv7的异常处理设计得相当精巧,尤其是FIQ和IRQ的区分,我在做高速数据采集卡时深有体会——选错中断类型,性能直接腰斩。
4.1 异常向量表:CPU的“紧急联络簿”
ARMv7的异常向量表,是一张固定在内存低地址(或高地址,由SCTLR.V控制)的表。每个异常类型对应一个32位的指令,通常是跳转指令。CPU检测到异常后,硬件自动跳转到对应表项执行。
这张表长这样:
| 地址偏移 | 异常类型 | 进入模式 | 优先级 |
|---|---|---|---|
| 0x00 | 复位 (Reset) | SVC | 1 (最高) |
| 0x04 | 未定义指令 (Undef) | Undef | 6 |
| 0x08 | 软件中断 (SWI) | SVC | 6 |
| 0x0C | 预取中止 (Prefetch Abort) | Abort | 5 |
| 0x10 | 数据中止 (Data Abort) | Abort | 2 |
| 0x14 | 保留 | — | — |
| 0x18 | IRQ | IRQ | 4 |
| 0x1C | FIQ | FIQ | 3 |
关键点:每个表项只有4字节,所以通常放一条跳转指令(如 LDR PC, [PC, #offset] 或 B),跳到真正的处理函数。我见过新手直接把处理函数写在表项里,结果代码一超过4字节就把后面的表项覆盖了——调试时异常满天飞。
4.2 异常优先级:谁更紧急?
ARMv7的异常优先级是硬件固定的。复位最高,数据中止第二,FIQ第三,IRQ第四。为什么数据中止比FIQ优先级高?因为内存访问错误必须立即处理,否则系统状态可能彻底崩溃。
优先级顺序(从高到低):
- 复位 (Reset) — 最高,系统启动或重启
- 数据中止 (Data Abort) — 内存访问失败
- FIQ — 快速中断
- IRQ — 普通中断
- 预取中止 (Prefetch Abort) — 指令预取失败
- SWI 和 未定义指令 — 最低
注意:异常优先级只在同时发生多个异常时才有意义。一旦CPU进入某个异常处理,除非更高优先级的异常到来,否则不会被打断。我曾经在调试一个数据中止处理程序时,里面又触发了IRQ,结果IRQ被挂起直到数据中止处理完才响应——这其实是ARMv7的正常行为。
4.3 中断响应流程:硬件做了什么?
当IRQ或FIQ发生时,CPU硬件自动完成以下步骤:
- 保存返回地址:将当前PC保存到对应模式的LR寄存器(LR_irq 或 LR_fiq)
- 保存CPSR:将当前CPSR保存到SPSR_irq 或 SPSR_fiq
- 切换模式:CPU进入IRQ或FIQ模式,并禁用同级中断(IRQ模式下自动禁用IRQ,FIQ模式下自动禁用FIQ和IRQ)
- 跳转向量表:PC强制指向0x18(IRQ)或0x1C(FIQ)
处理完中断后,软件必须:
- 恢复现场(通用寄存器)
- 执行
SUBS PC, LR, #4返回(注意IRQ和FIQ的LR需要减4,因为流水线原因) - 恢复CPSR(硬件自动完成)
小技巧:我习惯在中断入口处先保存LR和SPSR到栈中,防止嵌套中断时被覆盖。虽然ARMv7的IRQ模式有自己的LR和SPSR,但一旦使能嵌套,这些寄存器就会被破坏。
4.4 FIQ与IRQ的区别:为什么FIQ更快?
FIQ(快速中断)和IRQ(普通中断)是ARMv7最经典的设计之一。FIQ的设计目标就是“快”,它比IRQ快在哪里?
| 特性 | IRQ | FIQ |
|---|---|---|
| 优先级 | 较低(第4) | 较高(第3) |
| 向量表位置 | 0x18 | 0x1C(最后一项) |
| 私有寄存器 | 无(共享R0-R12) | 有R8-R12(FIQ模式独享) |
| 中断屏蔽 | 只屏蔽IRQ | 屏蔽FIQ和IRQ |
| 典型用途 | 普通外设中断 | 高速数据流、DMA、实时性要求高的场景 |
FIQ快的核心原因有两个:
- 寄存器银行:FIQ模式有自己专用的R8-R12,这意味着处理函数不需要压栈保存这些寄存器。IRQ模式则必须保存所有用到的寄存器。
- 向量表位置:FIQ在向量表最后一项(0x1C),可以直接把处理函数放在表项后面,省去一次跳转。IRQ则必须跳转。
实战经验:我在做一款高速ADC采集卡时,数据采样率是10Msps,每次采样完成触发中断。一开始用IRQ,结果发现中断响应时间太长,导致数据丢失。后来改成FIQ,把R8-R12用于数据搬运,中断处理时间从2.3μs降到了0.8μs。嗯,这就是FIQ的威力。
4.5 异常处理流程图
下面这张图展示了ARMv7从异常发生到处理完成的完整流程:
4.6 避坑指南:我踩过的那些坑
做ARMv7中断调试,有几个地方特别容易出问题:
- LR没减4:IRQ和FIQ的返回地址需要
SUBS PC, LR, #4,因为流水线导致LR保存的是当前指令+8。我刚开始做时直接MOV PC, LR,结果程序跑飞了。 - SPSR没恢复:如果中断处理中修改了CPSR,返回时必须用
SUBS指令(带S后缀),否则SPSR不会自动恢复到CPSR。 - FIQ模式用了R0-R7:FIQ的快速优势在于R8-R12是私有的,如果处理函数里大量使用R0-R7,那和IRQ就没区别了。我建议FIQ处理函数尽量只用R8-R12。
- 嵌套中断使能不当:在IRQ处理中使能IRQ(清除I位)可以实现嵌套,但必须确保栈空间足够。我曾经在嵌套中断里递归调用,结果栈溢出——嗯,那次调试花了我一整天。
个人习惯:我写中断处理函数时,会在入口处用 STMFD SP!, {R0-R12, LR} 保存所有寄存器,即使FIQ模式也这样做——虽然牺牲了一点性能,但调试时能省很多事。等系统稳定后,再优化成只保存必要的寄存器。
4.7 总结
ARMv7的异常和中断处理,核心就是向量表、优先级、硬件自动保存现场、软件恢复现场这四个环节。FIQ和IRQ的区别,说白了就是FIQ用硬件资源换速度——专用寄存器、更高优先级、更少的跳转。选哪个,取决于你的实时性要求。
我在项目中一般这样选:普通外设(UART、I2C、GPIO)用IRQ,高速数据流(DMA、ADC、以太网)用FIQ。记住一点:FIQ不是万能的,它的处理函数必须足够短,否则再快也没用。