U-Boot 源码结构:顶层目录与关键子模块

拿到一份 U-Boot 源码,第一眼看到几十个目录,别慌。我刚开始接触 U-Boot 时,也对着 ls -l 的输出发过呆。后来慢慢摸清了门道——其实它的目录结构很有规律,说白了就是「平台无关的核心逻辑」和「平台相关的硬件适配」两条线。

这一章,我带你把顶层目录和几个关键子目录过一遍。你不需要记住每个文件,但得知道遇到问题时该去哪个目录翻代码。

顶层目录:一眼看穿骨架

U-Boot 的顶层目录大概有 20 多个。我挑几个最常用的说说:

目录/文件 作用 我常用的场景
board/ 板级初始化代码,每个板子一个子目录 调新板子时,第一个改的就是这里
arch/ CPU 架构相关代码(ARM、RISC-V、x86 等) 移植到新 CPU 时,必须动这里
common/ 核心框架代码,命令解析、环境变量等 改环境变量逻辑时,常来翻
cmd/ 所有命令行命令的实现 加自定义命令时,就在这里加文件
drivers/ 外设驱动(串口、网卡、Flash、MMC 等) 调驱动时,80% 的时间泡在这里
include/ 全局头文件、配置宏定义 改配置时,configs/ 子目录是常客
lib/ 通用库函数(字符串、CRC、加密等) 写工具函数时,先看看这里有没有现成的
net/ 网络协议栈(TFTP、NFS、DHCP 等) 搞网络启动时,必看
fs/ 文件系统支持(ext4、FAT、UBIFS 等) 做 OTA 时,经常要调文件系统读写
tools/ 主机端工具(mkimage、env 工具等) 打包固件、制作环境变量镜像时用
我的习惯:拿到一个新板子的 BSP,我第一件事就是看 board/ 和 include/configs/ 下的文件。这两个目录基本决定了板子能不能跑起来。

board/ 目录:板级定制的灵魂

board/ 目录下,每个厂商或板型都有一个子目录。比如 board/ti/am335x/、board/rockchip/evb_rk3399/。里面通常包含:

  • 板级初始化代码(.c 和 .S 文件)—— 设置时钟、DDR、引脚复用
  • 板级头文件 —— 定义板子特有的宏
  • Kconfig 或 Makefile —— 编译控制

我记得有一次调一个新板子,DDR 死活初始化不过。折腾了两天,最后发现是 board/ 下的时序参数配错了。嗯,这种坑踩过一次就记住了。

避坑:千万不要在 board/ 里写通用逻辑。我曾经见过有人把网卡驱动直接塞进板级目录里,结果换板子时全得重写。驱动就该放 drivers/,板级目录只放「这块板子特有」的东西。

arch/ 目录:CPU 架构的根据地

arch/ 目录按 CPU 架构分:arch/arm/、arch/riscv/、arch/x86/ 等。每个架构下又分:

  • cpu/ —— CPU 核的初始化(MMU、Cache、异常向量)
  • include/ —— 架构相关的头文件
  • lib/ —— 架构相关的库函数(比如 cache 操作、原子操作)
  • mach-xxx/ —— SoC 厂商的特定代码(比如 mach-sunxi/、mach-imx/)

为什么要把架构和板级分开?你想想看,ARM 的 MMU 初始化逻辑,所有 ARM 芯片都能共用。但 DDR 的时序参数,每个板子都不一样。分层之后,复用性就好多了。

common/ 目录:U-Boot 的心脏

common/ 是 U-Boot 的核心框架。这里面的东西,你几乎每天都会打交道:

  • main.c —— 启动主流程,从 board_init_r() 到命令行循环
  • cli.c / cli_hush.c —— 命令行解析器
  • env_*.c —— 环境变量后端(env_nand.c、env_mmc.c、env_sf.c 等)
  • bootm.c —— 启动内核的核心逻辑
  • image.c / image-fdt.c —— 镜像解析和 FDT 处理

我个人觉得,common/ 是理解 U-Boot 工作流程的最佳入口。你从 main.c 的 board_init_r() 开始读,一路跟下去,就能把整个启动流程串起来。

cmd/ 目录:命令的百宝箱

你在 U-Boot 命令行敲的每个命令,都能在 cmd/ 下找到对应的 .c 文件。比如:

命令 源文件 说明
printenv cmd/nvedit.c 打印环境变量
setenv cmd/nvedit.c 设置环境变量
saveenv cmd/nvedit.c 保存环境变量到存储设备
tftp cmd/net.c TFTP 下载文件
mmc cmd/mmc.c MMC/SD 卡操作
sf cmd/sf.c SPI Flash 操作
bootm cmd/bootm.c 启动内核镜像

如果你想加一个自定义命令,比如 ota_update,直接在 cmd/ 下新建一个 ota.c,然后用 U_BOOT_CMD 宏注册就行。我后面讲 OTA 实战时,会手把手带你写一个。

关键点:每个命令文件都通过 U_BOOT_CMD 或 U_BOOT_LONGHELP 宏注册。编译系统会自动扫描并链接。你不需要手动改 Makefile,除非你要加新的编译选项。

drivers/ 目录:硬件驱动的集散地

drivers/ 是 U-Boot 里最大的目录之一。它按设备类型分子目录:

  • drivers/serial/ —— 串口驱动(调试输出全靠它)
  • drivers/net/ —— 网卡驱动(TFTP、NFS 的基础)
  • drivers/mmc/ —— MMC/SD 卡驱动
  • drivers/mtd/ —— NAND Flash、SPI Flash 驱动
  • drivers/spi/ —— SPI 控制器驱动
  • drivers/i2c/ —— I2C 控制器驱动
  • drivers/usb/ —— USB 主机/设备驱动

U-Boot 现在全面采用 Driver Model (DM)。说白了,就是给每个驱动定义一个 struct udevice,通过设备树来匹配和初始化。这样做的好处是:换板子时,只要改设备树,驱动代码基本不用动。

我曾经在一个项目里,把同一套网卡驱动用在了三块不同板子上。每块板子的 PHY 地址、复位引脚不一样,但驱动代码一个字没改——全在设备树里配的。这就是 DM 的魅力。

一张图看懂 U-Boot 源码结构

下面这张图,把 U-Boot 的源码分层和关键目录的关系画出来了。你可以把它当个地图用:

U-Boot 源码结构分层图 应用层(命令与交互) cmd/(命令实现) · common/cli.c(命令行解析) · common/console.c(控制台) 核心框架层(启动与资源管理) common/main.c(启动主流程) · common/bootm.c(内核启动) common/env_*.c(环境变量) · lib/(通用库) · fs/(文件系统) 驱动层(硬件抽象与 DM 框架) drivers/serial/ · drivers/net/ · drivers/mmc/ · drivers/mtd/ drivers/spi/ · drivers/i2c/ · drivers/usb/ · drivers/gpio/ 硬件抽象层(架构与板级) arch/arm/cpu/(CPU 初始化) · arch/arm/mach-xxx/(SoC 代码) board/(板级初始化与配置) 用户交互入口 敲命令、配环境变量 启动流程核心 环境变量、镜像解析 硬件驱动集合 DM 框架、设备树匹配 CPU/SoC 适配 MMU、Cache、异常

几个实用技巧

最后,分享几个我平时翻源码的小习惯:

  • 找函数定义:用 grep -rn "function_name" * 或 cscope。U-Boot 代码量大,别硬翻。
  • 看配置宏:在 include/configs/ 下找板子的头文件,比如 am335x_evm.h。里面定义了所有板级配置。
  • 加调试打印:在 common/ 或 drivers/ 里加 printf(),重新编译烧录。这是最笨但最有效的方法。
  • 理解 Kconfig:U-Boot 现在用 Kconfig 管理配置。在 board/、arch/、drivers/ 下都有 Kconfig 文件。想加新功能,先看看对应的 Kconfig 有没有选项。
我的建议:别试图一次性搞懂所有目录。先聚焦 board/、arch/、common/、cmd/、drivers/ 这五个。等你需要调网络时,再去看 net/;需要搞文件系统时,再去看 fs/。一口吃不成胖子,做嵌入式更是这样。