2. 嵌入式启动流程概览

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 分两步:

  1. 选择默认配置:make <board>_defconfig
  2. 微调配置: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 整体架构与编译流程 U-Boot 整体架构与编译流程 make xxx_defconfig make menuconfig 生成 .config make -j4 → Kbuild 递归编译各子目录 链接生成 u-boot ELF → 转换各种格式 u-boot.bin u-boot.img u-boot.dtb u-boot.srec 配置 → 编译 → 链接 → 格式转换,最终产物烧录到 Flash 或由 SPL 加载

这张图从左到右展示了 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)练手。这些板子的配置和编译流程经过充分验证,出问题容易排查。等熟悉了流程,再移植到自己的板子上。