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 和 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 的典型执行流程:
- 设置栈指针(SP),调用 board_init_f()
- 初始化串口(方便打印调试信息)
- 初始化 DDR 控制器(配置时序、频率)
- 从 Flash/SD 卡读取 U-Boot 镜像到 DDR
- 跳转到 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,通常的步骤是:
- 在
board/下新建一个目录,复制相近板子的代码 - 修改
include/configs/下的头文件,配置 DDR 参数、时钟频率 - 在
arch/arm/dts/下添加设备树文件 - 编译,烧录,调试
核心建议: 不要从头写 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 的本质是「硬件初始化器」+「内核加载器」。把这两件事搞明白,移植和调试就有了方向。