ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式Linux开发:U-Boot移植与Kbuild构建系统实战指南

嵌入式Linux开发:U-Boot移植与Kbuild构建系统实战指南 1. 从零搞懂 U-Boot 移植与 Kbuild为什么它是绕不开的第一道坎搞嵌入式 Linux 的人迟早会撞上 U-Boot 移植这件事。你拿到一块新板子SoC 是新的DDR 是新的Flash 也是新的厂商给的 BSP 可能只覆盖了自家 EVB量产板一改硬件就全废。这时候你要做的第一件事就是把 U-Boot 跑起来——串口能出字DDR 能初始化存储能读写网络能通最后能把内核拉起来。这一整套流程就是 U-Boot 移植。而 Kbuild是 U-Boot 从 2014 年左右开始全面引入的构建系统它脱胎于 Linux 内核的 Kconfig Kbuild 体系。很多从裸机或者 STM32CubeMX 那套 IDE 工程转过来的朋友第一次看到 U-Boot 源码目录里满屏的Kconfig、Makefile、defconfig脑子是懵的为什么改个配置要make menuconfig为什么板级目录下有个MAINTAINERS为什么make xxx_defconfig之后还要make这些问题的答案全在 Kbuild 这套机制里。这篇文章面向的是已经会写 C 语言、用过至少一款 MCU、想往嵌入式 Linux 方向走的开发者。我会把 U-Boot 移植的完整链路拆开重点讲清楚 Kbuild 在移植过程中扮演的角色——它不是配个编译选项这么简单而是决定了你的板级代码怎么被组织、怎么被选中、怎么被编译进最终镜像。看完之后你应该能独立完成一块新板子的 U-Boot 移植并且知道每一步为什么这么做。先说结论U-Boot 移植的核心工作量80% 在板级目录的创建和配置20% 在驱动适配。而 Kbuild 就是把这 80% 串起来的那根线。搞不懂 Kbuild你连我的板子代码为什么没被编译进去都查不出来。2. U-Boot 移植的整体思路与 Kbuild 的定位2.1 移植到底在移什么很多人对移植这个词有误解以为是把 U-Boot 源码改一改就能跑。实际上 U-Boot 的架构设计已经帮你做了绝大部分工作你要做的是填空而不是重写。具体来说移植工作分为三个层次第一层是板级配置。你需要告诉 U-Boot我用的是哪颗 SoC、DDR 多大、从哪个设备启动、串口用哪个、有哪些外设。这些信息通过defconfig文件和板级头文件来定义。第二层是板级初始化代码。DDR 初始化参数、引脚复用配置、时钟树设置这些和硬件强相关的东西需要你根据原理图和 SoC 手册来填。第三层是驱动适配。如果某个外设的驱动 U-Boot 里已经有了你只需要在设备树或者板级文件里使能它如果没有才需要自己写。Kbuild 贯穿了这三层。defconfig是 Kbuild 的配置入口板级Makefile是 Kbuild 的编译入口Kconfig是 Kbuild 的选项定义入口。你不理解 Kbuild就不知道这三者之间怎么联动。2.2 为什么 U-Boot 要用 Kbuild 而不是自己写 Makefile这个问题值得展开说。早期 U-Boot 确实是用手写 Makefile 加include/configs/xxx.h头文件来配置的那种方式有几个致命问题配置项散落在头文件里#define CONFIG_XXX到处都是改一个配置要翻好几个文件没有依赖管理改了配置不知道哪些文件需要重新编译无法做配置的合法性检查比如你同时使能了两个互斥的功能编译时才报错板级代码和通用代码混在一起维护成本极高Kbuild 引入之后配置项集中在Kconfig文件里定义通过menuconfig可视化选择生成的.config文件是唯一的配置真相来源。编译系统根据.config自动决定编译哪些文件、传什么宏定义。这套机制在 Linux 内核里验证了二十多年U-Boot 直接拿来用是最稳妥的选择。2.3 Kbuild 三大件Kconfig、Makefile、.configKbuild 的核心就三个东西搞懂它们的关系后面所有操作都是顺理成章组件作用位置谁维护Kconfig定义配置选项的菜单结构、依赖关系、默认值各目录下开发者.config用户最终选择的配置集合源码根目录make menuconfig 生成Makefile根据 .config 决定编译哪些文件、怎么编译各目录下开发者defconfig某个板子的默认配置模板configs/ 目录移植者工作流是这样的你先make xxx_defconfigKbuild 把configs/xxx_defconfig复制成.config然后你可以make menuconfig微调Kbuild 读取所有Kconfig文件生成菜单你改完后写回.config最后makeKbuild 读取.config把CONFIG_XXXy的项转成编译宏同时决定哪些目录、哪些文件参与编译。注意.config是生成文件不要手动编辑后提交到 git。要固化配置应该用make savedefconfig生成精简的 defconfig再覆盖到configs/目录下。3. 板级目录创建与 Kbuild 配置实操3.1 找到你的参考板移植第一步不是新建目录而是找参考板。U-Boot 源码里board/目录下按厂商分目录configs/目录下是各板子的 defconfig。你要找的是同一颗 SoC 或者同一系列 SoC的板子。举个例子假设你要移植一颗基于 ARM Cortex-A7 的国产 SoC那你可以先看board/下有没有同厂商的板子或者看arch/arm/mach-xxx/下有没有对应的 SoC 支持。找到参考板之后把它的板级目录、defconfig、设备树全部复制一份改成你自己的名字。这一步的意图很明确U-Boot 里同一颗 SoC 的 DDR 初始化、时钟配置、引脚复用代码是高度相似的直接复用能省掉大量调试时间。我见过有人从零开始写 DDR 初始化调了两周没跑通最后发现参考板的参数改两个值就行了。3.2 创建板级目录和文件假设参考板是board/vendor/ref_board/你的板子叫my_board操作如下cp -r board/vendor/ref_board board/vendor/my_board cp configs/ref_board_defconfig configs/my_board_defconfig cp arch/arm/dts/ref_board.dts arch/arm/dts/my_board.dts然后修改board/vendor/my_board/下的文件Makefile把obj-y ref_board.o改成obj-y my_board.oKconfig修改板子名称和配置项my_board.c修改board_init等函数里的硬件相关代码MAINTAINERS更新维护者信息可选但建议这里重点说Makefile和Kconfig因为它们是 Kbuild 的入口。3.3 板级 Makefile 的写法与 obj-y 机制U-Boot 的板级 Makefile 通常很短核心就一行obj-y my_board.oobj-y的意思是这个目标无条件编译进 U-Boot。Kbuild 里常见的变量有obj-y编译进最终镜像obj-m编译成模块U-Boot 里基本不用obj-$(CONFIG_XXX)根据配置决定是否编译obj-$(CONFIG_SPL_BUILD)只在 SPL 阶段编译如果你有多个源文件可以这样写obj-y my_board.o obj-y ddr_init.o obj-$(CONFIG_MY_BOARD_EXTRA) extra_feature.oextra_feature.o只有在.config里CONFIG_MY_BOARD_EXTRAy时才会被编译。这就是 Kbuild 的威力——配置和编译自动联动不需要你手动维护文件列表。实操心得板级 Makefile 里不要写复杂的逻辑保持简单。复杂的条件编译应该放到 Kconfig 里定义选项Makefile 只做obj-$(CONFIG_XXX)这种简单判断。我见过有人在 Makefile 里写 shell 判断结果交叉编译环境下各种诡异问题排查起来非常痛苦。3.4 Kconfig 文件的编写要点板级Kconfig定义了这块板子特有的配置选项。一个典型的板级 Kconfig 长这样if TARGET_MY_BOARD config SYS_BOARD default my_board config SYS_VENDOR default vendor config SYS_CONFIG_NAME default my_board config MY_BOARD_DDR_SIZE int DDR size in MB default 512 help Set the DDR size for my board. endif几个关键点if TARGET_MY_BOARD保证这些选项只在选中这块板子时出现SYS_BOARD、SYS_VENDOR、SYS_CONFIG_NAME是 U-Boot 构建系统必须的三个变量分别对应板级目录名、厂商目录名、头文件名自定义选项用config定义类型可以是bool、int、string、hexhelp里写清楚这个选项的作用方便自己和别人后续维护TARGET_MY_BOARD这个宏是在arch/arm/mach-xxx/Kconfig里定义的你需要在那个文件里加上你的板子选项config TARGET_MY_BOARD bool Support my_board select CPU_V7 select SUPPORT_SPL help Support for my_board based on xxx SoC.select语句会自动使能依赖的配置项比如CPU_V7表示这是 Cortex-A7 核。这样你在 menuconfig 里选中TARGET_MY_BOARD时相关依赖自动打开不会出现配置遗漏。3.5 defconfig 的生成与精简defconfig是你板子的默认配置模板放在configs/目录下。最开始的版本可以从参考板复制然后逐步修改。但手动改容易出错推荐的做法是先make my_board_defconfigmake menuconfig调整配置make savedefconfig生成精简版defconfig把生成的defconfig覆盖到configs/my_board_defconfigsavedefconfig会去掉所有默认值的配置项只保留和默认值不同的项这样 defconfig 文件非常干净diff 起来一目了然。我见过有人直接提交完整的.config作为 defconfig几千行改一个配置根本看不出改了什么。注意savedefconfig生成的 defconfig 依赖 Kconfig 里的 default 值。如果你改了 Kconfig 的 defaultdefconfig 的行为可能变化。所以 Kconfig 的 default 值要慎重设置一般设成最通用的值。4. 编译流程拆解与关键环节实现4.1 从 make 到镜像完整编译链路执行make之后Kbuild 做的事情按顺序是读取.config生成include/config/auto.conf和include/generated/autoconf.h根据auto.conf决定进入哪些子目录编译每个子目录的 Makefile 根据obj-y和obj-$(CONFIG_XXX)决定编译哪些.o所有.o链接成u-bootELF 格式objcopy生成u-boot.bin纯二进制如果有 SPL还会生成u-boot-spl.bin最后打包成u-boot.img或者u-boot.itb取决于启动方式autoconf.h是 C 代码里能用到配置宏的关键。你在 C 文件里写#ifdef CONFIG_MY_BOARD_DDR_SIZE编译器能看到这个宏就是因为 Kbuild 把.config里的CONFIG_MY_BOARD_DDR_SIZE512转成了#define CONFIG_MY_BOARD_DDR_SIZE 512写进autoconf.h。4.2 交叉编译工具链配置U-Boot 编译需要指定交叉编译器。有两种方式方式一命令行指定make CROSS_COMPILEaarch64-linux-gnu- my_board_defconfig make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)方式二在环境变量里设置export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm make my_board_defconfig make -j$(nproc)ARCH变量告诉 Kbuild 去arch/arm/目录下找架构相关代码。ARM 32 位是armARM 64 位是arm64RISC-V 是riscvx86 是x86。实操心得工具链版本很关键。U-Boot 对 GCC 版本有要求太老的编译不过太新的可能有 warning 被当成 error。我一般用 Linaro 或者 ARM 官方发布的工具链版本选比 U-Boot 发布时间晚半年到一年的兼容性最好。如果编译报错unknown register name之类的八成是工具链太老。4.3 编译产物分析与验证编译完成后根目录下会生成一堆文件重点看这几个文件说明用途u-bootELF 格式带符号调试用可以用 gdb 加载u-boot.bin纯二进制烧录到存储设备u-boot.map链接映射表查符号地址、分析大小u-boot.srecS-Record 格式某些烧录工具用spl/u-boot-spl.binSPL 二进制一级启动加载器System.map符号表查函数地址验证编译是否成功最直接的方法是看u-boot.bin的大小和u-boot.map里的内存布局。如果u-boot.bin大小异常比如只有几 KB说明链接有问题可能板级代码没被编译进去。用arm-linux-gnueabi-objdump -h u-boot可以看各个段的大小和地址确认.text、.data、.bss的布局是否符合预期。用nm u-boot | grep board_init可以确认你的板级函数有没有被链接进去。4.4 烧录与串口验证编译出u-boot.bin之后烧录到板子上。烧录方式取决于启动介质SD 卡启动dd ifu-boot.bin of/dev/sdX bs512 seek2具体偏移看 SoC 手册SPI Flash 启动用烧录器或者 U-Boot 自己的sf命令eMMC 启动通过 USB 下载工具或者量产工具串口下载某些 SoC 支持 UART 启动用厂商工具下载烧录后接串口波特率一般是 115200 8N1。上电后如果能看到 U-Boot 的启动信息说明移植基本成功U-Boot 2023.10 (Jan 01 2024 - 00:00:00 0000) CPU: xxx Model: my_board DRAM: 512 MiB MMC: mmcfe2b0000: 0 Loading Environment from MMC... OK In: serial Out: serial Err: serial Net: eth0: ethernetfe300000 Hit any key to stop autoboot: 0如果串口没输出排查顺序是串口线接对没有、波特率对不对、DDR 初始化有没有过、时钟配置对不对。DDR 没过的话代码根本跑不到串口初始化那一步。5. 常见问题与排查技巧实录5.1 编译阶段常见问题问题一make my_board_defconfig报错No rule to make target原因通常是configs/my_board_defconfig文件不存在或者arch/arm/mach-xxx/Kconfig里没有定义TARGET_MY_BOARD。检查这两个地方确保板子选项被正确注册。问题二编译通过但u-boot.bin里没有我的板级代码用nm u-boot | grep my_board确认符号是否存在。如果不存在检查board/vendor/my_board/Makefile里的obj-y有没有写对以及board/vendor/Kconfig里有没有source board/vendor/my_board/Kconfig。问题三autoconf.h里没有我定义的配置宏检查Kconfig里的config定义有没有被if TARGET_MY_BOARD包住以及TARGET_MY_BOARD是否在.config里为y。用make menuconfig搜索一下你的配置项看它是否可见。5.2 运行阶段常见问题问题一串口无输出这是最常见也最头疼的问题。排查步骤确认串口线接的是 UART0 还是 UART1很多板子默认调试串口不是 UART0确认波特率有些 SoC 默认 1500000不是 115200确认 DDR 初始化是否通过可以在 DDR 初始化后加个 GPIO 翻转用示波器看确认时钟配置串口时钟不对的话波特率会偏问题二DDR 初始化失败DDR 参数和硬件强相关参考板的参数不一定适用你的板子。需要根据 DDR 颗粒手册和 SoC 手册重新计算。重点参数包括时序参数tRCD、tRP、tRAS 等、刷新率、驱动强度、ODT 配置。这部分建议用 SoC 厂商提供的 DDR 配置工具生成参数不要手算。问题三网络不通先确认 PHY 芯片型号和地址然后在设备树或者板级文件里配置正确的 PHY 地址和接口模式RGMII/MII。用mii info命令可以看 PHY 是否被识别用mdio read可以读 PHY 寄存器确认链路状态。5.3 问题速查表现象可能原因排查方法编译报错找不到头文件板级头文件路径不对检查SYS_CONFIG_NAME和include/configs/下的文件名编译通过但功能缺失配置项没使能make menuconfig搜索配置项确认依赖满足串口乱码波特率或时钟不对换几个常用波特率试检查时钟树配置DDR 容量识别错误DDR 参数不对用厂商工具重新生成参数启动卡在 SPLSPL 大小超限检查CONFIG_SPL_MAX_SIZE精简 SPL 代码环境变量保存失败存储设备没初始化检查CONFIG_ENV_IS_IN_XXX配置避坑技巧移植新板子时先不要改 DDR 参数直接用参考板的参数把 U-Boot 跑起来确认串口能输出。然后再逐步改 DDR 参数、时钟参数、引脚配置。一次改太多出问题根本不知道是哪个改动导致的。我一般用 git 管理移植过程每改一个功能就 commit 一次出问题可以快速回退对比。6. Kbuild 进阶SPL、设备树与多板支持6.1 SPL 的 Kbuild 配置SPLSecondary Program Loader是 U-Boot 的第一阶段运行在 SRAM 里负责初始化 DDR 然后加载完整的 U-Boot。SPL 的编译和主 U-Boot 是分开的通过CONFIG_SPL_BUILD宏区分。在板级 Makefile 里你可以这样区分obj-y my_board.o obj-$(CONFIG_SPL_BUILD) spl_board.ospl_board.o只在编译 SPL 时被包含。SPL 的代码要尽量精简因为 SRAM 通常只有几十 KB。DDR 初始化代码、时钟初始化代码放 SPL其他都放主 U-Boot。SPL 的配置在Kconfig里用config SPL_XXX定义比如SPL_MMC_SUPPORT、SPL_SERIAL_SUPPORT。这些选项在 menuconfig 的 SPL / TPL 菜单下。6.2 设备树在 Kbuild 里的处理U-Boot 的设备树和 Linux 类似放在arch/arm/dts/下。Kbuild 会自动编译.dts文件生成.dtb然后打包进 U-Boot 镜像。在arch/arm/dts/Makefile里你需要加上你的设备树dtb-$(CONFIG_TARGET_MY_BOARD) my_board.dtbdtb-$(CONFIG_XXX)表示当配置项为y时编译对应的.dtb。U-Boot 启动时会根据板子 ID 选择正确的设备树。设备树里要描述的东西包括串口、MMC、网络、I2C、SPI、GPIO 等。U-Boot 的设备树和 Linux 的设备树可以共用大部分内容但 U-Boot 特有的配置比如u-boot,dm-pre-reloc需要单独加。6.3 多板支持的 Kbuild 组织方式如果你的产品线有多块板子共用同一颗 SoCKbuild 支持在一个板级目录下管理多块板子。做法是在Kconfig里定义多个TARGET_XXX在Makefile里用条件编译区分obj-$(CONFIG_TARGET_BOARD_A) board_a.o obj-$(CONFIG_TARGET_BOARD_B) board_b.o obj-y common.ocommon.o是共用的初始化代码board_a.o和board_b.o是各自特有的代码。这样组织的好处是共用代码只维护一份板级差异清晰可见。实操心得多板支持时尽量把差异抽象成配置项而不是用#ifdef在代码里到处判断。比如 DDR 大小不同就定义CONFIG_DDR_SIZE代码里读配置项而不是#ifdef BOARD_A。这样新增板子时只需要加配置不用改代码。7. 移植完成后的验证与优化7.1 功能验证清单U-Boot 跑起来只是第一步还要验证各个功能是否正常。我一般按这个清单过一遍串口输入输出正常能进命令行bdinfo能正确显示板子信息、DDR 大小、时钟频率mmc list能识别到存储设备mmc dev 0能切换fatls mmc 0能读取 FAT 分区ext4ls mmc 0能读取 ext4 分区ping能通tftp能下载文件env print能显示环境变量env save能保存boot能正常启动内核每一项都过了才算移植完成。7.2 启动速度优化U-Boot 默认配置为了兼容性启动速度不一定最优。量产产品通常要求快速启动优化手段包括关闭不必要的命令和驱动减小 U-Boot 体积关闭CONFIG_CMD_XXX里用不到的命令用CONFIG_SKIP_RELOCATE_UBOOT跳过重定位如果内存布局允许关闭串口输出CONFIG_SILENT_CONSOLE用 SPL 直接加载内核跳过完整 U-BootFalcon 模式这些优化每一项都能省几百毫秒加起来能从几秒降到几百毫秒。但要注意优化后调试会变困难建议先保证功能正常再逐步优化。7.3 代码提交与维护移植完成后把改动整理成干净的 commit。U-Boot 社区对代码风格有要求提交前跑一下checkpatch.pl./scripts/checkpatch.pl --strict your_patch.patch常见问题包括行尾空格、tab 和空格混用、注释风格不对、commit message 格式不对。虽然自己用的板子不一定要提交上游但保持代码风格一致后续升级 U-Boot 版本时合并冲突会少很多。我个人在实际操作中的体会是U-Boot 移植最难的不是写代码而是排查问题。串口没输出的时候你面对的是一个黑盒只能靠经验和逻辑一步步缩小范围。我的习惯是每改一个地方就编译烧录验证一次虽然麻烦但能保证每次问题都定位到具体改动。另外SoC 手册和参考板的原理图一定要仔细看很多问题的答案就在手册的某个角落里。
返回列表