第1章 ARM Cortex-A系列启动详解:异常向量表、启动模式与SVC模式切换
各位同学,咱们今天正式开讲。做游戏机Bootloader,ARM Cortex-A系列是绕不开的坎。我这些年拆过的游戏机,从PSP到Switch,核心清一色都是ARM架构。说白了,搞懂Cortex-A的启动流程,你就能看懂八成游戏机的底层运作。
这一章,咱们聚焦四个核心点:异常向量表、启动模式、SVC模式切换,还有MMU初始化前的准备工作。嗯,内容有点干,但都是硬通货。
1.1 异常向量表——系统的“紧急电话本”
异常向量表是什么?我习惯把它比作一张“紧急电话本”。CPU遇到异常,比如按了电源键、触发了中断,它会立刻查这个表,找到对应的处理函数。
这张表固定在地址 0x00000000 或 0xFFFF0000(由SCTLR.V位控制)。每个表项占4字节,一共8个异常。
| 偏移地址 | 异常类型 | 触发场景 |
|---|---|---|
| 0x00 | 复位 (Reset) | 上电或按复位键 |
| 0x04 | 未定义指令 (Undef) | CPU不认识这条指令 |
| 0x08 | 软件中断 (SWI) | 执行SVC指令 |
| 0x0C | 预取中止 (Prefetch Abort) | 取指令时内存出错 |
| 0x10 | 数据中止 (Data Abort) | 读写数据时内存出错 |
| 0x14 | 保留 (Reserved) | 别用,留给未来 |
| 0x18 | IRQ (普通中断) | 外设请求CPU处理 |
| 0x1C | FIQ (快速中断) | 高速外设请求,优先级最高 |
核心要点:复位异常是Bootloader的入口。CPU上电后,第一条指令就是从0x00000000处取出来的。所以,你的Bootloader代码必须从这儿开始。
我在项目中遇到过一个问题:某款国产游戏机,启动时偶尔会卡死。查了半天,发现是异常向量表被意外覆盖了。嗯,从那以后,我都在向量表前面加一段保护代码,防止被乱写。
1.2 启动模式——FIQ与IRQ的选择
Cortex-A支持两种中断模式:IRQ和FIQ。你想想看,为什么要有两个?
说白了,FIQ是VIP通道。它有自己的专用寄存器(R8-R14),不用像IRQ那样压栈保存。所以FIQ响应更快,适合处理高速数据流,比如游戏机的DMA传输。
但FIQ也有代价——它只能挂一个中断源。IRQ虽然慢一点,但可以挂多个。我个人的习惯是:
- FIQ:给最高优先级的外设,比如视频刷新中断
- IRQ:给普通外设,比如按键、音频
小技巧:在Bootloader阶段,我建议先关闭所有中断。等MMU和栈都初始化好了,再按需开启。否则,中断来了你没栈可用,直接崩掉。
关闭中断的代码很简单:
// 关闭IRQ和FIQ
MRS R0, CPSR
ORR R0, R0, #0xC0 // 设置I位和F位
MSR CPSR_c, R0
1.3 SVC模式切换——Bootloader的“特权模式”
ARM有七种运行模式,但Bootloader最常用的是SVC(超级用户模式)。为什么?因为SVC有最高权限,能访问所有系统资源。
CPU上电后,默认进入SVC模式。但如果你不小心切到了其他模式,比如用户模式(USR),那就麻烦了——很多关键寄存器都访问不了。
我曾经犯过这个错:写了一个Bootloader,调试时发现MMU死活配不上。查了两天,才发现代码跑在用户模式下。唉,那两天真是白费了。
切换到SVC模式的方法:
// 切换到SVC模式
MRS R0, CPSR
BIC R0, R0, #0x1F // 清除模式位
ORR R0, R0, #0x13 // 设置为SVC模式
MSR CPSR_c, R0
警告:切换模式时,一定要确保栈指针(SP)已经设置好。SVC模式有自己的栈,如果没设,函数调用会直接崩溃。我见过有人在这上面栽跟头,整块板子变砖。
1.4 MMU初始化前的准备——别急着开MMU
很多新手一上来就开MMU,结果系统直接挂掉。为什么?因为MMU开启后,CPU访问的都是虚拟地址。如果你的页表没配好,CPU根本找不到代码在哪儿。
我个人建议,MMU初始化前,先做好三件事:
- 关掉所有中断——防止中断处理时地址错乱
- 设置好栈指针——每个模式都要有独立的栈
- 清空TLB和Cache——避免旧数据干扰新页表
清空TLB的代码示例:
// 清空TLB
MOV R0, #0
MCR p15, 0, R0, c8, c7, 0 // 清空TLB
DSB // 确保操作完成
ISB // 刷新指令流水线
嗯,这里要注意:DSB和ISB这两个指令,很多人会漏掉。DSB保证前面的操作都完成了,ISB保证后面的指令重新读取。少了任何一个,都可能出现诡异的问题。
避坑指南:我曾经在MMU初始化时,忘了关Cache。结果页表被Cache缓存了,修改后没生效。系统跑着跑着就跳转到错误地址。从那以后,我每次初始化MMU,都先关Cache,再操作页表,最后开MMU。顺序一步都不能错。
1.5 实战:一个最小Bootloader的启动流程
说了这么多,咱们来点实际的。一个最小Bootloader的启动流程,大概是这样:
// 伪代码,展示启动流程
_start:
// 1. 设置异常向量表
LDR PC, Reset_Handler
LDR PC, Undef_Handler
LDR PC, SWI_Handler
// ... 其他异常向量
Reset_Handler:
// 2. 关闭中断
MRS R0, CPSR
ORR R0, R0, #0xC0
MSR CPSR_c, R0
// 3. 设置SVC模式栈
LDR SP, =SVC_STACK_TOP
// 4. 初始化关键硬件(时钟、内存控制器等)
BL SystemInit
// 5. 准备页表(但先不开MMU)
BL PageTableInit
// 6. 开启MMU
BL MMUEnable
// 7. 跳转到主程序
BL main
你看,流程很清晰。每一步都有它的道理。我刚开始做Bootloader时,总觉得这些步骤多余。后来被坑了几次,才明白——每一步都是在给后面的代码铺路。
个人经验:调试Bootloader时,我习惯在每个关键步骤后加一个LED闪烁。比如,初始化完内存,LED闪一下;配好页表,再闪一下。这样,系统卡在哪儿,一眼就能看出来。比用调试器快多了。
小结
这一章的内容,说白了就是Bootloader的“起手式”。异常向量表是入口,启动模式决定权限,SVC模式是工作环境,MMU准备是基础。把这四点吃透了,后面的内容就好办了。
下一章,咱们聊聊具体的硬件初始化——时钟、内存、串口。这些东西配不好,你的Bootloader连个“Hello World”都打印不出来。嗯,到时候见。