3. U-Boot 配置系统:Kconfig、Makefile、defconfig 的工作原理
U-Boot 的配置系统,说白了就是一套「搭积木」的规则。你告诉它要哪些功能,它帮你把对应的代码编译进去。很多刚接触移植的朋友,上来就改源码,改完发现编译不过,或者功能没打开——其实就是没搞懂这三兄弟怎么配合的。
我个人习惯把这三者的关系画成一个三角:Kconfig 定义选项,defconfig 保存选择,Makefile 执行编译。缺一个,配置系统就转不起来。
核心关系一句话:
- 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 的标准流程是:
make <板子名>_defconfig—— 加载默认配置make menuconfig—— 按需修改选项make savedefconfig—— 生成精简的 defconfig- 把生成的
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,就跳过。
编译流程总结:
make xxx_defconfig→ 读取configs/xxx_defconfig,生成.configmake→ 顶层 Makefile 读取.config,逐层进入子目录- 子目录 Makefile 根据
obj-$(CONFIG_xxx)决定编译哪些 .c 文件 - 链接器把所有 .o 文件链接成 u-boot 镜像
3.4 三者的协作流程
我画了一张流程图,帮你理清整个配置到编译的完整链路:
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 移植的入口,搞懂了它,后面的驱动移植、启动流程分析才会顺手。