2. U-Boot源码结构:顶层目录结构、关键子目录功能、Makefile与Kconfig构建系统
好,咱们正式开始啃U-Boot的源码。很多新手拿到U-Boot第一反应就是——这目录也太多了吧?别慌,我刚开始看的时候也晕。其实它的结构非常清晰,说白了就是一套「硬件抽象 + 板级适配 + 通用框架」的组合。今天我就带你把这套结构拆开来看。
2.1 顶层目录结构:一眼看穿U-Boot的家底
你解压U-Boot源码后,第一眼看到的顶层目录大概有20多个。但真正核心的,其实就那么几个。我习惯把它们分成三类:
| 类别 | 目录名 | 一句话说明 |
|---|---|---|
| 核心框架 | common/ | 命令解析、环境变量、控制台等通用逻辑 |
| 核心框架 | include/ | 全局头文件、配置宏、数据结构定义 |
| 核心框架 | lib/ | 字符串、CRC、加密等基础库函数 |
| 硬件适配 | board/ | 各家开发板的板级初始化代码 |
| 硬件适配 | cpu/ | 不同CPU架构的启动与异常处理 |
| 硬件适配 | drivers/ | 串口、网卡、Flash、I2C等外设驱动 |
| 构建系统 | configs/ | 各板子的默认配置文件(defconfig) |
| 构建系统 | arch/ | 架构相关的汇编启动代码和链接脚本 |
| 构建系统 | scripts/ | Kconfig解析、dtc编译等构建脚本 |
board/ 和 configs/ 目录下找同名文件夹。这俩目录基本决定了这块板子能不能跑起来。
2.2 关键子目录功能详解
2.2.1 board/ —— 板级灵魂
这个目录下每个子文件夹对应一款开发板或一个系列。比如 board/ti/am335x/ 就是TI的AM335x系列。里面通常有:
board.c:板级初始化函数board_init_f()和board_init_r(),负责时钟、DDR、GPIO的早期配置。mux.c:引脚复用配置,说白了就是告诉芯片哪个脚当串口、哪个脚当网口。Kconfig:板级特有的配置选项,比如DDR型号、网卡PHY地址。
我记得有一次移植一块国产板子,死活起不来串口。查了两天,最后发现是 board.c 里漏配了一个PLL分频系数。嗯,这种坑踩过一次就记住了。
2.2.2 cpu/ —— 架构核心
这里按CPU架构分目录,比如 cpu/armv7/、cpu/armv8/、cpu/mips/。里面最关键的是:
start.S:汇编入口,设置异常向量表、初始化C运行环境、跳转到board_init_f()。cpu.c:CPU核心初始化,比如使能ICache/DCache、设置SMP启动。interrupts.c:中断控制器初始化。
cpu/ 下的汇编代码是第一个要盯的地方。我曾经在 start.S 里加过GPIO点灯来定位卡死位置,这招虽然土,但真管用。
2.2.3 drivers/ —— 外设驱动仓库
这个目录下按设备类型分:serial/、net/、spi/、mmc/、usb/ 等。每个驱动通常包含:
xxx.c:驱动实现,遵循U-Boot的驱动模型(DM)。Kconfig:驱动相关的配置选项。Makefile:编译规则。
U-Boot现在的驱动模型(Driver Model)挺成熟的。你写一个新驱动,只需要实现几个回调函数,然后注册到udevice框架里就行。我建议你调试网卡驱动时,先在 drivers/net/ 里找一个现成的、同芯片型号的驱动对照着看,效率高很多。
2.2.4 common/ —— 命令与框架
这个目录是U-Boot的「大脑」。里面包含了:
command.c:命令注册与解析框架。你敲的每个命令,最终都会走到这里来匹配。console.c:控制台输入输出管理。env_common.c:环境变量读写接口。main.c:U-Boot主循环,也就是那个等待你敲命令的shell。
如果你要添加自定义命令,只需要在 common/ 下新建一个 cmd_xxx.c,然后用 U_BOOT_CMD 宏注册就行。我经常用这个方法来加一些调试用的内存读写命令,方便定位问题。
2.3 Makefile与Kconfig构建系统
U-Boot的构建系统是从Linux内核抄来的,说白了就是 Kconfig + Makefile + 递归编译 这套组合拳。理解它,你才能知道编译时到底选了哪些文件、哪些功能。
2.3.1 Kconfig:配置的「开关面板」
Kconfig文件定义了一堆配置选项,比如 CONFIG_CMD_MEMORY、CONFIG_DM_SERIAL。你执行 make menuconfig 时看到的图形界面,就是由这些Kconfig文件生成的。
配置选项的传递路径是这样的:
- Kconfig 定义选项 → 生成
.config文件 .config被转换为include/autoconf.mk(给Makefile用)- 同时生成
include/generated/autoconf.h(给C代码用)
举个例子,你在 configs/xxx_defconfig 里写了 CONFIG_CMD_MEMORY=y,那么编译时 common/cmd_memory.c 就会被编译进去。
depends on 依赖没满足,导致宏定义虽然存在,但实际代码被条件编译跳过了。所以,改配置后一定要检查 include/autoconf.mk 里宏是否真的被定义了。
2.3.2 顶层Makefile:编译的「总指挥」
顶层Makefile负责整个编译流程。它的核心逻辑是:
- 解析
.config,确定要编译哪些子目录。 - 递归进入每个子目录,执行子目录的Makefile。
- 链接所有目标文件,生成
u-boot、u-boot.bin、u-boot.img等镜像。
每个子目录下的Makefile通常长这样:
# drivers/serial/Makefile 示例
obj-$(CONFIG_DM_SERIAL) += serial-uclass.o
obj-$(CONFIG_SYS_NS16550) += ns16550.o
obj-$(CONFIG_PL011_SERIAL) += pl011.o
这里的 obj-$(CONFIG_XXX) 就是条件编译。如果 CONFIG_SYS_NS16550 为 y,就把 ns16550.o 编译进去。
2.3.3 构建流程速览
我画了一张图,帮你把整个构建流程串起来:
2.4 实战:如何快速定位一个文件属于哪个目录
你拿到一个U-Boot源码,想找一个功能对应的代码文件,怎么找?我一般用这招:
- 看命令名: 比如
md命令,去common/下找cmd_memory.c。 - 看驱动类型: 比如网卡驱动,去
drivers/net/下找对应芯片型号的.c文件。 - 看配置宏: 在源码根目录执行
grep -r "CONFIG_XXX" --include="*.c" --include="*.h",直接定位引用位置。 - 看链接脚本: 如果想知道某个函数被放在镜像的哪个段,去
arch/arm/cpu/u-boot.lds里看链接脚本。
include/configs/ 下找到板子的头文件,比如 am335x_evm.h。这个文件里定义了板级所有的宏开关,相当于板子的「配置清单」。看懂了它,你就知道这块板子开了哪些功能、用了哪些驱动。
好了,U-Boot的源码结构就讲到这里。你只要记住:board/ 管板子、cpu/ 管架构、drivers/ 管外设、common/ 管命令、Kconfig/Makefile 管编译。这套逻辑搞清楚了,后面调试的时候你就能快速找到该改哪个文件、该看哪段代码。