第四章 ARM Cortex-A启动流程:异常向量表、SVC模式、MMU与Cache初始化、跳转到Linux内核
好,咱们接着往下聊。上一章我们把汇编基础打牢了,这一章要动真格的了——真正把Bootloader跑起来。
ARM Cortex-A的启动流程,说白了就是一条从复位到Linux内核的必经之路。我做了这么多年底层开发,发现很多新手栽就栽在这几个关键点上:异常向量表放错位置、MMU配置把系统搞挂、Cache没处理好导致数据不一致。嗯,咱们一个一个来拆解。
4.1 异常向量表:系统的“紧急联络簿”
异常向量表是什么?你可以把它想象成一张表格,CPU遇到各种异常时,会自动跳到这里查“该找谁处理”。
我个人习惯把异常向量表放在0x00000000或者0xFFFF0000这两个地址。为什么?因为ARM硬件设计就这么规定的——复位后CPU强制从0x00000000开始取指令。
核心要点:异常向量表必须4字节对齐,每个表项是一条跳转指令(B或LDR)。
来看一个典型的向量表代码:
@ 异常向量表,放在代码段最开头
.section .vectors, "ax"
.globl _start
_start:
b reset_handler @ 0x00: 复位
b undef_handler @ 0x04: 未定义指令
b svc_handler @ 0x08: 软件中断(SWI)
b prefetch_abort_handler @ 0x0C: 预取中止
b data_abort_handler @ 0x10: 数据中止
b . @ 0x14: 保留
b irq_handler @ 0x18: 普通中断
b fiq_handler @ 0x1C: 快速中断
这里有个坑,我曾经踩过——向量表里每条指令占4字节,但B指令的跳转范围有限。如果你的处理函数离得太远,B指令跳不过去,那就出大事了。我建议用LDR PC, [PC, #offset]这种方式,或者直接放一个跳转表。
避坑指南:我曾经在一个项目里,因为链接脚本没配好,异常向量表被放到了0x8000开头的位置,结果CPU复位后直接跑飞。查了两天才发现是链接脚本的VMA设错了。记住:复位向量必须在链接脚本中指定为0x00000000。
4.2 进入SVC模式:给CPU“换身份”
复位后CPU默认在什么模式?ARM架构规定,复位后CPU进入SVC模式(超级用户模式)。但有些情况下,你可能需要显式切换一下。
为什么要用SVC模式?说白了,Bootloader要干的事——配置时钟、初始化DDR、设置MMU——这些都需要最高权限。SVC模式就是那个“管理员账号”。
@ 确保进入SVC模式
reset_handler:
@ 读取当前模式
mrs r0, cpsr
@ 清除模式位,设置为SVC模式
bic r0, r0, #0x1F
orr r0, r0, #0x13 @ SVC模式,IRQ/FIQ使能
msr cpsr, r0
你想想看,如果这时候你还在User模式,连CPSR都改不了,那后面的硬件初始化根本没法做。所以第一步就是确认自己“有权限”。
我个人习惯在进入SVC模式后,顺手把栈指针也设好。因为C语言函数调用需要栈,而SVC模式有自己的栈指针(SP_svc)。
@ 设置SVC模式的栈指针
ldr sp, =__svc_stack_top
小技巧:栈顶地址我一般放在DDR的高端地址,比如0x80000000附近。这样栈向下增长时,不会和代码段、数据段冲突。
4.3 MMU与Cache初始化:给CPU装上“导航系统”
MMU(内存管理单元)和Cache,这两个东西是Cortex-A性能的关键。但初始化顺序搞反了,系统直接死给你看。
我记得第一次做Cortex-A9的板子,上来就开了MMU,结果DDR还没初始化完,系统直接挂掉。后来才明白:先初始化DDR,再配置页表,最后开MMU。
4.3.1 页表配置
ARM Cortex-A的MMU使用两级页表。一级页表叫L1页表,每个表项对应1MB的内存块。二级页表叫L2页表,可以细到4KB。
对于Bootloader来说,我建议直接用L1页表就够了。为什么?因为Bootloader不需要那么细的粒度,1MB的映射精度完全够用。
@ 一级页表项格式(简化版)
@ bit[1:0] = 0b10 表示段映射(Section)
@ bit[18:12] = 域(Domain)
@ bit[31:20] = 基地址(1MB对齐)
#define MMU_SECTION_ENTRY(base, flags) \
((base) | (flags))
@ 示例:映射0x00000000 - 0x00100000为设备内存
ldr r0, =0x00000000
orr r0, r0, #0xC06 @ 设备内存,可读写,不可Cache
str r0, [r1, #0x000] @ 写入页表
关键点:页表必须放在1MB对齐的地址上。我一般放在DDR的起始位置,比如0x80000000,然后页表从0x80004000开始(留出4KB给异常向量表)。
4.3.2 Cache初始化
Cache这东西,开得好性能翻倍,开不好数据全乱。我见过最典型的错误是:开了Cache但没做Invalidate。
为什么?因为DDR上电后的初始状态是不确定的。Cache里可能残留着上电时的垃圾数据。如果你直接开Cache,CPU读到的可能是这些垃圾数据,而不是真正的内存内容。
@ 使能L1 Cache前,先Invalidate
@ 使能L1 Cache前,先Invalidate
mrc p15, 0, r0, c1, c0, 0 @ 读取SCTLR
bic r0, r0, #0x0004 @ 关闭数据Cache
bic r0, r0, #0x1000 @ 关闭指令Cache
mcr p15, 0, r0, c1, c0, 0 @ 写入SCTLR
dsb @ 数据同步屏障
@ Invalidate整个L1 Cache
mov r0, #0
mcr p15, 0, r0, c7, c5, 0 @ 无效化指令Cache
mcr p15, 0, r0, c7, c6, 0 @ 无效化数据Cache
mcr p15, 0, r0, c7, c10, 4 @ 数据同步屏障
dsb
isb
嗯,这里要注意:Invalidate和Clean是两回事。Invalidate是把Cache标记为无效,Clean是把脏数据写回内存。Bootloader阶段一般只需要Invalidate,因为还没有脏数据。
4.3.3 开启MMU
页表配好了,Cache也Invalidate了,接下来就是开MMU。这一步很简单,但顺序不能错:
@ 开启MMU和Cache
mrc p15, 0, r0, c1, c0, 0 @ 读取SCTLR
orr r0, r0, #0x1 @ 使能MMU
orr r0, r0, #0x1000 @ 使能指令Cache
orr r0, r0, #0x0004 @ 使能数据Cache
mcr p15, 0, r0, c1, c0, 0 @ 写入SCTLR
dsb
isb
避坑指南:我曾经在一个项目里,开了MMU之后直接访问外设寄存器,结果系统卡死。查了半天才发现——外设寄存器的地址空间没有映射到页表里。记住:开MMU之前,确保所有要访问的地址都有对应的页表项。
4.4 跳转到Linux内核:最后的“接力棒”
好了,硬件初始化完了,MMU和Cache都开了,接下来就是把控制权交给Linux内核。这一步看似简单,但有几个硬性要求:
- CPU必须在SVC模式——Linux内核要求启动时处于SVC模式
- MMU必须关闭——等等,我刚才不是开了MMU吗?
对,这里有个矛盾:Bootloader开了MMU,但Linux内核要求启动时MMU关闭。怎么办?
我个人习惯的做法是:在跳转前关闭MMU和Cache,然后跳转到Linux内核的入口地址。Linux内核启动后会自己重新配置MMU。
@ 跳转到Linux内核
@ 关闭MMU和Cache
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x1 @ 关闭MMU
bic r0, r0, #0x1000 @ 关闭指令Cache
bic r0, r0, #0x0004 @ 关闭数据Cache
mcr p15, 0, r0, c1, c0, 0
dsb
isb
@ 设置参数并跳转
mov r0, #0 @ 机器类型ID
ldr r1, =0x80000000 @ ATAGS或DTB地址
ldr r2, =0x00000000 @ 保留
ldr pc, =0x80008000 @ Linux内核入口地址
你想想看,为什么Linux内核要求MMU关闭?因为内核要重新建立自己的页表,如果Bootloader的页表还开着,两个页表会打架。所以跳转前必须把MMU关干净。
小技巧:跳转前记得把中断关了。我曾经因为没关中断,跳转到内核后一个未处理的中断触发,导致内核启动失败。加一句cpsid if就能解决。
4.5 总结与避坑清单
好了,这一章的内容就这些。我整理了一个避坑清单,你写代码时可以对照着检查:
| 步骤 | 常见错误 | 解决方案 |
|---|---|---|
| 异常向量表 | 向量表地址不对齐 | 确保4字节对齐,链接脚本指定VMA |
| SVC模式 | 忘记设置栈指针 | 进入SVC模式后立即设置SP_svc |
| MMU初始化 | 页表未覆盖所有地址 | 检查页表映射范围,包括外设地址 |
| Cache初始化 | 未做Invalidate | 开Cache前必须Invalidate整个Cache |
| 跳转内核 | MMU未关闭 | 跳转前关闭MMU和Cache |
我个人觉得,ARM Cortex-A的启动流程就像一场接力赛。Bootloader是第一棒,Linux内核是第二棒。交接棒的时候,一定要把状态清理干净,不然下一棒跑不起来。
下一章我们会讲设备树(DTB)的解析和传递,这是Bootloader和内核之间最重要的“通信协议”。到时候见。