BootROM 与一级引导:SoC 内部固化程序的作用
说实话,很多做嵌入式开发的朋友,天天跟 U-Boot、内核打交道,却很少真正关注过芯片上电后那「第一口奶」是谁喂的。我早年做一款工业控制板时,就因为在 BootROM 阶段踩了个坑,整批板子差点报废。从那以后,我对这一块就格外上心。
今天咱们就来聊聊,SoC 上电后,那个藏在芯片内部的 BootROM 到底干了些什么,以及它是怎么把 SPL 从 NAND、eMMC 或者 SD 卡里拽出来的。
BootROM 是什么?它凭什么「先跑」?
BootROM 是固化在 SoC 内部的一块只读存储器里的程序。芯片一上电,CPU 的复位向量直接指向它。你想想看,这时候 DDR 还没初始化,Flash 控制器可能都还没配好,能跑的只有这片「铁打的」ROM 代码。
核心要点: BootROM 是 SoC 启动流程的「第一棒」,它负责初始化最基本的硬件(比如时钟、引脚复用),然后根据启动介质选择策略,把下一级引导程序加载到内部 SRAM 中执行。
我见过不少工程师把 BootROM 和 BIOS 混为一谈。其实不一样——BIOS 存在 SPI Flash 里,可以更新;BootROM 是 Mask ROM,出厂就焊死了,改不了。嗯,这也是为什么 SoC 的启动流程一旦定下来,后面就很难大改。
启动介质的选择:谁来决定从哪启动?
SoC 怎么知道该从 NAND 启动,还是从 SD 卡启动?
通常有两种方式:
- 硬件引脚采样: 芯片复位时,BootROM 会去读几个特定 GPIO 的电平状态,组合成一个「启动序号」。我用的 i.MX6 系列就是这种方式,四个 BOOT_CFG 引脚,能组合出几十种启动方案。
- 内部 fuse 配置: 有些芯片会烧写 eFuse,BootROM 直接读 fuse 值决定启动顺序。这种方式更灵活,但烧了就不能改,适合量产定型的产品。
| 方式 | 优点 | 缺点 | 典型芯片 |
|---|---|---|---|
| 硬件引脚采样 | 调试方便,可随时换介质 | 占用 GPIO,布线麻烦 | i.MX6、AM335x |
| eFuse 配置 | 量产稳定,防篡改 | 烧写后不可逆 | Zynq、STM32MP1 |
我个人习惯在开发阶段用引脚采样,量产时再切到 eFuse。这样既能灵活调试,又能保证最终产品的安全性。
从 NAND Flash 加载 SPL:最折腾的一种
NAND Flash 有个毛病——它天生带坏块,而且读取需要 ECC 校验。BootROM 要从中把 SPL 完整无误地读出来,其实挺费劲的。
典型的流程是这样的:
- BootROM 初始化 NAND 控制器,设置时序参数。
- 从固定的块地址(通常是第 0 块)开始读取。
- 每读一页,做硬件 ECC 校验。如果校验失败,标记坏块,跳到下一块继续读。
- 读到完整的 SPL 镜像后,校验其头部信息(比如 Magic Number、CRC)。
- 校验通过,把 SPL 拷贝到内部 SRAM,跳转执行。
我曾经踩过的坑: 有一款 NAND Flash 的坏块管理策略跟 BootROM 预期的不一致。BootROM 默认跳过坏块后直接读下一块,但我的 SPL 镜像跨块存放时没处理好偏移量,导致读出来的数据是乱的。后来我不得不在烧录工具里手动调整 SPL 的存放位置,避开前几个坏块区域。
从 eMMC 加载 SPL:相对省心
eMMC 内部自带 FTL(Flash Translation Layer)和坏块管理,BootROM 不需要操心这些。它只需要通过标准的 MMC 协议,从指定的分区或偏移地址读取数据就行。
这里有个关键点——eMMC 的 Boot Partition。eMMC 规范里定义了两个专用的 Boot 分区,容量不大(通常是 4MB 或 8MB),但访问速度很快。BootROM 会优先尝试从 Boot Partition 1 读取 SPL。
小技巧: 如果你用 eMMC 做启动介质,建议把 SPL 放在 Boot Partition 里,而不是用户数据分区。这样既能避免被误擦除,又能获得更快的读取速度。我在项目里就是这么干的,启动时间缩短了将近 30 毫秒。
从 SD 卡加载 SPL:最灵活,也最啰嗦
SD 卡跟 eMMC 协议类似,但有个区别——SD 卡没有专门的 Boot Partition。BootROM 只能从卡的用户数据区,在固定的偏移位置(通常是 LBA 0 或 LBA 1)读取 SPL。
为什么说它啰嗦?因为 SD 卡的初始化流程比 eMMC 复杂:
- 先要发送 CMD0 让卡进入空闲状态。
- 然后发 CMD8 检测卡的类型(SD 1.x 还是 2.0)。
- 再发 ACMD41 协商电压范围。
- 最后发 CMD2、CMD3 获取 CID 和 RCA。
这一套握手下来,光协议交互就得几十毫秒。不过好处是——调试太方便了。我开发阶段几乎全用 SD 卡,插拔换镜像,比烧写 NAND 快多了。
SPL 的加载与校验:BootROM 的最后一步
不管从哪种介质读数据,BootROM 最后都会做同一件事:把 SPL 放到内部 SRAM 里,然后跳过去执行。
这里有个容量限制——内部 SRAM 通常很小。以我常用的 AM335x 为例,内部 SRAM 只有 64KB,而 SPL 镜像一般控制在 32KB 以内。为什么?因为 BootROM 自己也要占一部分 SRAM 做栈和临时变量。
// 典型的 SPL 头部结构(以 U-Boot SPL 为例)
struct spl_header {
uint32_t magic; // 魔数,用于校验
uint32_t checksum; // 校验和
uint32_t offset; // SPL 在镜像中的偏移
uint32_t length; // SPL 长度
uint32_t load_addr; // 加载到 SRAM 的地址
uint32_t exec_addr; // 执行入口地址
uint8_t reserved[8]; // 保留
};
BootROM 读到这个头部后,会做三件事:
- 校验 Magic Number,确认这是合法的 SPL。
- 计算校验和,确保数据没损坏。
- 把 SPL 主体从介质读到 load_addr 指定的 SRAM 位置。
- 跳转到 exec_addr,把控制权交给 SPL。
到这一步,BootROM 的任务就完成了。它就像个接力赛的第一棒选手,把棒子稳稳交到 SPL 手里,然后功成身退。
整体流程可视化
下面这张图把整个 BootROM 到 SPL 的加载过程串起来了。你可以对照着看,每一步的输入输出是什么,决策点在哪里。
你看,从芯片上电到 SPL 跑起来,中间经历了硬件初始化、介质识别、协议交互、数据校验等多个环节。每一步都可能出问题,这也是为什么我总跟团队说——启动流程不是「能跑就行」,而是要理解每个环节的边界条件和容错机制。
好了,BootROM 和一级引导这部分就聊到这儿。下一棒交给 SPL,它要干的事情更复杂——初始化 DDR、搭建临时运行环境,为加载 U-Boot 主程序做准备。不过那是后面章节的内容了,咱们先把今天这张图刻在脑子里就行。