1. ARMv7架构概述:Cortex-A/R/M系列介绍、ARMv7-A处理器模式与寄存器组、异常与中断处理机制
各位同学,咱们今天正式开讲ARMv7。说实话,ARMv7这个架构在我职业生涯里占了很大比重。从早期的手机SoC到后来的工业控制器,我调试过的ARMv7芯片少说也有几十款。你想想看,一个架构能活这么多年,一定有它的道理。
ARMv7不是单一的产品线,它分成了三个系列:Cortex-A、Cortex-R和Cortex-M。这三个系列虽然都叫ARMv7,但设计哲学完全不同。我刚开始接触时也搞混过,后来踩了几个坑才真正理解它们的区别。
1.1 Cortex-A/R/M系列介绍
先看一张总览图,把三个系列的定位搞清楚。
这张图我画了很多遍,每次给新团队培训都用它。三个系列的核心差异,说白了就是性能、实时性、功耗三个维度的取舍。
我的经验:选型时最容易犯的错误是用Cortex-A去做硬实时控制。我曾经见过一个项目,工程师用A8跑裸机做电机控制,结果中断抖动大到电机直接啸叫。后来换成R4,问题立刻解决。记住:A系列跑Linux,R系列跑RTOS,M系列跑裸机或轻量RTOS,这个分工别搞反了。
1.2 ARMv7-A处理器模式与寄存器组
接下来我们深入Cortex-A系列。ARMv7-A有9种处理器模式,我列个表方便你对照。
| 模式 | 缩写 | 权限等级 | 用途 |
|---|---|---|---|
| User | USR | PL0 | 普通应用程序运行 |
| FIQ | FIQ | PL1 | 快速中断处理 |
| IRQ | IRQ | PL1 | 普通中断处理 |
| Supervisor | SVC | PL1 | 操作系统内核、系统调用 |
| Monitor | MON | PL1 | 安全扩展(TrustZone) |
| Abort | ABT | PL1 | 数据/指令异常 |
| Hyp | HYP | PL2 | 虚拟化扩展(仅A15等) |
| Undefined | UND | PL1 | 未定义指令异常 |
| System | SYS | PL1 | 特权任务(共享User寄存器) |
这张表你得背下来。为什么?因为调试的时候,看当前模式就能猜出大概出了什么问题。比如程序跑飞进了UND模式,八成是指令编码错了或者PC指针跳到了数据区。
寄存器组这块,ARMv7-A有16个通用寄存器R0-R15,外加CPSR和SPSR。但不同模式下,部分寄存器是banked的——也就是有物理副本。
调试技巧:我调试中断嵌套问题时,经常遇到一种情况:在IRQ模式里修改了R13(SP),退出中断后系统崩溃。原因就是IRQ模式有自己的banked R13,和SVC模式的R13不是同一个物理寄存器。你改的是IRQ的栈指针,但内核用的是SVC的栈。这个坑我至少帮三个团队排查过。
寄存器banking的规则,我整理成了一张速查表:
| 寄存器 | USR/SYS | SVC | IRQ | FIQ | ABT | UND | MON | HYP |
|---|---|---|---|---|---|---|---|---|
| R0-R7 | 共用 | 共用 | 共用 | 共用 | 共用 | 共用 | 共用 | 共用 |
| R8-R12 | 共用 | 共用 | 共用 | FIQ独有 | 共用 | 共用 | 共用 | 共用 |
| R13(SP) | 共用 | SVC_SP | IRQ_SP | FIQ_SP | ABT_SP | UND_SP | MON_SP | HYP_SP |
| R14(LR) | 共用 | SVC_LR | IRQ_LR | FIQ_LR | ABT_LR | UND_LR | MON_LR | HYP_LR |
| SPSR | 无 | SVC_SPSR | IRQ_SPSR | FIQ_SPSR | ABT_SPSR | UND_SPSR | MON_SPSR | HYP_SPSR |
注意FIQ模式,它连R8-R12都是banked的。为什么?因为FIQ设计目标就是极速响应,省去保存和恢复寄存器的开销。我在做高速数据采集时用过FIQ,中断响应能压到几十纳秒级别。
1.3 异常与中断处理机制
ARMv7-A的异常向量表固定在0x00000000或0xFFFF0000(由SCTLR.V位控制)。每个异常入口占4字节,通常是一条跳转指令。
; 典型的异常向量表(基址0x00000000)
0x00000000: LDR PC, Reset_Addr ; 复位
0x00000004: LDR PC, Undef_Addr ; 未定义指令
0x00000008: LDR PC, SWI_Addr ; 软中断(SVC)
0x0000000C: LDR PC, Prefetch_Addr ; 预取指中止
0x00000010: LDR PC, Data_Addr ; 数据中止
0x00000014: LDR PC, Hyp_Addr ; 虚拟化(可选)
0x00000018: LDR PC, IRQ_Addr ; 普通中断
0x0000001C: LDR PC, FIQ_Addr ; 快速中断
异常处理的流程,我画个流程图帮你理解:
这里有个关键点:LR在异常入口保存的是返回地址,但不同异常需要不同的偏移量。我列个表:
| 异常类型 | LR保存的值 | 返回指令 | 偏移原因 |
|---|---|---|---|
| 复位 | 不适用 | 不适用 | — |
| 未定义指令 | 当前指令地址+4 | MOVS PC, LR | LR指向下一条未执行指令 |
| SVC | 当前指令地址+4 | MOVS PC, LR | 同上 |
| 预取指中止 | 当前指令地址+4 | SUBS PC, LR, #4 | LR指向未执行的指令,需回退 |
| 数据中止 | 当前指令地址+8 | SUBS PC, LR, #8 | LR指向下下条,需回退两条 |
| IRQ | 当前指令地址+4 | SUBS PC, LR, #4 | LR指向未执行的指令 |
| FIQ | 当前指令地址+4 | SUBS PC, LR, #4 | 同上 |
我曾经踩过的坑:刚做嵌入式那会儿,写中断服务程序返回时用了MOVS PC, LR,结果程序跑飞。查了半天才发现IRQ返回必须用SUBS PC, LR, #4。为什么?因为IRQ发生时,当前指令还没执行完,LR保存的是下一条指令的地址,直接返回会跳过当前指令。这个bug我调了整整一个通宵。
中断优先级方面,ARMv7-A硬件只区分FIQ和IRQ两级。FIQ优先级高于IRQ。但实际SoC中,中断控制器(GIC)会扩展出更多优先级。GIC支持最多256级优先级,但具体实现看芯片。
嵌套中断处理是个难点。默认情况下,ARM进入IRQ模式后会屏蔽IRQ(CPSR的I位置1)。如果你想支持嵌套,需要在ISR里手动开中断。但要注意:开中断前必须确保栈空间足够,否则嵌套几次就栈溢出了。
我的建议:做嵌套中断时,先切到SVC模式再开中断。因为IRQ模式的栈通常只分配了很小的空间(很多BSP只给256字节),而SVC模式的栈是内核栈,空间大得多。具体做法:在ISR入口保存现场后,执行CPS #0x13切换到SVC模式,然后开中断。返回时先关中断,再切回IRQ模式,最后用SUBS PC, LR, #4返回。
最后提一下FIQ和IRQ的选择。FIQ的优势不仅是优先级高,更重要的是它有独立的R8-R12,省去了保存这些寄存器的开销。但FIQ的向量表入口只有一个,不像IRQ可以挂多个中断源。实际项目中,我通常把最紧急、最频繁的中断分配给FIQ,比如定时器滴答、高速ADC转换完成等。