2. BootROM的初始化流程:上电复位、时钟初始化、堆栈指针设置、异常向量表建立

好,咱们正式开始聊BootROM。上一章我们说了BootROM是什么,这一章我们深入进去,看看它上电后到底干了哪些事。

芯片一上电,PC指针直接指向BootROM的起始地址。这时候,整个系统还处于“混沌”状态——时钟没配、内存没初始化、连堆栈都没有。BootROM的任务,就是在这么简陋的环境下,把系统扶上马,送一程。

核心要点:BootROM的初始化流程可以概括为四个步骤——上电复位、时钟初始化、堆栈指针设置、异常向量表建立。顺序不能乱,一步错,后面全崩。

BootROM 初始化四步走 ① 上电复位 PC指向复位向量 ② 时钟初始化 配置PLL/晶振 ③ 设置SP 建立临时堆栈 ④ 异常向量表 建立中断入口 整个流程在几十微秒内完成,为后续加载U-Boot做准备 典型BootROM大小:16KB ~ 128KB 执行时间:通常 < 100ms(从复位到跳转)

2.1 上电复位——一切从这里开始

按下电源键,或者看门狗超时触发复位,芯片的复位引脚会拉低再释放。这个瞬间,硬件会自动做几件事:

  • PC指针加载复位向量——从0x00000000(或芯片指定的复位地址)读取第一条指令
  • 大部分外设处于复位状态——GPIO默认输入、定时器停止、DMA不工作
  • 内部SRAM可能还未就绪——需要等待电源稳定

我个人习惯把复位分为两种:冷复位和热复位。冷复位是上电,所有寄存器回到默认值;热复位是软件触发的,有些寄存器会保留状态。我在调试一个IoT项目时,就遇到过热复位后某个外设没被正确重置,导致U-Boot加载失败。嗯,从那以后我写复位代码都会加一句:if (复位原因 == 热复位) 强制重置关键外设。

小技巧:很多芯片的复位状态寄存器(RST_SR)会记录复位原因。BootROM里读一下这个寄存器,可以判断是上电复位、看门狗复位还是软件复位。调试时特别有用。

2.2 时钟初始化——给系统一颗跳动的心脏

芯片刚上电时,用的通常是内部低速振荡器(比如32kHz或几MHz)。这个频率太慢了,跑不了复杂代码。所以BootROM要做的第一件大事,就是配置PLL,把主频提上去。

时钟初始化的典型步骤:

  1. 等待外部晶振稳定(如果是外部晶振方案)
  2. 配置PLL的倍频系数和分频系数
  3. 等待PLL锁定(Lock)
  4. 将系统主时钟切换到PLL输出
  5. 配置各总线时钟(AHB、APB等)的分频比

说白了,这一步就是给芯片“超频”——从几MHz跑到几百MHz。但要注意,时钟配错了系统直接死掉。我曾经在一个项目中,把PLL的倍频系数写成了0(非法值),结果芯片直接“罢工”,连调试器都连不上。最后用逻辑分析仪抓晶振波形才发现问题。

避坑指南:配置PLL时,一定要先读芯片手册的“PLL配置限制”章节。有些芯片要求倍频系数必须是偶数,有些要求VCO频率必须在某个范围内。别问我怎么知道的——我踩过这个坑,板子画了三版才改对。

下面是一个典型的PLL初始化代码片段(以ARM Cortex-A系列为例):

/* 伪代码:PLL初始化 */
void pll_init(void) {
    // 1. 等待外部晶振稳定
    while (!(OSC_STATUS & OSC_READY_MASK));
    
    // 2. 配置PLL:输入24MHz,输出800MHz
    // 倍频系数 = 200,分频系数 = 6
    PLL_CFG = (200 << 16) | (6 << 8) | PLL_ENABLE;
    
    // 3. 等待PLL锁定
    while (!(PLL_STATUS & PLL_LOCK_MASK));
    
    // 4. 切换主时钟
    CLK_SEL = CLK_SOURCE_PLL;
    
    // 5. 配置总线时钟
    AHB_CLK_DIV = 1;  // AHB = 800MHz
    APB_CLK_DIV = 4;  // APB = 200MHz
}

2.3 堆栈指针设置——给C语言一个家

时钟配好了,接下来要设置堆栈指针(SP)。你想想看,没有堆栈,C语言函数怎么调用?局部变量放哪?函数返回地址存哪?

BootROM通常用汇编语言写,但很快就要跳转到C代码。所以必须在跳转前,把SP指向一块可用的内存区域。这块内存可能是:

  • 芯片内部的SRAM——速度快,但容量小(通常几十KB到几百KB)
  • BootROM自带的临时RAM——有些芯片会在BootROM里划一小块RAM做临时堆栈

设置SP的汇编代码极其简单,但位置选不好会出大问题:

@ 伪代码:设置堆栈指针
@ 假设内部SRAM起始地址为0x20000000,大小为64KB
@ 堆栈从高地址向低地址生长,所以SP指向SRAM末尾

ldr sp, =0x20010000   @ SRAM基址 + 64KB = 栈顶

这里有个细节:堆栈大小要预留多少?我个人习惯留4KB给BootROM阶段用。为什么?因为BootROM的C代码通常很简单,就调用几个初始化函数,4KB绰绰有余。但如果你要在这里做复杂的解压缩或校验,那就要多留一些。

重要提醒:堆栈溢出是嵌入式开发中最隐蔽的bug之一。BootROM阶段没有MMU,没有内存保护,堆栈一旦溢出,直接覆盖后面的代码或数据。系统会以各种诡异的方式死掉——死机、跑飞、或者更糟,看起来正常但偶尔抽风。

2.4 异常向量表建立——给中断一个入口

最后一步,建立异常向量表。ARM架构下,异常向量表通常放在0x00000000地址(或者通过VBAR寄存器重定位)。BootROM需要把各个异常的处理函数地址填到对应的位置。

典型的ARM异常向量表布局:

地址偏移 异常类型 说明
0x00 复位 上电或复位后第一条指令
0x04 未定义指令 CPU遇到不认识的操作码
0x08 软件中断(SVC) 系统调用入口
0x0C 预取中止 取指令时发生错误
0x10 数据中止 读写数据时发生错误
0x14 保留 未使用
0x18 IRQ 普通中断请求
0x1C FIQ 快速中断请求

BootROM阶段,大部分异常其实不会发生。但为了安全,我们通常会给每个异常都填一个“死循环”或者“打印错误信息后复位”的处理函数。这样万一出了意外,系统不会完全失控。

我记得有一次调试,芯片上电后莫名其妙进入“未定义指令”异常。查了半天,发现是BootROM代码里有一条指令的机器码被编译器优化错了。还好异常向量表里填了打印函数,让我看到了错误信息,不然真不知道从哪查起。

@ 伪代码:建立异常向量表
.section .vectors, "ax"
.global _start

_start:
    ldr pc, reset_handler      @ 0x00: 复位
    ldr pc, undef_handler      @ 0x04: 未定义指令
    ldr pc, svc_handler        @ 0x08: 软件中断
    ldr pc, prefetch_handler   @ 0x0C: 预取中止
    ldr pc, data_handler       @ 0x10: 数据中止
    ldr pc, unused_handler     @ 0x14: 保留
    ldr pc, irq_handler        @ 0x18: IRQ
    ldr pc, fiq_handler        @ 0x1C: FIQ

reset_handler:
    @ 这里就是BootROM的入口,开始执行初始化
    bl pll_init
    bl sp_init
    bl main                    @ 跳转到C语言主函数

经验之谈:有些芯片支持VBAR(Vector Base Address Register),可以把异常向量表重定位到任意地址。BootROM阶段我建议把向量表放在BootROM内部,等U-Boot加载后再重定位到DDR里。这样即使后面要更新异常处理,也不会影响BootROM的稳定性。

小结

BootROM的初始化流程,说白了就是四步:复位→配时钟→设堆栈→建向量表。每一步都环环相扣,缺一不可。你想想看,没有时钟,CPU跑不动;没有堆栈,C代码没法跑;没有向量表,异常来了直接死机。

这四步做完,BootROM就可以开始干正事了——检测启动设备、加载U-Boot。下一章我们会详细聊启动设备检测的逻辑,包括怎么判断是从NAND Flash、SD卡还是UART启动。

嗯,今天就先到这里。如果你在实际项目中遇到BootROM初始化相关的问题,欢迎随时交流。


可可精品资料库