☰
U-Boot移植必懂:Kbuild构建系统配置与编译流程解析
2026/10/8 13:04:22 网站建设 项目流程

我最早接触U-Boot移植的时候,卡住的不是某个外设驱动,而是编译本身。照着教程执行make xxx_defconfig,一切正常,接着make,屏幕上蹦出一堆让我摸不着头脑的语法和错误。翻遍网上文章,大家只会说“把这块改一下、那块加上”,没人告诉我U-Boot用的Kbuild构建系统,跟单片机那种顶层Makefile完全是两套逻辑。后来在Linux内核的文档里翻了很久,才明白U-Boot的Kbuild就是内核构建系统的近亲。这篇文章想把这块补上:从Kbuild的角度,把U-Boot移植时你绕不开的配置流程、目录结构、编译链路讲清楚,再附上我亲测踩过的坑。适合刚准备移植U-Boot、或者已经在移植但被构建系统折腾得头疼的人。

1. 移植U-Boot先过Kbuild这关:它和普通Makefile到底差在哪

1.1 单片机式Makefile的限制

玩过STM32或各类MCU的人,大多习惯了“一个顶层Makefile管所有源文件”的模式。你定义一堆SRC变量,然后gcc -c逐个编译,最后链接。这种模式在单个芯片、十几个文件的项目里完全够用,遇到性能问题也可以手动调整编译顺序,问题都不大。

但U-Boot面对的是另一回事:几十个架构、几百家厂商、上千块开发板、成千上万个可选驱动。如果任何一个板子都直接把全世界的源文件编译进去,固件体积会直接失控,而且代码维护也根本没法做——你可能会在启动流程里看到一堆跟你的板子毫无关系的驱动代码。Kbuild要解决的核心问题就是两个:第一,怎么让每一个配置组合都只编译对应的文件;第二,怎么让这个编译过程在几十层目录里可维护。

U-Boot作为从Linux内核“继承”下来的体系,解决方式也是从内核那里继承的,也就是递归式Kbuild。这套东西不是为“简单项目”设计的,它的目标就是支撑超大规模、多平台、可裁剪的固件编译。所以如果你带着“Makefile就应该写在顶层”的思维来移植U-Boot,第一步就会觉得处处别扭。

1.2 Kbuild的递归构建与obj-y变量

Kbuild核心机制是“每个目录一份Makefile”。与普通项目不一样,这份Makefile不负责真正的编译,只声明本目录下哪些目标参与构建。格式大概是这样的:

# drivers/i2c/Makefile(示意,U-Boot中类似写法随处可见) obj-$(CONFIG_DM_I2C) += i2c-uclass.o obj-$(CONFIG_I2C_MUX) += i2c-mux.o

如果CONFIG_DM_I2C=y,则i2c-uclass.o会进编译列表;如果=n,连编译都不会发生。这就是“裁剪”的本质:配置项既影响C代码里的#ifdef,也影响Makefile里的obj-y变量。一个功能没开启,对应源文件压根不会出现在编译命令里,而不是编译了再用宏抠掉。

真正干活的编译脚本是scripts/Makefile.build,它从顶层往下逐层进入各个目录,读取目录下Makefile声明的obj-y / obj-$(CONFIG_*),编译出目标文件,最后还会打包成built-in.a,交给上一级。顶层再把这些built-in.a按照链接脚本拼装成u-boot镜像。

为什么要这样设计?因为如果把驱动源码直接写在顶层Makefile里,每新增一个文件都要维护一个全局路径列表;而分散到每个目录后,新增驱动只需在对应目录的Makefile加一行obj-y,互不干扰,厂商与社区协作也方便。对移植者来说,这个设计带来的实际影响是:当你找不到某个功能为什么没编译时,第一反应应该是去对应目录的Makefile看CONFIG条件,而不是去看顶层链接脚本。

1.3 Kconfig负责“开关”,Makefile负责“组合”

如果说Makefile是Kbuild的“组装车间”,那么Kconfig就是“开关面板”。每个目录下都有Kconfig文件,里面定义了一系列config条目,描述某个选项可以选y/n及依赖关系。比如:

config DM_SERIAL bool "Enable Driver Model for Serial" depends on DM help Enable support for UART under driver model.

这些Kconfig在make menuconfig时展示成菜单,你按空格选择y/n,最终结果写入.config。Makefile再通过obj-$(CONFIG_DM_SERIAL)读取这个结果。所以一个功能从“可配置”到“编进固件”要经过三关:Kconfig里定义 -> .config里置y -> 对应目录Makefile中用到这个变量。任何一环断了,功能都进不了u-boot.bin。

这一条理解到位之后,移植时很多困惑会自然消失。比如你在网上照抄一个defconfig片段,结果某项没生效,多半就是依赖不满足或者Kconfig里根本不存在这个名字,conf工具把这一行静默丢掉了。

2. 一条命令从配置到镜像:defconfig、Kconfig和编译脚本如何串起来

2.1 make xxx_defconfig到底做了什么

第一次移植时总要先执行make xxx_defconfig。configs目录下有很多defconfig文件,比如imx6ull_myboard_defconfig。这个文件不是完整的编译配置,它只是“预设的一组非默认值”。

执行make xxx_defconfig时,U-Boot会调用scripts/kconfig/conf工具,把defconfig内容与全树Kconfig定义做匹配,生成一个完整的.config。有些选项defconfig没写,就使用Kconfig里的default;有些选项defconfig写了,就以defconfig为准。命令要带ARCH和CROSS_COMPILE,实际使用类似于:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig

如果defconfig里某一行写了Kconfig中不存在的配置名,conf工具会打一个warning,然后忽略这一行。这一点特别坑,后面第4节会详细说。你可以理解成:defconfig是“基础衣架上的穿着清单”,Kconfig是“衣柜目录”,conf工具负责按着穿着清单到衣柜里找衣服,找不到就跳过,最后给你一张完整的穿衣表(.config)。

2.2 make之后:从.config到autoconf.h再到u-boot.bin

执行make后,Kbuild先读取.config,生成include/generated/autoconf.h。C代码里的#ifdef CONFIG_XXX读的就是autoconf.h。同时还会生成include/config/auto.conf等文件,供Makefile使用。

然后scripts/Makefile.build递归遍历所有目录。每个子目录根据obj-y变量编译出目标文件,打包成本目录的built-in.a。顶层再通过arch/arm/cpu/u-boot.lds链接脚本,把这些built-in.a和必要的启动入口文件(比如start.o)链接成u-boot ELF,再用objcopy转成u-boot.bin。

整个过程可以用下面这条命令观察:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8 V=1

加上V=1后,每个文件的实际编译命令都会打印出来。移植初期我建议打开V=1,把输出重定向到build.log,后面排查“哪个文件没编进去”时能直接翻日志,比在源码里猜快很多。

2.3 defconfig不是全量配置:默认值合并的理解

初学者最容易产生的一个误解:defconfig写了什么,.config就是什么。实际上,.config是defconfig与Kconfig默认值合并的结果。U-Boot甚至专门提供了一个命令make savedefconfig,用当前.config反向生成一份“最精简”的defconfig,只保留非默认项。很多厂商在发布板卡时用的defconfig就是这样生成的。

举个例子:如果你在Kconfig里把某个宏default y了,即使defconfig里不写,.config里也是y;反之如果defconfig里写了某个宏=y,但Kconfig有depends on不满足,conf会悄悄丢掉它。因此检查一个功能配置是否真的生效,最可靠的办法是直接查.config,而不是反复去读defconfig。

在移植阶段,我一般会习惯性地执行一下:

cp .config configs/myboard_defconfig.orig make savedefconfig diff configs/myboard_defconfig.orig defconfig

通过这个diff,能立刻看出当前生效配置里哪些项与defconfig里的显式项不一致,排查问题非常有帮助。

3. 添加一块新板卡:Kbuild视角的最小改动清单

3.1 确定SoC目录归属并创建板级目录

移植前先确认SoC架构目录归属。U-Boot的board目录按vendor组织,例如board/samsung、board/rockchip、board/freescale等。你要么在已有vendor目录下加一个子目录,要么新建自己的vendor目录,比如:

board/mycompany/myboard/ ├── Kconfig ├── MAINTAINERS ├── Makefile └── myboard.c

之所以必须放在board目录,是因为arch/arm/Kconfig里会通过source把这些board Kconfig统一引入。Makefile则是给Kbuild用的;myboard.c里面需要提供board_init、dram_init等入口函数。这个目录名会被多个配置宏引用:CONFIG_SYS_BOARD、CONFIG_SYS_VENDOR。Kbuild会根据这些变量去定位哪些文件参与编译。

一个很常见的新手错误是board目录放对了但defconfig里CONFIG_SYS_BOARD名称写错,导致链接时找不到board的built-in.a,报undefined reference。这时候你去看编译日志,可能只觉得是链接脚本问题,其实是配置项和目录名不匹配。

3.2 编写defconfig:五要素不能少

configs目录下新建myboard_defconfig(新版U-Boot可能按vendor分目录存放,比如configs/mycompany/,机制相同)。一个最基础的defconfig要包含以下几类内容:

CONFIG_SYS_CPU="armv7" CONFIG_SYS_SOC="myvendor-soc" CONFIG_SYS_BOARD="myboard" CONFIG_SYS_VENDOR="mycompany" CONFIG_SYS_CONFIG_NAME="myboard" CONFIG_TARGET_MYBOARD=y CONFIG_DEFAULT_DEVICE_TREE="myboard"

这些配置项的作用可以梳理成下表:

配置项作用关联目录
CONFIG_SYS_CPU指定CPU架构字符串arch/arm/cpu/
CONFIG_SYS_SOC指定SoC平台字符串arch/arm/mach-xxx/
CONFIG_SYS_BOARD指定板卡名board/mycompany/myboard/
CONFIG_SYS_VENDOR指定厂商名board/mycompany/
CONFIG_SYS_CONFIG_NAME决定板级头头文件位置include/configs/myboard.h
CONFIG_DEFAULT_DEVICE_TREE决定编译哪个dtsarch/arm/dts/

这类配置项很多是在U-Boot的一级Kconfig(比如arch/arm/Kconfig或顶层Kconfig)中定义好的,defconfig只是给它们赋值。如果你在defconfig里写了一个Kconfig没有的CONFIG_SYS_SOC值,编译时也可能只产生告警或直接导致链接错误,所以最好对照已有板卡的defconfig做一个模板,再逐项替换。

3.3 让Kconfig认识你的板卡

上面写的defconfig里有一行CONFIG_TARGET_MYBOARD=y,但如果你现在去执行make myboard_defconfig,conf工具会报“ignoring unknown option CONFIG_TARGET_MYBOARD”——因为Kconfig树里还没有这个选项。

需要创建board/mycompany/myboard/Kconfig:

if TARGET_MYBOARD config SYS_BOARD default "myboard" config SYS_VENDOR default "mycompany" config SYS_CONFIG_NAME default "myboard" endif

然后必须让上层Kconfig把它include进来。通常做法是在arch/arm/Kconfig中,找到对应SoC架构区域,添加:

source "board/mycompany/myboard/Kconfig"

同时定义选项本身:

config TARGET_MYBOARD bool "Support MyBoard board" select DM select OF_CONTROL help MyCompany MyBoard based on ...

理解这里的逻辑:select表示选中TARGET_MYBOARD后强制开启后面的选项。比如TARGET_MYBOARD=y就必须启用DM、OF_CONTROL。但这不能乱用,第4节会详细讲。另外要注意,defconfig里CONFIG_TARGET_MYBOARD=y要落到.config里,前提是这一项在Kconfig中可见、没有违背depends on关系;一旦可见且无依赖冲突,它就会作为一个bool项被置y。

3.4 在include/configs和dts/Makefile里登记

传统上,板级基础参数仍然放在include/configs/myboard.h里,例如:

#ifndef __CONFIG_H #define __CONFIG_H #include <linux/sizes.h> #define CONFIG_SYS_SDRAM_BASE 0x80000000 #define CONFIG_SYS_SDRAM_SIZE (512 * 1024 * 1024) #define CONFIG_SYS_LOAD_ADDR CONFIG_SYS_SDRAM_BASE #include "myboard_common.h" #endif

这个头文件会被Kbuild自动找到,前提是你defconfig里的CONFIG_SYS_CONFIG_NAME="myboard"。如果名字对不上,编译期会报找不到头文件的错误,或者板级配置全部缺失。设备树方面,在arch/arm/dts下放myboard.dts,并在该目录Makefile里登记:

dtb-$(CONFIG_TARGET_MYBOARD) += myboard.dtb

这样编译时Kbuild就会把dts编成dtb,并通过CONFIG_OF_CONTROL嵌入到u-boot.bin。很多刚接触U-Boot的人会忽略这个Makefile登记,结果defconfig里CONFIG_DEFAULT_DEVICE_TREE写对了,但编译出来的镜像没有包含dtb,启动时直接卡在“device tree not found”。这个坑极其常见。

3.5 从配置到编译的验证步骤

做完上面这些,回到U-Boot根目录:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

重点看两处:

  1. make myboard_defconfig时是否有“ignoring unknown option”警告;如果有,说明defconfig里有Kconfig不认识的项。
  2. make结束时u-boot.bin是否生成,以及lds链接日志里是否有你板级目录的built-in.a。

还可以用grep CONFIG_TARGET_MYBOARD .config确认选项没丢,用strings u-boot | grep U-Boot看版本字符串。如果一切正常,串口上通常能出现U-Boot自己的启动信息,哪怕DDR初始化还没调好,至少到了印刷logo这一步,说明Kbuild这条主线已经通了。我一般把这一步当作“移植成功的一半”,剩下的事情都建立在这条主线可靠的基础上。

4. 第一次移植最容易踩的坑:Kconfig的“选择陷阱”和宏定义的代际冲突

4.1 配置项被静默丢弃:依赖不满足的完整排查

先给一个真实场景。假设你移植时想在defconfig里加一行CONFIG_USB_GADGET=y,然后满怀期待地make,结果u-boot.bin里根本没有相关代码。第一步,grep CONFIG_USB_GADGET .config。如果找不到,说明它在配置阶段就被conf丢弃了。第二步,去drivers/usb/gadget/Kconfig看这个选项的定义,通常它是config USB_GADGET,depends on更底层的USB相关开关。如果最底层的USB支持没有开启,USB_GADGET哪怕defconfig里写了也会静默失效。

第三步,在menuconfig里按/搜索“USB_GADGET”,检查它是不是灰色的、为什么变灰,能直观看到依赖链。这一步比看代码更高效,因为Kconfig会直接列出这个选项当前不满足的依赖是什么。经验总结就是:defconfig不是Oracle,写了不等于生效。移植时凡是“功能没编进去”,先查依赖链,再查Makefile,最后才怀疑代码本身。这个顺序可以省下大量无效调试时间。

4.2 select与depends on:依赖链被打破的警告

Kconfig中定义板卡选项时,很多人喜欢用select把功能一口气打开。select的逻辑是“如果你选我,我就强制选别人”,看起来省事,但它有个容易爆雷的地方:select不检查被选方的depends on约束。比如:

config TARGET_MYBOARD bool "Support MyBoard board" select PHY_FIXED

而PHY_FIXED本身可能有depends on DM或depends on NET,如果这些前提没开,Kconfig会打出类似warning: (TARGET_MYBOARD) selects PHY_FIXED which has unmet direct dependencies的提示。有时候可以编译过,但行为不符合预期;有时候直接导致配置系统内部不一致,生成出来的宏互相矛盾。

我的建议:新板卡定义里,能不用select就不用。尽量把必须的依赖写成depends on,或者在上层Kconfig里用default + implies的方式。让Kconfig的依赖关系显式化,出现问题时一眼能看出谁依赖谁。另外,如果你在menuconfig里某个选项选了y,但保存后.config里莫名其妙少了很多项目,十有八九也是select的依赖链出问题。这时候最快的方法是make savedefconfig,再对比改动,把多余的select清掉。

4.3 CONFIG_SYS头文件与Kconfig的过渡期矛盾

U-Boot过去这些年在逐步把配置项目从板级头文件迁移到Kconfig,但直到现在,include/configs/xxx.h里依然保留了大量CONFIG_SYS_开头的板级参数。原因很简单,有些参数和具体硬件地址、内存分布强相关,直接写头文件比做成菜单更直观。过渡期最烦的事情是“同名宏双源”问题。比如一个宏既在Kconfig里能被配置,又在你自己的板级头文件里#define了一遍,行为就会很迷惑。

我之前遇到过CONFIG_BOOTCOMMAND在头文件里写的是从网络启动,但menuconfig里又被set成从eMMC启动,最后固件行为跟头文件完全不一致。排查了一天,才发现自己给同一个宏在两个地方都做了定义。避坑经验:新移植板卡时,把以CONFIG_SYS_开头的、和硬件地址强相关的宏放在include/configs/xxx.h;把功能开关(比如使能某个外设、选择启动介质)放到Kconfig/defconfig里去控制。一个宏只允许一个来源。命名上尽量避开与已有Kconfig选项完全相同的名字,除非你确实希望它们联动。

4.4 设备树匹配失败的处理

U-Boot在启动早期会根据板级代码里设置的of_root或OF_LIST去找dtb。一个常见问题是:dts文件编译进了u-boot.bin,但启动时报ERROR: Did not find a cmdline Flattened Device Tree或卡住。排查链路可以这样走:

  1. 确认arch/arm/dts/Makefile里有没有dtb-$(CONFIG_TARGET_MYBOARD) += myboard.dtb这一行;
  2. 确认.config里CONFIG_DEFAULT_DEVICE_TREE="myboard"和CONFIG_OF_CONTROL=y存在;
  3. 用strings u-boot | grep myboard看看dtb的标识是否被编入。如果有myboard字符串但仍然识别不了,多半是dts的model或compatible属性与板级代码不匹配;
  4. 如果完全没有字符串,则配置或Makefile登记有问题,回到Kconfig链路上查。

这套思路也适用于其他外设:“配置存在吗 -> 编译进来了吗 -> 运行时匹配上了吗”,三步走完基本能定位90%的移植问题。

4.5 沉淀成一张可复用的排查表

把上面这些经验归纳成一张表,后续遇到问题可以按行对照:

排查步骤操作常见发现
1. 查配置grep CONFIG_XXX .config选项消失 → 依赖不满足或名称错误
2. 查编译grep xxx.o build.log没编译 → 对应目录Makefile的obj-y条件不匹配
3. 查宏grep CONFIG_XXX include/generated/autoconf.hC代码没拿到宏 → 回到配置阶段
4. 查运行strings u-boot / 启串口日志固件有但找不到设备 → dts或model匹配问题

这张表看起来简单,但在实际移植中非常救命。很多人花几个小时在驱动代码里打log,最后发现只是配置阶段就把它丢了,白白浪费一个下午。

5. 移植调试中反复用到的Kbuild操作清单

5.1 用savedefconfig抽取最小配置

当你在menuconfig里反复调了一堆选项后,想导出给合作伙伴或记录到版本库里,不要直接copy .config。因为.config里全是默认值冗余项,别人没有办法通过它看出你到底改了什么。正确姿势是在U-Boot根目录执行:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- savedefconfig

Kbuild会生成一个只保留非默认项的defconfig文件,你再把它替换configs/myboard_defconfig。这样一来,以后任何一个人make myboard_defconfig,都能复现你的完整配置状态。这个操作对git提交尤其友好:diff看起来简洁清晰,review的人一眼能看出改动。

5.2 用O=做外部构建,用V=1看详细日志

在多个板卡间切换或者同一板卡维护多个配置时,我强烈建议用O=把编译产物放到独立目录:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=build-myboard myboard_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=build-myboard -j8

这样源码树始终干净,切板卡时不用反复make distclean,也能同时保留不同board的构建结果。O=目录可以放在源码树内部,也可以指向外部绝对路径,关键是首次使用该目录时必须先执行一次defconfig,不能在空目录直接make。

出现问题时,加上V=1重新编译,把输出存到日志:

make ARCH=arm CROSS_COMPILE=... O=build-myboard V=1 2>&1 | tee build.log

检查是否真的编译了某个驱动文件,直接grep build.log。比如你想确认网卡驱动有没有参与编译,搜phy.c,比对它前面的gcc命令和参数。这比在源码里猜可靠得多。

5.3 menuconfig搜索与增量编译的注意事项

menuconfig界面里按/可以搜索配置名,输入关键字比如RTC或DM,会列出所有匹配项,还标注了依赖关系和当前值。这对判断“为什么这个选项没出现”非常有用。增量编译时有一个容易忽略的规则:修改Kconfig之后,必须重新执行一次make myboard_defconfig或make olddefconfig,让.config吸收新选项;否则新增的Kconfig项不会出现在.config里。同样,修改defconfig之后直接make,Kbuild理论上会检测到.config过期并提示run make olddefconfig,但不同版本的提示方式不一样,最稳妥还是重新跑一次defconfig命令。

还有一个跟Kbuild没有直接关系但很影响体验的操作:把CROSS_COMPILE写进环境变量或make变量配置文件,例如根目录的Makefile里临时加一行CROSS_COMPILE ?= arm-linux-gnueabihf-。这样后续命令不用每次带这个参数,但要注意别把这个改动提交到共用的版本库里。

最后说一点我自己的体会。U-Boot这套Kbuild体系,初看确实吓人,但它的设计逻辑其实非常朴素:Kconfig管“能不能用”,Makefile管“编不编”,defconfig管“默认怎么选”。把这三条线串起来想,再复杂的移植流程也就是在每条线上做加减法。我每次拿到一块新板卡,第一件事不是看驱动,而是先把这套最小的Kbuild链路跑通,让串口能看到U-Boot自己的logo;只要这一步稳了,后面的DDR、时钟、外设排查就有了落脚点。希望这篇梳理对你也有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询