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 函数
我曾经踩过的坑: 有一次移植 U-Boot 到新板子,死活跑不到命令行。最后发现是 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。

配置流程是这样的:

  1. 执行 make xxx_defconfig,加载板卡的默认配置。
  2. 执行 make menuconfig,进入图形化配置界面,按需开关功能。
  3. 配置结果保存在 .config 文件中。
  4. 编译时,.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 代码时,可以对照着看。

U-Boot 源码结构概览 U-Boot 源码结构概览 顶层目录结构 核心代码 arch/ board/ common/ drivers/ 关键文件:start.S / board.c 配置与构建 configs/ scripts/ Kconfig / Makefile 编译系统核心 U-Boot 启动流程(关键文件对应) 上电复位 start.S board_init_f() relocate_code() board_init_r() main_loop() 💡 关键文件对应关系 start.S → 汇编入口,初始化 CPU 和栈 board.c → C 语言初始化,DRAM、串口、驱动 common/ → 命令解析、环境变量、主循环

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 源码对你来说就不再是黑盒了。