3、ROM Code(BootROM)详解:BootROM的位置与大小、基本硬件初始化、启动设备检测与加载
好,咱们今天聊聊 BootROM。这东西在嵌入式系统里,说白了就是芯片上电后跑的第一段代码。我刚开始做嵌入式那会儿,总觉得 BootROM 是个黑盒子,后来踩了几个坑才真正搞明白它到底干了什么。
3.1 BootROM 的位置与大小
BootROM 是固化在芯片内部的一块只读存储器。你想想看,芯片一上电,CPU 核心复位后第一件事就是去取指令。这时候外部存储器(比如 Flash、SD 卡)还没初始化呢,CPU 根本访问不了。所以必须有一块“与生俱来”的代码在芯片内部等着它。
这块 ROM 通常放在芯片的地址空间最顶端或最底端。以 ARM Cortex-M 系列为例,复位后 CPU 从 0x00000000 取向量表,而 0x00000000 就映射到了内部的 BootROM 区域。对于 Cortex-A 系列,比如我常用的 i.MX6,它从 0x00200000 开始执行 BootROM 代码。
| 芯片系列 | BootROM 起始地址 | 典型大小 |
|---|---|---|
| ARM Cortex-M (STM32) | 0x00000000 (映射) | 32KB - 128KB |
| ARM Cortex-A (i.MX6) | 0x00200000 | 96KB - 256KB |
| RISC-V (GD32VF103) | 0x00000000 | 16KB - 64KB |
| TI OMAP | 0x40000000 | 64KB - 192KB |
BootROM 的大小通常只有几十到几百 KB。为什么这么小?因为芯片内部 ROM 面积很贵,而且 BootROM 的任务其实很明确——它不需要跑操作系统,只需要把下一级启动代码(比如 SPL 或 U-Boot)加载进来就行。
3.2 基本硬件初始化
BootROM 的第一件事,就是初始化最基本的硬件。这时候系统还处于“裸奔”状态,所有外设都没配置。BootROM 需要让 CPU 能跑起来,让内存能用上。
3.2.1 时钟与 PLL 初始化
芯片上电后,默认时钟通常来自内部低速振荡器(比如 32KHz 或 8MHz)。这个速度跑 BootROM 还行,但要初始化外部存储器就太慢了。所以 BootROM 会先配置 PLL(锁相环),把 CPU 主频提上去。
我举个例子,以 STM32F4 的 BootROM 流程为例:
// 伪代码:BootROM 时钟初始化流程
void BootROM_ClockInit(void) {
// 1. 等待 HSI 振荡器稳定
while(!(RCC->CR & RCC_CR_HSIRDY));
// 2. 配置 PLL:输入 8MHz,倍频到 168MHz
RCC->PLLCFGR = (8 << 0) // PLLM = 8
| (336 << 6) // PLLN = 336
| (0 << 16) // PLLP = 2 (168MHz)
| (4 << 24); // PLLQ = 4
// 3. 使能 PLL 并等待锁定
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 4. 切换系统时钟到 PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
这里有个坑要注意:PLL 的配置参数必须严格遵循芯片手册。倍频系数、分频系数都有范围限制。我曾经在项目里把 PLLN 设成了 400,结果芯片直接锁死,连调试器都连不上。后来只能通过拉复位引脚才救回来。
3.2.2 SDRAM 控制器初始化
时钟搞定后,下一步就是初始化 SDRAM 控制器。为什么这么急?因为 BootROM 要把下一级启动代码从外部存储设备搬到内存里运行。没有内存,代码就没地方放。
SDRAM 控制器的初始化是个精细活。时序参数必须和具体 SDRAM 芯片匹配。我整理了一份典型的初始化步骤:
- 配置 SDRAM 控制器时钟——确保内存时钟频率正确
- 设置时序参数——CAS 延迟、突发长度、刷新周期等
- 配置地址映射——行列地址线分配、Bank 选择
- 发送初始化命令——预充电、自动刷新、模式寄存器设置
- 启动自动刷新——SDRAM 需要定期刷新,否则数据会丢
以 i.MX6 的 BootROM 为例,它初始化 DDR3 的代码大概长这样:
// 伪代码:i.MX6 BootROM DDR3 初始化
void BootROM_DDR3_Init(void) {
// 1. 使能 DDR 时钟
CCM->CCGRx |= (3 << 6); // 使能 MMDC 时钟
// 2. 配置 DDR 控制器参数
MMDC->MDCFG0 = (5 << 0) // CAS 延迟 = 5
| (3 << 8) // 突发长度 = 8
| (7 << 16); // 刷新周期
// 3. 配置 DDR 时序
MMDC->MDREF = 0x00000000; // 关闭刷新
MMDC->MDCTL = 0x00000000; // 进入配置模式
// 4. 发送初始化序列
MMDC->MDSCR = 0x00000008; // NOP 命令
delay(200); // 等待 200 个时钟周期
MMDC->MDSCR = 0x00000004; // 预充电所有 Bank
delay(100);
MMDC->MDSCR = 0x0000000A; // 自动刷新 (2 次)
delay(100);
// 5. 设置模式寄存器
MMDC->MDSCR = 0x0000001B; // MR0: CAS=5, BL=8
delay(50);
// 6. 启动自动刷新
MMDC->MDREF = 0x0000003C; // 刷新周期 = 60
MMDC->MDCTL = 0x00000001; // 进入正常模式
}
3.3 启动设备检测与加载
内存准备好了,BootROM 就要去找启动代码了。但问题来了——芯片怎么知道从哪里启动?是从 SPI Flash 启动,还是从 SD 卡,还是从 NAND Flash?
答案是:BootROM 会按照预设的优先级顺序,逐个尝试每个启动设备。这个优先级通常由芯片的 BOOT 引脚电平决定,或者由 OTP(一次性可编程)位配置。
3.3.1 启动设备检测流程
我画了一张流程图,帮你理清 BootROM 的检测逻辑:
这个流程看起来简单,但实际实现时有很多细节。每个启动设备都有自己的一套通信协议。比如 SPI Flash 需要配置 SPI 控制器的时钟极性和相位;SD 卡需要发送 CMD0、CMD8、ACMD41 等一系列命令来完成初始化。
3.3.2 启动头格式与校验
BootROM 怎么判断一个设备里有没有有效的启动代码?答案是:检查启动头(Boot Header)。启动头通常包含以下信息:
| 字段 | 偏移 | 大小 | 说明 |
|---|---|---|---|
| 魔数 (Magic Number) | 0x00 | 4 字节 | 固定值,如 0xEA0003FE |
| 加载地址 | 0x04 | 4 字节 | 代码要加载到 SDRAM 的哪个地址 |
| 代码长度 | 0x08 | 4 字节 | 启动代码的总字节数 |
| 校验和 | 0x0C | 4 字节 | CRC32 或简单累加和 |
| 入口地址 | 0x10 | 4 字节 | 跳转执行的起始地址 |
| 保留/配置 | 0x14 | 12 字节 | 芯片特定配置参数 |
BootROM 会先读取启动头的前几个字节,检查魔数是否正确。如果魔数匹配,再计算校验和。都通过了,才把后续的代码加载到 SDRAM 中。
3.3.3 加载与跳转
校验通过后,BootROM 会把启动代码从外部设备读到 SDRAM 中。这个过程通常使用 DMA 来加速,因为 BootROM 本身很小,用 CPU 逐字节拷贝太慢了。
加载完成后,BootROM 会做最后几件事:
- 关闭中断(防止加载过程中残留的中断请求干扰)
- 设置栈指针(SP)为 SDRAM 中的临时栈
- 跳转到入口地址执行
跳转的那一刻,BootROM 的使命就完成了。从此 CPU 开始执行加载到 SDRAM 中的下一级启动代码(通常是 SPL 或 U-Boot)。
3.4 小结
BootROM 虽然代码量不大,但它是整个启动链的基石。它负责初始化最基本的硬件(时钟、PLL、SDRAM 控制器),然后按照预设的优先级检测启动设备,找到有效的启动头后把代码加载到内存并跳转执行。
理解 BootROM 的工作原理,对后续调试启动问题非常有帮助。比如板子起不来,你可以先判断是 BootROM 阶段就卡住了,还是加载后的代码出了问题。用示波器量一下 SDRAM 的时钟引脚,或者用逻辑分析仪抓一下 SPI Flash 的通信波形,很快就能定位问题。