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 整体流程:一张图说清楚

上面三步不是孤立的,它们有严格的先后顺序。我画了张流程图,帮你理清关系:

BootROM初始化核心流程 芯片上电复位 设置CPU模式为SVC 初始化各模式堆栈指针 异常向量表从ROM拷贝到RAM 跳转到C语言入口 关键说明 • 顺序不可颠倒 • 每步需做校验 • 失败则系统无法启动 常见问题 • SP设错 → 栈溢出 • 模式设错 → 权限不足 • 向量表未重定位 → 中断无法注册

从图上你能看到,这四步是串行的。任何一步出问题,系统都起不来。我调试过一块开发板,上电后一直复位,最后发现是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设错了,或者向量表没搬对。