4. 异常与中断处理:ARM异常向量表、IRQ/FIQ机制、GICv2/v3控制器、中断嵌套与优先级

异常与中断,说白了就是CPU的“紧急事件处理机制”。

我刚开始做嵌入式时,总觉得这玩意儿就是查表跳转,没啥技术含量。直到有一次,产品在现场频繁死机,我查了三天三夜,最后发现是中断优先级配置错了——一个低优先级的中断把高优先级的给堵死了。嗯,从那以后,我再也不敢小看异常处理了。

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

ARM处理器在复位后,会从0x00000000或0xFFFF0000(由SCTLR.V位决定)开始执行。这个起始地址放的就是异常向量表。

每个异常类型占用4字节,存放一条跳转指令。典型的向量表长这样:

; ARM异常向量表(以ARMv7-A为例)
0x00000000:  LDR PC, =Reset_Handler      ; 复位
0x00000004:  LDR PC, =Undef_Handler      ; 未定义指令
0x00000008:  LDR PC, =SVC_Handler        ; 系统调用(SWI)
0x0000000C:  LDR PC, =Prefetch_Handler   ; 预取指中止
0x00000010:  LDR PC, =Data_Handler       ; 数据中止
0x00000014:  LDR PC, =IRQ_Handler        ; 普通中断
0x00000018:  LDR PC, =FIQ_Handler        ; 快速中断

关键点:

  • 每个向量4字节,不能多不能少
  • 用LDR PC, =xxx而不是B指令,因为B指令跳转范围有限(±32MB)
  • FIQ在最后一位,允许从FIQ入口直接开始执行代码,省一次跳转

我个人习惯把向量表放在SRAM里,运行时重映射。这样可以在运行时动态修改中断处理函数,调试起来方便很多。

4.2 IRQ与FIQ机制:普通快递 vs 闪送

ARM提供了两种中断请求线:IRQ(普通中断)和FIQ(快速中断)。

FIQ的设计初衷是处理那些“一秒都不能等”的事件。它有几个特权:

  • 有自己的影子寄存器(R8-R14),不用压栈保护现场
  • 优先级高于IRQ
  • 向量表在最后一位,可以直接在入口处写代码

我在项目中遇到过一个问题:一个高速数据采集系统,用IRQ处理ADC数据,结果采样率一高就丢数据。后来改成FIQ,直接把数据搬运到DMA缓冲区,问题就解决了。

避坑指南:

我曾经以为FIQ就是“更快的中断”,后来发现它快是因为省了现场保护。但如果你在FIQ里调用了需要压栈的函数,那优势就没了。所以FIQ里尽量只做最核心的操作,比如置个标志位、触发DMA。

4.3 GICv2/v3控制器:中断的“交通警察”

早期的ARM芯片中断控制器很简单,就是一堆寄存器。到了多核时代,ARM推出了通用中断控制器(GIC)。

4.3.1 GICv2架构

GICv2分为两个部分:

  • Distributor(分发器):管理所有中断源,配置优先级、使能/禁用、触发方式
  • CPU Interface(CPU接口):每个CPU核一个,负责向核发送中断信号

中断类型有三种:

类型 ID范围 说明
SGI(软件触发中断) 0-15 用于核间通信,写GICD_SGIR触发
PPI(私有外设中断) 16-31 每个核独享,如本地定时器
SPI(共享外设中断) 32-1019 所有核共享,如UART、GPIO

配置一个SPI中断的典型流程:

// 1. 配置中断触发方式(边沿/电平)
GICD_ICFGR[irq_num/16] |= (trigger << (2*(irq_num%16)));

// 2. 设置优先级(0-255,0最高)
GICD_IPRIORITYR[irq_num] = priority;

// 3. 设置目标CPU
GICD_ITARGETSR[irq_num] = cpu_mask;

// 4. 使能中断
GICD_ISENABLER[irq_num/32] |= (1 << (irq_num%32));

// 5. 在CPU接口使能中断组
GICC_CTLR |= 0x1;  // 使能组0中断

4.3.2 GICv3的新特性

GICv3主要解决多核扩展问题。我参与过一个64核ARM服务器项目,GICv2根本撑不住,必须上v3。

GICv3的关键变化:

  • Redistributor(再分发器):每个核一个,替代了CPU Interface
  • 支持更多中断ID:从1024扩展到16K+
  • LPI(本地外设中断):基于消息的中断,通过ITS(中断翻译服务)路由
  • 中断分组:支持安全/非安全世界的中断隔离

注意:

GICv3的寄存器地址和v2完全不同。如果你从v2移植代码到v3,别想着只改个基地址就完事。我见过有人这么干,结果中断全乱套了。

4.4 中断嵌套与优先级:谁先谁后?

中断嵌套,说白了就是一个中断正在处理,另一个更高优先级的中断来了,要不要打断它?

ARM默认行为是:进入IRQ后自动屏蔽IRQ(设置CPSR的I位),所以默认不支持嵌套。要实现嵌套,你得在中断处理函数里手动开中断。

典型的嵌套实现:

IRQ_Handler:
    // 保存现场
    SUB     lr, lr, #4
    STMFD   sp!, {r0-r12, lr}

    // 读取中断号
    LDR     r0, =GICC_IAR
    LDR     r0, [r0]
    AND     r1, r0, #0x3FF    // 中断ID

    // 手动开中断,允许嵌套
    MRS     r0, CPSR
    BIC     r0, r0, #0x80     // 清除I位(开IRQ)
    MSR     CPSR_c, r0

    // 调用处理函数
    BL      handle_irq

    // 关中断,写EOI
    MRS     r0, CPSR
    ORR     r0, r0, #0x80     // 设置I位(关IRQ)
    MSR     CPSR_c, r0

    LDR     r0, =GICC_EOIR
    STR     r1, [r0]

    // 恢复现场
    LDMFD   sp!, {r0-r12, pc}^

优先级管理有几个原则:

  • 高优先级中断可以打断低优先级中断
  • 同级中断不能互相打断
  • FIQ可以打断IRQ,反之不行

我的经验:

中断嵌套不是越多越好。嵌套层数太多,栈空间消耗大,而且容易出优先级反转的问题。我一般建议:

  • 实时性要求高的(如电机控制)用FIQ,不嵌套
  • 普通外设中断用IRQ,最多嵌套2-3层
  • 能用轮询解决的,就别用中断

你想想看,如果一个低优先级的中断正在处理,突然来了个高优先级的,CPU得先保存现场、跳转、处理、恢复现场,然后再回来继续处理原来的中断。这一来一回,光上下文切换就浪费不少时间。所以,合理规划优先级,比盲目追求嵌套深度重要得多。

嗯,关于异常与中断,今天就聊到这儿。下一章我们讲讲内存管理单元(MMU),那又是一个让人又爱又恨的东西。