2. BootROM的初始化流程:上电复位、时钟初始化、堆栈指针设置、异常向量表建立
好,咱们正式开始聊BootROM。上一章我们说了BootROM是什么,这一章我们深入进去,看看它上电后到底干了哪些事。
芯片一上电,PC指针直接指向BootROM的起始地址。这时候,整个系统还处于“混沌”状态——时钟没配、内存没初始化、连堆栈都没有。BootROM的任务,就是在这么简陋的环境下,把系统扶上马,送一程。
核心要点:BootROM的初始化流程可以概括为四个步骤——上电复位、时钟初始化、堆栈指针设置、异常向量表建立。顺序不能乱,一步错,后面全崩。
2.1 上电复位——一切从这里开始
按下电源键,或者看门狗超时触发复位,芯片的复位引脚会拉低再释放。这个瞬间,硬件会自动做几件事:
- PC指针加载复位向量——从0x00000000(或芯片指定的复位地址)读取第一条指令
- 大部分外设处于复位状态——GPIO默认输入、定时器停止、DMA不工作
- 内部SRAM可能还未就绪——需要等待电源稳定
我个人习惯把复位分为两种:冷复位和热复位。冷复位是上电,所有寄存器回到默认值;热复位是软件触发的,有些寄存器会保留状态。我在调试一个IoT项目时,就遇到过热复位后某个外设没被正确重置,导致U-Boot加载失败。嗯,从那以后我写复位代码都会加一句:if (复位原因 == 热复位) 强制重置关键外设。
小技巧:很多芯片的复位状态寄存器(RST_SR)会记录复位原因。BootROM里读一下这个寄存器,可以判断是上电复位、看门狗复位还是软件复位。调试时特别有用。
2.2 时钟初始化——给系统一颗跳动的心脏
芯片刚上电时,用的通常是内部低速振荡器(比如32kHz或几MHz)。这个频率太慢了,跑不了复杂代码。所以BootROM要做的第一件大事,就是配置PLL,把主频提上去。
时钟初始化的典型步骤:
- 等待外部晶振稳定(如果是外部晶振方案)
- 配置PLL的倍频系数和分频系数
- 等待PLL锁定(Lock)
- 将系统主时钟切换到PLL输出
- 配置各总线时钟(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初始化相关的问题,欢迎随时交流。
可可精品资料库