3. Bootloader 原理与编译:U-Boot 源码结构、配置与编译流程、常用命令

Bootloader 是什么?说白了,它就是上电后第一个跑起来的程序。硬件复位后,CPU 会从一个固定地址取指令——通常是 NOR Flash 或 SPI Flash 的起始位置。Bootloader 就躺在那儿,负责把内核从存储介质搬到内存,然后跳转过去。

在嵌入式 Linux 世界里,U-Boot 几乎是事实标准。我这些年经手的项目,从 ARM9 到 Cortex-A72,清一色用的都是 U-Boot。为什么?因为它支持架构多、驱动全、社区活跃。你想想看,一个 bootloader 能帮你做网络启动、烧写 Flash、调试内核,甚至还能当简单的命令行用——这活儿也就 U-Boot 能干得漂亮。

3.1 U-Boot 源码结构

U-Boot 的源码目录乍一看挺吓人,但其实分层很清楚。我习惯把它分成三层:平台无关层、平台相关层和板级层。

目录/文件 作用 说明
arch/ CPU 架构相关代码 arm、riscv、x86 等子目录,启动汇编、MMU、异常向量都在这里
board/ 板级初始化代码 每个板子一个子目录,比如 board/ti/am335x/
common/ 通用功能模块 命令行解析、环境变量、HUSH shell 等
drivers/ 设备驱动 串口、网卡、MMC、NAND、SPI Flash 等
include/ 头文件与配置 configs/ 下放板级配置头文件
net/ 网络协议栈 TFTP、NFS、DHCP 等
fs/ 文件系统支持 ext4、FAT、JFFS2 等
tools/ 主机端工具 mkimage、fdtgrep 等

嗯,这里要注意:arch/arm/ 下面还有 cpu/ 子目录,里面按厂商分——armv7/、armv8/。每个目录里都有 start.S,那是 CPU 复位后的第一条指令。我当年调试一块新板子时,就是卡在这个 start.S 里的 MMU 初始化上,折腾了两天才发现是页表地址没对齐。

核心理解:U-Boot 的启动流程可以简化为:start.S → board_init_f() → relocate_code() → board_init_r() → main_loop()。其中 relocate_code() 会把 U-Boot 自身从 Flash 拷贝到 DDR,这一步做不好,后面全白搭。

3.2 配置与编译流程

U-Boot 的配置系统经历过一次大改。老版本用 make *_config,新版本统一用 make *_defconfig。我个人更推荐新方式,因为 Kconfig 的图形化配置界面确实好用。

编译一套 U-Boot 的典型步骤是这样的:

  1. 清理:make distclean —— 把之前编译的残留清干净
  2. 配置:make <板子名>_defconfig —— 比如 make am335x_evm_defconfig
  3. 菜单配置(可选):make menuconfig —— 调整内核参数、驱动开关
  4. 编译:make -j$(nproc) —— 多核编译,省时间

编译完成后,你会得到几个关键文件:

文件 说明
u-boot.bin 纯二进制镜像,可直接烧入 Flash
u-boot.img 带 U-Boot 头部的镜像,用于网络启动
u-boot.srec Motorola S-Record 格式,用于串口下载
MLO / SPL 第一级 bootloader,用于启动完整 U-Boot

我的习惯:每次改完配置,我都会先 make savedefconfig,把当前配置精简保存为 defconfig 文件。这样下次换内核版本时,直接拿这个文件来 make xxx_defconfig,省得重新点菜单。

说到 SPL,我得提一嘴。很多现代 SoC 内部 SRAM 很小,装不下完整 U-Boot。所以有了 SPL(Secondary Program Loader)——它是个迷你版 U-Boot,只做两件事:初始化 DDR,然后把完整 U-Boot 从 Flash 搬到 DDR。我曾经在 i.MX6 平台上遇到过 SPL 编译出来超过 64KB 限制的问题,最后发现是开启了不必要的驱动,关掉就好了。

3.3 常用命令

U-Boot 的命令行是个宝贝。你想想看,在没操作系统的情况下,能直接操作内存、读写 Flash、通过网络传文件——这调试能力太强了。

我按使用频率排个序:

命令 功能 典型用法
printenv 打印环境变量 printenv bootargs
setenv 设置环境变量 setenv bootargs 'console=ttyO0,115200'
saveenv 保存环境变量到 Flash saveenv
tftp 通过 TFTP 下载文件 tftp 0x82000000 zImage
fatload 从 FAT 分区加载文件 fatload mmc 0:1 0x82000000 zImage
bootm 启动内核镜像 bootm 0x82000000
bootz 启动 zImage 格式内核 bootz 0x82000000
mmc MMC/SD 卡操作 mmc dev 0 切换到第一个 MMC 设备
sf SPI Flash 操作 sf probe; sf read 0x82000000 0x80000 0x400000
md 内存显示 md 0x82000000 10 显示 16 字节
mw 内存写入 mw 0x82000000 0xdeadbeef 4

避坑指南:我曾经在调试一块新板子时,用 setenv 改了 bootcmd 后忘了 saveenv,结果重启后还是老样子。更惨的是有一次我把 baudrate 设成了 9600,串口直接乱码,只能重新烧写 U-Boot。所以改环境变量时,先 printenv 备份,再 setenv,最后 saveenv——这个顺序别搞反了。

实际调试中,我最常用的组合是:

# 从 TFTP 下载内核和设备树
tftp 0x82000000 zImage
tftp 0x88000000 am335x-evm.dtb

# 设置启动参数
setenv bootargs console=ttyO0,115200 root=/dev/nfs nfsroot=192.168.1.100:/nfsroot ip=192.168.1.10

# 启动内核
bootz 0x82000000 - 0x88000000

这套流程在开发阶段特别管用。你想想看,不用反复烧写 Flash,改完内核直接 tftp 下载就能跑,迭代速度快多了。

3.4 U-Boot 启动流程与核心逻辑

为了让你更直观地理解 U-Boot 的启动过程,我画了张流程图:

U-Boot 启动流程 CPU 复位,从 0x0 取指 start.S:设置异常向量、关中断 board_init_f:初始化时钟、串口、DDR relocate_code:拷贝自身到 DDR board_init_r:初始化驱动、环境变量 main_loop:进入命令行 / 自动启动 SPL 模式下,前两步在 SRAM 中执行,完成 DDR 初始化后加载完整 U-Boot 到 DDR 完整 U-Boot 模式下,所有步骤都在 DDR 中执行

这张图把 U-Boot 的启动路径画得很清楚了。从 CPU 复位到最终进入命令行,中间每一步都有明确的职责。我在调试时经常用 md 命令查看内存中的代码段,确认 relocate_code 是否成功——如果拷贝后的地址和链接地址一致,说明重定位没问题。

一句话总结:U-Boot 的本质就是一个硬件初始化器 + 文件加载器 + 命令行解释器。搞懂它的源码结构、编译流程和常用命令,你就能在嵌入式 Linux 开发中游刃有余。