3. 开发环境搭建

拿到一份U-Boot源码,很多人第一反应是懵的。几百个文件夹,几千个文件,从哪看起?

我刚开始接触U-Boot时也这样。后来带团队做移植,慢慢摸清了门道。说白了,U-Boot的目录结构设计得挺讲究——它把硬件相关、硬件无关、命令处理、驱动框架分得清清楚楚。

这一章,我就带你把这些关键目录挨个捋一遍。

3.1 顶层目录:U-Boot的“总指挥部”

先看顶层。你解压U-Boot源码后,第一眼看到的就是这些:

u-boot/
├── arch/          # 架构相关代码(CPU体系结构)
├── board/         # 板级相关代码(具体开发板)
├── common/        # 通用功能模块
├── cmd/           # 命令行命令实现
├── drivers/       # 设备驱动
├── include/       # 头文件
├── lib/           # 库函数
├── net/           # 网络协议栈
├── fs/            # 文件系统支持
├── disk/          # 磁盘分区处理
├── dts/           # 设备树源文件
├── scripts/       # 编译脚本、配置工具
├── tools/         # 主机端工具
├── configs/       # 默认配置文件(defconfig)
├── Makefile       # 顶层Makefile
└── Kconfig        # 顶层Kconfig配置菜单

顶层目录里,Makefile和Kconfig是编译系统的入口。你执行make menuconfig时,读的就是Kconfig;你执行make时,顶层Makefile会递归进入各个子目录。

我的习惯:拿到一个新板子的U-Boot,我第一件事就是看顶层Makefile里的ARCH和CROSS_COMPILE变量。这两个变量设对了,编译才可能过。

3.2 arch目录:CPU架构的“心脏”

arch/目录存放的是与CPU体系结构直接相关的代码。不同架构的CPU,启动流程、寄存器操作、异常处理完全不同,这些差异全收在这里。

arch/
├── arm/           # ARM架构(最常用)
│   ├── cpu/       # 具体CPU型号(armv7、armv8等)
│   ├── include/   # 架构相关头文件
│   ├── lib/       # 架构通用库函数
│   ├── mach-*/    # 厂商/SoC系列(mach-sunxi、mach-rockchip等)
│   └── dts/       # 架构级设备树
├── riscv/         # RISC-V架构
├── x86/           # x86架构
├── mips/          # MIPS架构
├── powerpc/       # PowerPC架构
└── sandbox/       # 模拟测试架构

以ARM为例,arch/arm/cpu/下面按ARMv7、ARMv8等版本分目录。每个目录里都有start.S——这是U-Boot的第一条指令,负责初始化CPU、设置栈指针、跳转到C语言入口。

关键点:移植U-Boot到新板子时,如果CPU架构是已有的(比如ARM Cortex-A7),你基本不用动arch/。但如果换了一个新架构(比如从ARM换成RISC-V),那arch/几乎要重写。

3.3 board目录:板级定制的“战场”

这是移植工作中改动最多的目录。board/下面按厂商或板子型号组织,存放与具体硬件平台相关的初始化代码。

board/
├── allwinner/     # 全志系列
│   ├── sunxi/     # sunxi平台通用
│   └── orangepi_zero2/  # 具体板子
├── rockchip/      # 瑞芯微系列
│   ├── common/    # Rockchip通用代码
│   └── evb_rk3568/ # 具体评估板
├── freescale/     # 飞思卡尔/NXP系列
└── ...

每个板子目录里通常包含:

  • board.c:板级初始化函数,比如DDR初始化、GPIO配置、时钟设置
  • Kconfig:板级配置选项
  • Makefile:编译规则
  • lowlevel_init.S:极早期汇编初始化(有些板子放在这里)
我曾经踩过的坑:有一次移植,DDR参数配错了,U-Boot死活跑不起来。折腾了两天才发现——board目录下的DDR初始化时序和实际硬件颗粒不匹配。所以,board目录里的代码,一定要对照原理图和芯片手册来改,不能想当然。

3.4 common目录:通用功能的“工具箱”

common/目录存放的是与硬件无关的通用代码。不管你的板子是ARM还是RISC-V,这些代码基本都能直接用。

文件/子目录 作用
main.c U-Boot主循环,命令解析入口
board_r.c 板级运行时初始化(第二阶段)
board_f.c 板级前初始化(第一阶段,在relocate之前)
console.c 控制台输入输出管理
env_*.c 环境变量存储与加载(env_nand.c、env_mmc.c等)
image.c 镜像格式解析(uImage、FIT等)
autoboot.c 自动启动逻辑(倒计时、bootcmd执行)

你想想看,common/main.c里的main_loop()函数,就是U-Boot启动后等待你输入命令的地方。这个函数在所有板子上都一样——它只负责“读命令、找命令、执行命令”。

3.5 cmd目录:命令的“仓库”

你在U-Boot命令行里敲的每一个命令,实现代码都在cmd/目录下。

cmd/
├── bootm.c        # bootm命令(启动内核)
├── go.c           # go命令(跳转到指定地址)
├── load.c         # 文件加载命令(loadb、loadx等)
├── mmc.c          # mmc命令(MMC/SD卡操作)
├── nand.c         # nand命令(NAND Flash操作)
├── net.c          # 网络命令(ping、tftpboot等)
├── fat.c          # fat命令(FAT文件系统操作)
├── ext4.c         # ext4命令(ext4文件系统操作)
├── gpio.c         # gpio命令(GPIO操作调试)
├── i2c.c          # i2c命令(I2C总线操作)
├── sf.c           # sf命令(SPI Flash操作)
├── usb.c          # usb命令(USB操作)
└── ...

每个命令文件的结构很固定:一个do_xxx()函数实现命令逻辑,一个U_BOOT_CMD宏注册命令。比如:

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

U_BOOT_CMD(
    gpio, 4, 0, do_gpio,
    "GPIO 操作命令",
    "set <pin> <value>  - 设置GPIO输出值\n"
    "get <pin>           - 读取GPIO输入值"
);
调试小技巧:移植初期,我习惯先把cmd/gpio.c和cmd/i2c.c编译进去。硬件出问题时,用gpio命令点灯、用i2c命令读传感器,比加打印快多了。

3.6 drivers目录:设备驱动的“大本营”

drivers/是U-Boot里代码量最大的目录。它按设备类型组织,每个子目录对应一类外设驱动。

drivers/
├── serial/        # 串口驱动(调试串口最关键)
├── mmc/           # MMC/SD卡驱动
├── net/           # 网卡驱动(以太网PHY、MAC)
├── spi/           # SPI总线驱动
├── i2c/           # I2C总线驱动
├── gpio/          # GPIO控制器驱动
├── usb/           # USB主机/设备驱动
├── mtd/           # MTD设备驱动(NAND、SPI Flash)
├── video/         # 显示驱动(LCD、HDMI)
├── pinctrl/       # 引脚控制驱动
├── clk/           # 时钟驱动
├── power/         # 电源管理驱动(PMIC)
└── ...

U-Boot的驱动模型(Driver Model,简称DM)从2014年开始引入。现在新版本的U-Boot,大部分驱动都基于DM框架。DM的好处是:驱动代码可以复用,板子只需要在设备树里描述硬件连接,驱动就能自动匹配。

移植重点:串口驱动是第一个要调通的。串口不工作,你连U-Boot的打印都看不到,后面所有调试都无从谈起。我每次移植新板子,第一件事就是确认drivers/serial/里有没有对应SoC的串口驱动,没有就自己写。

3.7 目录关系:一张图看懂

这些目录不是孤立的。它们通过编译系统和初始化流程串联起来。下面这张图展示了关键目录之间的关系:

U-Boot 关键目录关系与启动流程 顶层 Makefile / Kconfig arch/ 架构相关代码 board/ 板级初始化代码 common/ 通用功能模块 cmd/ 命令实现 drivers/ 设备驱动 用户输入命令 → 执行 驱动操作硬件外设 启动流程:arch/start.S → board/lowlevel_init → common/board_f → common/board_r → main_loop 编译时:顶层Makefile根据ARCH和BOARD变量,递归编译arch、board、drivers、cmd等目录

从这张图能看出:

  • 编译阶段:顶层Makefile根据你选的ARCH和BOARD,决定编译哪些目录
  • 启动阶段:先跑arch/里的汇编代码,再进board/做板级初始化,然后common/接管通用流程,最后进入main_loop等待命令
  • 运行阶段:你敲命令,cmd/里的代码被调用;命令需要操作硬件时,调用drivers/里的驱动

3.8 其他重要目录

除了上面几个核心目录,还有几个也值得了解:

目录 作用 移植时关注度
include/ 头文件,包括configs/下的板级配置头文件 高(要改配置宏)
dts/ 设备树源文件(.dts、.dtsi),描述硬件拓扑 高(新板子要写dts)
configs/ 默认配置文件(xxx_defconfig) 高(要新建defconfig)
lib/ 通用库函数(字符串、数学、加密等) 低(基本不用动)
net/ 网络协议栈(ARP、IP、UDP、TFTP等) 中(网卡驱动调通后可用)
fs/ 文件系统支持(FAT、ext4、UBIFS等) 中(需要文件系统操作时关注)
tools/ 主机端工具(mkimage、fdtgrep等) 低(编译时自动构建)

3.9 小结:移植时你该关注哪些目录?

总结一下,如果你要做U-Boot移植,按优先级关注这些目录:

  1. 第一优先级:board/(板级代码)、include/configs/(配置头文件)、configs/(defconfig)
  2. 第二优先级:dts/(设备树)、drivers/(外设驱动,特别是serial和mmc)
  3. 第三优先级:arch/(如果CPU架构已支持,基本不动)
  4. 基本不动:common/、cmd/、lib/、net/、fs/
我的经验:别试图一次性搞懂所有目录。先让U-Boot在板子上跑起来,串口能看到打印,然后再逐个功能去调。目录结构只是地图,真正的路要一步步走。