最近在整理U-Boot移植的第二阶段时,发现很多同学不是不会改代码,而是卡在“怎么让U-Boot认识我的板子”这一关。手里拿着自己画的板子,或者某个冷门评估板,翻遍网上的教程,发现全是拿官方开发板举例,一到“如何把自己的板子加进去”,要么含糊带过,要么直接说“改一个现有板子就行”。我在给一块基于Cortex-A7双核SoC的自研板做移植时也走过这段弯路,翻了不少补丁邮件和官方文档才彻底弄明白:添加开发板这件事,本质上是在U-Boot的源码组织体系里完成一次完整的“身份登记”,让Kconfig菜单、配置系统和编译脚本都能找到它。这篇文章就结合Ubuntu 20.04环境,把第一阶段的操作完整捋一遍。内容不算高深,但每步的“为什么”我都会讲清楚,适合正在做U-Boot移植、被海量宏定义绕晕的开发者参考。
1. 为什么要自己“添加开发板”,直接改现成的不好吗
很多人的第一反应是:U-Boot已经支持了同系列SoC的官方板子,那我直接在官方板的defconfig和头文件上改不就行了?短时间实验也许能跑,但这不是移植,是在打补丁。等你想换一颗DDR颗粒、换一个启动介质、想让代码进Git管理的时候,混乱就开始出现了。
1.1 U-Boot是怎么“认识”一块新板的
放在十年前,U-Boot靠根目录下的boards.cfg文件维护所有板卡列表,改一行记录,编译系统就能识别。现在这套机制已经被彻底淘汰,取而代之的是Kconfig体系。你执行make xxx_defconfig时,实际发生的事件链是:
- 根据
configs/xxx_defconfig里的配置项,生成.config; - Kconfig根据
.config把依赖关系解析出来,生成include/generated/autoconf.h; - 编译系统再根据
CONFIG_SYS_CONFIG_NAME这个关键变量,去include/configs/目录寻找对应的板级配置头文件; - 最终所有宏汇总,参与C源码的预处理和编译。
所以,所谓“添加自己的开发板”,就是要在源码里做好三件事:提供板级支持代码(board/<vendor>/<board>目录)、提供配置入口(configs/<board>_defconfig)、在SoC对应的Kconfig中登记新板子(让菜单里出现这个选项)。三者缺一不可。
1.2 不添加板的两种错误移植姿势
第一种姿势是“借壳上市”。直接把官方板的board.c改一改,板子名字、DDR参数、环境变量位置全塞进去。代码能编过,但当你需要修改启动流程时会很痛苦:官方板和自研板的外设、时钟、复位逻辑往往不一样,改来改去,最后这个目录里堆积了无数#ifdef,连原作者都认不出来。
第二种姿势是“只复制不登记”。不少初学者会把一个官方板目录整个复制一份,改成自己的名字,然后兴奋地执行make myboard_defconfig,结果提示找不到文件。原因很简单:configs/目录下没有对应的defconfig,Kconfig菜单里也不存在这个目标板选项,编译系统根本不会把新目录编译进去。代码写得再好,编译系统不知道它的存在,等于白写。
正确理解是:U-Boot不是把“你的板子”当作一个独立项目,而是把它视作源码库里的一员。你想让它被支持,就必须让源码库的“登记制度”认可它。这也是为什么官方一直鼓励直接向社区提交新板支持补丁,而不是自己私底下改。
2. Ubuntu 20.04交叉编译环境,准备好再动手
标题里特意带了Ubuntu 20.04,说明这个版本本身没坑。但我在不少交流群里看到有人卡在环境上,不是包没装全,就是工具链选错。UBoot构建依赖了一系列主机软件包,缺少任何一个,都会在编译中途爆出莫名其妙的错误。
2.1 依赖包清单和安装命令
先执行一遍系统更新,然后一次性安装下面这些包:
sudo apt update sudo apt install -y build-essential git flex bison \ device-tree-compiler libssl-dev libncurses5-dev \ u-boot-tools逐个说下它们的作用:
build-essential:提供gcc、make等基础编译工具;flex、bison:生成设备树编译器(dtc)需要的解析器代码,缺少时会报flex: command not found;libssl-dev:编译U-Boot里的工具程序(比如mkimage)时,需要openssl头文件,少它会报openssl/xxx.h: No such file or directory;libncurses5-dev:make menuconfig依赖的终端图形库;device-tree-compiler:提供dtc命令,用于编译设备树;u-boot-tools:提供mkimage等工具,后续打包镜像会用。
如果你用的是Ubuntu 22.04及以后,libncurses5-dev会变成过渡包,直接装libncurses-dev即可。20.04还是老名字,按上面来就好。
2.2 交叉编译工具链的选择
这里说的交叉编译工具链,是指运行在x86主机上、生成ARM机器码的编译器。工具链能不能用对,直接决定后面是顺利还是折腾。
以ARM 32位平台为例,最简单的方式是直接用Ubuntu源里自带的工具链:
sudo apt install -y gcc-arm-linux-gnueabihf装好后编译器的前缀固定为arm-linux-gnueabihf-。如果你的SoC是ARM 64位(Cortex-A53、A72等),则应安装:
sudo apt install -y gcc-aarch64-linux-gnu对应前缀为aarch64-linux-gnu-。这里特别提醒:32位和64位的工具链不要混用。我之前见过有人拿32位工具链去编译64位U-Boot,汇编阶段报了一堆莫名其妙的指令错误,看起来像源码问题,其实就是工具链位数不匹配。
为什么推荐Ubuntu自带的工具链而不是去网上下某个Linaro版本?因为20.04源里的gcc-arm-linux-gnueabihf是9.x版本,编译U-Boot完全够用,省去了配置环境变量、处理libstdc++.so.6缺失等麻烦。只有当你需要编译原厂SDK里的其他组件(比如DDR初始化固件),且原厂明确要求特定工具链版本时,才建议切换到原厂提供的交叉工具链。
2.3 检查环境是否真的可用
装完后不要急着编译,先验证一次:
arm-linux-gnueabihf-gcc -v which arm-linux-gnueabihf-gcc echo $PATH如果第一个命令能打印出版本信息,且which能找到编译器路径,说明工具链可用。还有一个容易忽略的检查项:确认你的Ubuntu是64位系统。32位系统上无法运行现代交叉编译器。
如果你是在Windows下用WSL来编译,需要注意一点:WSL里编译U-Boot没问题,但后续如果要通过串口/USB下载镜像,WSL对硬件设备的访问是比较麻烦的,通常需要借助usbipd之类的方案。建议纯编译工作在WSL里做,涉及烧录和调试还是回到真正的Linux主机或虚拟机里。
3. 以官方参考板为模板,落地自己的board目录
环境准备好之后,就进入正题。这里我强烈建议“站在参考板的肩膀上”,而不是从零写一个board.c。从零写一个能用的板级文件门槛很高,涉及到时钟、DDR、串口等一大堆初始化逻辑,新手没必要重复造轮子。
3.1 参考板选型原则
选参考板的标准有优先级排序:优先选同一颗SoC、其次选同系SoC、再次选启动方式相近(比如都是SD卡启动)的板子。
举个例子,我的自研板用的是某款Cortex-A7双核SoC,DDR3L颗粒,SD卡启动。那么我就先在这个SoC对应的arch/arm/mach-xxx/目录下找官方支持的评估板,然后看它的board/<vendor>/目录结构。如果源码树里压根没有同SoC的支持目录,那就得先去把该SoC的mach目录补上,这属于更基础的分支,不在本篇文章范围。今天假设你的SoC已经被主线支持,只是官方板目录里没有你的板。
3.2 复制与改写board目录
开一个终端,进入U-Boot源码根目录:
cd u-boot # 创建新板目录,名字建议用小写字母+中划线,不要带空格 mkdir -p board/dev/devboard-a7 # 参考官方板目录复制,假设官方板叫evb-a7 cp -r board/dev/evb-a7/* board/dev/devboard-a7/复制完先做一轮清理,把编译过程中生成的.o、.cmd等临时文件删干净,否则后面可能把过期产物混进新板子里:
cd board/dev/devboard-a7 find . -name "*.o" -delete find . -name "*.cmd" -delete find . -name "*.su" -delete然后逐文件修改。先看Kconfig:
config TARGET_DEVBOARD_A7 bool "Support DevBoard A7" if TARGET_DEVBOARD_A7 config SYS_BOARD string default "devboard-a7" config SYS_VENDOR string default "dev" config SYS_SOC string default "ax7" config SYS_CONFIG_NAME string default "devboard-a7" endif这段配置的含义是:向菜单里注册一个名为DEVBOARD_A7的目标板选项,同时告诉编译系统,这个板子的目录名、厂商名、SoC名、板级配置头文件名分别是什么。SYS_CONFIG_NAME尤其重要,后续编译系统就是靠它去include/configs/下寻找devboard-a7.h的。
接着修改Makefile:
obj-y := devboard-a7.o这句就是把devboard-a7.c加入编译。如果你的参考板目录里还有lowlevel_init.S、ddr.c之类的文件,记得一并保留原名或同步改名,并体现在Makefile里。
最后是MAINTAINERS文件,填上你自己的维护者信息。这里不需要真实人名,格式参考官方风格即可。社区对维护者的要求是“出了问题有人能回应”,不是“填个名就行了”。
3.3 板级头文件与板级代码的关系
在U-Boot的旧式配置体系中,include/configs/<board>.h是板子宏定义的大本营。你可以看到大量CONFIG_SYS_...宏,比如DDR基地址、环境变量偏移、U-Boot在内存中的链接地址等等。现在很多宏已经在往Kconfig迁移,但板级头文件依然是有效且常见的手段。
对第一阶段来说,可以直接从参考板的头文件复制一份,改成include/configs/devboard-a7.h,然后暂时保留参考板的宏,等后面适配DDR和启动介质时再逐个修改。很多朋友不理解“为什么板级C文件里没有太多代码”,这是因为U-Boot把硬件初始化拆分得很开:SoC相关的初始化在arch/arm/mach-xxx/目录,DDR初始化可能通过dram_init()函数回调,真正跟板子强相关的代码就集中在board_init()、board_mmc_init()这几个函数里。你能在devboard-a7.c里看到的大多数代码,本质是承接SoC和板子的“接线”工作。
4. 让Kconfig和configs认识你的新板
目录建好、文件改完之后,别急着编译,还差最后一步登记。这一步做不对,前面的活等于白干。
4.1 从boards.cfg到Kconfig的登记制度
老版本的U-Boot是通过boards.cfg文件来维护板卡列表的,网上一些老教程会让你在这个文件里加一行。而现代U-Boot已经彻底Kconfig化,你得在SoC对应的架构菜单里显式引入新板的Kconfig文件,菜单里才会出现你的板子。
具体操作是找到SoC对应的架构Kconfig。比如我用的SoC是ax7,对应arch/arm/mach-ax7/Kconfig。在这个文件里加入一行:
source "board/dev/Kconfig"注意,这行要放在合适的位置,通常是该SoC的板卡选择区。加入之后,执行:
make menuconfig在ARM architecture的菜单里应该能看到Support DevBoard A7这个选项。这一步可以验证前面的登记是否成功。
4.2 编写defconfig与最小配置
登记完成只是让U-Boot“认识”你,真正决定怎么编译,还是需要configs/devboard-a7_defconfig。创建一个新文件:
CONFIG_TARGET_DEVBOARD_A7=y CONFIG_DEFAULT_DEVICE_TREE="devboard-a7"这里CONFIG_TARGET_DEVBOARD_A7=y会触发板级Kconfig里的所有依赖项,CONFIG_DEFAULT_DEVICE_TREE指向后续要添加的设备树文件名。如果你这一阶段还不想引入设备树,可以暂时不写设备树相关项,甚至在板级Kconfig里取消CONFIG_OF_CONTROL,但要注意参考板可能默认依赖设备树,贸然关掉会导致编译失败。
更稳妥的做法是:从参考板的arch/arm/dts/目录里复制对应dts文件,改名为devboard-a7.dts,然后在arch/arm/dts/Makefile里把新dts加进目标列表。设备树内容可以先沿用,等后续驱动适配再真正修改。这样既满足构建链,又不会让第一阶段卡死。
4.3 用savedefconfig生成极简配置
很多新手习惯自己手写defconfig,结果漏写一堆依赖项。其实更好的方法是先生成一个临时配置,再回写精简defconfig:
make devboard-a7_defconfig make menuconfig # 根据需求调整选项 make savedefconfig cp defconfig configs/devboard-a7_defconfigmake savedefconfig会根据当前Kconfig依赖关系,自动把.config精简成最简defconfig。这个命令能省去大量手工整理时间,也能避免因为缺了某个隐含依赖导致的“明明配置了却没用”问题。
完成这一步后,执行make devboard-a7_defconfig,编译系统会读取configs/devboard-a7_defconfig生成.config,再根据Kconfig生成autoconf.h。你可以打开include/generated/autoconf.h,如果能看到CONFIG_TARGET_DEVBOARD_A7的宏定义,说明整条登记链路已经打通。
5. 第一次编译实战:看log、修报错、拿到bin
配置链路打通后,正式编译,这一阶段的目标非常朴素:让U-Boot编译通过,生成可用的u-boot.bin。专题中的第一期已经处理过U-Boot自身的编译框架,所以你其实是有“这套源码能编过”的心理预期的,剩下的问题只可能出在新板登记环节。
5.1 编译命令与产物说明
正式编译前先设置环境变量:
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf-如果你是64位平台,ARCH要对应用户平台配置,工具链前缀也要同步改。然后:
make devboard-a7_defconfig make -j4 V=1这里我特意建议-j4而不是-j$(nproc)。第一次编译时,如果错误信息淹没在多线程输出里,排查会非常费劲。V=1能打印完整编译命令,适合排错。等确认能编过之后,再放开多线程。
编译成功后,重点看几个产物:
u-boot.bin:裸机U-Boot镜像,后续要打包进启动介质;u-boot.map:链接映射文件,排查链接地址、符号分配时很有用;spl/目录下的u-boot-spl.bin:如果你启用了SPL(Secondary Program Loader),这里就是第一阶段引导程序;u-boot.dtb:如果启用了设备树控制,会有设备树二进制。
第一阶段不需要着急把镜像烧进板子,先确认“能编过、产物齐全”就已经成功了一大半。
5.2 新手最容易撞上的五个编译错误
我整理了几个常见报错,基本覆盖了大部分新手会踩的坑:
| 报错信息 | 根因 | 解决方式 |
|---|---|---|
arm-linux-gnueabihf-gcc: command not found | 交叉工具链未安装或不在PATH | apt install gcc-arm-linux-gnueabihf,检查环境变量 |
flex: command not found | 主机缺少flex | apt install flex |
openssl/xxx.h: No such file or directory | 缺少libssl-dev | apt install libssl-dev |
Error: unknown pseudo-op:.word`` | 编译器不是ARM工具链,或者用32位工具链编64位代码 | 检查CROSS_COMPILE和ARCH设置 |
undefined reference to 'board_init' | 板级Kconfig里的SYS_VENDOR/SYS_SOC和实际目录不匹配,导致板级文件没被编译 | 检查Kconfig中SYS_BOARD、SYS_VENDOR的default值 |
第五个错误特别典型。U-Boot会自动根据CONFIG_SYS_BOARD和CONFIG_SYS_VENDOR拼出目录路径,进而在对应Makefile里编译obj-y。如果你默认值写错了,编译系统找不到板级文件,函数定义就等于不存在。遇到链接错误时,先回查Kconfig里的这几个字符串,比在C代码里找函数体快得多。
5.3 定位编译问题的排查思路
编译报错时,不要慌着粘贴到网上求答案。我自己的排查顺序是:
- 先看报错发生在哪个文件、哪个函数。
- 检查这个文件是不是预期中的板级文件。很多时候是Kconfig登记没生效,导致整个目录没被编译进去。
- 打开
include/generated/autoconf.h,确认关键宏是否存在。宏没有,就去查defconfig和Kconfig依赖。 - 如果是汇编阶段的指令错误,优先怀疑交叉工具链版本或ARCH设置,而不是怀疑源码。
- 用
make clean重来一次,排除旧产物干扰。
还有个小技巧是在board/dev/devboard-a7/Makefile里临时加一行$(warning enter new board build),确认这个目录是否真的进入了编译流程。这种“打桩”手法在定位构建问题时非常有效。
另外,如果你的配置里启用了CONFIG_OF_CONTROL且还没添加设备树,编译大概率会在dtb阶段失败。遇到这种情况,先确认arch/arm/dts/Makefile里是否加入了devboard-a7.dtb(或你的设备树文件名)。这是“添加开发板(1)”阶段最容易忽略的一步,我在做参考板复制时也会经常忘记。
6. 第一阶段验收标准与后续规划
第一次编译跑通后,我建议给自己定一个验收清单,避免“能编过”就误以为“板子已经能启动”。
我的验收标准很简单:执行make devboard-a7_defconfig && make -j4能稳定通过,u-boot.bin生成,且板级目录、defconfig、Kconfig三者的名字保持一致。再进一步,把编译产物用串口工具烧到SD卡,能看到U-Boot串口输出,哪怕停在“Unknown command”都没关系,至少说明板级代码没有被编译系统丢弃。
这时候你的board/dev/devboard-a7/目录已经成了独立的“私房定制”目录,后续做DDR适配、网络驱动、MMC驱动时都可以在这里改,不会污染官方代码。我个人强烈建议在添加开发板之前先git init并打一个tag,每次能正常编译就提交一次。嵌入式源码调试最怕“改了一堆不知道哪里坏了”,版本管理能帮你随时回到已知可用状态。
还有一点想单独提醒:U-Boot的配置体系是“多入口、共终点”的。defconfig不是唯一配置入口,include/configs里的头文件、Kconfig里的default值也能改变编译结果。所以排查问题时,别只盯着一处看,要在autoconf.h里找最终答案。
这篇先写到这里,下一期会从设备树的引入开始,把devboard-a7.dts、DDR初始化、环境变量分区逐步展开。对正在自己动手移植U-Boot的朋友来说,这个阶段最重要的不是追求跑通复杂功能,而是让U-Boot源码心甘情愿地接纳你的板子。等这条登记链路完全疏通,后面的工作会顺畅很多。