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),那又是一个让人又爱又恨的东西。