3. BootROM的初始化:CPU模式设置、堆栈指针(SP)初始化、异常向量表重定位
芯片上电后,第一条指令从哪里开始执行?嗯,就是BootROM。这块只读存储器里固化的代码,是整个系统的起点。今天咱们就聊聊BootROM最核心的三件事:CPU模式设置、堆栈指针初始化、异常向量表重定位。
说白了,这三件事做不好,后面的代码根本跑不起来。我在一个IoT项目里就吃过这个亏——BootROM里SP设错了,结果一进中断就崩,查了两天才找到原因。
3.1 CPU模式设置:从默认状态到可控状态
大部分ARM Cortex-A系列芯片,上电后默认进入SVC模式(Supervisor Mode)。为什么是SVC?因为这是最高权限模式之一,可以访问所有系统资源。你想想看,BootROM要做初始化,权限不够怎么行?
但这里有个坑:不同厂商的BootROM实现不一样。有些芯片上电后可能处于Secure Monitor模式,有些甚至处于Undefined模式。所以第一步,必须明确设置CPU模式。
核心原则:BootROM必须在执行任何复杂逻辑前,将CPU置于一个已知的、权限足够的模式下。通常选择SVC模式或Secure SVC模式。
代码实现很简单,就几条汇编指令:
@ 设置CPU为SVC模式,关闭IRQ和FIQ
mrs r0, cpsr
bic r0, r0, #0x1F @ 清除模式位
orr r0, r0, #0xD3 @ 设置为SVC模式(0x13) + 关IRQ(0x80) + 关FIQ(0x40)
msr cpsr, r0
我个人习惯在设置模式后,立刻读回CPSR做一次校验。虽然多花几个周期,但能避免某些芯片的硅片bug导致模式设置失败。
3.2 堆栈指针(SP)初始化:给C语言铺路
BootROM早期代码用汇编写,但很快就要跳转到C语言。C语言需要栈——局部变量、函数调用、参数传递都依赖栈。所以SP必须提前设好。
SP设在哪里?这是个好问题。我见过三种做法:
| 方案 | SP位置 | 优点 | 缺点 |
|---|---|---|---|
| 片内SRAM顶部 | 内部SRAM的最高地址 | 访问速度快,无需初始化DRAM | 空间有限,通常只有几十KB |
| BootROM末尾 | BootROM未使用的区域 | 不占用SRAM | BootROM通常是只读的,不可写 |
| 专用栈内存 | 芯片内部专用SRAM | 独立安全,不受其他模块影响 | 成本高,少见 |
实际项目中,绝大多数芯片选择方案一:片内SRAM顶部。为什么?因为BootROM阶段DRAM还没初始化,只能用片内SRAM。而SRAM的顶部地址,通常就是芯片手册里说的"Boot Stack Pointer"。
经验之谈:SP初始化时,记得给栈留够余量。我曾经在一个项目里把SP设在SRAM最顶端,结果栈向下增长时,刚好覆盖了关键数据区。后来我习惯在SP下方留出至少256字节的"安全区",宁可浪费一点空间,也别踩雷。
多模式CPU还需要为每种异常模式单独设置SP:
@ 为各模式设置栈指针
ldr sp, =__stack_svc @ SVC模式栈
msr cpsr, #0xD2 @ 切换到IRQ模式
ldr sp, =__stack_irq @ IRQ模式栈
msr cpsr, #0xD1 @ 切换到FIQ模式
ldr sp, =__stack_fiq @ FIQ模式栈
msr cpsr, #0xD7 @ 切换到ABT模式
ldr sp, =__stack_abt @ ABT模式栈
msr cpsr, #0xDB @ 切换到UND模式
ldr sp, =__stack_und @ UND模式栈
msr cpsr, #0xD3 @ 切回SVC模式
为什么要给每种模式单独设栈?因为异常发生时,CPU会自动切换到对应模式,如果那个模式的SP没设好,一压栈就崩。嗯,这个坑我踩过——IRQ模式SP没初始化,结果中断一进来,直接写到了未知地址。
3.3 异常向量表重定位:从ROM到RAM
异常向量表,就是CPU发生异常时跳转的入口地址表。通常放在地址0x00000000处。但BootROM在ROM里,ROM是只读的,没法动态修改向量表。
这就引出问题了:如果我想在运行时注册新的中断处理函数,怎么办?
答案就是——重定位。把向量表从ROM拷贝到RAM,然后让CPU指向RAM里的新表。
注意:不是所有CPU都支持向量表重定位。ARM Cortex-M系列通过VTOR寄存器实现,Cortex-A系列则依赖MMU或CP15协处理器。如果你的芯片不支持硬件重定位,那就只能用跳转指令做软件转发。
ARM Cortex-A系列的典型做法:
@ 将异常向量表从ROM拷贝到RAM
ldr r0, =__vectors_start @ ROM中的向量表起始地址
ldr r1, =__vectors_end @ ROM中的向量表结束地址
ldr r2, =__vectors_ram @ RAM中的目标地址
copy_loop:
ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blo copy_loop
@ 设置CP15的向量基址寄存器
mrc p15, 0, r0, c12, c0, 0 @ 读取VBAR
ldr r1, =__vectors_ram
mcr p15, 0, r1, c12, c0, 0 @ 写入VBAR,完成重定位
重定位完成后,CPU再发生异常,就会直接跳转到RAM里的向量表。这时候,你就可以动态修改中断处理函数了。
3.4 整体流程:一张图说清楚
上面三步不是孤立的,它们有严格的先后顺序。我画了张流程图,帮你理清关系:
从图上你能看到,这四步是串行的。任何一步出问题,系统都起不来。我调试过一块开发板,上电后一直复位,最后发现是SP初始化时用了错误的SRAM基地址——芯片手册写的是0x20000000,实际是0x20010000。差了一个偏移量,整个栈就废了。
3.5 避坑指南
做BootROM开发这些年,我总结了几条血泪教训:
- SP方向别搞反:ARM的栈是向下增长的(满递减)。如果你把SP设在SRAM底部,栈一增长就写到未初始化区域了。我曾经把SP设在SRAM起始地址,结果压栈时直接覆盖了代码段——嗯,那次调试花了一整天。
- 向量表对齐:ARM要求向量表按32字节对齐(Cortex-M)或按4KB对齐(Cortex-A)。不对齐的话,CPU可能跳转到错误地址。我建议你在链接脚本里显式声明对齐属性。
- 关中断再初始化:在SP和向量表没准备好之前,千万别开中断。否则一个意外中断进来,连压栈的地方都没有。BootROM默认关中断,但有些芯片的复位默认值不标准,最好显式关闭。
- 校验不可少:每做完一步,读回寄存器确认一下。多花几个周期,换来的是调试时的安心。
小技巧:如果你在调试BootROM,可以在SP初始化后,立刻往栈里写一个固定模式(比如0xDEADBEEF),然后在后续代码里检查这个模式是否被覆盖。这样能快速发现栈溢出问题。
好了,BootROM的初始化三步曲就聊到这儿。这三步虽然基础,但每一步都关系到系统能否正常启动。下次你看到芯片上电后黑屏,不妨先从这三步查起——八成是SP设错了,或者向量表没搬对。