3. U-Boot 编译系统:Kconfig、Makefile、配置与编译流程详解

很多刚接触 U-Boot 的朋友,第一反应就是执行 make,然后等着出镜像。但一旦遇到编译报错,或者想裁剪某个驱动,就完全懵了。说白了,U-Boot 的编译系统比普通单片机工程复杂得多,它借鉴了 Linux 内核那套 Kconfig + Makefile 的玩法。今天我就带你把它拆开揉碎,看看里面到底是怎么运作的。

3.1 整体架构:三层结构

U-Boot 的编译系统可以看作三层:配置层、解析层、构建层。我个人习惯把这三层对应到三个核心工具:Kconfig、Makefile、以及编译脚本。

核心理解:配置层决定“要什么”,解析层决定“怎么找”,构建层决定“怎么编”。

U-Boot 编译系统三层结构 配置层(Kconfig) Kconfig 文件 → menuconfig / defconfig → .config 解析层(Makefile) 顶层 Makefile → scripts/Makefile.build → 子目录 Makefile 构建层(编译与链接) 编译 .c → .o → 链接生成 u-boot.bin / u-boot.img 配置决定编译内容,Makefile 决定编译规则,最终输出可执行镜像

3.2 Kconfig:配置系统的基石

Kconfig 是 U-Boot 配置系统的核心。它定义了一系列配置选项,比如 CONFIG_CMD_MEMORY、CONFIG_DM_GPIO 等等。每个选项都有类型(bool、int、string)、依赖关系、默认值。

你运行 make menuconfig 时,看到的那个图形界面,就是解析这些 Kconfig 文件渲染出来的。我刚开始接触时,以为这些配置是写死在头文件里的,后来才发现,include/configs/ 下的头文件只是历史遗留,新代码都走 Kconfig 了。

3.2.1 Kconfig 语法速览

一个典型的 Kconfig 条目长这样:

config CMD_MEMORY
    bool "memory commands"
    default y
    help
      Support for memory commands (md, mm, mw, cp, cmp, etc.)

这里 bool 表示开关选项,default y 表示默认开启。如果某个驱动依赖另一个选项,可以用 depends on:

config DM_GPIO
    bool "Driver Model for GPIO"
    depends on DM
    help
      Enable driver model for GPIO.

我的经验:调试时如果某个驱动死活不生效,先检查 .config 里对应的 CONFIG_* 有没有被 depends on 给屏蔽掉。我曾经花了一下午查一个网卡驱动,最后发现是 depends on PHY_LIB 没开。

3.2.2 defconfig 与 .config

每个板子都有一个默认配置文件,放在 configs/ 目录下,比如 configs/stm32mp15_defconfig。执行 make stm32mp15_defconfig 时,系统会把这份文件复制成 .config,然后运行 make olddefconfig 自动补全所有未定义的选项。

文件 作用 是否可手动修改
configs/xxx_defconfig 板级默认配置,版本管理用 建议通过 make savedefconfig 更新
.config 当前实际使用的配置 可以手动改,但运行 menuconfig 后会被覆盖
include/config/auto.conf 由 .config 自动生成,供 Makefile 使用 不要手动改

注意:千万不要直接修改 .config 后提交到 Git,别人拉下来重新 make menuconfig 就丢了。正确的做法是 make savedefconfig,然后替换 configs/ 下的文件。

3.3 Makefile:编译的指挥中心

U-Boot 的 Makefile 体系非常庞大,但核心逻辑并不复杂。顶层 Makefile 负责整体构建流程,然后递归调用子目录的 Makefile。

3.3.1 顶层 Makefile 做了什么

顶层 Makefile 主要干三件事:

  1. 解析 .config,生成 include/config/auto.conf
  2. 设置交叉编译工具链,比如 CROSS_COMPILE=arm-linux-gnueabihf-
  3. 调用 scripts/Makefile.build,逐层编译各个子目录

举个例子,当你执行 make 时,顶层 Makefile 会找到 u-boot.bin 这个目标,它的依赖是 u-boot(无后缀的 ELF 文件),而 u-boot 又依赖所有 .o 文件。这些 .o 文件怎么来?就是通过递归编译各个子目录。

3.3.2 子目录 Makefile 的写法

U-Boot 子目录的 Makefile 非常简洁,通常只包含 obj-y 和 obj-$(CONFIG_*):

obj-y += stm32mp1_clk.o
obj-$(CONFIG_CLK_STM32MP1) += clk_stm32mp1.o
obj-$(CONFIG_PINCTRL_STM32MP1) += pinctrl_stm32mp1.o

这里 obj-y 表示总是编译,obj-$(CONFIG_CLK_STM32MP1) 表示只有当 CONFIG_CLK_STM32MP1 为 y 时才编译。如果配置为 m(模块),U-Boot 不支持,因为它是裸机程序。

关键点:U-Boot 的 Makefile 通过 obj-* 变量实现条件编译,所有源文件路径都是相对于 $(srctree) 的。

3.4 配置与编译完整流程

从零开始编译 U-Boot,通常就两步:配置 + 编译。但这两步背后发生的事情,值得你了解。

3.4.1 配置阶段

执行 make stm32mp15_defconfig 时:

  • 读取 configs/stm32mp15_defconfig
  • 写入 .config
  • 运行 make olddefconfig,自动补全所有未定义的选项为默认值
  • 生成 include/config/auto.conf 和 include/config/autoconf.h

如果你执行 make menuconfig,还会额外启动一个基于 ncurses 的图形界面,修改后保存回 .config。

3.4.2 编译阶段

执行 make 时:

  1. 顶层 Makefile 读取 .config 和 auto.conf
  2. 设置 ARCH、CROSS_COMPILE 等环境变量
  3. 遍历 lib/、common/、drivers/、board/ 等目录
  4. 每个子目录根据 obj-y 和 obj-$(CONFIG_*) 编译对应的 .c 文件为 .o
  5. 所有 .o 文件链接成 u-boot ELF 文件
  6. 通过 objcopy 生成 u-boot.bin(纯二进制)
  7. 可选步骤:生成 u-boot.img(带 U-Boot 头部)

避坑指南:我曾经在编译时遇到 undefined reference to ... 错误,查了半天发现是某个 .o 文件没被编译进来。原因很简单——对应的 obj-$(CONFIG_XXX) 里的 CONFIG_XXX 没开。所以遇到链接错误,第一反应应该是去 .config 里搜那个符号对应的配置项。

3.5 常见编译问题与排查方法

问题现象 可能原因 排查方法
make menuconfig 报错 缺少 ncurses 库 sudo apt install libncurses-dev
编译报错 implicit declaration 头文件路径不对或宏未定义 检查 include/config/autoconf.h
链接报错 undefined reference 对应驱动未编译 检查 .config 中 CONFIG_* 是否开启
生成的 u-boot.bin 过大 开启了太多调试选项 关闭 CONFIG_DEBUG 和 CONFIG_LOG

重要:如果你修改了 .config,一定要执行 make olddefconfig 或重新 make menuconfig,否则 auto.conf 不会更新,编译结果可能还是旧的。

3.6 小结

U-Boot 的编译系统虽然复杂,但核心逻辑很清晰:Kconfig 管配置,Makefile 管编译,两者通过 .config 和 auto.conf 衔接。你只要记住:

  • 配置不对,编译白费
  • 链接报错,先查配置
  • 不要手动改 .config 提交

嗯,掌握了这些,你就能在 U-Boot 的编译世界里游刃有余了。