3. U-Boot 编译系统:Kconfig、Makefile、配置与编译流程详解
很多刚接触 U-Boot 的朋友,第一反应就是执行 make,然后等着出镜像。但一旦遇到编译报错,或者想裁剪某个驱动,就完全懵了。说白了,U-Boot 的编译系统比普通单片机工程复杂得多,它借鉴了 Linux 内核那套 Kconfig + Makefile 的玩法。今天我就带你把它拆开揉碎,看看里面到底是怎么运作的。
3.1 整体架构:三层结构
U-Boot 的编译系统可以看作三层:配置层、解析层、构建层。我个人习惯把这三层对应到三个核心工具:Kconfig、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 主要干三件事:
- 解析 .config,生成
include/config/auto.conf - 设置交叉编译工具链,比如
CROSS_COMPILE=arm-linux-gnueabihf- - 调用 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 时:
- 顶层 Makefile 读取
.config和auto.conf - 设置
ARCH、CROSS_COMPILE等环境变量 - 遍历
lib/、common/、drivers/、board/等目录 - 每个子目录根据
obj-y和obj-$(CONFIG_*)编译对应的.c文件为.o - 所有
.o文件链接成u-bootELF 文件 - 通过
objcopy生成u-boot.bin(纯二进制) - 可选步骤:生成
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 的编译世界里游刃有余了。