3. U-Boot 配置系统:Kconfig、Makefile、defconfig 的工作原理

U-Boot 的配置系统,说白了就是一套「搭积木」的规则。你告诉它要哪些功能,它帮你把对应的代码编译进去。很多刚接触移植的朋友,上来就改源码,改完发现编译不过,或者功能没打开——其实就是没搞懂这三兄弟怎么配合的。

我个人习惯把这三者的关系画成一个三角:Kconfig 定义选项,defconfig 保存选择,Makefile 执行编译。缺一个,配置系统就转不起来。

核心关系一句话:

  • Kconfig:菜单系统,定义「有什么功能可以选」
  • defconfig:默认配置,记录「我选了哪些功能」
  • Makefile:构建脚本,根据配置决定「编译哪些文件」
U-Boot 配置系统三角关系 Kconfig 定义功能选项 defconfig 保存选择结果 Makefile 执行条件编译 配置流程:make menuconfig → 修改 Kconfig 选项 → 保存到 defconfig → Makefile 读取执行 三者缺一不可,任何一个环节出错都会导致配置失效

3.1 Kconfig:配置菜单的「菜单项」

Kconfig 文件定义了一个个配置选项。每个选项有名字、类型、依赖关系和帮助信息。你运行 make menuconfig 时看到的界面,就是由这些 Kconfig 文件生成的。

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

config CONFIG_CMD_BOOTM
    bool "bootm command"
    default y
    help
      Boot an application image from memory.
      This is the standard U-Boot boot command.

这里有几个关键字段:

  • config:定义选项名,最终会变成 CONFIG_CMD_BOOTM=y
  • bool:类型,表示「要么选要么不选」
  • default y:默认值,不修改时默认开启
  • help:帮助信息,menuconfig 里按 ? 能看到

我的经验:移植时最常踩的坑是依赖关系没配好。比如你选了某个驱动,但它依赖的时钟或 GPIO 控制器没选,编译直接报错。我曾经在 i.MX6 平台上折腾了一下午,最后发现是 depends on 链断了一环。

Kconfig 支持的类型不止 bool,还有:

类型 说明 典型用法
bool 二选一:y / n 功能开关
tristate 三选一:y / m / n Linux 驱动模块(U-Boot 很少用)
int 整数 缓冲区大小、超时时间
hex 十六进制数 内存地址、寄存器基址
string 字符串 设备树路径、默认环境变量

3.2 defconfig:你的「配置快照」

defconfig 文件保存在 configs/ 目录下,命名规则是 <板子名>_defconfig。比如我常用的 mx6ull_14x14_evk_defconfig。

这个文件的内容很简单,就是一行行 CONFIG_xxx=y 或 CONFIG_xxx=值。但它只记录与默认值不同的选项,不是全部选项。这样文件很小,也容易维护。

CONFIG_ARM=y
CONFIG_TARGET_MX6ULL_14X14_EVK=y
CONFIG_DM=y
CONFIG_DM_MMC=y
CONFIG_CMD_BOOTM=y
CONFIG_BOOTCOMMAND="run distro_bootcmd"

注意:defconfig 里不要写 CONFIG_xxx=n。因为 Kconfig 的默认值已经是 n,写了反而多余。而且如果上层依赖改了,这个 n 可能会覆盖掉正确的默认值。我见过有人把 CONFIG_DM=n 写进 defconfig,结果整个驱动模型都关了,板子起不来。

生成 defconfig 的标准流程是:

  1. make <板子名>_defconfig —— 加载默认配置
  2. make menuconfig —— 按需修改选项
  3. make savedefconfig —— 生成精简的 defconfig
  4. 把生成的 defconfig 文件复制到 configs/ 目录

嗯,这里要注意:make savedefconfig 生成的 defconfig 是最小化的,只包含与 Kconfig 默认值不同的选项。这样提交到版本管理时,diff 非常干净。

3.3 Makefile:把配置变成代码

配置选好了,怎么让编译器知道该编译哪些文件?答案在 Makefile 里。

U-Boot 的 Makefile 体系分三层:

  • 顶层 Makefile:负责整体构建流程,包含所有子目录
  • 子目录 Makefile:每个模块自己的编译规则
  • .config 文件:由 defconfig 生成,Makefile 通过它判断条件

子目录 Makefile 里最常见的写法是:

obj-$(CONFIG_CMD_BOOTM) += cmd_bootm.o
obj-$(CONFIG_DM_MMC) += mmc-uclass.o
obj-$(CONFIG_MXC_UART) += serial_mxc.o

这里的 obj-$(CONFIG_xxx) 是 Makefile 的条件赋值语法。如果 CONFIG_CMD_BOOTM=y,那么 obj-y 就包含 cmd_bootm.o,这个文件会被编译。如果没定义或为 n,就跳过。

编译流程总结:

  1. make xxx_defconfig → 读取 configs/xxx_defconfig,生成 .config
  2. make → 顶层 Makefile 读取 .config,逐层进入子目录
  3. 子目录 Makefile 根据 obj-$(CONFIG_xxx) 决定编译哪些 .c 文件
  4. 链接器把所有 .o 文件链接成 u-boot 镜像

3.4 三者的协作流程

我画了一张流程图,帮你理清整个配置到编译的完整链路:

配置到编译完整流程 make xxx_defconfig 生成 .config 文件 make menuconfig(可选) 更新 .config make(编译) 生成 u-boot.bin 关键文件:.config(当前配置)→ include/configs/(板级头文件)→ include/autoconf.mk(Makefile 可读) 配置信息最终会转化为 C 语言的宏定义和 Makefile 的条件变量

3.5 避坑指南

做移植这几年,我在配置系统上栽过不少跟头。挑几个典型的说说:

我曾经犯过的错:

  • 直接改 .config 文件:跑 make menuconfig 后修改被覆盖,白改。正确做法是改 defconfig 或 menuconfig 里改。
  • 忘了 make savedefconfig:手动编辑 defconfig 漏了依赖选项,别人用你的 defconfig 编译不过。
  • Kconfig 里写死地址:比如把 DDR 配置写进 Kconfig 的 default,换板型就得改 Kconfig,非常不灵活。建议用设备树或头文件。
  • 依赖链断裂:选了 CONFIG_DM_MMC 但没选 CONFIG_DM,编译报一堆 undefined reference。

我的调试技巧:如果配置没生效,先检查 .config 文件里有没有对应的 CONFIG_xxx=y。然后看 include/autoconf.mk 里有没有生成对应的变量。最后去子目录 Makefile 里搜 obj-$(CONFIG_xxx),确认条件匹配。三步走下来,99% 的问题都能定位。

3.6 小结

Kconfig、defconfig、Makefile 这三者,说白了就是一套「定义-选择-执行」的流水线。你不需要背下所有语法,但一定要理解它们之间的数据流向。移植时遇到配置问题,顺着这条链查下去,很快就能找到根因。

嗯,这一节的内容就到这里。记住:配置系统是 U-Boot 移植的入口,搞懂了它,后面的驱动移植、启动流程分析才会顺手。