2. U-Boot源码结构:顶层目录结构、关键子目录功能解析

拿到一份U-Boot源码,第一反应是什么?我当年第一次解压出来,看到满屏的目录名,说实话有点懵。但别怕,这东西的目录结构其实很有规律。说白了,它就是一套为嵌入式系统量身定制的启动引导程序,每个目录都有自己的使命。

这一章,我带你把U-Boot的顶层目录和关键子目录挨个捋一遍。你搞清楚了这些,后面改代码、加驱动、调分区,心里就有底了。

2.1 顶层目录结构一览

先看个全貌。U-Boot的顶层目录大概有二十多个,我挑最重要的说。你可以在源码根目录下执行 ls -l 看看,大概长这样:

├── arch/          # 架构相关代码(CPU、SoC级)
├── board/         # 板级支持代码(具体开发板)
├── common/        # 通用功能模块
├── configs/       # 默认配置文件
├── doc/           # 文档
├── drivers/       # 设备驱动
├── dts/           # 设备树源文件
├── fs/            # 文件系统支持
├── include/       # 头文件
├── lib/           # 库函数
├── net/           # 网络协议栈
├── scripts/       # 编译脚本
├── tools/         # 主机端工具
├── cmd/           # 命令行命令
├── env/           # 环境变量处理
└── examples/      # 示例代码

嗯,这里要注意:arch/ 和 board/ 是移植工作的核心。我见过不少新手一上来就改 common/ 里的东西,结果编译一堆冲突。其实大部分板级适配,你只需要动 arch/ 和 board/ 就够了。

核心原则:U-Boot采用三层分离设计——架构层(arch)、板级层(board)、通用层(common/drivers)。你改代码时,先想清楚改的是哪一层。

2.2 关键子目录深度解析

2.2.1 arch/ —— 架构的灵魂

这个目录存放的是CPU架构相关的代码。说白了,就是跟具体芯片内核绑定的那部分。比如ARM、RISC-V、x86、MIPS等等。进去看看:

arch/
├── arm/           # ARM架构
│   ├── cpu/       # CPU核初始化(如 cortex-a, armv8)
│   ├── mach-xxx/  # SoC厂商代码(mach-sunxi, mach-rockchip)
│   ├── lib/       # 架构通用库
│   └── dts/       # 设备树
├── riscv/         # RISC-V架构
├── x86/           # x86架构
└── sandbox/       # 模拟测试架构

我个人习惯,拿到一个新板子,先看 arch/arm/cpu/ 下的启动文件。比如 start.S,那是CPU上电后执行的第一段代码。我在项目中遇到过一个问题:某款ARM芯片在低温下启动不稳定,查了半天,最后发现是 arch/arm/cpu/ 里某个时序配置的等待周期太短。嗯,这种坑,不翻底层代码根本想不到。

小技巧:如果你要移植U-Boot到新板子,先找到 arch/ 下最接近的参考平台,复制一份出来改。别从零开始写,那太折磨人了。

2.2.2 board/ —— 板子的身份证

board/ 目录存放的是具体开发板的初始化代码。每个板子一个子目录,里面通常包含:

  • board.c —— 板级初始化函数(如 board_init_f, board_init_r)
  • Kconfig —— 板级配置选项
  • MAINTAINERS —— 维护者信息
  • Makefile —— 编译规则

举个例子,我手头有块全志H3的板子,它的board目录结构大概是:

board/sunxi/
├── board.c        # 板级初始化
├── Kconfig        # 配置选项
├── MAINTAINERS    # 维护者
└── Makefile       # 编译

你想想看,board/ 和 arch/ 的区别在哪?arch/ 管的是CPU怎么跑,board/ 管的是板子上外设怎么配。比如DDR3的时序参数、GPIO的复用功能、电源管理芯片的初始化,这些都在 board/ 里。

我曾经踩过一个坑:某块板子在U-Boot阶段网络不通,我查了三天驱动,最后发现是 board.c 里没有配置PHY的复位引脚。你说冤不冤?所以,板级初始化代码一定要仔细核对原理图。

2.2.3 common/ —— 公共工具箱

common/ 是U-Boot的公共功能模块。这里面的代码不依赖具体硬件,属于"通用逻辑"。比如:

  • main.c —— U-Boot主循环(命令解析、启动流程)
  • cli.c —— 命令行交互
  • bootm.c —— 启动内核的核心函数
  • autoboot.c —— 自动启动逻辑
  • hush.c —— Shell解析器

这里我要强调一点:尽量不要修改 common/ 下的代码。除非你确实需要改动U-Boot的通用行为,否则改这里会影响所有平台。我见过有人为了加一个调试打印,直接改了 common/main.c,结果提交代码后被review打回来——因为其他板子也被影响了。

警告:修改 common/ 目录下的文件前,先问自己三个问题:这个改动是所有平台都需要的吗?能不能放在 board/ 或 arch/ 里?有没有更局部的实现方式?

2.2.4 drivers/ —— 外设驱动大本营

drivers/ 是U-Boot的设备驱动集合。它的子目录按设备类型划分:

子目录 功能 典型文件
serial/ 串口驱动 ns16550.c, pl011.c
net/ 网卡驱动 dw_eth.c, sun8i_emac.c
mmc/ SD/MMC/eMMC驱动 sunxi_mmc.c, dw_mmc.c
spi/ SPI总线驱动 spi-sunxi.c, spi-nor.c
i2c/ I2C总线驱动 i2c-gpio.c, mvtwsi.c
gpio/ GPIO驱动 sunxi_gpio.c
usb/ USB主机/设备驱动 ehci-sunxi.c, musb.c
video/ 显示驱动 sunxi_display.c

U-Boot的驱动模型(Driver Model)从2014年开始引入,现在已经是主流。它的核心思想是:驱动通过设备树匹配,运行时动态绑定。你写一个新驱动,只需要实现几个回调函数,然后注册到驱动框架里就行。

举个例子,我最近在给一块新板子加网卡驱动。流程大概是:

  1. 在 drivers/net/ 下写驱动文件,实现 probe、send、recv 等接口
  2. 在设备树中描述网卡硬件信息(基地址、中断、PHY地址)
  3. 在Kconfig中使能驱动编译
  4. 重新编译,烧录,测试

你看,有了驱动模型,加驱动变得很规范。不像早期U-Boot,驱动代码散落在各处,找起来都费劲。

2.2.5 其他重要目录

除了上面四个核心目录,还有几个你一定会打交道的:

  • configs/ —— 存放各板子的默认配置文件(如 orangepi_zero_defconfig)。编译前先 make xxx_defconfig,就是从这里读取配置。
  • dts/ —— 设备树源文件(.dts 和 .dtsi)。现在新板子基本都靠设备树描述硬件,这个目录越来越重要。
  • cmd/ —— U-Boot命令行命令的实现。比如 cmd/bootm.c 实现了 bootm 命令,cmd/nand.c 实现了 nand 命令。你想加自定义命令?在这里加。
  • include/ —— 头文件。特别是 include/configs/ 下,每个板子有一个头文件,里面定义了很多宏开关。老版本U-Boot的配置主要靠这些头文件,现在逐渐被Kconfig替代,但很多项目还在用。
  • net/ —— 网络协议栈。U-Boot有自己的TCP/IP协议栈,虽然精简,但够用。支持TFTP、NFS、DHCP等协议。
  • fs/ —— 文件系统支持。U-Boot可以读写FAT、EXT4、UBIFS等文件系统,方便从存储设备加载内核。

2.3 知识体系结构图

下面这张图,把U-Boot源码的核心目录和它们的关系画出来了。你看一眼,就能明白整个框架是怎么组织的。

U-Boot源码核心目录结构图 U-Boot 源码核心目录结构 U-Boot 根目录 arch/ 架构层 board/ 板级层 common/ 通用层 cpu/ (start.S, cache) mach-xxx/ (SoC代码) lib/ (架构库函数) board.c (板级初始化) Kconfig (板级配置) DDR/GPIO/PHY配置 main.c (主循环) cli.c (命令行) bootm.c (启动内核) drivers/ (外设驱动) serial/ net/ mmc/ spi/ i2c/ usb/ 其他重要目录 configs/ (默认配置) dts/ (设备树) cmd/ (命令实现) include/ (头文件) 三层分离设计原则 架构层(arch) → 板级层(board) → 通用层(common/drivers)

2.4 目录间的协作关系

搞清楚了每个目录是干什么的,还得知道它们怎么配合。我画个简单的启动流程,你就明白了:

  1. arch/arm/cpu/start.S:上电后第一条指令,设置异常向量、初始化CPU模式
  2. arch/arm/lib/:调用板级初始化函数 board_init_f()
  3. board/xxx/board.c:执行板级初始化——时钟、DDR、串口
  4. common/main.c:进入主循环,解析命令行或自动启动
  5. drivers/:根据设备树加载驱动,初始化外设
  6. cmd/bootm.c:执行 bootm 命令,启动操作系统

你看,这个流程里,arch/、board/、common/、drivers/ 各司其职,环环相扣。我当年调试一块板子时,发现启动到一半就卡住了。顺着这个流程查,最后定位到是 board.c 里DDR初始化参数配错了。嗯,这种问题,如果不了解目录间的调用关系,排查起来就像无头苍蝇。

调试建议:遇到启动问题,先确认卡在哪个阶段。在 board_init_f()、board_init_r()、main_loop() 等关键函数入口加串口打印,能快速定位问题所在目录。

2.5 本章小结

这一章我们把U-Boot的源码目录结构过了一遍。核心就记住三点:

  • arch/ 管CPU架构,移植新芯片先看这里
  • board/ 管板级硬件,调外设先看这里
  • common/ + drivers/ 管通用逻辑和驱动,尽量不动