4、ARM异常与中断处理:异常向量表、异常优先级、中断响应流程、FIQ与IRQ的区别
异常与中断处理,可以说是ARM处理器最核心的机制之一。我做了这么多年嵌入式,见过太多系统崩溃、响应超时的问题,十有八九都是中断处理没搞明白。说白了,处理器就是个“单线程”的家伙,它怎么同时响应键盘、定时器、网络数据包?靠的就是异常中断机制。
4.1 异常向量表——处理器的“紧急电话本”
异常向量表,你可以把它想象成一张“紧急电话本”。处理器遇到不同异常时,会查这个表,找到对应的处理函数入口。
ARM的异常向量表通常放在内存的低地址端(0x00000000开始),或者高地址端(0xFFFF0000开始)。具体位置由系统控制协处理器决定。我个人习惯把向量表放在低地址,调试起来更直观。
| 异常类型 | 向量地址 | 优先级 | 模式 |
|---|---|---|---|
| 复位 | 0x00000000 | 1(最高) | SVC |
| 未定义指令 | 0x00000004 | 6 | UND |
| 软件中断(SWI) | 0x00000008 | 6 | SVC |
| 预取指令中止 | 0x0000000C | 5 | ABT |
| 数据中止 | 0x00000010 | 2 | ABT |
| IRQ | 0x00000018 | 4 | IRQ |
| FIQ | 0x0000001C | 3 | FIQ |
每个向量表项只有4字节空间。4字节能干嘛?放一条跳转指令呗。比如:
; 典型的ARM异常向量表
LDR PC, Reset_Handler ; 0x00 复位
LDR PC, Undef_Handler ; 0x04 未定义指令
LDR PC, SWI_Handler ; 0x08 软件中断
LDR PC, Prefetch_Handler ; 0x0C 预取中止
LDR PC, Data_Handler ; 0x10 数据中止
NOP ; 0x14 保留
LDR PC, IRQ_Handler ; 0x18 IRQ
LDR PC, FIQ_Handler ; 0x1C FIQ
嗯,这里要注意:LDR PC, xxx 是一条伪指令,它会从内存中加载目标地址到PC。为什么不用 B 指令?因为 B 有跳转范围限制(±32MB),而 LDR PC 可以跳到任意地址。
核心要点:异常向量表必须放在正确的位置,且每个表项必须是跳转指令。我曾经见过一个项目,向量表被意外覆盖了,结果一触发中断就跑飞——那场面,真是“叫天天不应”。
4.2 异常优先级——谁先谁后?
多个异常同时发生时,处理器得有个“先来后到”的规矩。ARM的异常优先级从高到低是这样的:
- 复位(最高)—— 谁也别跟我抢
- 数据中止 —— 内存访问出问题了,得赶紧处理
- FIQ —— 快速中断,优先级高
- IRQ —— 普通中断
- 预取指令中止 —— 指令取址失败
- SWI 和 未定义指令(最低)—— 软件触发的,不急
你想想看,为什么复位优先级最高?这还用说吗,系统都重启了,其他异常都得靠边站。数据中止比FIQ还高,是因为内存访问错误如果不立即处理,可能导致数据损坏。
实战经验:我在做一款工业控制器时,发现FIQ和IRQ的优先级设置直接影响系统实时性。FIQ处理高速数据流,IRQ处理按键等低速事件。如果反过来,高速数据就可能丢包。
4.3 中断响应流程——处理器到底干了啥?
当一个中断发生时,处理器内部会执行一系列“自动操作”。我建议你把这个流程刻在脑子里:
- 保存现场:自动保存当前PC到LR(链接寄存器),保存CPSR到SPSR
- 切换模式:切换到IRQ或FIQ模式,并禁用同级中断
- 跳转向量表:PC指向对应的异常向量地址
- 执行中断服务程序(ISR):你写的处理代码
- 恢复现场:从SPSR恢复CPSR,从LR恢复PC
用代码表示就是:
; IRQ中断服务程序示例
IRQ_Handler:
SUB LR, LR, #4 ; 修正返回地址(ARM模式下)
STMFD SP!, {R0-R12, LR} ; 保存寄存器
BL C_IRQ_Handler ; 调用C语言处理函数
LDMFD SP!, {R0-R12, PC}^ ; 恢复寄存器并返回
这里有个坑:SUB LR, LR, #4 是必须的。为什么?因为ARM是流水线架构,中断发生时PC已经指向当前指令+8了。如果不减4,返回后会跳过一条指令。我曾经在这个问题上折腾了一整天,最后发现是流水线导致的偏移。
避坑指南:我曾经在STM32F4上调试一个中断,发现每次返回后程序都跑飞。查了半天,原来是中断返回时忘了用 ^ 后缀(表示从SPSR恢复CPSR)。记住:LDMFD SP!, {R0-R12, PC}^ 中的 ^ 不能少!
4.4 FIQ与IRQ的区别——快在哪里?
FIQ(Fast Interrupt Request)和IRQ(Interrupt Request)是ARM处理器的两种中断类型。很多人以为FIQ只是优先级高一点,其实远不止如此。
我总结了一下,FIQ比IRQ“快”在三个地方:
| 特性 | IRQ | FIQ |
|---|---|---|
| 优先级 | 较低(4) | 较高(3) |
| 专用寄存器 | 无(共用R0-R12) | 有(R8-R12专用) |
| 向量表位置 | 0x18 | 0x1C(最后一项) |
| 中断嵌套 | 可被FIQ打断 | 不可被打断 |
| 典型应用 | 普通外设中断 | 高速数据流、DMA |
第一,FIQ有专用寄存器。 ARM在FIQ模式下提供了R8_fiq到R12_fiq这5个专用寄存器。这意味着进入FIQ中断时,不需要保存这5个寄存器,省去了压栈出栈的时间。IRQ模式下没有这个待遇,所有寄存器都得保存。
第二,FIQ在向量表的末尾。 你想想看,FIQ的向量地址是0x1C,而IRQ是0x18。这意味着你可以直接在FIQ向量地址处写代码,不用跳转。比如:
; FIQ可以直接在向量表处写代码
FIQ_Handler:
; 直接在这里处理,不用跳转
; 因为R8-R12是专用的,不用保存
; 处理完直接返回
SUBS PC, LR, #4
而IRQ必须跳转到别处,多了一次跳转开销。
第三,FIQ优先级更高,且不可被中断。 一旦进入FIQ,处理器会自动禁用IRQ和FIQ。这意味着FIQ不会被任何中断打断,保证了实时性。IRQ则可以被FIQ打断。
我的建议:在实际项目中,把对时间要求苛刻的任务(比如高速ADC采样、PWM精确控制)放在FIQ里处理。把按键扫描、LED显示这类“不急”的任务放在IRQ里。我在一个电机控制项目中就是这么做的,效果非常好。
4.5 异常处理的“潜规则”
最后,分享几个我在项目中踩过的坑:
- 中断服务程序要短小精悍:不要在ISR里做复杂运算,能用标志位就用标志位。我见过有人在ISR里做浮点运算,结果系统直接卡死。
- 注意中断嵌套深度:虽然ARM支持中断嵌套,但嵌套太深会导致栈溢出。我一般限制嵌套不超过3层。
- FIQ不是万能的:FIQ虽然快,但因为它不可被打断,如果处理时间过长,会阻塞其他中断。我建议FIQ处理时间控制在10微秒以内。
- 向量表对齐:ARM要求向量表32字节对齐。如果不对齐,处理器可能跳转到错误地址。
好了,关于ARM异常与中断处理的核心内容就这些。记住一句话:中断是嵌入式系统的灵魂,搞懂了中断,你就掌握了ARM处理器的命脉。