3、启动流程分析:典型嵌入式系统的启动流程

各位好,我是老张。今天咱们聊聊嵌入式系统启动这件事。说实话,很多刚入行的朋友觉得启动就是「上电 -> 跑系统」,但实际拆开来看,这里面的门道可不少。

我最早接触这块时,也被这四段式启动搞得有点懵。后来在项目里踩过几次坑,才真正理解每一级存在的意义。嗯,咱们今天就把这个流程掰开揉碎了讲。

3.1 整体启动链路概览

典型的嵌入式系统,从按下电源到进入操作系统,大致走这么四步:

  1. ROM Code —— 芯片出厂固化的第一段代码
  2. SPL (Secondary Program Loader) —— 二级加载器,也叫 MLO 或 x-loader
  3. U-Boot —— 通用引导加载程序,完整版
  4. Kernel —— Linux 内核,最终的操作系统

你想想看,为什么不能一步到位直接跑内核?说白了,硬件刚上电时,DDR 还没初始化,存储控制器还没配好,你拿什么跑内核?所以必须分级加载,逐级「铺路」。

核心思想:每一级 Bootloader 都只做「刚好够用」的事,把更复杂的工作交给下一级。这叫「渐进式初始化」。

3.2 ROM Code 阶段

ROM Code 是芯片出厂时固化在内部 ROM 里的代码。用户改不了,也删不掉。它的任务非常单一:

  • 初始化最基本的硬件:CPU 时钟、内部 SRAM、看门狗
  • 从预定义的启动介质读取下一级代码(SPL)
  • 验证签名(如果开启了安全启动)

我记得有一次做 i.MX6 项目,板子死活起不来。查了半天,发现是 BOOT_CFG 引脚电平没拉对,ROM Code 读错了启动介质。嗯,这种问题排查起来特别费劲,因为 ROM Code 本身没有调试输出。

个人经验:设计板子时,一定要把 BOOT 模式选择引脚留出测试点。我习惯在原理图阶段就标注清楚,方便后续调试。

ROM Code 通常只有几十 KB,运行在内部 SRAM 里。它不初始化 DDR,因为 DDR 初始化需要大量配置参数,ROM Code 放不下这么多逻辑。

3.3 SPL 阶段

SPL 是 ROM Code 加载进来的第二段代码。它的大小通常限制在几十 KB 到 100 KB 左右,因为内部 SRAM 就那么大。

SPL 的职责:

  • 初始化 DDR 控制器
  • 配置时钟树,提升 CPU 频率
  • 从存储介质(eMMC、NAND、SD 卡)加载完整的 U-Boot
  • 可选的:校验 U-Boot 的完整性

这里有个坑,我当年栽过。SPL 里初始化 DDR 时,时序参数配错了。结果 U-Boot 加载到 DDR 后,跑几步就崩。查了两天才发现是 DDR 的刷新周期没算对。所以,DDR 初始化参数一定要对照芯片手册和内存颗粒的 datasheet 反复核对

注意:SPL 通常不支持复杂的文件系统。它用原始块地址读取 U-Boot 镜像。所以 U-Boot 在存储介质上的位置是固定的,不能随便挪。

3.4 U-Boot 阶段

U-Boot 是完整的引导加载程序。它运行在 DDR 里,有完整的驱动框架、命令行界面、网络协议栈。

U-Boot 的主要工作:

  • 初始化更多外设:网卡、USB、显示控制器
  • 加载设备树(DTB)
  • 从文件系统(FAT、ext4)读取内核镜像
  • 设置内核启动参数(bootargs)
  • 跳转到内核入口

我个人习惯在 U-Boot 里加一个「启动计数」功能。每次成功进入内核就递增一个计数器,如果连续几次启动失败,就回滚到上一个版本。这个机制救过我不少次。

关键点:U-Boot 是用户可定制的。你可以添加自己的命令、修改启动逻辑、甚至做 A/B 分区升级。它是整个启动链中最灵活的一环。

3.5 Kernel 阶段

内核是最终的目标。U-Boot 把控制权交给内核后,Bootloader 的任务就结束了。

内核启动过程:

  1. 解压自身(如果是压缩镜像)
  2. 初始化内存管理、进程调度
  3. 挂载根文件系统
  4. 启动 init 进程(通常是 systemd 或 busybox)

这里有个细节:U-Boot 跳转到内核时,需要把设备树地址放在指定寄存器里(ARM 下是 r2)。我见过有人忘了传设备树,内核直接 panic。嗯,这种低级错误排查起来特别尴尬。

3.6 各阶段对比总结

阶段 存储位置 运行环境 大小限制 主要职责
ROM Code 芯片内部 ROM 内部 SRAM 几十 KB 加载 SPL,安全验证
SPL 外部存储(固定偏移) 内部 SRAM ~100 KB 初始化 DDR,加载 U-Boot
U-Boot 外部存储(文件系统) DDR 几百 KB ~ 几 MB 加载内核,提供命令行
Kernel 外部存储(文件系统) DDR 几 MB ~ 几十 MB 管理硬件,运行应用

3.7 实际项目中的注意事项

基于我这些年做 Bootloader 的经验,给你几个建议:

  • 调试输出要分级:ROM Code 通常没有输出,SPL 可以用串口打印简单信息,U-Boot 可以开完整日志。每一级都要有办法确认「我跑到这里了」。
  • 启动时间要量化:我习惯在每一级入口和出口打时间戳。这样能精确知道哪一级耗时最长,方便优化。
  • 考虑失败路径:如果 SPL 加载 U-Boot 失败怎么办?如果 U-Boot 校验内核签名失败怎么办?这些都要有 fallback 机制。

避坑指南:我曾经在一个项目里,SPL 和 U-Boot 用了不同的串口波特率。结果 SPL 打印正常,U-Boot 打印乱码。排查了半天才发现是波特率没统一。所以,整个启动链的串口配置一定要保持一致

好了,这一章的内容就到这里。启动流程看似简单,但每一级都有它的设计哲学。理解了为什么这么分,你写 Bootloader 时就知道该把代码放在哪一级了。