第2章:U-Boot源码结构——顶层目录解析与构建系统
好,咱们直接进入正题。上一章聊了Bootloader到底是个什么角色,这一章我们把手伸进U-Boot的源码里,看看它到底是怎么组织的。说实话,我第一次打开U-Boot源码树的时候,也有点懵——目录太多了。但别怕,摸清楚规律之后,你会发现它的结构其实非常清晰。
2.1 顶层目录结构总览
U-Boot的顶层目录,说白了就是一套“按功能分家”的布局。我习惯把它分成三大块:平台相关、通用功能、构建系统。下面这个表帮你快速建立全局印象:
| 目录/文件 | 一句话说明 | 我常去翻它的场景 |
|---|---|---|
arch/ |
CPU架构相关代码(ARM、RISC-V、x86等) | 移植新板子时,先看arch/arm/ |
board/ |
具体板级初始化、外设配置 | 调试DDR参数时,必改这里 |
common/ |
核心框架、命令解析、环境变量 | 想改启动流程时,翻common/ |
drivers/ |
各类外设驱动(串口、网卡、MMC等) | 加新硬件时,先找对应子目录 |
include/ |
全局头文件、配置宏、数据结构 | 查寄存器定义时,翻include/ |
net/ |
网络协议栈(TFTP、NFS、DHCP) | 调试网络启动时,改这里 |
cmd/ |
命令行命令的实现 | 想加自定义命令时,看这里 |
lib/ |
通用库函数(字符串、CRC、加密) | 写工具函数时,先看看有没有现成的 |
fs/ |
文件系统支持(ext4、FAT、UBIFS) | 需要从存储读文件时,用这里 |
doc/ |
文档和README | 遇到新特性时,先翻doc/ |
tools/ |
主机端工具(mkimage、env工具) | 制作镜像时,用tools/下的工具 |
board/ 和 arch/ 两个目录。因为板级初始化代码和CPU启动代码,是移植工作的起点。其他目录虽然也重要,但可以后面再细看。
2.2 核心目录深度解析
2.2.1 arch/ —— CPU架构的“家”
arch/ 目录下按CPU架构分文件夹。比如 arch/arm/、arch/riscv/、arch/x86/。每个架构下面又细分:
- cpu/:CPU核的启动代码、异常处理、缓存操作。比如ARM的
cpu/armv7/、cpu/armv8/。 - include/:架构相关的头文件,比如寄存器地址、中断向量表定义。
- lib/:架构相关的库函数,比如内存拷贝、cache刷新。
- dts/:设备树源文件(Device Tree Source),描述硬件拓扑。
我记得有一次移植一个新ARM芯片,发现启动到一半就挂了。查了半天,原来是 arch/arm/cpu/armv8/start.S 里的异常向量表没对齐。嗯,这种底层细节,真的只有翻 arch/ 才能找到。
2.2.2 board/ —— 板级定制的“战场”
board/ 目录按厂商和板子名称组织。比如 board/ti/am335x/、board/rockchip/evb_rk3399/。每个板子目录下通常包含:
- board.c:板级初始化函数,比如
board_init_f()、board_init_r()。 - Kconfig:板级配置选项。
- MAINTAINERS:维护者信息。
- Makefile:编译规则。
board/ 里改DDR时序参数,改完发现编译不过。后来才意识到,board/ 下的代码是跟 arch/ 里的CPU初始化紧密耦合的。改板级代码时,一定要同步检查 arch/ 里的启动流程。否则,很容易出现“板子初始化到一半就崩了”的情况。
2.2.3 common/ —— U-Boot的“大脑”
common/ 目录是U-Boot的核心逻辑所在。这里包含了:
- main.c:U-Boot的主循环,命令解析、执行。
- cli.c:命令行接口实现。
- env_common.c:环境变量管理。
- bootm.c:启动内核的核心函数。
- autoboot.c:自动启动逻辑。
说白了,你敲的每一个U-Boot命令,最终都会落到 common/ 里的某个函数去处理。我调试启动流程时,最喜欢在 common/main.c 里加打印,看看到底卡在哪一步。
2.2.4 drivers/ —— 外设驱动的“仓库”
drivers/ 目录按设备类型分文件夹:
| 子目录 | 包含的驱动 |
|---|---|
serial/ |
串口驱动(UART) |
net/ |
网卡驱动(MAC、PHY) |
mmc/ |
MMC/SD卡驱动 |
spi/ |
SPI总线及设备驱动 |
i2c/ |
I2C总线及设备驱动 |
usb/ |
USB主机/设备驱动 |
gpio/ |
GPIO驱动 |
每个驱动目录下,通常有 Kconfig 和 Makefile,用来控制哪些驱动被编译进U-Boot。你想想看,如果板子上用的是某个冷门网卡芯片,大概率就是在这里加驱动。
2.2.5 include/ —— 全局配置的“枢纽”
include/ 目录里最重要的文件是 configs/ 子目录下的板级配置头文件。比如 include/configs/am335x_evm.h。这些头文件定义了:
- 内存布局(DDR地址、大小)
- 外设基地址(UART、MMC、网卡)
- 环境变量默认值
- 启动命令(bootcmd)
另外,include/ 下还有 linux/、u-boot/ 等子目录,存放各种数据结构定义和宏。我经常在 include/linux/mtd/ 里查NAND Flash的分区定义。
2.3 构建系统:Makefile与Kconfig
U-Boot的构建系统,说白了就是“Kconfig + Makefile”的组合。Kconfig负责“选什么”,Makefile负责“怎么编”。
2.3.1 Kconfig —— 配置的“菜单”
Kconfig文件分布在各个目录下,比如 arch/arm/Kconfig、drivers/net/Kconfig。它们定义了一系列配置选项,比如:
config CMD_BOOTM
bool "Enable bootm command"
default y
help
This enables the 'bootm' command which is used to boot
an operating system from memory.
执行 make menuconfig 时,U-Boot会读取所有Kconfig文件,生成一个配置菜单。你勾选或取消选项,最终会生成 .config 文件。
CMD_NET,它可能会自动依赖 NET 和 CMD_BOOTP。这种依赖关系,在Kconfig里用 depends on 和 select 来控制。
2.3.2 Makefile —— 编译的“流水线”
U-Boot的Makefile体系是递归的。顶层 Makefile 负责:
- 读取
.config,确定哪些模块被选中。 - 递归进入各个子目录,执行子目录的
Makefile。 - 链接所有目标文件,生成
u-boot可执行文件。
每个子目录的 Makefile 通常长这样:
obj-y += board.o
obj-$(CONFIG_CMD_BOOTM) += bootm.o
obj-$(CONFIG_CMD_NET) += net.o
这里 obj-y 表示总是编译,obj-$(CONFIG_XXX) 表示条件编译。如果 .config 里定义了 CONFIG_CMD_BOOTM=y,那么 bootm.o 就会被编译进去。
Makefile 里加一行 obj-y += my_test.o。但记得调试完删掉,否则提交代码时会出问题。我曾经就因为这个,被同事吐槽过。
2.4 构建流程与配置关系图
下面这张SVG图,帮你理清U-Boot从配置到编译的完整流程:
2.5 实战:如何快速定位代码
最后,分享几个我常用的“找代码”技巧:
- 找板级初始化:先看
board/目录,找到对应板子的board.c。里面通常有board_init_f()和board_init_r()。 - 找CPU启动代码:去
arch/目录,找到对应架构的start.S。这是CPU上电后执行的第一段代码。 - 找命令实现:比如想查
bootm命令,去cmd/目录找bootm.c。 - 找驱动:根据设备类型,去
drivers/下找对应子目录。比如网卡驱动在drivers/net/。 - 找配置宏:先在
include/configs/下找板级头文件,如果找不到,再去include/下全局搜索。
include/ 目录,结果发现它定义在 arch/arm/include/ 里。所以,搜索时别只盯着一个目录,多用 grep -r 全局搜索。U-Boot的代码量不小,但只要你掌握了目录结构,定位问题会快很多。