2. ARMv7异常处理模型:异常向量表、异常优先级、异常返回机制、FIQ与IRQ的区别

异常处理,说白了就是CPU在遇到突发状况时的应急响应机制。不管是外部设备来了中断,还是程序自己跑飞了(比如除零),CPU都得有一套标准的流程去处理。ARMv7的异常模型设计得相当精巧,我当年刚接触时也觉得有点绕,但摸清楚之后,你会发现它其实非常符合直觉。

2.1 异常向量表:CPU的“紧急联络簿”

异常向量表,你可以把它想象成一张贴在CPU门口的电话号码表。每种异常类型对应一个固定的入口地址。当异常发生时,CPU会强制跳转到这个地址去执行那里的指令。

ARMv7的向量表基址由VBAR(Vector Base Address Register)寄存器控制。默认情况下,它指向0x00000000或0xFFFF0000,但现代操作系统(比如Linux)都会重新设置它。

关键点: 向量表中的每一项是4字节,通常存放一条跳转指令(如LDR PC, [PC, #offset]),而不是异常处理函数的完整代码。

下面这张表列出了ARMv7中常见的异常类型及其优先级和向量偏移:

异常类型 向量偏移 优先级 触发场景
复位 (Reset) 0x00 1 (最高) 上电或复位按键
未定义指令 0x04 6 CPU不认识这条指令
软件中断 (SVC) 0x08 6 执行SVC指令,用户态请求内核服务
预取中止 0x0C 5 取指令时地址或权限错误
数据中止 0x10 2 读写数据时地址或权限错误
IRQ 0x18 4 普通中断请求
FIQ 0x1C 3 快速中断请求
我的经验: 调试裸机程序时,如果发现程序跑飞后总是跳到0x04(未定义指令),十有八九是编译器生成了CPU不支持的指令,或者你的代码里混入了Thumb和ARM指令的切换错误。

2.2 异常优先级:谁更紧急谁先上

当多个异常同时发生时,CPU得有个裁决机制。ARMv7的优先级是硬件固定的,不能像Cortex-M系列那样软件配置。复位优先级最高,数据中止排第二,FIQ第三,IRQ第四。

这里有个细节容易忽略:FIQ的优先级高于IRQ。为什么?因为FIQ设计之初就是为了处理那些对实时性要求极高的场景,比如高速数据传输。FIQ在响应时,CPU会自动屏蔽IRQ,确保自己不被干扰。

优先级逻辑其实很简单:

  • 高优先级异常可以抢占低优先级异常。
  • 低优先级异常不能打断高优先级异常的处理。
  • 同级异常不会互相嵌套(除非你手动修改CPSR)。
避坑指南: 我曾经在一个项目中,IRQ处理函数里不小心触发了数据中止(比如访问了未映射的内存),结果CPU直接卡死。因为数据中止的优先级高于IRQ,它会立即抢占IRQ处理流程,而如果数据中止处理函数又没写好,系统就彻底崩了。

2.3 异常返回机制:怎么进去的,怎么出来

异常返回,核心就是恢复现场。CPU在进入异常时,会自动做几件事:

  1. 保存当前PC到LR(链接寄存器),但会根据异常类型做偏移修正。
  2. 保存当前CPSR到SPSR(保存的程序状态寄存器)。
  3. 切换处理器模式,并屏蔽某些中断。

返回时,我们需要手动恢复。ARMv7提供了两种标准返回方式:

  • 方式一: MOVS PC, LR —— 将LR的值赋给PC,同时将SPSR恢复回CPSR。适用于SVC、未定义指令等异常。
  • 方式二: SUBS PC, LR, #4 —— 先修正LR(因为IRQ/FIQ返回时需要回退一条指令),再恢复。适用于IRQ和FIQ。

为什么IRQ/FIQ要减4?因为CPU在响应中断时,当前指令可能还没执行完。返回时得重新执行那条被中断的指令,所以PC要回退4字节。

核心公式:
// 对于IRQ/FIQ
LR_irq = PC + 4;  // 硬件自动保存的值
返回地址 = LR_irq - 4;  // 需要手动修正

// 对于SVC/未定义指令
LR_svc = PC + 4;  // 硬件自动保存的值
返回地址 = LR_svc;  // 直接返回即可

2.4 FIQ与IRQ的区别:快与稳的抉择

FIQ和IRQ是ARMv7里最常用的两种外部中断。很多人觉得它们只是名字不同,其实内部差异很大。我整理了一个对比表,方便你理解:

对比项 IRQ FIQ
优先级 较低(4) 较高(3)
响应延迟 较慢(需保存更多寄存器) 较快(硬件自动保存寄存器少)
专用寄存器 无(使用通用寄存器) 有(R8~R12是FIQ模式专用)
中断屏蔽 只能被FIQ或更高优先级异常打断 进入时自动屏蔽IRQ
典型应用 按键、UART、定时器等普通外设 DMA、高速数据流、实时性要求高的任务

FIQ为什么快?因为它有自己专属的寄存器组(R8_fiq ~ R12_fiq)。进入FIQ时,CPU不需要把这些寄存器压栈,直接切换寄存器组就行。而IRQ没有专用寄存器,必须手动保存R0~R12,这就多了不少开销。

我的建议: 除非你的系统对实时性有苛刻要求(比如音频采样、电机控制),否则尽量用IRQ。FIQ虽然快,但专用寄存器少,处理复杂逻辑时反而容易捉襟见肘。我见过有人把所有中断都挂到FIQ上,结果代码写得无比痛苦。

2.5 异常处理流程总览

为了让你对整个流程有个直观印象,我画了一张图。它展示了从异常发生到返回的完整路径:

ARMv7 异常处理流程 异常发生 查向量表 (VBAR + offset) 硬件自动操作 保存PC→LR, CPSR→SPSR, 切换模式 跳转异常处理函数 执行处理逻辑 异常返回 (MOVS/SUBS PC, LR) 注意 FIQ模式有专用寄存器 IRQ模式需手动保存R0~R12 返回地址修正因异常而异

这张图把整个流程串起来了。你仔细看,从异常发生到查表、硬件保存现场、跳转执行、最后返回,每一步都是环环相扣的。我当年调试一个中断嵌套的问题时,就是靠这张图一步步排查,才发现是返回地址没修正对。

2.6 实战中的避坑点

最后,分享几个我在项目中踩过的坑:

  • 向量表对齐: VBAR寄存器要求向量表32字节对齐。如果你把向量表放在一个未对齐的地址上,CPU会直接跑飞。我曾经犯过这个错,查了整整一天。
  • SPSR的保存: 如果你的异常处理函数里又调用了其他函数(嵌套),记得先保存SPSR,否则子函数返回时会覆盖它。
  • FIQ的“快”是双刃剑: FIQ虽然快,但它的专用寄存器只有R8~R12。如果你的处理逻辑需要大量局部变量,反而会因为寄存器不够用而被迫压栈,得不偿失。
我曾经: 在一个工控项目中,把所有的外设中断都配置成了FIQ,想着“快总比慢好”。结果FIQ处理函数里调用了printf打印调试信息,printf内部又触发了SVC异常,而SVC的优先级低于FIQ,导致系统死锁。从那以后,我再也不敢在FIQ里干“重活”了。

嗯,异常处理这块内容比较多,但只要你把向量表、优先级、返回机制这三个核心点吃透,剩下的就是多写代码多调试了。