4. U-Boot源码结构分析:目录、关键文件与编译系统

好,我们进入第四章。前面几章我们把Bootloader的概念、启动流程、硬件基础都过了一遍。从这章开始,咱们要真正动手“解剖”U-Boot了。我个人觉得,理解源码结构是掌握U-Boot的第一步,也是最关键的一步。你想想看,一个几万行甚至几十万行的代码仓库,如果没有清晰的结构,你连从哪里下手都不知道。

这一章,我会带你从三个层面来拆解:目录结构、三个关键文件、以及编译系统。说白了,就是让你知道U-Boot的“骨架”长什么样,“心脏”在哪里跳,以及怎么把它“组装”起来。

4.1 U-Boot目录结构:一张地图走天下

我第一次接触U-Boot源码时,第一反应是“这玩意儿怎么这么大?” 别慌,它其实很有条理。我们把它当成一张地图,记住几个核心区域就行。

核心原则:U-Boot的目录设计遵循“架构相关”与“板级相关”分离的原则。通用代码放在顶层,特定于CPU或开发板的代码放在各自的子目录下。

下面这张SVG图,是我为你画的U-Boot源码“导航图”。你可以把它当作一个索引,后面讲到具体文件时,随时回来对照。

U-Boot 源码核心目录结构图 U-Boot 源码核心目录结构 U-Boot 根目录 arch/ board/ common/ drivers/ arm/ x86/ riscv/ sandbox/ ti/ freescale/ cli/ spl/ serial/ net/ mmc/ include/ lib/ tools/ scripts/ 💡 说明: arch/ 存放CPU架构相关代码;board/ 存放具体开发板代码;common/ 存放通用命令和功能; drivers/ 存放外设驱动;include/ 存放头文件;lib/ 存放库函数;tools/ 存放主机工具。

我们来逐个看看这些目录是干什么的:

目录名 作用 我的经验
arch/ CPU架构相关代码。比如arm、x86、riscv。这里面是汇编和C的底层实现。 移植到新CPU时,这里是你第一个要改的地方。
board/ 具体开发板代码。每个厂商或板子一个子目录。 我习惯先在这里找参考板,复制一份再改。
common/ 通用功能。包括命令行、环境变量、启动流程等。 main.c就在这里,它是C语言的入口。
drivers/ 外设驱动。串口、网卡、MMC、USB等。 调试新硬件时,这里是你最常光顾的地方。
include/ 头文件。包括配置头文件(configs/)。 配置板子就是改这里面的头文件。
lib/ 库函数。比如字符串操作、CRC校验等。 一般不用动,但可以学习里面的实现。
tools/ 主机端工具。比如mkimage用于制作镜像。 烧录时经常用到,后面实战会讲。
scripts/ 编译脚本和配置文件。 Kconfig和Makefile都在这里。

我的小技巧:刚开始看U-Boot源码时,别试图一次性读完所有目录。先盯着 arch/、board/ 和 common/ 这三个目录看,它们构成了U-Boot的主干。其他目录像 drivers/ 和 lib/,用到哪个再看哪个。

4.2 三个关键文件:U-Boot的“心脏”

目录结构是骨架,那代码就是血肉。在U-Boot里,有三个文件特别重要,它们分别代表了启动的三个阶段:汇编入口、C语言初始化、命令行交互。

4.2.1 start.S:从零开始的汇编世界

start.S 是U-Boot的“第一行代码”。CPU上电后,第一条指令就是从这里开始执行的。它通常位于 arch/arm/cpu/armv7/start.S 这样的路径下。

它干了哪些事?

  • 设置CPU模式:比如从SVC模式切换到管理模式。
  • 关闭中断:在初始化完成前,不允许任何中断打扰。
  • 设置栈指针:为C语言运行做准备。没有栈,C代码寸步难行。
  • 清零BSS段:把未初始化的全局变量清零。
  • 跳转到C入口:最后调用 board_init_f(),进入C语言世界。

注意:start.S 是纯汇编写的。不同架构(ARM、x86、RISC-V)的start.S完全不同。我曾经在移植到一款RISC-V开发板时,因为没仔细看start.S里的异常向量表设置,导致中断一直不响应,查了两天才发现是向量表地址没对齐。嗯,这种坑,踩过一次就记住了。

4.2.2 board.c:板级初始化的“大管家”

board.c 通常位于 board/你的板子/ 目录下。它是板级初始化的核心。start.S 跳转到 board_init_f() 后,就进入了 board.c 的地盘。

它主要做这些事:

  • 初始化时钟:让CPU和总线跑起来。
  • 初始化串口:这样你才能看到打印信息。我调试时最依赖的就是串口输出。
  • 初始化DRAM:检测并配置内存控制器。
  • 初始化NAND/MMC:为加载内核做准备。
  • 调用 board_init_r():进入“重定位”后的第二阶段初始化。

说白了,board.c 就是U-Boot的“硬件适配层”。你换一块开发板,主要就是改这个文件。

4.2.3 main.c:命令行交互的“大脑”

main.c 位于 common/ 目录下。当U-Boot完成所有初始化后,就会进入 main_loop() 函数。这个函数就是U-Boot的命令行交互循环。

它的核心逻辑:

  1. 检查是否有自动启动命令(比如 bootcmd 环境变量)。
  2. 如果没有,或者用户按下任意键打断,就进入命令行。
  3. 解析用户输入的命令,比如 printenv、tftp、bootm。
  4. 执行对应的命令处理函数。
  5. 循环等待下一个命令。

一句话总结:start.S 负责“活过来”,board.c 负责“认识硬件”,main.c 负责“听你指挥”。

4.3 编译系统:Kconfig + Makefile 的协奏曲

U-Boot的编译系统,说白了就是一套“配置-编译”的自动化流程。它借鉴了Linux内核的编译方式,使用 Kconfig 进行配置,Makefile 进行编译。

我刚开始用的时候,觉得这东西好复杂。后来发现,你只需要记住几个关键步骤和命令就行。

4.3.1 配置阶段:Kconfig 与 defconfig

Kconfig 是一种图形化或菜单式的配置系统。你运行 make menuconfig 时,看到的那个蓝底菜单就是它。

  • Kconfig 文件:定义了有哪些配置选项,比如是否支持某个驱动。
  • defconfig 文件:每个开发板都有一个默认配置文件,比如 configs/sandbox_defconfig。

配置流程很简单:

# 先加载默认配置
make sandbox_defconfig

# 然后可以手动微调
make menuconfig

配置完成后,会生成一个 .config 文件,里面记录了所有你选择的选项。

4.3.2 编译阶段:Makefile 的魔法

配置好之后,直接 make 就行。U-Boot的Makefile会做以下几件事:

  1. 读取 .config 文件,决定哪些文件需要编译。
  2. 递归进入各个子目录,编译对应的 .c 和 .S 文件。
  3. 链接所有目标文件,生成最终的 u-boot 可执行文件。
  4. 根据配置,生成不同格式的镜像,比如 u-boot.bin、u-boot.img。

避坑指南:我曾经在编译时遇到“找不到头文件”的错误。后来发现,是因为我改了 include/configs/ 下的头文件,但没有清理之前的编译缓存。解决办法是:先 make distclean,再重新配置和编译。记住,改配置或头文件后,一定要 make clean 或 make distclean。

4.3.3 编译产物:你最终要烧录的东西

编译完成后,你会在源码根目录下看到一堆文件。其中最重要的几个:

文件名 说明 烧录场景
u-boot ELF格式的可执行文件,带调试信息。 用于GDB调试,不直接烧录。
u-boot.bin 纯二进制镜像,去掉了ELF头部。 直接烧录到NAND/NOR Flash。
u-boot.img U-Boot自描述的镜像格式,带头部信息。 通过U-Boot自身或SPL加载。
u-boot.srec Motorola S-record格式,文本形式。 通过串口或JTAG烧录。

嗯,到这里,U-Boot的源码结构、关键文件和编译系统就讲完了。你可能会觉得信息量有点大,没关系,后面几章我们会反复用到这些知识。特别是当你开始动手移植和烧录时,你会对今天讲的内容有更深的体会。