2、Bootloader原理与U-Boot实战:Bootloader的作用、U-Boot源码结构、U-Boot配置与编译、U-Boot常用命令与启动流程

2.1 Bootloader 到底在干什么?

很多刚接触嵌入式的朋友,上来就盯着内核代码看。但我个人习惯,先搞明白 Bootloader。为什么?

你想想看,嵌入式设备上电后,CPU 从复位向量开始执行。这时候内存还没初始化,内核镜像还在 Flash 里躺着。谁来把内核搬到内存里?谁来给内核传递启动参数?

说白了,Bootloader 就是硬件和操作系统之间的「桥梁」。它负责三件事:

  • 初始化硬件:时钟、DDR、串口、网卡……让板子「活过来」。
  • 加载内核:从 Flash、网络或 SD 卡把内核镜像读到内存。
  • 传递控制权:跳转到内核入口,把系统交给内核。

核心观点:没有 Bootloader,内核就是一堆躺在存储介质上的二进制数据,CPU 根本不知道去哪找它。

我在项目中遇到过一块板子,上电后串口死活没输出。查了半天,发现是 Bootloader 里 DDR 时序配错了。嗯,这种问题最头疼——你连个打印都看不到。

2.2 U-Boot 源码结构:别被目录吓到

U-Boot 是目前嵌入式 Linux 领域事实上的标准 Bootloader。我第一次看它的源码树,说实话有点懵——目录太多了。但摸清规律后,其实很简单。

核心目录就这几个:

目录 作用 我常用的场景
arch/ CPU 架构相关代码(ARM、RISC-V、x86 等) 改启动汇编、异常向量表
board/ 具体板级支持代码 加新板子时,复制参考板改
drivers/ 外设驱动(串口、网卡、MMC、NAND 等) 移植新网卡驱动
include/configs/ 板级配置头文件 改环境变量、分区表、DDR 参数
cmd/ 命令行命令实现 加自定义调试命令
common/ 通用框架代码 很少动,除非改启动流程

我的习惯:拿到一个新板子,先看 board/ 和 include/configs/。这两个目录决定了板子能不能跑起来。

2.3 U-Boot 配置与编译:三步走

配置编译 U-Boot 其实不复杂。我总结了三步:

  1. 清理:make distclean —— 确保没有残留的旧配置。
  2. 配置:make <板子名>_defconfig —— 加载板级默认配置。
  3. 编译:make -j$(nproc) —— 开多核编译,省时间。

举个例子,我用过的 i.MX6ULL 板子:

# 清理
make distclean

# 配置(以米尔科技的板子为例)
make mx6ull_14x14_evk_defconfig

# 编译
make -j4

编译完成后,会在当前目录生成 u-boot.bin 或 u-boot.imx(取决于平台)。

注意:交叉编译器必须提前配好。我见过有人直接敲 make,结果调用了宿主机的 gcc,编译出来的东西根本跑不了。记得设置 CROSS_COMPILE=arm-linux-gnueabihf-。

我曾经在一个项目里,因为没注意编译器版本,U-Boot 编译出来在板子上跑着跑着就死机。折腾了两天,最后发现是编译器优化选项和 U-Boot 代码不兼容。从那以后,我固定用 Linaro 的 gcc 7.5 版本。

2.4 U-Boot 常用命令:调试利器

U-Boot 启动后,会进入一个命令行界面。这里我列几个最常用的命令:

命令 功能 实战场景
printenv 打印所有环境变量 查看 bootargs、bootcmd 是否正确
setenv 设置环境变量 修改内核启动参数
saveenv 保存环境变量到 Flash 改完参数后保存,否则重启就丢了
tftp 通过网络下载文件 开发阶段最常用,不用反复烧写 Flash
fatload 从 FAT 分区加载文件 从 SD 卡或 eMMC 的 boot 分区读内核
bootm 启动内核镜像 手动启动内核,调试用
go 跳转到指定地址执行 调试裸机程序或独立 APP

举个例子,我调试内核时常用的流程:

# 设置服务器 IP 和板子 IP
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10

# 通过 tftp 下载内核和设备树
tftp 0x80800000 zImage
tftp 0x83000000 imx6ull.dtb

# 设置启动参数
setenv bootargs console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait

# 启动内核
bootm 0x80800000 - 0x83000000

避坑指南:我曾经在 bootm 时忘了传设备树地址,内核启动到一半就 panic 了。记住,现代内核必须带设备树,bootm <kernel_addr> - <dtb_addr> 中间的 - 表示没有 initramfs。

2.5 U-Boot 启动流程:从汇编到 C

U-Boot 的启动流程,我画了一张图,方便你理解:

U-Boot 启动流程 U-Boot 启动流程 第一阶段:汇编代码 (start.S) 设置异常向量表 · 初始化 CPU 模式 · 关中断 初始化 DDR 控制器 · 设置栈指针 (SP) 第二阶段:C 语言入口 (board_init_f) 初始化串口 · 时钟 · 定时器 · 网卡 · 存储设备 进入命令行 / 自动启动内核 如果 bootdelay 倒计时为 0 直接执行 bootcmd

整个流程可以概括为:

  • 汇编阶段:在 arch/arm/cpu/armv7/start.S 里。设置异常向量、关 MMU 和 Cache、初始化 DDR、设置栈指针。然后跳转到 _main。
  • C 语言阶段:从 board_init_f() 开始。初始化串口(让你能看到打印)、时钟、网卡、存储设备。然后调用 board_init_r(),进入主循环。
  • 最终阶段:如果 bootdelay 倒计时内没有按键中断,就执行 bootcmd 环境变量里的命令(通常是加载并启动内核)。否则进入命令行,等你敲命令。

关键点:汇编阶段是「无栈」的,不能用全局变量,不能调用复杂函数。所有操作都得小心翼翼。到了 C 语言阶段,有了栈和 BSS 段,才真正「放开手脚」。

我记得有一次调试,板子在 board_init_f 里就卡死了。串口打印只输出了两行。后来一步步加 GPIO 翻转,定位到是某个外设的时钟没开。嗯,这种问题,没有调试器就只能靠「点灯大法」。

2.6 小结

Bootloader 是嵌入式系统的「第一公里」。U-Boot 虽然代码量大,但核心逻辑并不复杂:初始化硬件 → 加载内核 → 跳转。配置编译也就三条命令的事。真正花时间的,是调试硬件适配和排查启动问题。