1、启动链概述

各位同学,咱们今天聊聊嵌入式系统里一个特别基础、但又特别容易被忽视的话题——启动链。说白了,就是芯片上电之后,到底是怎么一步步把操作系统跑起来的。

我刚开始做嵌入式那会儿,总觉得这玩意儿没啥好学的。不就是上电、跑代码嘛,能有多复杂?直到有一次,我调试一块新板子,焊好之后死活起不来。查了三天,最后发现是启动配置引脚拉错了,芯片压根没从正确的介质加载代码。嗯,从那以后,我再也不敢小看启动链了。

什么是启动链

启动链,英文叫 Boot Chain,也叫启动流程。它描述的是:从芯片复位到操作系统内核运行,中间经历的所有软件执行阶段。

你想想看,芯片刚上电的时候,内存还没初始化,外设还没配置,文件系统更是无从谈起。这时候你让它直接跑 Linux 内核,它连代码放哪儿都不知道。所以必须有一个分阶段、逐步建立运行环境的过程。这个过程,就是启动链。

核心观点:启动链的本质,是一个「从无到有」的环境构建过程。每一阶段都为下一阶段准备好必要的硬件和软件条件。

为什么需要启动链

这个问题我问过不少新人。有人回答「因为芯片需要初始化」,也有人回答「因为要加载操作系统」。都对,但不够本质。

我个人习惯从三个角度来理解这个问题:

  • 硬件限制:芯片上电时,内部 SRAM 极小(通常几十 KB),根本放不下一个完整的操作系统。必须先用一小段代码初始化外部 DRAM,才能把大块代码搬进去。
  • 存储介质差异:代码可能放在 NAND Flash、eMMC、SD 卡、SPI NOR Flash 甚至网络服务器上。每种介质的读取方式都不一样,需要专门的驱动来加载。
  • 安全与可靠性:现代系统需要校验镜像签名、检测硬件完整性、支持回滚机制。这些逻辑不能和操作系统混在一起,必须独立出来。

我曾经在一个工业项目里遇到过:产品出货后,有 5% 的机器在低温环境下启动失败。查到最后,是 Bootloader 里 DRAM 初始化时序留的余量不够。温度一低,时序漂移,内存训练失败。你看,启动链里的每一行代码,都直接决定了产品的可靠性。

启动链的典型阶段

大多数嵌入式系统的启动链,可以抽象为三个阶段。我画了一张图,方便你理解整体流程:

嵌入式系统启动链典型阶段 嵌入式系统启动链典型阶段 阶段一 ROM Code (固化在芯片内部) 阶段二 Bootloader (SPL / U-Boot) 阶段三 OS Kernel (Linux / RTOS) 每个阶段都承担特定职责:ROM Code 初始化最小硬件, Bootloader 加载并校验内核,OS Kernel 接管系统管理。 图:嵌入式系统启动链三阶段流程

下面我逐个阶段展开说说。

阶段一: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 里多了个网络加载功能。万变不离其宗。