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 其实不复杂。我总结了三步:
- 清理:
make distclean—— 确保没有残留的旧配置。 - 配置:
make <板子名>_defconfig—— 加载板级默认配置。 - 编译:
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 的启动流程,我画了一张图,方便你理解:
整个流程可以概括为:
- 汇编阶段:在
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 虽然代码量大,但核心逻辑并不复杂:初始化硬件 → 加载内核 → 跳转。配置编译也就三条命令的事。真正花时间的,是调试硬件适配和排查启动问题。