2. 嵌入式系统启动流程:从复位向量到BootROM、SPL、U-Boot、内核启动的完整链路

嵌入式系统上电那一刻,芯片内部到底发生了什么?

很多人以为就是「BootROM 加载 U-Boot,U-Boot 加载内核」这么简单。但实际项目中,我见过太多人在这个环节翻车——要么 SPL 起不来,要么 U-Boot 找不到内核,要么启动时间死活压不下去。

这一章,咱们把整个链路掰开揉碎,从复位向量一直走到内核启动。你想想看,搞懂了这条路径,以后调启动、做 OTA、优化开机速度,心里就有底了。

核心认知:嵌入式启动是一个「信任链」——每一级加载器都要验证并加载下一级。BootROM 信任 SPL,SPL 信任 U-Boot,U-Boot 信任内核。任何一个环节断裂,系统就起不来。

2.1 复位向量:一切从这里开始

芯片上电或复位后,CPU 会从预定义的地址取第一条指令。这个地址就是复位向量。

架构 复位向量地址 说明
ARM Cortex-A 0x00000000 或 0xFFFF0000 由硬件配置引脚或 SoC 设计决定
ARM Cortex-M 0x00000000(向量表基址) 前 4 字节存栈顶,后 4 字节存复位函数地址
RISC-V 0x80000000 或 0x00000000 由 mtvec 寄存器决定,常见于 0x80000000
x86 0xFFFFFFF0 传统 BIOS 启动,现在 UEFI 已不同

我调试过一个 i.MX6 的项目,复位后死活不进 BootROM。查了半天,发现是硬件设计把 BOOT_MODE 引脚拉错了,导致芯片直接跳到了无效地址。嗯,这种坑,原理图 review 阶段就该堵住。

2.2 BootROM:芯片出厂自带的「最小引导器」

BootROM 是固化在芯片内部 ROM 里的一段代码。芯片厂商写死了,你改不了。

它的任务很简单:

  • 初始化最基本的硬件(时钟、SRAM、启动介质控制器)
  • 根据启动引脚或 OTP 配置,选择启动介质(NAND、eMMC、SD 卡、SPI NOR、UART 等)
  • 从启动介质读取下一级加载器(通常是 SPL 或 U-Boot)到内部 SRAM
  • 跳转到加载器入口

个人经验:BootROM 阶段最容易出问题的是启动介质时序不匹配。比如你用的 eMMC 是 HS200 模式,但 BootROM 只支持 HS50,那就会读不到数据。我遇到过一块板子,换了个批次 eMMC 就起不来了,最后发现是 BootROM 的初始化时序太紧,新批次芯片响应慢了 2ns。

2.3 SPL:Secondary Program Loader

为什么需要 SPL?因为 BootROM 通常只能访问内部 SRAM,空间很小(几十 KB 到几百 KB)。而 U-Boot 完整版动辄几百 KB,放不下。

SPL 就是 U-Boot 的「瘦身版」——它只做三件事:

  1. 初始化 DRAM——这是最关键的一步,因为只有 DRAM 初始化好了,才能把完整的 U-Boot 加载进去
  2. 从启动介质读取 U-Boot——支持 NAND、eMMC、SD 卡、SPI Flash 等
  3. 跳转到 U-Boot——设置好栈指针和参数,然后跳转

我习惯把 SPL 叫做「撬锁工具」——它撬开 DRAM 这把锁,让后续的大块头能进来。

避坑指南:我曾经在一个项目里,SPL 初始化 DRAM 后,U-Boot 加载到内存里却跑不起来。查了三天,发现是 SPL 里 DDR 的时序参数配错了——CAS 延迟设小了 1 个 cycle。这种问题,用示波器量 DQS 信号就能看出来,但很多人不会往那想。

2.4 U-Boot:完整的引导加载器

U-Boot 进入 DRAM 后,才算真正「活过来」。它做的事情比 SPL 多得多:

  • 完整硬件初始化——网卡、USB、存储、显示等外设
  • 环境变量加载——从存储介质读取 bootargs、bootcmd 等配置
  • 网络协议栈——支持 TFTP、NFS 等网络启动方式
  • 文件系统支持——ext4、FAT、UBIFS 等
  • 设备树处理——修改或覆盖设备树节点
  • 启动内核——最终调用 bootm 或 booti 命令

说白了,U-Boot 就是一个「微型操作系统」。它有自己的命令行、文件系统、网络协议栈,甚至还能跑简单的脚本。

2.5 内核启动:最后一棒

U-Boot 启动内核时,会做这几步:

  1. 准备启动参数——把 bootargs 字符串放到指定内存位置(通常是 ATAG 或设备树方式)
  2. 加载内核镜像——从存储介质读到内存,如果是压缩镜像则先解压
  3. 加载设备树——把 .dtb 文件读到内存,告诉内核硬件布局
  4. 跳转到内核入口——设置 CPU 为 SVC 模式,关 MMU 和 cache,然后跳转

内核收到控制权后,会做自己的初始化:

  • 解压自身(如果是 zImage)
  • 初始化内存管理
  • 初始化中断控制器
  • 初始化调度器
  • 挂载根文件系统
  • 启动 init 进程

关键点:U-Boot 和内核之间通过设备树传递硬件信息。设备树里写错了某个外设的寄存器地址,内核就会挂掉。我调试过一个网卡不工作的问题,最后发现是设备树里中断号写错了——U-Boot 能 ping 通,但内核不行,因为 U-Boot 用的是轮询模式,内核用的是中断模式。

2.6 完整启动流程图

下面这张图把整个链路串起来了。我建议你把它存下来,以后调启动问题时对着看。

嵌入式系统完整启动链路 1. 复位向量 2. BootROM 初始化时钟/SRAM → 读取启动介质 → 加载SPL到SRAM 3. SPL (Secondary Program Loader) 初始化DRAM → 从存储介质读取U-Boot → 跳转到U-Boot 4. U-Boot (完整引导加载器) 完整硬件初始化 → 加载环境变量 → 网络/文件系统支持 加载内核镜像 + 设备树 → 准备启动参数 → 跳转到内核 5. 内核启动 解压内核 → 内存管理初始化 → 中断/调度器初始化 挂载根文件系统 → 启动init进程 → 用户空间就绪 系统启动完成 ✅ 信任链传递 BootROM → SPL SPL → U-Boot U-Boot → 内核 常见瓶颈 DRAM初始化失败 存储介质时序不匹配 设备树配置错误

2.7 各阶段时间分布

做 OTA 升级时,启动时间是个硬指标。我整理了一份典型的时间分布,供你参考:

阶段 典型耗时 优化空间 说明
BootROM 10~50 ms 小 固化在芯片里,基本动不了
SPL 20~100 ms 中 DRAM 初始化是主要耗时点,可以优化时序参数
U-Boot 100~500 ms 大 去掉不必要的驱动、缩短环境变量加载时间
内核启动 500 ms ~ 3 s 大 裁剪内核、使用 initramfs、优化驱动加载顺序
用户空间 1~5 s 中 优化 init 脚本、并行启动服务

我的建议:做 OTA 升级方案时,一定要考虑回滚场景的启动时间。如果升级后的系统启动变慢了,用户可能以为设备死机了。我曾经在一个车机项目里,升级后 U-Boot 阶段多了 200ms,客户投诉说「开机黑屏时间变长了」——最后发现是环境变量里多了一个无效的 bootargs 参数,U-Boot 解析它花了额外时间。

2.8 调试启动问题的常用手段

启动出问题了,怎么定位?我常用的方法:

  • 串口打印——最基础也最有效。BootROM 通常有固定的打印信息,SPL 和 U-Boot 可以加自己的 debug 打印
  • GPIO 示波器——在关键节点拉高/拉低 GPIO,用示波器看时间点。我习惯在 SPL 入口、DRAM 初始化完成、U-Boot 入口各放一个 GPIO 翻转
  • JTAG 调试——能单步执行,但需要硬件支持。适合查 SPL 阶段的死锁或异常
  • 分析启动日志——U-Boot 的 bootstage 功能可以记录各阶段耗时,用 bootstage report 命令查看

避坑指南:我曾经遇到一个情况——SPL 加载 U-Boot 时,明明从 eMMC 读出来了,但校验和不对。查了两天,发现是 eMMC 的读取命令参数错了,导致读出来的数据偏移了 512 字节。这种问题,用 JTAG 读内存对比原始数据,一眼就能看出来。

好了,这一章的内容就到这里。启动链路搞清楚了,后面讲 OTA 升级的「原子性」和「回滚机制」时,你就能理解为什么每一级加载器都要做校验——因为任何一个环节的失败,都可能导致设备变砖。