2. U-Boot 源码结构:顶层目录与关键子目录
拿到一份 U-Boot 源码,第一眼看到的就是顶层那一堆目录和文件。很多人觉得乱,其实它的布局很有章法。我最早接触 U-Boot 时也被绕晕过,后来跟着一个老工程师捋了一遍,才明白每个目录的定位。
说白了,U-Boot 的源码结构遵循一个原则:平台无关代码与平台相关代码分离。你想想看,U-Boot 要支持几百种开发板,如果所有代码都混在一起,那维护起来就是噩梦。
2.1 顶层目录结构
先看顶层有哪些东西。我习惯用 ls -1 列出来,核心目录大概这些:
arch/ # CPU/架构相关代码
board/ # 板级相关代码
common/ # 通用功能模块
configs/ # 默认配置文件
doc/ # 文档
drivers/ # 设备驱动
dts/ # 设备树源文件
fs/ # 文件系统支持
include/ # 头文件
lib/ # 库函数
net/ # 网络协议栈
scripts/ # 构建脚本
tools/ # 主机端工具
嗯,这里要注意:configs/ 目录里放的是各开发板的默认配置,比如 stm32mp15_defconfig。你执行 make xxx_defconfig 时,就是从这读取的。
核心思路:U-Boot 的目录划分本质上是「三明治结构」—— 底层架构(arch)、中间驱动(drivers)、上层功能(common)。板级代码(board)像胶水一样粘合它们。
2.2 关键子目录功能
2.2.1 arch/ —— 架构核心
这是 U-Boot 最底层的部分。每种 CPU 架构一个子目录:
arch/
├── arm/ # ARM 架构
├── x86/ # x86 架构
├── riscv/ # RISC-V 架构
├── mips/ # MIPS 架构
└── sandbox/ # 模拟测试环境
每个架构目录下又分 cpu/、include/、lib/、mach-xxx/。比如 arch/arm/cpu/armv7/ 里放的就是 Cortex-A7/A8/A9 的启动代码。
我记得有一次调试一个 ARMv8 的板子,启动卡在 lowlevel_init 里。最后发现是 arch/arm/cpu/armv8/start.S 中 cache 初始化顺序有问题。这种问题,你不熟悉 arch 目录结构根本无从下手。
2.2.2 board/ —— 板级定制
每个开发板或厂商一个子目录。比如:
board/
├── ti/
│ ├── am335x/ # BeagleBone 相关
│ └── omap5/ # OMAP5 相关
├── st/
│ └── stm32mp1/ # STM32MP1 系列
└── rockchip/
└── evb_rk3399/ # RK3399 评估板
这里放的是板级初始化代码,比如 DDR 参数、GPIO 配置、时钟树设置。我个人的经验是:移植 U-Boot 到新板子时,80% 的工作量都在 board/ 目录下。
避坑指南:我曾经在 board/ 目录里改了一个 DDR 时序参数,结果板子死活起不来。后来发现是 board 目录下的 .c 文件和 dts 设备树里的配置冲突了。记住:board/ 里的代码是 C 语言级别的初始化,dts 是硬件描述,两者必须一致。
2.2.3 common/ —— 通用模块
这是 U-Boot 的「工具箱」。所有与硬件无关的功能都在这:
| 文件/模块 | 功能说明 |
|---|---|
| main.c | U-Boot 主循环,命令解析入口 |
| cli.c | 命令行接口实现 |
| hush.c | Shell 解析器(类似 bash 的子集) |
| bootm.c | 启动内核的核心逻辑 |
| env_common.c | 环境变量管理 |
| image.c | 镜像格式解析(uImage、FIT) |
你想想看,你在 U-Boot 命令行敲 bootm 0x80000000,背后就是 common/bootm.c 在干活。它负责解析镜像头、校验、解压、跳转。
2.2.4 drivers/ —— 驱动仓库
U-Boot 的驱动模型叫 DM (Driver Model),从 2014 年开始引入。目录结构按设备类型划分:
drivers/
├── serial/ # 串口驱动
├── spi/ # SPI 控制器驱动
├── i2c/ # I2C 控制器驱动
├── mmc/ # MMC/SD 卡驱动
├── net/ # 网卡驱动
├── gpio/ # GPIO 驱动
├── clk/ # 时钟驱动
├── pinctrl/ # 引脚复用驱动
└── ram/ # DDR 初始化驱动
每个驱动目录下都有 Kconfig 和 Makefile。U-Boot 的 DM 模型和 Linux 内核很像,但更轻量。我刚开始用 DM 时觉得多此一举,后来发现它让驱动代码复用性大大提高。
注意:U-Boot 的驱动和 Linux 驱动不是一回事。U-Boot 驱动只做最基本的初始化,比如让串口能打印、让网卡能 ping 通。别指望它有 Linux 那样的电源管理或中断处理。
2.3 Makefile 与 Kconfig 构建系统
U-Boot 的构建系统是从 Linux 内核抄来的,准确说是 Kbuild 系统。核心就两个东西:Kconfig 负责配置,Makefile 负责编译。
2.3.1 Kconfig —— 配置菜单
Kconfig 文件定义了配置选项。比如你想开启某个功能,就在 menuconfig 里勾选。看一个例子:
# drivers/serial/Kconfig
config SERIAL_NS16550
bool "NS16550 serial port driver"
help
Support for NS16550 compatible UART.
这是最常见的串口控制器,很多板子都用它。
配置选项有三态:y(编译进 U-Boot)、m(模块,U-Boot 里很少用)、n(不编译)。顶层 Kconfig 通过 source 语句包含子目录的 Kconfig:
source "arch/Kconfig"
source "board/Kconfig"
source "drivers/Kconfig"
我个人习惯是先 make menuconfig 看看默认配置,再手动改 .config 文件。但要注意,.config 是自动生成的,手动改完后最好执行 make olddefconfig 来同步。
2.3.2 Makefile —— 编译规则
U-Boot 的 Makefile 分三层:
- 顶层 Makefile:定义全局规则,比如
make all、make clean。还负责生成u-boot.bin、u-boot.img等目标文件。 - 子目录 Makefile:每个子目录一个,用
obj-y和obj-$(CONFIG_XXX)控制哪些文件编译。 - scripts/Makefile.build:通用的编译规则,被各子目录 Makefile 调用。
看一个子目录 Makefile 的例子:
# drivers/serial/Makefile
obj-$(CONFIG_SERIAL_NS16550) += ns16550.o
obj-$(CONFIG_SERIAL_PL01X) += pl01x.o
obj-$(CONFIG_SERIAL_S5P) += s5p_serial.o
这里 CONFIG_SERIAL_NS16550 如果为 y,ns16550.o 就会被编译进 U-Boot。如果为 n,就跳过。
构建流程总结:make xxx_defconfig → 生成 .config → make → 顶层 Makefile 读取 .config → 递归进入各子目录 → 根据 obj-y 编译 → 链接生成 u-boot 可执行文件 → 后处理生成 u-boot.bin、u-boot.img。
2.4 整体结构关系图
下面这张图展示了 U-Boot 源码的核心模块关系。我画这张图时参考了当年自己移植 U-Boot 时的笔记,希望能帮你建立整体认知。
2.5 构建流程速查
最后给一个快速上手指南。假设你拿到一块新板子:
- 找默认配置:
ls configs/ | grep 你的板子 - 配置:
make xxx_defconfig - 定制:
make menuconfig微调功能 - 编译:
make -j4 - 产物:
u-boot.bin就是烧录文件
如果编译报错,先看 include/configs/ 下的头文件有没有配错。我遇到过好几次,都是板级头文件里的宏定义和 Kconfig 冲突导致的。
个人技巧:我习惯在 board/ 目录下加一个 README 文件,记录这块板子的特殊配置和坑。比如 DDR 频率、串口引脚、网络芯片型号。下次再移植时,翻翻笔记能省一半时间。