3、启动流程分析:典型嵌入式系统的启动流程
各位好,我是老张。今天咱们聊聊嵌入式系统启动这件事。说实话,很多刚入行的朋友觉得启动就是「上电 -> 跑系统」,但实际拆开来看,这里面的门道可不少。
我最早接触这块时,也被这四段式启动搞得有点懵。后来在项目里踩过几次坑,才真正理解每一级存在的意义。嗯,咱们今天就把这个流程掰开揉碎了讲。
3.1 整体启动链路概览
典型的嵌入式系统,从按下电源到进入操作系统,大致走这么四步:
- ROM Code —— 芯片出厂固化的第一段代码
- SPL (Secondary Program Loader) —— 二级加载器,也叫 MLO 或 x-loader
- U-Boot —— 通用引导加载程序,完整版
- 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 的任务就结束了。
内核启动过程:
- 解压自身(如果是压缩镜像)
- 初始化内存管理、进程调度
- 挂载根文件系统
- 启动 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 时就知道该把代码放在哪一级了。