第2章 U-Boot概述:从历史到实战
U-Boot,全称 Universal Boot Loader,是嵌入式世界里用得最广的引导程序。说实话,我入行那会儿,用的还是 RedBoot 和旧版 U-Boot 1.x。那时候调试个板子,烧错一次就得重新擦写 NOR Flash,等得人心焦。后来 U-Boot 慢慢统一了江湖,现在几乎成了事实标准。
这一章,咱们不扯虚的。我带你看看 U-Boot 是怎么一路演变过来的,它的目录结构长什么样,以及怎么把它编译起来。这些都是后续实战的基础。
2.1 U-Boot 的历史与版本演进
U-Boot 最初是德国工程师 Wolfgang Denx 在 1999 年发起的项目,当时叫 PPCBoot,只支持 PowerPC 架构。后来合并了 ARMBoot 等项目,才改名叫 U-Boot。我记得 2008 年左右,U-Boot 1.3.x 系列开始支持 NAND Flash 启动,那会儿我正好在做一款基于 AT91SAM9260 的工控板,第一次用上 NAND 启动,感觉打开了新世界。
版本演进大致可以分成几个阶段:
| 阶段 | 版本范围 | 主要特点 | 我的印象 |
|---|---|---|---|
| 早期 | 1.x (2000-2008) | 支持 PowerPC、ARM,配置靠手动改头文件 | 改个配置要翻半天代码,很痛苦 |
| 中期 | 2009.xx - 2013.xx | 引入 Kconfig 配置系统,支持 Device Tree | 配置终于清爽了,但迁移旧板子很折腾 |
| 现代 | 2014.xx - 至今 | Kconfig 全面化,支持 FIT Image、EFI、OP-TEE | 功能越来越强,但代码量也翻了几倍 |
这里有个关键节点:2014 年。从那以后,U-Boot 全面转向 Kconfig + Device Tree 的配置方式。如果你还在用老版本,我建议尽快升级。为什么?因为新版的驱动框架更统一,社区支持也更好。我曾经帮客户把一个 2012 年的项目迁移到 2020 年的 U-Boot,光是适配设备树就花了两周,但迁移完以后,后续维护轻松太多了。
核心观点:U-Boot 的版本演进本质上是「从硬编码到配置化,从单架构到多架构」的过程。理解这个趋势,你就能明白为什么现在大家都用 Kconfig 和设备树。
2.2 U-Boot 的目录结构
第一次打开 U-Boot 源码,你可能会被一堆文件夹吓到。别慌,我带你捋一遍核心目录。记住,你不需要记住每个目录,但要知道关键目录是干什么的。
u-boot/
├── arch/ # 架构相关代码(ARM、RISC-V、x86 等)
├── board/ # 板级初始化代码
├── common/ # 通用功能(命令行、环境变量等)
├── configs/ # 默认配置文件(defconfig)
├── doc/ # 文档
├── drivers/ # 驱动(串口、网卡、存储等)
├── dts/ # 设备树源文件
├── fs/ # 文件系统支持(ext4、fat、jffs2 等)
├── include/ # 头文件
├── lib/ # 库函数(crc、sha、lzma 等)
├── net/ # 网络协议栈
├── scripts/ # 编译脚本、Kconfig 工具
├── tools/ # 主机端工具(mkimage、fdtget 等)
└── Makefile # 顶层 Makefile
我个人习惯,拿到一个新板子,先看 board/ 和 configs/ 这两个目录。前者告诉你板级初始化做了什么,后者告诉你默认配置是什么。举个例子,你想看某款 i.MX6 板子的配置,直接去 configs/mx6ul_14x14_evk_defconfig 就能找到。
还有一个容易被忽略的目录:tools/。里面有个叫 mkimage 的工具,专门用来打包内核镜像。我早期做项目时,经常因为镜像格式不对导致 U-Boot 启动失败,后来才发现是 mkimage 参数用错了。嗯,这里要注意:mkimage 的 -A、-T、-C 参数必须和目标板匹配,否则 U-Boot 会直接拒绝加载。
小技巧:如果你在调试时发现 U-Boot 打印 "Bad Magic Number",八成是镜像头格式不对。用 tools/mkimage -l <image> 检查一下,就能看到问题出在哪。
2.3 U-Boot 的编译与配置系统
U-Boot 的编译系统,说白了就是 Kconfig + Kbuild。这套东西从 Linux 内核移植过来的,如果你熟悉内核编译,上手会很快。如果不熟悉,也别怕,我一步步说清楚。
2.3.1 配置流程
配置 U-Boot 分两步:
- 选择默认配置:
make <board>_defconfig - 微调配置:
make menuconfig(可选)
举个例子,编译一个 Raspberry Pi 3 的 U-Boot:
# 第一步:选择默认配置
make rpi_3_defconfig
# 第二步(可选):打开图形界面调整
make menuconfig
# 第三步:编译
make -j4
这里有个坑:make menuconfig 依赖 ncurses 库。如果你在服务器上编译,可能没装这个库。我曾经在 CentOS 上折腾了半天,最后发现 yum install ncurses-devel 就解决了。Ubuntu 上则是 apt install libncurses-dev。
2.3.2 Kconfig 结构
U-Boot 的 Kconfig 文件分散在各个子目录里。顶层 Kconfig 会 source 各个子目录的配置项。你想想看,这样设计的好处是什么?模块化。每个驱动、每个功能都有自己的 Kconfig,互不干扰。
我举个例子,你想开启 USB 支持,需要配置:
# 在 menuconfig 中进入 Device Drivers -> USB
# 选中 CONFIG_USB 和 CONFIG_USB_EHCI_HCD
对应的 Kconfig 文件在 drivers/usb/Kconfig 里。打开看看,你会发现每个选项都有 depends on 和 select 依赖关系。嗯,这里要注意:不要手动改 .config 文件,依赖关系可能不满足,编译会报错。
警告:我曾经见过有人直接编辑 .config 文件,结果漏掉了某个依赖,编译出来的 U-Boot 在板子上跑不起来,串口完全没输出。排查了整整一天,最后发现是时钟配置没选对。所以,老老实实用 menuconfig 吧。
2.3.3 编译产物
编译完成后,你会得到几个关键文件:
| 文件 | 说明 | 用途 |
|---|---|---|
| u-boot.bin | 原始二进制镜像 | 直接烧录到 NOR/NAND Flash |
| u-boot.img | 带 U-Boot 头的镜像 | 通过 SPL 或 BootROM 加载 |
| u-boot.srec | Motorola S-record 格式 | 通过串口下载(老式调试器) |
| u-boot.dtb | 设备树二进制文件 | 与 U-Boot 一起烧录 |
我个人习惯,调试阶段用 u-boot.bin 直接烧录,量产阶段用 u-boot.img 配合 SPL。为什么?因为 SPL 可以做一些早期初始化,比如 DDR 训练,然后再加载完整的 U-Boot。这个后面章节会详细讲。
2.4 U-Boot 整体架构图
下面这张图,是我根据多年经验总结的 U-Boot 整体架构。它展示了从配置到编译,再到最终运行的完整链路。
这张图从左到右展示了 U-Boot 的完整构建流程。配置阶段生成 .config,编译阶段递归处理各个子目录,链接阶段生成 ELF 文件,最后转换成各种烧录格式。你想想看,这个流程和 Linux 内核的构建是不是很像?没错,U-Boot 就是借鉴了 Linux 的构建系统。
2.5 避坑指南与个人经验
最后,分享几个我这些年踩过的坑:
- 交叉编译器版本不匹配:U-Boot 对编译器版本有要求。比如,ARM 架构建议用 gcc-arm-none-eabi 或 Linaro 工具链。我试过用 Ubuntu 自带的 gcc-arm-linux-gnueabihf 编译,结果链接时报了一堆 undefined reference。后来换成 Linaro 的 7.5.0 版本,一次通过。
- 设备树与 U-Boot 版本不匹配:如果你从 Linux 内核里拷贝设备树到 U-Boot,注意检查绑定。U-Boot 的设备树绑定可能比内核旧,有些属性不识别。我曾经因为一个
clock-frequency属性写错了格式,导致串口波特率不对,打印全是乱码。 - SPL 与 U-Boot 的链接地址冲突:如果你同时编译 SPL 和 U-Boot,确保它们的链接地址不重叠。我见过有人把 SPL 放在 0x20000000,U-Boot 也放在 0x20000000,结果 SPL 跳转到 U-Boot 时直接覆盖了自己,板子死机。
我的建议:刚开始接触 U-Boot 时,先找一个官方支持的开发板(比如 Raspberry Pi、BeagleBone Black)练手。这些板子的配置和编译流程经过充分验证,出问题容易排查。等熟悉了流程,再移植到自己的板子上。