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 的「瘦身版」——它只做三件事:
- 初始化 DRAM——这是最关键的一步,因为只有 DRAM 初始化好了,才能把完整的 U-Boot 加载进去
- 从启动介质读取 U-Boot——支持 NAND、eMMC、SD 卡、SPI Flash 等
- 跳转到 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 启动内核时,会做这几步:
- 准备启动参数——把 bootargs 字符串放到指定内存位置(通常是 ATAG 或设备树方式)
- 加载内核镜像——从存储介质读到内存,如果是压缩镜像则先解压
- 加载设备树——把 .dtb 文件读到内存,告诉内核硬件布局
- 跳转到内核入口——设置 CPU 为 SVC 模式,关 MMU 和 cache,然后跳转
内核收到控制权后,会做自己的初始化:
- 解压自身(如果是 zImage)
- 初始化内存管理
- 初始化中断控制器
- 初始化调度器
- 挂载根文件系统
- 启动 init 进程
关键点:U-Boot 和内核之间通过设备树传递硬件信息。设备树里写错了某个外设的寄存器地址,内核就会挂掉。我调试过一个网卡不工作的问题,最后发现是设备树里中断号写错了——U-Boot 能 ping 通,但内核不行,因为 U-Boot 用的是轮询模式,内核用的是中断模式。
2.6 完整启动流程图
下面这张图把整个链路串起来了。我建议你把它存下来,以后调启动问题时对着看。
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 升级的「原子性」和「回滚机制」时,你就能理解为什么每一级加载器都要做校验——因为任何一个环节的失败,都可能导致设备变砖。