3. 异常与中断处理:异常向量表、异常优先级、中断响应流程、FIQ与IRQ的区别与实战选择
异常和中断,是嵌入式系统里最核心的机制之一。说白了,CPU 本来在流水线上干活,突然来了个紧急事件,它得停下来去处理。ARMv7 架构把这件事做得非常精细。我最早接触 ARM 时,总觉得中断就是“来了就跳转”,后来踩了不少坑才明白,这里面门道很深。
3.1 异常向量表:CPU的紧急电话本
ARMv7 的异常向量表,我习惯叫它“紧急电话本”。CPU 遇到异常时,会直接查这个表,跳转到对应的地址执行代码。
这张表固定在两个位置:
- 低端向量表:基地址
0x00000000 - 高端向量表:基地址
0xFFFF0000(通过 SCTLR.V 位控制)
我个人习惯用高端向量表,因为低端地址经常被 BootROM 或 Flash 占用,容易冲突。
| 异常类型 | 偏移地址 | 优先级 | 模式 |
|---|---|---|---|
| Reset | 0x00 | 1(最高) | SVC |
| Undefined Instruction | 0x04 | 6 | UND |
| SWI(SVC) | 0x08 | 6 | SVC |
| Prefetch Abort | 0x0C | 5 | ABT |
| Data Abort | 0x10 | 2 | ABT |
| IRQ | 0x18 | 4 | IRQ |
| FIQ | 0x1C | 3 | FIQ |
关键点:每个向量表条目只有 4 字节。你放不下完整的中断服务函数,只能放一条跳转指令。最常见的是 LDR PC, [PC, #offset] 或者 B 指令。
我的习惯:我会在向量表里放 LDR PC, [PC, #offset],然后在后面紧跟一个函数指针数组。这样修改中断入口时,只需要改指针,不用动向量表本身。
3.2 异常优先级:谁先抢到CPU
ARMv7 的异常优先级是硬件固定的。你想想看,Reset 优先级最高,这没得说。Data Abort 优先级排第二,为什么?因为数据访问出错必须立刻处理,否则数据可能损坏。
优先级顺序(从高到低):
- Reset
- Data Abort
- FIQ
- IRQ
- Prefetch Abort
- SWI & Undefined Instruction(同级)
这里有个坑:FIQ 优先级高于 IRQ,但低于 Data Abort。我在项目中遇到过,一个 Data Abort 打断了 FIQ 处理,导致高速数据采集丢了一帧。后来我调整了代码,在 FIQ 入口处先屏蔽 Data Abort 中断源,才解决这个问题。
注意:异常优先级是硬件决定的,你无法改变。但你可以通过 GIC(通用中断控制器)来调整 IRQ 和 FIQ 的中断源优先级。这是两回事,别搞混了。
3.3 中断响应流程:CPU到底做了什么
当一个中断发生时,CPU 内部会执行一系列操作。我把它拆成 5 步:
- 保存现场:自动将 CPSR 保存到 SPSR_
,将返回地址保存到 LR_ 。 - 切换模式:CPU 切换到 IRQ 或 FIQ 模式,并禁用同级中断(IRQ 自动屏蔽 IRQ,FIQ 自动屏蔽 FIQ 和 IRQ)。
- 跳转向量表:PC 指向向量表对应条目,执行跳转指令。
- 执行中断服务:进入 C 函数或汇编处理函数。
- 恢复现场:从 SPSR 恢复 CPSR,从 LR 恢复 PC,继续执行被中断的代码。
嗯,这里要注意:第 2 步里,FIQ 模式会自动屏蔽 IRQ,但 IRQ 模式不会屏蔽 FIQ。这意味着 FIQ 可以抢占 IRQ。这是设计上的一个权衡。
核心代码示例:IRQ 入口的典型汇编处理
IRQ_Handler:
SUB LR, LR, #4 ; 修正返回地址
SRSDB SP!, #0x12 ; 保存 SPSR_irq 和 LR_irq 到 IRQ 栈
CPS #0x13 ; 切换到 SVC 模式
PUSH {R0-R3, R12, LR} ; 保存通用寄存器
BL C_IRQ_Handler ; 调用 C 函数
POP {R0-R3, R12, LR} ; 恢复通用寄存器
CPS #0x12 ; 切回 IRQ 模式
RFEIA SP! ; 恢复 SPSR 和 PC
避坑指南:我曾经在 IRQ 入口忘记 SUB LR, LR, #4,结果程序返回后一直跑飞。ARM 的 LR 在中断时保存的是 PC+4,而不是正确的返回地址。这个 #4 的修正,新手最容易忘。
3.4 FIQ与IRQ的区别:不只是优先级
很多人以为 FIQ 和 IRQ 的区别就是优先级高低。其实远不止这些。我列个表,你看完就明白了。
| 特性 | IRQ | FIQ |
|---|---|---|
| 优先级 | 低(4) | 高(3) |
| 向量表位置 | 0x18 | 0x1C |
| 屏蔽关系 | 不屏蔽 FIQ | 屏蔽 IRQ 和 FIQ |
| 私有寄存器 | 无(共享 R8-R12) | 有(R8_fiq - R12_fiq) |
| 典型用途 | 通用外设中断 | 高速数据流、DMA、实时性要求高的场景 |
| 响应速度 | 较慢(需保存更多寄存器) | 较快(利用私有寄存器减少保存开销) |
FIQ 有 5 个私有寄存器(R8-R12),这意味着在 FIQ 处理函数里,你不需要保存和恢复这些寄存器。你想想看,少了 5 个寄存器的压栈出栈操作,速度能不快吗?
实战选择建议:
- 用 IRQ 处理普通外设中断,比如按键、UART 接收、定时器。
- 用 FIQ 处理高速数据流,比如 SPI 从设备、高速 ADC 采样、DMA 完成中断。
- 如果系统里只有一个中断源且要求极低延迟,直接用 FIQ。
3.5 实战选择:我踩过的坑
我曾经在一个高速数据采集项目里,用 IRQ 处理 SPI 中断。结果发现,当系统负载高时,SPI 数据会丢包。为什么?因为 IRQ 被其他中断打断了,导致 SPI FIFO 溢出。
后来我把 SPI 中断改成 FIQ,问题解决了。FIQ 的私有寄存器让上下文切换几乎零开销,而且它不会被 IRQ 打断,保证了数据流的连续性。
但 FIQ 也不是万能的。另一个项目里,我把所有中断都塞进 FIQ,结果系统响应变得很奇怪。因为 FIQ 会屏蔽所有中断,包括更高优先级的 Data Abort。一旦 FIQ 处理时间过长,系统就失去了对关键错误的响应能力。
我的原则:FIQ 只放一个中断源,且处理时间要极短。如果多个中断源共享 FIQ,你需要在 FIQ 里轮询中断状态,这会抵消 FIQ 的速度优势。
3.6 知识结构图:异常与中断处理全景
下面这张图,我把整个异常与中断处理的核心逻辑画出来了。你可以对照着看,加深理解。
这张图把异常源、优先级、向量表和响应流程串起来了。你多看几遍,应该能建立起一个完整的认知框架。
3.7 小结
异常与中断处理,是 ARMv7 架构里最需要“手感”的部分。向量表怎么放、优先级怎么排、FIQ 和 IRQ 怎么选,这些都不是死记硬背的,而是在实际项目中一点点磨出来的。
我个人建议,新手先从 IRQ 入手,把通用中断流程跑通。等你对中断响应时间有了感觉,再尝试 FIQ。别一上来就追求极致性能,先把基础打牢。