4. U-Boot源码结构:目录、关键文件与编译系统
U-Boot 的源码,说实话,第一次打开的时候确实有点懵。目录多,文件杂,不知道从哪看起。我当年刚接触时,对着 start.S 发了好几天呆。
但摸清楚它的骨架之后,你会发现它其实很有条理。说白了,U-Boot 就是一个针对嵌入式硬件的、高度可配置的裸机程序。它的源码结构,就是围绕「硬件初始化 → 板级配置 → 命令交互 → 启动内核」这条线展开的。
这一章,我就带你把它拆开看看。重点讲三个东西:目录结构、三个关键文件、以及Kconfig/Makefile 编译系统。
4.1 目录结构:U-Boot 的骨架
U-Boot 的顶层目录,大概有二十多个。但真正核心的,其实就那么几个。我习惯把它们分成三类:
| 类别 | 目录名 | 一句话说明 |
|---|---|---|
| 核心代码 | arch/ |
CPU 架构相关代码,比如 ARM、RISC-V、x86。这是移植的重点。 |
board/ |
具体板卡的代码。每个板卡一个子目录,比如 board/samsung/odroid/。 |
|
common/ |
通用功能模块,比如命令解析、环境变量、控制台。 | |
drivers/ |
各种外设驱动,串口、网卡、Flash、MMC 都在这里。 | |
| 配置与构建 | configs/ |
板卡的默认配置文件,比如 xxx_defconfig。 |
include/configs/ |
旧版配置头文件(.h),新版本逐渐被 Kconfig 替代。 |
|
scripts/ |
Kconfig 和 Makefile 的辅助脚本。 | |
| 工具与文档 | tools/ |
主机端工具,比如 mkimage、fdtgrep。 |
doc/ |
文档,但说实话,很多已经过时了,我一般直接看代码。 |
configs/ 和 board/ 目录下找对应的文件。这两个目录能告诉你,这个板子用了什么 CPU、什么外设、内存多大。比看 datasheet 快多了。
嗯,这里要注意,arch/ 目录下面还会按 CPU 架构细分。比如 ARM 架构下,还有 arch/arm/cpu/、arch/arm/include/、arch/arm/lib/ 等。你想想看,一个 U-Boot 要支持几十种 CPU 架构,几百种板卡,没有这种分层结构,代码早就乱成一锅粥了。
4.2 三个关键文件:从上电到命令行
U-Boot 的启动流程,可以浓缩成三个关键文件。我当年调试启动卡死的问题,就是盯着这三个文件一步步查的。
4.2.1 start.S:第一行代码
这是 U-Boot 的入口。CPU 上电后,第一条指令就在这里。它通常放在 arch/arm/cpu/armv7/start.S 这样的路径下。
它干了什么?说白了,就是给 C 语言运行环境「铺路」:
- 设置 CPU 为 SVC(超级用户)模式
- 关闭中断(MMU 和 Cache 先别开,太早开容易出问题)
- 设置栈指针(SP),这样我们才能调用 C 函数
- 清零 BSS 段(全局变量初始化为 0)
- 最后,跳转到
board_init_f()这个 C 函数
start.S 里栈指针设置错了。CPU 的栈空间和内存地址没对齐,导致一调用 C 函数就崩。嗯,从那以后,我每次移植都会先确认 start.S 里的栈地址对不对。
4.2.2 board.c:板级初始化的心脏
这个文件通常位于 board/你的板卡名/ 目录下,或者 arch/arm/lib/board.c(旧版本)。它是 U-Boot 第二阶段初始化的核心。
它主要做了这些事:
- board_init_f():初始化 DRAM、串口、定时器等基础硬件。这时候还不能用全局变量,因为 BSS 段还没准备好(嗯,这是个细节)。
- relocate_code():把 U-Boot 自身从 Flash 拷贝到 DRAM 中。因为 Flash 太慢了,跑起来不痛快。
- board_init_r():重新初始化板级资源,这时候可以正常使用全局变量了。然后初始化各种驱动、文件系统、网络协议栈。
- 最后,进入
main_loop(),等待用户输入命令。
我个人习惯,在移植新板子时,会先在这个文件里加几个调试打印。比如在 board_init_f() 里打印 DRAM 的地址和大小,确认内存没问题了,再往下走。
4.2.3 common/:命令与功能的仓库
common/ 目录下,放的是 U-Boot 的「通用」功能。比如:
main.c:主循环,处理用户输入的命令command.c:命令注册与查找框架env_common.c:环境变量(bootargs、bootcmd 等)的读写console.c:控制台输入输出hush.c:命令行解析器(支持 if/then/else 那种)
你想想看,你在 U-Boot 命令行敲一个 printenv,背后就是 common/ 里的代码在干活。它把命令字符串解析出来,找到对应的函数指针,然后执行。
start.S 负责「活下来」,board.c 负责「站起来」,common/ 负责「跑起来」。
4.3 编译系统:Kconfig 与 Makefile
U-Boot 的编译系统,是从 Linux 内核抄过来的。它用 Kconfig 做配置,用 Makefile 做编译。这套系统,说实话,刚开始用的时候觉得挺绕,但用熟了之后,你会发现它真的很强大。
4.3.1 Kconfig:图形化的配置菜单
Kconfig 文件定义了配置选项。比如,你想让 U-Boot 支持网络功能,就需要在配置菜单里选中 CONFIG_CMD_NET。
配置流程是这样的:
- 执行
make xxx_defconfig,加载板卡的默认配置。 - 执行
make menuconfig,进入图形化配置界面,按需开关功能。 - 配置结果保存在
.config文件中。 - 编译时,
.config会被转换成include/autoconf.mk,供 Makefile 使用。
举个例子,你想开启 U-Boot 的 USB 支持:
# 在 menuconfig 中,进入 Device Drivers → USB Support
# 选中 [*] USB Support
# 然后保存退出
# 对应的 .config 里会多一行:
CONFIG_USB=y
.config 文件。用 make menuconfig 来改,它能自动处理依赖关系。比如你选了 USB 主机模式,它会自动帮你选上 USB 核心驱动。手动改的话,很容易漏掉依赖,编译报错。
4.3.2 Makefile:构建的指挥中心
U-Boot 的 Makefile 体系,分为三层:
- 顶层 Makefile:定义编译目标、链接脚本、生成
u-boot.bin等最终产物。 - 子目录 Makefile:每个目录下都有一个
Makefile,告诉编译系统这个目录下有哪些文件要编译。 - Kbuild Makefile:U-Boot 自己封装的一套编译规则,简化了驱动模块的编写。
比如,你想在 drivers/net/ 目录下添加一个新的网卡驱动,你只需要在 drivers/net/Makefile 里加一行:
obj-$(CONFIG_DRIVER_MY_NET) += my_net.o
然后,在 Kconfig 里定义 CONFIG_DRIVER_MY_NET,在 menuconfig 里选中它。编译时,my_net.c 就会被自动编译并链接进 U-Boot。
嗯,这里要注意,U-Boot 的 Makefile 对文件依赖关系处理得很好。你改了某个 .c 文件,它只会重新编译那个文件,然后重新链接。不会像某些老旧的嵌入式项目那样,改一行代码就要重新编译整个工程。
4.4 一张图看懂 U-Boot 源码结构
下面这张图,是我自己整理的 U-Boot 源码结构概览。它把目录、关键文件、编译系统串在了一起。你保存下来,以后看 U-Boot 代码时,可以对照着看。
4.5 避坑指南:源码结构相关的常见问题
最后,分享几个我实际项目中遇到的坑。希望能帮你少走弯路。
- 坑一: 在旧版 U-Boot 中,配置是写在
include/configs/xxx.h头文件里的。新版改成了 Kconfig。如果你拿到的 BSP 包是旧版,但你想用新版的编译方式,就会报一堆未定义的宏。我的建议是:先确认 U-Boot 版本,再决定用哪种配置方式。 - 坑二: 修改了
board.c后,一定要make clean再重新编译。因为 U-Boot 的依赖检测有时候不靠谱,特别是你改了头文件或者链接脚本时。不清干净,可能链接的还是旧的目标文件。 - 坑三: 在
start.S里加调试代码要小心。这时候栈还没设好,不能调用复杂的 C 函数。我一般只用汇编级别的打印,比如向串口寄存器直接写数据。
好了,U-Boot 的源码结构就讲到这里。你只要记住:目录结构看 arch/ 和 board/,启动流程看 start.S 和 board.c,配置编译看 Kconfig 和 Makefile。这三条线抓住了,U-Boot 源码对你来说就不再是黑盒了。