2. 启动流程概览:从SoC上电到操作系统启动的完整链路

各位同学,今天我们来聊聊整个启动流程的全貌。很多初学者一上来就盯着U-Boot的代码看,结果越看越晕。我个人习惯是,先搞清楚"从按下电源到系统跑起来,中间到底发生了什么",再去看细节。

说白了,嵌入式系统的启动就像一场接力赛。每个阶段都有自己的任务,跑完就把接力棒交给下一棒。我们来看看这根接力棒是怎么传递的。

2.1 完整启动链路:五棒接力

从SoC上电到操作系统启动,我把它拆成五个阶段。你想想看,每个阶段都有明确的输入和输出:

阶段 执行者 主要任务 输出
1. 上电复位 硬件逻辑 复位所有寄存器,从复位向量取指 PC指向ROM代码入口
2. Boot ROM 片上ROM固件 初始化时钟、检测启动介质、加载SPL SPL被加载到SRAM
3. SPL SPL(U-Boot第一阶段) 初始化DDR、加载U-Boot proper U-Boot proper被加载到DDR
4. U-Boot proper U-Boot第二阶段 初始化外设、加载设备树、启动内核 内核被加载到DDR并跳转
5. 操作系统 Linux内核 解压、初始化子系统、挂载根文件系统 用户空间启动

嗯,这里要注意:并不是所有芯片都有Boot ROM。有些低端MCU直接从片上Flash执行,跳过了Boot ROM阶段。但大部分应用处理器(比如i.MX、Rockchip、Allwinner)都有Boot ROM。

2.2 为什么需要两个阶段的U-Boot?

这个问题我经常被问到。为什么不能一个U-Boot搞定所有事情?

原因很简单:SRAM太小,DDR还没初始化。

SoC上电时,DDR控制器还没配置,DDR根本不能用。芯片内部只有一小块SRAM可用,通常只有几十KB到几百KB。而完整的U-Boot proper编译出来动辄几百KB甚至几MB,根本放不下。

所以,设计者想了个办法:

  • SPL:精简到极致,只做两件事——初始化DDR,加载U-Boot proper到DDR。
  • U-Boot proper:完整的引导加载程序,负责加载内核、设备树、ramdisk等。

核心原则:SPL是"最小可行引导程序",它的代码量必须能塞进SRAM。我见过一些项目为了省SRAM,SPL里连串口调试都砍掉了——结果出了问题根本没法查,得不偿失。

2.3 SPL与U-Boot proper的分工

我们来看一张流程图,把这两个阶段的关系理清楚:

U-Boot 两阶段启动流程 Boot ROM SPL(第一阶段) 运行在SRAM,初始化DDR 加载 U-Boot proper 到 DDR U-Boot proper(第二阶段) 运行在DDR,加载内核与设备树 Linux Kernel 关键约束 SRAM通常 64KB~512KB SPL必须小于SRAM大小 U-Boot proper 无此限制

经验之谈:我曾经在一个项目里,SPL编译出来刚好比SRAM大了2KB。当时想偷懒没优化,结果芯片死活起不来。后来发现Boot ROM加载SPL时会做大小校验,超了就拒绝执行。从那以后,我每次都会在链接脚本里加一个SECTION_SIZE检查。

2.4 SPL的"瘦身"哲学

SPL的代码量必须严格控制。我给大家列一下SPL通常包含什么、不包含什么:

模块 SPL是否包含 原因
DDR初始化 ✅ 必须 没有DDR,后续代码无处安放
串口输出 ✅ 强烈建议 调试救命用的,别省
时钟初始化 ✅ 必须 DDR需要特定时钟频率
存储驱动(MMC/NAND/SPI) ✅ 必须 用来读取U-Boot proper
文件系统支持 ❌ 通常不包含 太占空间,用原始块读取即可
网络协议栈 ❌ 不包含 SPL不需要网络功能
设备树解析 ❌ 不包含 SPL用硬编码配置
命令行交互 ❌ 不包含 SPL没有shell

避坑指南:我曾经见过一个团队,为了"功能完整",在SPL里加了FAT32文件系统支持。结果SPL膨胀到300KB,而芯片SRAM只有128KB。最后不得不重写,浪费了两周时间。记住:SPL不是U-Boot的缩水版,它是专门为SRAM环境设计的极简引导程序。

2.5 启动介质的选择与回退

Boot ROM怎么知道从哪个设备加载SPL?

大部分SoC的做法是:

  1. 读取硬件引脚电平(Boot Mode Pins)
  2. 根据引脚状态决定启动介质优先级
  3. 按优先级依次尝试,直到成功

举个例子,某款芯片的启动顺序可能是:

  • SD卡(如果检测到)
  • eMMC(如果检测到)
  • SPI NOR Flash
  • NAND Flash
  • USB下载模式(如果以上都失败)

嗯,这里有个细节:Boot ROM通常会在每个介质上查找特定的"签名"或"头部信息"。比如i.MX系列要求SPL镜像前128字节包含Image Vector Table(IVT),里面记录了SPL的加载地址和入口点。如果找不到合法的IVT,Boot ROM就认为这个介质不可用,跳到下一个。

关键点:SPL的存放位置和格式是由芯片厂商定义的,不是U-Boot自己说了算。你在移植U-Boot到新板子时,第一件事就是去查芯片参考手册里的"Boot ROM"章节,搞清楚它期望的镜像格式。

2.6 从SPL到U-Boot proper的跳转

SPL把U-Boot proper加载到DDR之后,怎么跳过去?

流程大致是这样:

  1. SPL从存储介质读取U-Boot proper镜像到DDR的指定地址
  2. SPL关闭中断、清理缓存(如果需要)
  3. SPL设置好栈指针,跳转到U-Boot proper的入口地址
  4. U-Boot proper重新初始化部分硬件(比如串口、定时器)
  5. U-Boot proper开始执行它的启动流程

这里有个容易踩的坑:DDR的配置参数。SPL初始化DDR时用的参数,U-Boot proper必须完全兼容。如果SPL和U-Boot proper用了不同的DDR时序配置,轻则内存访问出错,重则芯片死机。

我的做法:把DDR配置参数放在一个单独的头文件里,SPL和U-Boot proper都包含它。这样改参数时不会出现两边不一致的情况。这个习惯帮我避免了好几次线上事故。

2.7 小结:记住这三句话

好了,这一章的内容就这些。我不喜欢长篇大论的总结,你只要记住三句话:

  • 启动是接力赛:Boot ROM → SPL → U-Boot proper → Kernel,每一棒只做自己该做的事。
  • SPL存在的唯一理由:SRAM太小,装不下完整的U-Boot。所以SPL必须瘦身,只做"初始化DDR + 加载U-Boot proper"两件事。
  • 启动介质由硬件决定:Boot Mode引脚决定了从哪里加载SPL,镜像格式由芯片厂商定义,移植时先看手册。