1、启动链概述
各位同学,咱们今天聊聊嵌入式系统里一个特别基础、但又特别容易被忽视的话题——启动链。说白了,就是芯片上电之后,到底是怎么一步步把操作系统跑起来的。
我刚开始做嵌入式那会儿,总觉得这玩意儿没啥好学的。不就是上电、跑代码嘛,能有多复杂?直到有一次,我调试一块新板子,焊好之后死活起不来。查了三天,最后发现是启动配置引脚拉错了,芯片压根没从正确的介质加载代码。嗯,从那以后,我再也不敢小看启动链了。
什么是启动链
启动链,英文叫 Boot Chain,也叫启动流程。它描述的是:从芯片复位到操作系统内核运行,中间经历的所有软件执行阶段。
你想想看,芯片刚上电的时候,内存还没初始化,外设还没配置,文件系统更是无从谈起。这时候你让它直接跑 Linux 内核,它连代码放哪儿都不知道。所以必须有一个分阶段、逐步建立运行环境的过程。这个过程,就是启动链。
核心观点:启动链的本质,是一个「从无到有」的环境构建过程。每一阶段都为下一阶段准备好必要的硬件和软件条件。
为什么需要启动链
这个问题我问过不少新人。有人回答「因为芯片需要初始化」,也有人回答「因为要加载操作系统」。都对,但不够本质。
我个人习惯从三个角度来理解这个问题:
- 硬件限制:芯片上电时,内部 SRAM 极小(通常几十 KB),根本放不下一个完整的操作系统。必须先用一小段代码初始化外部 DRAM,才能把大块代码搬进去。
- 存储介质差异:代码可能放在 NAND Flash、eMMC、SD 卡、SPI NOR Flash 甚至网络服务器上。每种介质的读取方式都不一样,需要专门的驱动来加载。
- 安全与可靠性:现代系统需要校验镜像签名、检测硬件完整性、支持回滚机制。这些逻辑不能和操作系统混在一起,必须独立出来。
我曾经在一个工业项目里遇到过:产品出货后,有 5% 的机器在低温环境下启动失败。查到最后,是 Bootloader 里 DRAM 初始化时序留的余量不够。温度一低,时序漂移,内存训练失败。你看,启动链里的每一行代码,都直接决定了产品的可靠性。
启动链的典型阶段
大多数嵌入式系统的启动链,可以抽象为三个阶段。我画了一张图,方便你理解整体流程:
下面我逐个阶段展开说说。
阶段一:ROM Code
ROM Code 是芯片出厂时固化在内部 ROM 里的一段小程序。它不可修改,是芯片上电后执行的第一段代码。
它的职责很明确:
- 配置最基本的时钟和电源
- 初始化内部 SRAM(通常只有几十 KB)
- 根据启动引脚(Boot Mode Pins)选择从哪个介质加载下一阶段代码
- 从选定介质读取 Bootloader 到内部 SRAM
- 跳转到 Bootloader 入口
一个小技巧:调试新板子时,我习惯先确认 ROM Code 阶段是否正常。方法很简单——用示波器量一下启动引脚的电平,再检查对应介质的时钟信号有没有起来。如果 ROM Code 没跑通,后面全是白费功夫。
阶段二:Bootloader
Bootloader 是启动链里最灵活、也最复杂的阶段。它通常分为两个子阶段:
| 子阶段 | 运行位置 | 主要任务 | 典型大小 |
|---|---|---|---|
| SPL / MLO | 内部 SRAM | 初始化 DRAM 控制器,加载完整 Bootloader | 16~64 KB |
| U-Boot / UEFI | 外部 DRAM | 初始化外设、加载内核、提供交互式命令行 | 200 KB ~ 2 MB |
为什么需要两个子阶段?说白了,还是空间问题。内部 SRAM 太小,放不下功能完整的 Bootloader。所以先跑一个极简的 SPL(Secondary Program Loader),把 DRAM 初始化好,再把完整的 U-Boot 从存储介质搬到 DRAM 里运行。
我曾经在一个项目里踩过坑:SPL 里初始化 DRAM 时,时序参数是从芯片参考设计里直接抄的。结果板子布线长了 2 厘米,信号延迟变了,DRAM 训练失败。后来我花了整整两天,一根一根量信号线,才把时序调对。所以我的建议是:DRAM 初始化参数一定要根据实际板子做校准,别偷懒。
阶段三:OS Kernel
Bootloader 的最后一步,是把操作系统内核镜像加载到 DRAM 中,然后跳转过去。内核开始运行后,启动链的使命就完成了。
内核接管后要做的事情包括:
- 初始化内存管理单元(MMU)
- 建立中断向量表
- 初始化调度器
- 挂载根文件系统
- 启动第一个用户态进程(通常是 init)
注意:Bootloader 和内核之间有一个「约定」——设备树(Device Tree)或 ACPI 表。Bootloader 必须把硬件信息以标准格式传递给内核。如果这个信息传错了,内核可能认不出外设,甚至直接 panic。我见过有人手动修改设备树,结果漏了一个 GPIO 控制器,导致触摸屏驱动加载失败。排查了整整一周。
启动链的典型配置示例
拿我常用的 i.MX6ULL 平台举个例子。它的启动配置是这样的:
// 启动模式引脚配置(BOOT_MODE[1:0])
// 00 - 从 Fuses 读取
// 01 - 串行下载(用于烧录)
// 10 - 内部启动(从存储介质加载)
// 11 - 保留
// 内部启动时,ROM Code 按以下顺序尝试加载:
// 1. eMMC 用户分区
// 2. SD 卡
// 3. NAND Flash
// 4. SPI NOR Flash
// 5. 串行下载(回退)
// 每个介质上,ROM Code 会查找特定的镜像格式:
// - eMMC/SD: 偏移 1KB 处查找 4KB 的 SPL 镜像
// - NAND: 查找页对齐的 SPL 镜像
// - SPI NOR: 偏移 0 处查找 SPL 镜像
你看,光是 ROM Code 阶段,就有这么多细节。每个芯片厂商的实现都不一样,但核心逻辑是相通的。
小结
启动链不是什么高深莫测的东西。它就是一套分阶段构建运行环境的机制。ROM Code 负责「从零到一」,Bootloader 负责「从一到百」,内核负责「从百到万」。每一阶段都只做自己该做的事,然后把接力棒交给下一棒。
理解了这个思路,你再看任何芯片的启动流程,都不会觉得陌生。无非是 ROM Code 里多了个安全校验,或者 Bootloader 里多了个网络加载功能。万变不离其宗。