
刚拿到一块没有 U-Boot 支持的新板子你大概会先百度一整天然后被各种 start.S、configs/*_defconfig、设备树、链接脚本搅得头大。U-Boot 移植这个活儿说难确实难难在它把汇编、C 语言初始化、Kconfig 配置、Kbuild 构建规则和硬件手册全混在一起但说简单也简单因为整个移植流程已经被 Kbuild 这套构建系统标准化了你只需要知道在哪个文件里加什么东西、改哪个宏、跑哪条 make 指令。 这篇内容就是围绕 U-Boot 移植和 Kbuild 展开的入门总结适合刚拿到一块新开发板、准备自己把 U-Boot 跑起来的朋友。我会先讲清楚移植到底在移什么再拆开 Kbuild 的配置与编译机制然后按真实操作顺序走一遍从复制参考板到生成可执行镜像的流程最后把那些藏在 make 输出背后的坑逐个挑出来。看完之后你会发现移植 U-Boot 不是拼命写代码而是“照着参考板改配置、调参数、看输出、再改”的循环Kbuild 就是你在这个循环里最需要熟悉的工具。 ## 1. U-Boot 移植入门先搞懂这三件事 ### 1.1 启动链路与“移植”的真实含义 在动代码之前我建议你先把 SoC 的启动流程画出来。现在的芯片上电后片内 ROM 里有一段出厂固件叫 BootROM它负责从 SPI NOR、SD 卡、eMMC、NAND 或 USB 等介质里加载一段小程序到 SRAM这段小程序通常是 U-Boot SPL。SPL 的任务是初始化外部 DDR、时钟和串口然后把完整的 U-Boot 主体加载进内存。主体 U-Boot 继续初始化更多外设最终通过网络、USB 或存储介质加载内核和设备树完成启动接力。 这里最关键的一点是移植 U-Boot 不等于重写 U-Boot。你真正要做的就是“让 SPL 能在你的板子上把 DDR 跑起来让 U-Boot 主体能稳定地初始化板载外设并且把内核引导起来”。听起来范围很大但落到具体文件上无非是板级目录下的一堆 C 文件、设备树源文件、头文件里的配置宏以及 Kconfig 和 defconfig 里的选择项。Kbuild 负责把这些分散的东西组织成最终可以烧录的 u-boot.bin所以它的作用一点不比写初始化代码小。 ### 1.2 Kbuild 为什么是移植流程里的关键一环 Kbuild 最早是 Linux 内核用的构建系统U-Boot 直接继承了这套思路。它不只是一个 Makefile而是由顶层 Makefile、子目录里的 Kconfig、Makefile 以及 scripts/ 下的一系列辅助脚本组成的完整框架。对移植工作来说Kbuild 的价值有三个配置可裁剪、层次清晰、依赖自动搞定。 配置可裁剪体现在 Kconfig 上U-Boot 把 CPU 架构、板卡型号、驱动、命令、文件系统等都以菜单的形式组织起来你通过 menuconfig 或者直接写 defconfig 来决定哪些代码编进去。层次清晰体现在目录结构上arch/ 下面是 CPU 相关代码board/ 下面是板级代码drivers/ 下面是外设驱动每个目录都有自己的 Kconfig 和 MakefileKbuild 会在编译时自动进入这些子目录。依赖自动搞定则是它会为你生成 include/autoconf.mk、include/generated/autoconf.h 等文件把 .config 里的配置项转换成编译器和代码能直接用的宏定义。 我经常拿“流水线”来类比 Kbuild源代码是原料Kconfig 是菜单defconfig 是订单Makefile 是流水线上的工序最后的二进制文件就是装箱成品。你说我要换一个 DDR 芯片那就在设备的 dts 和头文件里改参数而不是去手动调整几千行汇编和链接脚本。 ### 1.3 和 FreeRTOS、LVGL 等应用级移植不是一回事 网上搜“移植”两个字出来的结果大半是 FreeRTOS 移植、LVGL 移植、某个游戏引擎移植这种。这些移植虽然也叫移植但它们的核心工作通常是“让一套现成的应用代码跑在另一个平台上”重点在 API 适配、中断处理、内存分配和图形驱动对接。 U-Boot 移植所处的层次完全不同。它更接近 firmware 移植你需要面对的是芯片启动阶段那一堆汇编代码、内存控制器寄存器、时钟树配置以及 Kbuild 是如何决定哪些源文件参与链接的。换句话说应用级移植解决的是“让软件跑起来”U-Boot 移植解决的是“让硬件先活过来再给系统软件一个可运行的环境”。这两个难度完全不在一个量级所以不能拿 LVGL 移植的经验往 U-Boot 上套。这也是为什么很多初学者拿着现成的板卡资料改了半天配置文件却连串口都没有输出——因为他根本还没到“跑应用”那一步连最底层的启动链路都没建立起来。 ## 2. 拆开 Kbuild 这个盒子配置、递归 Make 和自动生成文件 ### 2.1 配置从哪里来Kconfig 树和 menuconfig U-Boot 源码里几乎每个有源文件的目录都会放一个 Kconfig 文件这些 Kconfig 定义了这个目录下有哪些可配置选项、依赖关系、默认值。顶层 Kconfig 通过 source 语句把各个子目录的 Kconfig 串成一整棵配置树。你执行 make menuconfig 时看到的那个层层嵌套的菜单界面其实就是在解析这棵配置树。 对移植来说你通常不需要在 menuconfig 里手忙脚乱地翻菜单而是先在 configs/ 目录下创建一个 defconfig然后在里面写下板卡所需的配置项。defconfig 的好处是它只保存与默认值不同的配置执行 make xxx_defconfig 时Kbuild 会根据 Kconfig 树自动展开生成完整的 .config 文件。我在首次接触 U-Boot 移植时吃过亏直接在 .config 里改了一堆东西结果下次 make clean 后全没了。正确做法是永远把自定义配置放在 defconfig 里让 Kbuild 通过 defconfig 重新生成 .config。 这里还想提醒一点改 Kconfig 文件之后如果你发现 menuconfig 里没有新增的选项通常是因为 Kconfig 文件里没有把它 source 进来或者是 Kconfig 语法错误导致解析失败。可以先用 make defconfig 测试一下如果报错Kconfig 解析器会明确指出是哪个文件哪一行出了问题。 ### 2.2 defconfig 和 .config 的真实作用 defconfig 的语法并不复杂本质就是一行一个 CONFIG_xxxy 或者 CONFIG_xxxstring。但它是 U-Boot 移植的起点里面的每一个条目都决定了最终镜像里包含什么功能。比如 text CONFIG_SYS_CONFIG_NAMEmyboard CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_TARGET_MYBOARDy CONFIG_SPLy CONFIG_SYS_MMC_OS_DEVICE0当你执行 make myboard_defconfig 之后Kbuild 会先根据 Kconfig 树里的依赖关系把 defconfig 展开成一个完整的 .config并生成一系列派生文件。这是很多初学者忽略的部分你以为 .config 才是配置实际上 Kbuild 还会生成 include/autoconf.mkMakefile 通过它来判断哪些条件编译要开生成 include/generated/autoconf.hC 源文件通过它来访问配置宏同时还会生成 include/config.h它指向 include/configs/myboard.h。我在实际调试中经常用一条命令来验证配置是否生效grep CONFIG_SPL include/config.h如果这里没有你要的宏要么是 defconfig 里没写要么是 Kconfig 依赖没满足后面的编译结果就一定会出问题。2.3 Makefile 的递归魔法scripts/Makefile.build顶层 Makefile 是整个构建过程的总调度员但它并不直接编译每个源文件而是通过 scripts/Makefile.build 递归地进入各级子目录这是 Kbuild 的核心机制之一。每个子目录里的 Makefile 通常只需要指定 obj-y、obj-$(CONFIG_xxx) 两种变量Kbuild 就能自动编译并收集目标文件。比如 board/vendor/myboard/Makefile 里可能只有一行obj-y myboard.oKbuild 在编译时发现这类 obj-y 变量就会把所有子目录收集到的 .o 文件按顺序排列最后通过顶层 Makefile 里的链接脚本把它们链接成 u-boot 可执行文件。这个“按顺序排列”在移植中非常重要因为很多底层的初始化代码对链接顺序敏感比如 vectors 必须放在固定位置start.o 必须最靠前。如果顺序不对链接阶段会报错或者即使链接成功上电后也会跑飞。我强烈建议在刚开始接触 U-Boot 时就养成一个习惯遇到 make 输出看不懂先加一个 V1 再看一遍。make V1 CROSS_COMPILEarm-linux-gnueabihf-V1 会让 Kbuild 显示每一条完整的编译命令、链接命令和变量展开结果。这能帮你确认源文件是否真的被编译了编译器是否真的收到了某个宏。很多“改了半天没效果”的问题用 V1 一眼就能看出来——你的文件根本没进到编译列表里。3. 从零添加一块新板卡的实操流程3.1 复制参考板千万不要从头开始写我现在自己动手移植一块新板子第一步永远是找参照物。所谓参照物就是一颗主控芯片尽量接近、DDR 或存储介质尽量相似的已有板卡。因为芯片厂商一般会提供对应的评估板支持比如 ST 的 eval 板、NXP 的 evk 板这些板卡的 U-Boot 支持代码通常已经很完整了。准备工作很简单在源码根目录下mkdir -p board/vendor/myboard cp -r board/vendor/refboard/* board/vendor/myboard/ mkdir -p arch/arm/dts cp arch/arm/dts/refboard.dts arch/arm/dts/myboard.dts cp configs/refboard_defconfig configs/myboard_defconfig cp include/configs/refboard.h include/configs/myboard.h复制完之后你必须把参考板目录下的文件名、Kconfig 内容、defconfig 里的 CONFIG_SYS_CONFIG_NAME、CONFIG_DEFAULT_DEVICE_TREE 都替换成新板卡的名字和文件路径。否则 Kbuild 找的还是参考板的头文件和设备树。这里有一个很多人踩过的细节board/vendor/myboard 下的 Makefile 里 obj-y 的文件名要和实际 .c 文件名一致路径也要正确。如果改了目录名但忘了改 Makefile 里的文件名编译时会直接报找不到源文件用 V1 排查时会看到 Makefile.build 试图访问旧路径。3.2 关键文件改动Kconfig、MAINTAINERS、defconfig 一个都不能少复制只是开始你可以把这四类文件看作新板卡的“身份证”。第一是 board/vendor/myboard/Kconfig里面至少要有一段config TARGET_MYBOARD bool MyBoard support select ARM64 select SYS_CONFIG_NAME help This is my first custom board.然后在 arch/arm/Kconfig 或者特定 SoC 的 Kconfig 里加入 source board/vendor/myboard/Kconfig。如果没有这行顶层 menuconfig 根本不会出现你的板卡选项make myboard_defconfig 也不会认为 TARGET_MYBOARD 是一个有效配置。第二是 configs/myboard_defconfig这里要写清楚CONFIG_TARGET_MYBOARDy CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_SYS_CONFIG_NAMEmyboard # 根据参考板保留其他必要配置 CONFIG_SPLy CONFIG_SYS_MMC_OS_DEVICE0第三是 include/configs/myboard.h这个头文件主要放一些 Kconfig 里不好表达的“板级硬件参数”比如内存基地址、串口基地址、环境变量分区布局等。举个例子#define CONFIG_SYS_SDRAM_BASE 0x80000000 #define CONFIG_SYS_INIT_RAM_ADDR 0x40000000 #define CONFIG_SYS_UART_BASE 0x50000000Kbuild 在处理 include/configs/myboard.h 时会自动把它转换成 include/config.h所以你在源码中直接写#include config.h就能拿到这些板级宏。第四是 MAINTAINERS 文件虽然它不影响编译但对于提交到上游和维护非常重要。我会在 board/vendor/myboard/MAINTAINERS 里写清楚板卡名称、维护者、Git 地址这样以后 U-Boot 版本升级时可以通过 get_maintainer.pl 找到这块板的负责人。改完这些先做一次配置尝试make CROSS_COMPILEarm-linux-gnueabihf- myboard_defconfig如果 Kconfig 没有报错并且 .config 里的 TARGET_MYBOARD 是 y说明板卡定义已经被 Kbuild 接受接下来才能进入编译环节。3.3 编译并验证最小镜像配置完成后直接编译make CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)首次编译时Kbuild 会先编译工具链相关的 host 程序比如 mkimage、dtc 以及各类生成脚本然后才会进入架构相关代码。如果你的机器缺少必要依赖比如 bison、flex、libssl-dev、device-tree-compiler这里就会报错。我一般会在编译前先安装sudo apt-get install -y bison flex make gcc-arm-linux-gnueabihf libssl-dev device-tree-compiler编译结束后在源码根目录下应该能看到这些产物u-bootELF 格式的 U-Boot 主体u-boot.bin去掉调试信息的二进制镜像u-boot.dtb由设备树源文件编译出来的二进制设备树u-boot-nodtb.bin不带设备树的二进制SPL如果开启 SPL则会在 spl/ 目录下生成 u-boot-spl.binu-boot.img适合 mkimage 封装后烧写的镜像格式拿到这些文件之后先别急着烧录。我建议先用 QEMU 或者厂商提供的仿真环境把 u-boot.bin 跑一次确认编译产物至少在软件层面能加载。如果手头有开发板则把 u-boot.bin 通过调试器或烧录工具写到启动介质中连接串口上电观察输出。3.4 串口与 DDR 参数看到输出才算迈出第一步U-Boot 启动的第一条串口输出通常来自 SPL 完成 DDR 初始化之后。如果你上电之后串口只有空白大概率是 DDR 初始化失败或者串口时钟配置不对。这时候要做的事情是翻 SoC 参考手册把内存控制器相关的时序参数、地址映射和时钟树配置对照参考板重新核对。常见的调试方法是在板级源码里临时加一个串口早期输出函数利用 SoC 的 BootROM 阶段已经初始化好的 UART 寄存器让代码在 DDR 初始化之前打印一个字符。比如很多 ARM 平台可以在 start.S 里添加一个 .weak 的 early_uart 函数或者在 board_init_f 之前调用 debug_uart 相关接口。这个输出只要能出来至少说明 CPU 已经开始执行你的 SPL 代码了比对着黑屏猜问题强太多。DDR 参数是另一个大头。不同厂商的 DDR 颗粒对刷新周期、时序参数、驱动强度要求不同U-Boot 里的 dts 文件一般只是描述内存控制器和 DDR 控制器的寄存器配置具体校准数据往往放在板级目录的 ddr.c 里。新人最容易犯的错误是直接照抄参考板 ddr 参数结果因为电压和频率不同导致初始化失败。我的做法是先降频首先把 DDR 频率设置为最低档确保内存控制器能稳定识别颗粒等串口输出稳定后再逐步提高频率。4. 移植过程中最常见的坑和排查方法4.1 完全没有串口输出链路问题还是 UART 参数问题这是所有移植者最先遇到、也最头疼的问题。我遇到过的原因有五六种SPL 没加到启动介质BootROM 找不到 SPLDDR 初始化失败卡死UART 引脚复用没配置波特率不对。排查顺序是固定的先用调试器确认 CPU 是否停留在 SPL 入口点确认 BootROM 是否正确加载了 SPL看 SDK 工具日志去掉 DDR 初始化直接编译一个最简串口测试检查 UART 时钟是否来自外部晶振是否已经使能用示波器或者逻辑分析仪看 TX 引脚上有没有电平翻转我在一次实际移植中卡了整整两天没用最后发现是 U-Boot 的 Kconfig 里有一个 CONFIG_SYS_NS16550 没有配置好导致串口驱动根本没被编进去。用 V1 编译时发现 drivers/serial/ns16550.c 完全没有出现在编译列表里问题一下定位了。4.2 链接错误和 undefined referenceSPL 编译时最常出现一批“undefined reference to”错误比如undefined reference to board_init_f undefined reference to dram_init undefined reference to lowlevel_init这些函数大部分在 board/ 或者 arch/ 目录下定义如果 Kbuild 没有把对应的 .o 文件链接进来或者 Kconfig 条件判断把它裁剪掉了就会报这种错。定位方法依然是 V1看链接命令里到底包含了哪些 .o 文件。还有一种情况是源文件写了但函数名拼错或者函数定义了却被 __weak 符号覆盖成空实现这种做法在 U-Boot 里很常见需要仔细阅读参考板的代码。针对 lowlevel_init 这个函数特别提醒一下如果你的 SPL 阶段并不需要很复杂的低层初始化可以直接在 board 代码里定义空函数但绝对不能把它的 .c 文件排除在编译列表之外否则链接器找不到符号。4.3 设备树和 CONFIG_OF_CONTROL 不匹配进入 U-Boot 主体阶段后最常见的现象是 U-Boot 能启动到命令行但执行 bootz 或 booti 加载内核时报设备树地址无效或者“Starting kernel ... ”之后就没有下文。这个问题的根源通常是 U-Boot 启动时没有把自己编译好的 dtb 传递给内核或者传给内核的 dtb 存放地址与内核解压地址冲突。检查点有三个defconfig 里 CONFIG_DEFAULT_DEVICE_TREE 是否和实际文件名一致dts 文件里 model、compatible 是否和内核驱动匹配bootcmd 中 fdt addr 命令指定的内存地址是否落在了 U-Boot 保留区域之外我在调试中经常用一条命令来确认 U-Boot 是否携带了正确的 dtbfdt addr 0x88000000 fdt print /model如果 fdt print 报错说明这个地址处没有合法设备树要么是 U-Boot 没把 dtb 加载到这里要么就是设备树编译过程有问题。此时回到主机上单独执行make arch/arm/dts/myboard.dtb确认 DTC 能成功编译这个设备树源文件再检查 make 输出里是否有 dtc 报错。4.4 配置文件改半天没生效Kbuild 缓存与清理问题这是个特别容易漏的坑。U-Boot 的 Kbuild 在编译过程中生成了大量带 .cmd 后缀的命令文件如果你修改了某个 Kconfig 或者头文件但 Kbuild 认为对应目标没有变化它就不会重新编译。这时候最有效的操作是make mrproper或者至少make clean make defconfig我曾遇到过一种情况修改了 include/configs/myboard.h 里的一个宏重新执行 make但编译产物完全没变最后发现是 Kbuild 对头文件依赖追踪没覆盖到某个生成文件。这种情况用make clean make myboard_defconfig make一套组合拳基本都能解决。不建议直接把 .config 删掉然后重新 make因为 .config 是 Kbuild 基于 defconfig 生成的如果你手动改过 .config删掉之后它会根据 Kconfig 默认值重新生成可能丢掉你需要的配置。正确的做法是修改 defconfig再用 make xxx_defconfig 重新生成。5. 提高成功率的几个个人习惯移植 U-Boot 走到后面你会发现困难不再是某个函数怎么写而是在几十个文件里找线索。我有几个习惯每次都能帮我省下大量排查时间。第一每次准备编译前先跑一次 make myboard_defconfig确保配置同步。因为 .config 可能因为各种原因被手动修改过重新生成一次可以避免“编译用的配置和 defconfig 不一致”的问题。第二把每次编译输出保存到日志文件比如make CROSS_COMPILEarm-linux-gnueabihf- -j8 build.log 21编译结束之后用 grep Error、grep undefined、grep warning 这些关键词过滤。几百行输出看起来吓人实际上错误信息往往只有几行被淹没在大量命令回显里了。第三在改 dts 和板级头文件之前先确认你即将改动的文件确实参与构建。用 V1 编译一次搜索源文件名看看它是否真的被编译过。这花不了两分钟却能避免“改了个寂寞”。第四也是最重要的一个习惯只在改动一处之后编译一次不要一次改 N 处再统一编译。U-Boot 移植中如果同时改了几个变量上电后出了问题你根本不知道是哪一步导致系统挂死。我自己的节奏是改一个串口时钟编译改一个 DDR 参数编译串口输出变化了再继续下一步。虽然编译时间长了点但排查效率会高得多。如果你能走到这一步说明新板卡的 U-Boot 已经能在它自己的生命周期里完成“初始化硬件、跳到命令行、执行 bootcmd”这一整套动作。剩下的事情就是继续丰富驱动、裁剪配置、优化启动时间以及把补丁整理成可以提交到社区的格式。这套方法论我第一次成功点亮板卡用了整整一周第二次移植同类板卡只用了半天第三次几乎只是照着参考板检查差异。Kbuild 这个盒子虽然复杂但它最大的价值就是把“机械化”的构建逻辑标准化了让你可以集中精力去做真正有挑战性的那部分——理解硬件、调参、定位问题。如果你现在也被移植 U-Boot 卡在某个阶段那我的建议只有一句别急着乱改代码先用 V1 把构建过程看透再针对性地动手。