3. Bootloader 原理与启动流程

做嵌入式 BSP 开发,Bootloader 是绕不开的第一道坎。说白了,它就是板子上电后第一个跑起来的程序。没有它,操作系统根本起不来。我个人习惯把 Bootloader 比作「硬件世界的 BIOS」,但比 BIOS 更灵活、更贴近底层。

3.1 Bootloader 到底在干什么?

你想想看,CPU 上电后,寄存器是随机值,内存还没初始化,连栈指针都不知道指哪。这时候直接跳进 Linux 内核?不可能的事。Bootloader 就是来解决这个问题的。

它的核心任务其实就三件:

  • 初始化硬件 —— 时钟、DDR、串口、存储控制器,这些必须提前搞定。
  • 加载操作系统 —— 从 Flash、SD 卡、网络把内核镜像搬到内存里。
  • 跳转到内核 —— 准备好启动参数,把控制权交给操作系统。

避坑提醒: 我曾经在一个项目中,Bootloader 把 DDR 初始化参数配错了,结果内核一解压就死机。查了整整两天,最后发现是时序参数抄错了数据手册。嗯,DDR 初始化这块,一定要对着芯片手册逐位核对。

3.2 典型启动流程:ROM → SPL → U-Boot

现在的嵌入式 SoC 越来越复杂,单级 Bootloader 已经不够用了。主流方案是三级跳:ROM code → SPL → U-Boot。为什么会这样?因为芯片内部 SRAM 太小,装不下完整的 U-Boot。

我画了一张流程图,帮你理清这个链条:

嵌入式系统三级启动流程 ROM Code 芯片内部固化 SPL 初始化DDR/时钟 U-Boot 加载内核/文件系统 从存储加载 从DDR加载 各阶段职责说明: • ROM Code:上电后CPU直接执行,初始化基础时钟和存储控制器,加载SPL到内部SRAM。 • SPL:全称Secondary Program Loader,初始化DDR控制器,将U-Boot从Flash拷贝到DDR。 • U-Boot:完整的Bootloader,提供命令行、网络下载、环境变量,最终加载内核并跳转。

这张图里,ROM Code 是芯片出厂就烧死的,你改不了。SPL 和 U-Boot 才是我们 BSP 工程师要操刀的部分。我建议你重点关注 SPL 到 U-Boot 的跳转逻辑,这里最容易出问题。

3.3 各阶段详细拆解

3.3.1 ROM Code 阶段

芯片上电后,CPU 从复位向量取第一条指令。这个地址通常映射到芯片内部的 ROM。ROM Code 做的事情很有限:

  • 配置系统时钟(一般是内部振荡器,跑个低速时钟)
  • 初始化片内 SRAM(通常只有几十 KB)
  • 检测启动介质(SD 卡、NAND、SPI Flash 等)
  • 将 SPL 从启动介质拷贝到 SRAM
  • 跳转到 SPL 入口

经验之谈: 有些芯片的 ROM Code 支持多种启动模式,通过硬件引脚选择。我遇到过一块板子,启动引脚虚焊,导致 ROM Code 一直检测不到介质,板子就卡死了。排查时用示波器量一下启动引脚的电压,往往能快速定位。

3.3.2 SPL 阶段

SPL 是 U-Boot 的一个精简版本。它的大小被严格限制,因为 SRAM 就那么点。SPL 的核心使命是:把 DDR 初始化好,然后把完整的 U-Boot 搬进去。

SPL 的典型执行流程:

  1. 设置栈指针(SP),调用 board_init_f()
  2. 初始化串口(方便打印调试信息)
  3. 初始化 DDR 控制器(配置时序、频率)
  4. 从 Flash/SD 卡读取 U-Boot 镜像到 DDR
  5. 跳转到 U-Boot 的入口地址

注意: SPL 阶段不要开中断,不要用复杂的驱动。我曾经在 SPL 里加了 DMA 传输,结果 DDR 还没稳定,DMA 直接读回了垃圾数据。SPL 越简单越好,够用就行。

3.3.3 U-Boot 阶段

U-Boot 进入 DDR 后,就像鱼入大海。它有了充足的内存空间,可以做更多事情:

  • 完整的命令行交互(串口输入命令)
  • 网络协议栈(TFTP 下载内核)
  • 文件系统支持(从 ext4、FAT 分区读取文件)
  • 环境变量管理(保存启动参数)
  • 设备树支持(FDT)

U-Boot 最后一步是调用 bootm 或 bootz 命令,将内核镜像解压到指定地址,设置好启动参数,然后跳转过去。这时候,Bootloader 的使命就完成了。

3.4 U-Boot 源码结构分析

U-Boot 的源码目录看起来复杂,其实有规律可循。我刚开始看的时候也被绕晕过,后来总结了一套方法:按硬件层级找文件。

目录/文件 作用 我关注的重点
arch/ CPU 架构相关代码(arm、riscv、x86 等) 启动汇编、异常向量、MMU 配置
board/ 具体板级支持代码 DDR 初始化、GPIO 配置、板级驱动
drivers/ 外设驱动(串口、网卡、存储) 移植时主要改这里
include/configs/ 板级配置头文件 宏定义开关,裁剪功能
common/ 通用命令和框架代码 命令行解析、环境变量
net/ 网络协议栈 TFTP、NFS 支持
fs/ 文件系统支持 ext4、FAT、UBIFS

举个例子,如果你要为一块新板子移植 U-Boot,通常的步骤是:

  1. 在 board/ 下新建一个目录,复制相近板子的代码
  2. 修改 include/configs/ 下的头文件,配置 DDR 参数、时钟频率
  3. 在 arch/arm/dts/ 下添加设备树文件
  4. 编译,烧录,调试

核心建议: 不要从头写 U-Boot 移植代码。找一块官方已经支持的、和你板子 SoC 相同的开发板,基于它的配置做修改。我每次接手新项目都是这么干的,能省下至少一周的时间。

3.5 启动流程中的关键代码路径

U-Boot 的启动入口在 arch/arm/lib/vectors.S(ARM 架构为例)。从复位向量开始,一路调用下去:

// 简化后的启动调用链
_start (vectors.S)
  → reset (lowlevel_init.S)
    → cpu_init_cp15
    → board_init_f (board.c)
      → 初始化串口、DDR、时钟
    → relocate_code (start.S)
      → 将U-Boot重定位到DDR末尾
    → board_init_r (board.c)
      → 初始化完整驱动、进入命令行
    → main_loop
      → 等待用户输入或自动启动内核

这里面,board_init_f 和 board_init_r 是 BSP 工程师最常打交道的地方。前者在 SRAM 里运行,后者在 DDR 里运行。我建议你在 board_init_f 里多打几个串口打印,方便定位死机位置。

调试技巧: 如果板子在 SPL 阶段就卡死了,串口又没输出,可以用 GPIO 点灯来定位。在关键函数入口拉高 GPIO,看灯亮到哪一步。这招虽然土,但在没有 JTAG 的时候特别好用。

嗯,Bootloader 这块内容就讲到这里。记住一句话:Bootloader 的本质是「硬件初始化器」+「内核加载器」。把这两件事搞明白,移植和调试就有了方向。