2. U-Boot 源码结构解析:顶层目录结构、board、arch、common、drivers、cmd 等关键目录功能分析

拿到一份 U-Boot 源码,第一反应是什么?我刚开始接触时,面对几百个文件夹确实有点懵。但别怕,U-Boot 的目录结构其实很有章法。说白了,它就是一套为嵌入式系统量身定制的「引导加载程序框架」。今天我就带你把它拆开看看。

2.1 顶层目录结构一览

我们先从顶层开始。你随便下载一个 U-Boot 版本(比如 v2023.10),解压后看到的顶层目录大概是这样:

├── arch/          # 体系结构相关代码
├── board/         # 板级支持代码
├── cmd/           # 命令行命令实现
├── common/        # 通用核心代码
├── configs/       # 板级默认配置文件
├── doc/           # 文档
├── drivers/       # 设备驱动
├── dts/           # 设备树源文件
├── fs/            # 文件系统支持
├── include/       # 头文件
├── lib/           # 库函数
├── net/           # 网络协议栈
├── scripts/       # 构建脚本
├── tools/         # 主机端工具
├── Makefile       # 顶层 Makefile
└── Kconfig        # 顶层配置菜单

嗯,这里要注意:不是每个目录你都需要深入。实际调试中,你 80% 的时间会泡在 arch/、board/、cmd/ 和 drivers/ 这四个目录里。其他的,知道它们存在就行。

2.2 arch/ 目录:体系结构的灵魂

arch/ 目录存放的是与 CPU 架构紧密相关的代码。U-Boot 支持 ARM、RISC-V、x86、MIPS、PowerPC 等主流架构。每个架构下又细分了 SoC 级代码。

以 ARM 为例,它的结构是这样的:

arch/arm/
├── cpu/           # CPU 核初始化(如 cortex-a7、cortex-a53)
├── dts/           # ARM 设备树文件
├── include/       # 架构级头文件
├── lib/           # 架构级库函数
├── mach-*/        # 各 SoC 厂商代码(mach-sunxi、mach-rockchip 等)
└── Kconfig        # 架构级配置

我个人习惯,拿到一块新板子,第一件事就是去 arch/arm/cpu/ 下看它的 CPU 初始化流程。为什么?因为 U-Boot 启动的第一条指令就在这里。你想想看,如果 CPU 都没跑起来,后面全是白搭。

关键点: arch/arm/cpu/ 下的 start.S 是 U-Boot 的入口文件。它负责设置异常向量表、初始化 CPU 模式、关闭 MMU 和 Cache,然后跳转到 board_init_f()。我在调试一块 i.MX6ULL 板子时,遇到过启动卡死,最后发现是 start.S 中中断向量表地址配置错了——这种问题,不看源码根本找不到。

2.3 board/ 目录:板级定制的战场

board/ 目录是板级支持的核心。每个板子在这里都有一个专属文件夹。比如:

board/
├── freescale/
│   └── mx6ullevk/       # i.MX6ULL 评估板
├── rockchip/
│   └── evb_rk3588/      # RK3588 评估板
├── ti/
│   └── am335x/          # AM335x 系列
└── ...

每个板级目录下通常包含:

文件/目录 作用
board.c 板级初始化函数,如 board_init()、dram_init()
Kconfig 板级配置选项
MAINTAINERS 维护者信息
Makefile 编译规则
imximage.cfg i.MX 系列专用的镜像头配置

我曾经踩过一个坑:某款基于 RK3288 的产品,DDR 参数在 board/rockchip/evb_rk3288/ 下配错了,导致 4GB 内存只认到 3GB。排查了整整两天,最后发现是 dram_init() 里 bank 数量写死了。所以说,板级目录是你调试硬件问题的第一站。

2.4 common/ 目录:U-Boot 的「操作系统」

common/ 目录存放的是 U-Boot 的核心框架代码。它不依赖具体硬件,是 U-Boot 的「操作系统层」。

common/
├── main.c           # U-Boot 主循环
├── command.c        # 命令注册与分发
├── console.c        # 控制台 I/O
├── board_r.c        # 重定位后的板级初始化
├── board_f.c        # 重定位前的板级初始化
├── cli.c            # 命令行交互
├── env_common.c     # 环境变量管理
└── ...

这里我要重点提一下 board_f.c 和 board_r.c。它们是 U-Boot 初始化的两个阶段:

  • board_f.c:在 Flash/ROM 中执行,完成早期初始化(时钟、串口、DRAM 初始化),然后将自身重定位到 DRAM。
  • board_r.c:在 DRAM 中执行,完成剩余初始化(设备驱动、文件系统、网络栈),最后进入主循环。

调试技巧: 如果你在 U-Boot 启动过程中卡住了,可以在 board_f.c 和 board_r.c 的初始化函数列表里加 printf() 定位。我习惯在 init_sequence_f[] 数组的每个函数入口打印一条信息,这样一眼就能看出是哪个阶段出了问题。

2.5 cmd/ 目录:你敲的每条命令都在这里

你在 U-Boot 命令行敲的 printenv、bootm、tftp,它们的实现都在 cmd/ 目录下。

cmd/
├── bootm.c          # 启动内核镜像
├── nand.c           # NAND Flash 操作
├── mmc.c            # MMC/SD 卡操作
├── net.c            # 网络命令
├── usb.c            # USB 命令
├── fat.c            # FAT 文件系统命令
├── ext4.c           # ext4 文件系统命令
├── gpio.c           # GPIO 操作
├── i2c.c            # I2C 操作
└── ...

每个命令的实现都遵循一个固定模式:

static int do_mycmd(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
    // 命令处理逻辑
    return 0;
}

U_BOOT_CMD(
    mycmd,      // 命令名
    3,          // 最大参数个数
    1,          // 是否重复执行
    do_mycmd,   // 处理函数
    "命令简短描述",
    "命令详细用法"
);

你想想看,如果你想给 U-Boot 加一个自定义命令,比如读取某个传感器的值,只需要在 cmd/ 下新建一个文件,用 U_BOOT_CMD 宏注册就行。我在项目中就经常这么干——调试硬件时,临时加一条命令读寄存器,比反复烧写固件快多了。

2.6 drivers/ 目录:设备驱动的汪洋

drivers/ 是 U-Boot 中最大的目录,没有之一。它按设备类型组织:

drivers/
├── serial/         # 串口驱动
├── spi/            # SPI 驱动
├── i2c/            # I2C 驱动
├── mmc/            # MMC/SD 卡驱动
├── net/            # 网卡驱动
├── usb/            # USB 驱动
├── gpio/           # GPIO 驱动
├── pinctrl/        # 引脚控制驱动
├── clk/            # 时钟驱动
├── mtd/            # 存储设备驱动(NAND、NOR、SPI Flash)
└── ...

U-Boot 的驱动模型(Driver Model,简称 DM)从 2014 年开始引入,现在已经是主流。它的核心思想是:驱动 = 设备 + 驱动 + uclass。

DM 三要素:

  • uclass:设备类,定义该类设备的通用接口(如 serial_ops、mmc_ops)
  • driver:具体驱动,实现 uclass 定义的接口
  • device:设备实例,绑定到具体的硬件

举个例子,串口驱动:uclass 定义 serial_putc()、serial_getc() 接口;driver 实现具体 UART 控制器的读写;device 对应板子上的某个 UART 端口。

我记得有一次调试 SPI Flash 驱动,死活读不到设备 ID。后来发现是 drivers/spi/ 下的驱动没有正确匹配设备树中的 compatible 字符串。U-Boot 的 DM 依赖设备树进行驱动绑定,设备树里写错了,驱动就加载不上。这个坑我踩过不止一次。

2.7 其他关键目录

目录 作用 我的建议
configs/ 板级 defconfig 文件,如 mx6ullevk_defconfig 改配置先看这里,别直接改 Kconfig
dts/ 设备树源文件(.dts、.dtsi) 硬件描述全在这,调试驱动必备
include/ 全局头文件,包括 configs/ 下的板级配置头文件 老版本 U-Boot 的配置主要靠头文件宏定义
lib/ 通用库函数(字符串、数学、CRC 等) 一般不需要动,但调试时可以用 lib/strto.c 里的函数
net/ 网络协议栈(TFTP、NFS、DHCP 等) 网络启动失败时,来这里看协议实现

2.8 知识结构图:U-Boot 源码目录关系

下面这张图展示了 U-Boot 各目录之间的依赖关系。你可以看到,arch/ 和 board/ 是硬件相关的底层,common/ 是核心框架,cmd/ 和 drivers/ 是功能实现,它们共同支撑起 U-Boot 的启动流程。

U-Boot 源码目录关系与启动流程 U-Boot 顶层 arch/ (体系结构) board/ (板级支持) dts/ (设备树) common/ (核心框架) cmd/ (命令实现) drivers/ (设备驱动) net/ / fs/ (协议栈) configs/ (配置) scripts/ (构建脚本) 说明: 箭头表示依赖关系。arch/ 和 board/ 提供硬件抽象,common/ 组织初始化流程,cmd/ 和 drivers/ 实现具体功能。

2.9 我的调试经验总结

最后,分享几个我在实际项目中积累的经验:

避坑指南:

  • 不要直接修改顶层 Makefile:板级配置应该在 configs/ 下的 defconfig 文件中修改,或者通过 make menuconfig 配置。
  • 设备树优先:新版本的 U-Boot 强烈依赖设备树。如果你发现某个驱动没加载,先去 dts/ 下检查设备树节点是否匹配。
  • 善用 git blame:遇到看不懂的代码,用 git blame 看看是谁写的、为什么这么写。我经常通过 commit message 找到问题的根源。
  • 从 board/ 入手:拿到一块新板子,先看 board/ 目录下的代码,再结合 configs/ 下的 defconfig,基本就能摸清它的硬件配置。