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 分三层:

  1. 顶层 Makefile:定义全局规则,比如 make all、make clean。还负责生成 u-boot.bin、u-boot.img 等目标文件。
  2. 子目录 Makefile:每个子目录一个,用 obj-y 和 obj-$(CONFIG_XXX) 控制哪些文件编译。
  3. 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 时的笔记,希望能帮你建立整体认知。

U-Boot 源码结构关系图 U-Boot 源码核心模块关系 顶层 Makefile + Kconfig .config 配置生成 arch/ (架构相关) board/ (板级定制) drivers/ (设备驱动) cpu/ 启动代码 lib/ 架构库 mach-xxx/ 平台 DDR 初始化 GPIO/时钟配置 板级特有逻辑 serial/spi/i2c mmc/net/gpio clk/pinctrl/ram common/ (通用功能模块) 最终链接生成 u-boot → u-boot.bin → u-boot.img

2.5 构建流程速查

最后给一个快速上手指南。假设你拿到一块新板子:

  1. 找默认配置:ls configs/ | grep 你的板子
  2. 配置:make xxx_defconfig
  3. 定制:make menuconfig 微调功能
  4. 编译:make -j4
  5. 产物:u-boot.bin 就是烧录文件

如果编译报错,先看 include/configs/ 下的头文件有没有配错。我遇到过好几次,都是板级头文件里的宏定义和 Kconfig 冲突导致的。

个人技巧:我习惯在 board/ 目录下加一个 README 文件,记录这块板子的特殊配置和坑。比如 DDR 频率、串口引脚、网络芯片型号。下次再移植时,翻翻笔记能省一半时间。