我最早接触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 | 决定编译哪个dts | arch/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重点看两处:
- make myboard_defconfig时是否有“ignoring unknown option”警告;如果有,说明defconfig里有Kconfig不认识的项。
- 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或卡住。排查链路可以这样走:
- 确认
arch/arm/dts/Makefile里有没有dtb-$(CONFIG_TARGET_MYBOARD) += myboard.dtb这一行; - 确认.config里
CONFIG_DEFAULT_DEVICE_TREE="myboard"和CONFIG_OF_CONTROL=y存在; - 用
strings u-boot | grep myboard看看dtb的标识是否被编入。如果有myboard字符串但仍然识别不了,多半是dts的model或compatible属性与板级代码不匹配; - 如果完全没有字符串,则配置或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.h | C代码没拿到宏 → 回到配置阶段 |
| 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- savedefconfigKbuild会生成一个只保留非默认项的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、时钟、外设排查就有了落脚点。希望这篇梳理对你也有用。