3. U-Boot源码结构解析:目录树、关键文件与配置系统

U-Boot 的源码,说实话,第一次打开的时候确实有点懵。我记得我刚入行那会儿,面对几十个文件夹、上千个文件,完全不知道从哪下手。后来做项目多了,慢慢摸清了它的套路。说白了,U-Boot 的源码结构虽然庞大,但逻辑非常清晰——它把「硬件相关」、「板级相关」、「通用逻辑」和「工具脚本」分得明明白白。

这一章,我就带你把这棵「源码树」从头到尾捋一遍。重点看三个东西:目录结构、三个关键文件、以及Kconfig/Makefile 配置系统。搞懂这些,你以后移植 U-Boot 就能少走很多弯路。

3.1 U-Boot 目录树分析

先看整体。U-Boot 的顶层目录大概有 20 多个,我挑最核心的给你讲。下面这个表格,是我自己总结的「必看目录清单」:

目录名 作用 我的建议
arch/ CPU 架构相关代码(ARM、RISC-V、x86 等) 移植时最常改的地方
board/ 具体板级初始化代码 每个板子一个文件夹
common/ 通用命令和核心逻辑 main.c 就在这里
drivers/ 各种外设驱动(串口、网卡、MMC 等) 裁剪时重点关注
include/ 头文件和配置文件 configs/ 子目录放板级配置
lib/ 通用库函数(字符串、CRC、压缩等) 一般不用动
net/ 网络协议栈 支持 TFTP、NFS 等
fs/ 文件系统支持(ext4、FAT、UBIFS 等) 按需裁剪
tools/ 主机端工具(mkimage、fdt 等) 编译时会用到
scripts/ Kconfig 和构建脚本 配置系统的核心

嗯,这里要注意:arch/ 和 board/ 是移植工作的主战场。我做过一个项目,CPU 是 ARM Cortex-A7,板子是自研的。当时我只需要在 arch/arm/ 下找到对应的 CPU 目录,然后在 board/ 下新建一个板级文件夹,大部分工作就完成了。

我的习惯:拿到一个新板子,先看 board/ 下有没有类似的参考板。有的话直接复制一份改,比从头写快得多。

3.2 关键文件功能解读

U-Boot 启动过程中,有三个文件是绕不开的。我按启动顺序给你讲:start.S → board.c → main.c。

3.2.1 start.S:第一行代码

这是 U-Boot 的入口点。CPU 上电后,第一条指令就从这里开始。它主要干三件事:

  • 设置 CPU 模式:关中断、设栈指针、切到 SVC 模式
  • 初始化关键硬件:时钟、DDR 控制器、串口
  • 重定位自身:把 U-Boot 从 Flash 拷贝到 DDR 中运行

我截一段典型的 ARM 启动代码给你看:

// arch/arm/cpu/armv7/start.S 片段
reset:
    /* 设置 CPU 为 SVC 模式,关中断 */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0xd3
    msr cpsr, r0

    /* 初始化关键寄存器 */
    bl cpu_init_cp15
    bl cpu_init_crit

    /* 跳转到 board_init_f */
    bl board_init_f

为什么先关中断?因为这时候中断向量表还没设好,万一来了个中断,CPU 就不知道跳哪去了。我在项目中遇到过一个问题:板子偶尔启动卡死,查了两天才发现是 start.S 里没关看门狗,导致在 DDR 初始化过程中被复位了。

避坑指南:我曾经在移植时忘了在 start.S 里关看门狗,结果板子反复重启。后来养成了习惯:任何板子,第一件事就是在 start.S 里加上关看门狗的代码。

3.2.2 board.c:板级初始化

从 start.S 跳出来后,控制权交给 board_init_f() 和 board_init_r()。这两个函数都在 common/board.c 里。

board_init_f 负责「前期初始化」——这时候 U-Boot 还在 Flash 里运行,只能做轻量级工作:

  • 初始化串口(为了能看到打印信息)
  • 初始化定时器
  • 初始化 DDR 控制器
  • 规划内存布局

board_init_r 负责「后期初始化」——这时候 U-Boot 已经重定位到 DDR 了,可以大展拳脚:

  • 初始化所有驱动(网卡、MMC、USB 等)
  • 设置环境变量
  • 使能中断
  • 进入主循环

你想想看,为什么要分两个阶段?因为早期代码在 Flash 里跑,速度慢、空间小,只能做最基本的事。等 DDR 起来了,再把完整代码搬过去,后面就快了。

3.2.3 common/main.c:命令行主循环

这是 U-Boot 的「用户界面」。初始化完成后,就进入 main_loop():

// common/main.c 核心逻辑
void main_loop(void)
{
    /* 检查是否有自动启动命令 */
    if (autoboot_command())
        return;

    /* 进入命令行交互模式 */
    cli_loop();
}

这里有两个分支:

  • 自动启动:如果设置了 bootcmd 环境变量,并且倒计时内没被用户打断,就自动执行启动命令(比如加载内核)
  • 命令行模式:否则进入交互式 Shell,等待用户输入命令

我个人习惯把 bootcmd 设成 tftp 0x82000000 zImage; bootz 0x82000000,这样开发时插上网线就能自动下载内核启动,省得每次手动敲命令。

3.3 配置系统:Kconfig / Makefile

U-Boot 的配置系统是从 Linux 内核学来的。说白了,就是用 make menuconfig 选功能,然后自动生成配置头文件。这套系统由两部分组成:

3.3.1 Kconfig:定义配置选项

Kconfig 文件定义了「有哪些配置项可以选」。比如你想支持网络功能,就会看到这样的定义:

// drivers/net/Kconfig 片段
config CMD_NET
    bool "Enable networking commands"
    default y
    help
      支持 ping、tftp 等网络命令

每个配置项都有类型(bool、int、string 等)、默认值和帮助信息。选上后,会在 include/config.h 里生成 #define CONFIG_CMD_NET 1。

3.3.2 Makefile:条件编译

Makefile 根据 Kconfig 生成的配置,决定编译哪些文件。比如:

// drivers/net/Makefile 片段
obj-$(CONFIG_CMD_NET) += net.o
obj-$(CONFIG_CMD_PING) += ping.o

如果 CONFIG_CMD_NET 为 y,就编译 net.o;否则跳过。这就是裁剪的基础——不选的功能,代码根本不编译。

核心思路:裁剪 U-Boot 就是通过 Kconfig 关掉不需要的功能,让 Makefile 只编译必要的代码。我做过一个项目,默认配置编译出来 800KB,裁剪后只剩 200KB,完全够用。

3.4 本章知识结构图

下面这张图,把 U-Boot 源码的核心脉络画出来了。从目录结构到关键文件,再到配置系统,一条线串起来:

U-Boot 源码核心脉络 U-Boot 目录树 arch/ | board/ | common/ | drivers/ | include/ | lib/ | net/ | fs/ | tools/ | scripts/ 三个关键文件(启动顺序) start.S board.c main.c CPU初始化 板级初始化 命令行循环 配置系统 Kconfig:定义配置选项 Makefile:条件编译 输出:裁剪后的 U-Boot 镜像 只包含选中的功能,体积更小、启动更快 移植时重点关注 arch/ 和 board/ 目录

3.5 小结

这一章我们干了三件事:

  • 把 U-Boot 目录树拆开看了,知道每个目录是干什么的
  • 分析了三个关键文件——start.S 是入口、board.c 是板级初始化、main.c 是用户交互
  • 搞懂了 Kconfig/Makefile 配置系统——说白了就是「选什么就编译什么」