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 优先级排第二,为什么?因为数据访问出错必须立刻处理,否则数据可能损坏。

优先级顺序(从高到低):

  1. Reset
  2. Data Abort
  3. FIQ
  4. IRQ
  5. Prefetch Abort
  6. SWI & Undefined Instruction(同级)

这里有个坑:FIQ 优先级高于 IRQ,但低于 Data Abort。我在项目中遇到过,一个 Data Abort 打断了 FIQ 处理,导致高速数据采集丢了一帧。后来我调整了代码,在 FIQ 入口处先屏蔽 Data Abort 中断源,才解决这个问题。

注意:异常优先级是硬件决定的,你无法改变。但你可以通过 GIC(通用中断控制器)来调整 IRQ 和 FIQ 的中断源优先级。这是两回事,别搞混了。

3.3 中断响应流程:CPU到底做了什么

当一个中断发生时,CPU 内部会执行一系列操作。我把它拆成 5 步:

  1. 保存现场:自动将 CPSR 保存到 SPSR_,将返回地址保存到 LR_。
  2. 切换模式:CPU 切换到 IRQ 或 FIQ 模式,并禁用同级中断(IRQ 自动屏蔽 IRQ,FIQ 自动屏蔽 FIQ 和 IRQ)。
  3. 跳转向量表:PC 指向向量表对应条目,执行跳转指令。
  4. 执行中断服务:进入 C 函数或汇编处理函数。
  5. 恢复现场:从 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 知识结构图:异常与中断处理全景

下面这张图,我把整个异常与中断处理的核心逻辑画出来了。你可以对照着看,加深理解。

ARMv7 异常与中断处理全景图 ARMv7 异常与中断处理全景图 异常源 Reset Undefined Instruction SWI (SVC) Prefetch Abort Data Abort IRQ FIQ 优先级(高→低) 1. Reset 2. Data Abort 3. FIQ 4. IRQ 5. Prefetch Abort 6. SWI / Undef 向量表 0x00: Reset 0x04: Undef 0x08: SWI 0x0C: Prefetch 0x10: Data Abort 0x18: IRQ 0x1C: FIQ 响应 保存 切换 跳转 执行 恢复 说明:异常源按优先级排序后,CPU 查向量表获取入口地址,然后执行 5 步响应流程。 FIQ 有私有寄存器,响应速度比 IRQ 快,适合高速数据流场景。 IRQ 特点 • 共享 R8-R12 • 不屏蔽 FIQ • 适合通用外设 • 响应较慢 FIQ 特点 • 私有 R8-R12 • 屏蔽 IRQ 和 FIQ • 适合高速数据流 • 响应极快

这张图把异常源、优先级、向量表和响应流程串起来了。你多看几遍,应该能建立起一个完整的认知框架。

3.7 小结

异常与中断处理,是 ARMv7 架构里最需要“手感”的部分。向量表怎么放、优先级怎么排、FIQ 和 IRQ 怎么选,这些都不是死记硬背的,而是在实际项目中一点点磨出来的。

我个人建议,新手先从 IRQ 入手,把通用中断流程跑通。等你对中断响应时间有了感觉,再尝试 FIQ。别一上来就追求极致性能,先把基础打牢。