2. ARM体系结构基础
做ARM架构驱动开发和Bootloader调试,第一步不是急着看寄存器手册,而是先建立一张“体系结构地图”:你面对的是哪一代ARM架构、哪一类Cortex内核、运行在什么异常级别、有哪些寄存器组、异常如何进入和返回、指令集是ARM还是Thumb。只有这张地图清晰,后面看启动代码、异常向量表、MMU配置、中断控制器初始化时,才不会把不同系列的特性混在一起。
一、ARM架构演进:从ARMv4到ARMv8的主线
ARM体系结构版本(Architecture)与具体内核(Core)是两个概念。架构版本定义指令集、异常模型、寄存器、内存模型等规范;内核是某公司基于某架构实现的具体产品。早期ARMv4/ARMv5以ARM7、ARM9为代表,主要面向嵌入式控制;ARMv6引入Thumb-2之前的增强特性;ARMv7是分水岭,明确划分为A、R、M三条产品线,并引入TrustZone、NEON、虚拟化扩展;ARMv8-A引入AArch64,寄存器宽度从32位扩展到64位,异常级别从7种模式改为EL0~EL3。
对驱动和Bootloader而言,最需要关注的是:异常模型是否从“模式”变为“异常级别”,寄存器是否分Bank,指令集是否支持A64,以及启动时默认处于哪个异常级别。例如Cortex-A53上电后通常从EL3开始,而Cortex-M4则没有EL概念,直接使用MSP/PSP和异常号。
二、Cortex-A/R/M系列差异:一张表看清定位
三条产品线不是性能高低之分,而是应用场景不同。Cortex-A面向应用处理器,跑Linux/Android,有MMU、Cache、NEON、虚拟化;Cortex-R面向实时控制,强调低延迟中断和锁步安全,常用于汽车刹车、硬盘控制器;Cortex-M面向微控制器,没有MMU,只有MPU,中断向量表固定在Flash开头,适合裸机或RTOS。
| 对比项 | Cortex-A | Cortex-R | Cortex-M |
|---|---|---|---|
| 典型场景 | 手机、平板、服务器 | 汽车电子、工业实时 | MCU、传感器、家电 |
| 内存管理 | MMU + Cache | MPU + Cache | MPU,通常无Cache |
| 异常模型 | EL0~EL3 / 7种模式 | 模式 + 快速中断 | 异常号 + 优先级 |
| 指令集 | A32/A64 + Thumb-2 | A32 + Thumb-2 | Thumb-2为主 |
| 启动方式 | 多级Boot,常从EL3 | 向量表可重定位 | 向量表固定或重定位 |
在Bootloader调试中,如果拿到的是Cortex-A芯片,要特别关注EL3→EL2→EL1的切换和SCR_EL3、HCR_EL2等寄存器;如果是Cortex-M,则重点看VTOR、MSP、PSP和NVIC优先级分组。
三、寄存器组:通用寄存器与Bank寄存器
以ARMv7-A为例,程序员可见的寄存器包括R0~R15。其中R13是栈指针SP,R14是链接寄存器LR,R15是程序计数器PC。R0~R7为所有模式共用,R8~R12在FIQ模式下有独立Bank,R13/R14在每个异常模式下都有独立副本。这种设计使得异常发生时,处理器可以快速切换到对应模式的栈和返回地址,而不必先保存现场。
ARMv8-A的AArch64则提供X0~X30通用寄存器,其中X30为LR,SP独立,PC不再是通用寄存器。异常级别EL0~EL3各有自己的SP_ELx和SPSR_ELx。调试时若发现“栈指针突然变了”,往往不是代码写错,而是异常进入后自动切换到了对应EL的栈。
; ARMv7-A 异常进入时的典型现场
; IRQ模式使用SP_irq、LR_irq
; 返回时用 SUBS PC, LR, #4
IRQ_Handler:
SUB LR, LR, #4
SRSDB SP!, #0x12 ; 保存返回状态
PUSH {R0-R3, R12, LR}
; ... 中断处理 ...
POP {R0-R3, R12, LR}
RFEIA SP! ; 异常返回
四、异常模型:从模式到异常级别
ARMv7-A有7种处理器模式:User、FIQ、IRQ、Supervisor、Abort、Undefined、System。异常向量表位于0x00000000或0xFFFF0000,每个异常占4字节,通常放一条跳转指令。异常发生时,处理器自动保存CPSR到SPSR,切换到对应模式,并跳转到向量表对应偏移。
ARMv8-A改为异常级别模型:EL0用户态、EL1内核态、EL2虚拟化、EL3安全监控。异常分为同步异常、IRQ、FIQ、SError四类,每类有独立的向量表基址VBAR_ELx。异常进入时,硬件自动保存PSTATE到SPSR_ELx,返回地址保存到ELR_ELx,并切换到目标EL的栈。Bootloader在EL3初始化时,必须设置好VBAR_EL3,否则任何异常都会导致跑飞。
调试口诀:先看当前EL/模式,再看SP和LR,最后看向量表基址。三者对不上,异常返回必然出错。
五、指令集概览:ARM、Thumb与A64
ARM指令集为32位定长,性能高但代码密度低;Thumb指令集为16位定长,代码密度高但功能受限;Thumb-2是混合指令集,16位和32位指令并存,兼顾密度和性能。Cortex-M只支持Thumb-2,Cortex-A/R同时支持ARM和Thumb-2。ARMv8-A的A64指令集为32位定长,但寄存器宽度64位,且不再支持条件执行几乎全部指令,改为条件选择指令CSEL等。
在Bootloader中,汇编启动代码常用Thumb-2以节省空间,但异常向量表第一条跳转指令必须注意对齐和指令集切换。若从ARM状态跳转到Thumb函数,需使用BX或BLX并确保目标地址最低位为1。
; 从ARM状态切换到Thumb状态
LDR R0, =thumb_func
ORR R0, R0, #1 ; 置位bit0表示Thumb
BX R0
thumb_func:
; Thumb-2代码
MOVS R1, #0x10
六、案例与误区复盘
某Cortex-A9项目在Bootloader中初始化中断后,一触发IRQ就死机。排查发现:向量表重定位到DDR后,没有正确设置VBAR,且IRQ模式栈指针未初始化。异常进入后压栈写到非法地址,导致Abort嵌套。修正方法是:在启动汇编中为每个模式设置独立栈,并确保VBAR指向对齐的向量表。
另一个常见误区是把Cortex-M的异常模型套到Cortex-A上。Cortex-M没有模式切换,中断直接使用MSP或PSP,向量表是地址数组;而Cortex-A需要手动保存现场、切换模式、处理SPSR。两者在驱动框架上完全不同,不能混用。
不要在没有设置好异常向量表和栈指针之前使能中断。Bootloader阶段尤其如此,否则一次中断就可能让整个启动流程崩溃。
总结来看,ARM体系结构基础的核心是“架构版本—内核系列—寄存器组—异常模型—指令集”这条主线。理解Cortex-A/R/M的差异,是为了选对驱动模型;理解寄存器Bank和异常级别,是为了写对启动和异常处理代码;理解指令集切换,是为了让汇编与C代码正确衔接。把这些基础打牢,后续的MMU、Cache、中断控制器和Bootloader移植才会顺理成章。