U-Boot移植实战:添加自定义开发板的板级适配流程
做Linux系统移植的人见到项目标题“Uboot移植2-- 在U-Boot 中添加自己的开发板”就知道这一关是躲不掉的。不管你手里的板子是公版拿来二次开发还是自己画的“魔改”板只要SoC没换U-Boot这层适配工作几乎都会走到同一条路上把开发板信息“塞”进U-Boot的板级支持体系里。这次项目还带了一个环境限定条件Ubuntu 20.04。我在这个环境下完整走了一遍从拷贝板级目录、改配置头文件、加设备树到最终生成属于自己板卡的defconfig并编译出u-boot.bin把过程中踩过的坑和背后原理一起整理出来。这篇东西适合什么人看已经能无脑编译U-Boot默认配置、但对“板级适配”只有模糊概念的人。它没法替你画原理图但可以让你搞清楚一个问题U-Boot认识一块新板子靠的到底是哪些文件、哪些宏、哪些配置项。1. 这块“开发板”到底该怎么加1.1 U-Boot的板级支持拆解先把这个概念彻底拆开。U-Boot本质上是一个跑在裸机上的引导程序它的所有“选择性”都建立在编译期就知道目标硬件长什么样的基础上。目标硬件的信息不是凭空变出来的而是靠一套组合文件告诉U-Boot的板级目录源码树下的board/vendor/xxx/存放这块板子独有的C文件比如板级初始化、拨码开关读取、上电时序控制这类代码。配置头文件include/configs/xxx.h这是早期U-Boot流传下来的核心配置里面定义DDR内存范围、U-Boot自身加载地址、串口号、波特率、环境变量分区等。设备树arch/arm/dts/xxx.dts现代U-Boot已经大量使用DTS来描述硬件资源驱动通过设备树拿地址、中断号、时钟频率而不是靠一堆硬编码宏。defconfigconfigs/xxx_defconfig基于Kconfig体系决定这一块板子要编译哪些功能、启用哪些命令、打包哪些设备树。很多人第一次加板卡时只改配置文件发现U-Boot根本不识别就是因为没看全这套体系。板级目录管“代码”配置头文件管“参数”设备树管“资源描述”defconfig管“功能选择”四个组件缺一个新板卡都跑不完整。1.2 为什么抄板比从零写更靠谱我的观点非常明确除非你要适配的SoC在U-Boot里完全没有支持否则永远不要从零写一个板级文件。U-Boot的社区维护逻辑决定了同一种SoC下各个板卡的差异远小于它们之间的共性。内存控制器、时钟树、中断控制器、串口IP这些最底层的代码早就在arch/arm/mach-xxx/里写好了board目录下的代码更多是“调用者”和“参数提供者”。选择参考板的条件有三个主SoC一致、DDR方案类似、启动介质接近。满足这三条复制过来改参数成功率非常高。如果参考板是同一颗SoC原厂评估板那更省事基本等于“按着标准答案改卷子”。1.3 添加开发板要改的四大块整体流程总结下来就是四步源码目录里建新板卡身份、配置系统里加上新板卡选择项、告诉U-Boot硬件的具体参数、生成并编译对应配置。我把每个位置要动的东西列成了一张表后面实操时对着看就不会漏功能模块对应目录/文件需要干什么板级初始化代码board/myvendor/myboard_evb/复制参考板C文件调整板级差异化逻辑传统配置宏include/configs/myboard_evb.h修改DDR、串口、加载地址、环境变量参数设备树资源arch/arm/dts/myboard_evb.dts定义内存、串口、网卡、启动介质等节点功能开关configs/myboard_evb_defconfig记录本板卡Kconfig选择结果2. Ubuntu 20.04开发环境准备2.1 安装交叉编译器与依赖Ubuntu 20.04是比较经典的发行版软件源里已经提供了完整的嵌入式交叉编译工具链。32位ARM平台用gcc-arm-linux-gnueabihf64位ARM平台用gcc-aarch64-linux-gnu。我这里项目目标是32位ARM SoC所以装的是前者。sudo apt update sudo apt install -y gcc-arm-linux-gnueabihf bison flex swig libssl-dev device-tree-compiler这几个依赖包缺一不可。bison和flex是Kconfig语法解析工具缺了直接导致make menuconfig无法运行device-tree-compiler提供dtc编译设备树时必须用libssl-dev是U-Boot构建mkimage等主机工具时的依赖。U-Boot代码库对32位ARM的默认工具链前缀就是arm-linux-gnueabihf-装完后可以先用arm-linux-gnueabihf-gcc -v验证一下。有一点需要提醒不要用arm-none-eabi-系列来编U-Boot。那个工具链是给裸机程序用的缺少Linux风格的系统调用支持编译过程中会在链接阶段报一堆莫名其妙的错误。我早期在这上面浪费过不少时间。2.2 拉取U-Boot源码源码获取方式直接用官方Git仓库即可建议加--depth 1只拉最新一次提交节省时间也避免仓库体积太大git clone --depth 1 https://github.com/u-boot/u-boot.git cd u-boot版本选择原则优先选SoC厂商BSP维护的版本或官方稳定tag而不是无脑追最新。U-Boot主分支改动非常频繁Kconfig结构、设备树绑定、驱动框架都在持续变化参考板代码是基于哪个版本写的你也最好停留在相近版本。我的习惯是把git tag列出来挑一个发布时间不太久、正好带目标SoC平台的版本然后切过去编译一次默认板子确认环境没问题。2.3 先列一份“新板卡差异清单”动手改代码之前强烈建议先把差异清单写出来。这个清单一般来自原理图和硬件参考手册核心就五个问题DDR颗粒型号和容量是多少、调试串口挂在哪个UART且引脚是否有复用、SD卡/eMMC/NOR Flash用哪个控制器、网卡PHY芯片型号和地址、板上有没有需要额外供电或复位延时的外设。这份清单的价值在于后面改配置头文件和设备树时每一个问题都对应一组实实在在的修改点。比如DDR容量直接决定CONFIG_SYS_SDRAM_SIZE填多少串口控制器地址直接决定设备树里uart节点的reg值。没有清单就改配置等于闭着眼睛猜参数出来的U-Boot烧进板子基本没反应。3. 实际在U-Boot中添加自己的开发板3.1 复制一份板级目录做地基我习惯把参考板命名为“refboard”自己的板子命名为“myboard_evb”vendor用“myvendor”下面整个过程都用这套假名你换自己的板名就行。先复制board目录和配置文件头cp -a board/myvendor/refboard board/myvendor/myboard_evb cp include/configs/refboard.h include/configs/myboard_evb.h这一步只是地基。接下来要做的是让U-Boot的构建系统认为这是一块新板卡而不是参考板的复制品。board目录下的C文件名如果不改其实问题不大但我更建议一起改掉避免后续git log追踪时和参考板混淆。cd board/myvendor/myboard_evb mv refboard.c myboard_evb.c注意C文件里面包含的头文件路径一般要同步改成myboard_evb.h文件名变了但内容还是参考板逻辑这一阶段能通过编译就行。3.2 让Kconfig认识新板卡光复制目录没用Kconfig体系还不知道世界上多了一块叫myboard_evb的板子。Kconfig的入口通常在arch/arm/mach-xxx/Kconfig你会看到一堆类似TARGET_REFBOARD的配置项。需要照葫芦画瓢加一个config TARGET_MYBOARD_EVB bool MyBoard EVB board select ARM imply ... # 按参考板继续补充 help MyBoard EVB is a development board using MyVendor design.再往下Kconfig里还要定义三个重要的默认值这三个值决定了U-Boot构建系统去哪里找板级文件和头文件config SYS_VENDOR default myvendor config SYS_BOARD default myboard_evb config SYS_CONFIG_NAME default myboard_evbSYS_VENDOR对应board下的顶级目录SYS_BOARD对应板级目录SYS_CONFIG_NAME对应 include/configs 里的头文件名。三者任何一个对不上编译时就会报找不到文件。加完之后还要检查arch/arm/mach-xxx/Kconfig的source是否引用了board目录下的Kconfig如果参考板的路由是显式的source board/myvendor/myboard_evb/Kconfig就要同步加上自己的路径。3.3 修改板级配置头文件include/configs/myboard_evb.h是传统宏配置的核心这一步需要对照差异清单仔细改。最关键的三类参数是内存、串口、加载地址。常见写法如下#ifndef __MYBOARD_EVB_H #define __MYBOARD_EVB_H #include configs/refboard.h #define CONFIG_SYS_SDRAM_BASE 0x80000000 #define CONFIG_SYS_SDRAM_SIZE (512 * 1024 * 1024) #define CONFIG_SYS_LOAD_ADDR CONFIG_SYS_SDRAM_BASE #define CONFIG_CONS_INDEX 1 #define CONFIG_SYS_BAUDRATE_TABLE { 115200, 57600 } #endifCONFIG_SYS_SDRAM_BASE是DDR映射到CPU地址空间的起始地址不同SoC差异很大必须查对应SoC的内存映射表确认。CONFIG_SYS_SDRAM_SIZE是物理内存大小如果板子上焊的是两颗256MB的颗粒且总线位宽正常识别填512MB没问题。CONFIG_CONS_INDEX是串口编号对应U-Boot眼中的控制台默认串口。这三个值错了表现基本都是上电后串口没有输出或者输出乱码。另外要留意新版U-Boot已经有大量配置项迁移到了Kconfig里头文件里的宏数量比老版本少了。如果你基于一个很老的板子移植建议把它头文件里的宏逐个对照Kconfig检查一遍已经被Kconfig替代的宏会被忽略不会报错但会误导排查。3.4 构建设备树节点设备树文件放在arch/arm/dts/下现代U-Boot的做法是直接继承参考板的dts或dtsi然后覆盖差异部分。写一个最小化的dts/dts-v1/; #include refboard.dtsi / { model MyBoard EVB; compatible myvendor,myboard-evb, myvendor,refboard; chosen { stdout-path uart1; }; memory80000000 { device_type memory; reg 0x80000000 0x20000000; }; }; uart1 { status okay; };DTS里的memory节点和头文件里的CONFIG_SYS_SDRAM_SIZE逻辑上要一致一个管的是运行时Linux看到的物理内存一个管的是U-Boot自己的内存布局认知。U-Boot早期用头文件的宏但它会把完整设备树传给内核所以两边对不上就是“U-Boot觉得板子有1GB内存Linux只看到512MB”的诡异问题。还要注意arch/arm/dts/Makefile里面有一长串dtb-y定义需要把自己的dts加进去否则make不会编译它。格式就是在对应SoC段落里补一行dtb-$(CONFIG_TARGET_MYBOARD_EVB) myboard_evb.dtb3.5 生成自己板卡的defconfig新板卡Kconfig加完之后最稳妥的方式是直接基于参考板defconfig改。先把参考板defconfig复制一份cp configs/refboard_defconfig configs/myboard_evb_defconfig然后不管用make menuconfig还是直接编辑文件都要把CONFIG_TARGET_REFBOARDy改成CONFIG_TARGET_MYBOARD_EVBy。这里特别提醒一点如果直接复制defconfig文件里可能残留一些参考板特有的开关比如某个GPIO驱动或自定义命令这些开关不会因为CONFIG_TARGET的切换而自动失效长期维护时容易遗留无用代码。建议编译通过后再用make menuconfig里逐项check一遍。defconfig是构建的最后一环它决定U-Boot最终长什么样。设置交叉编译环境后就可以进入实际编译流程了。3.6 交叉编译与烧写验证编译之前先做一次彻底清理避免残留配置干扰export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make distclean make myboard_evb_defconfig make -j$(nproc)第一次编译新板子建议先不用-j并行直接make单线程跑一次出错了看报错更方便。编译成功后产物集中在这样几个地方u-boot.bin整个U-Boot二进制一般烧到启动介质固定偏移。u-boot.dtb编译出的设备树SPL或U-Boot自身会加载它。spl/u-boot-spl.bin如果板子是SPLSecondary Program LoaderU-Boot两级启动结构烧录时用这个。SD卡启动常见的烧写方式是把u-boot.bin写入SD卡的偏移扇区比如偏移0x2000字节具体由SoC启动ROM决定sudo dd ifu-boot.bin of/dev/sdX bs512 seek16 convfsync注意/dev/sdX一定要确认是目标SD卡误写会直接毁掉磁盘数据这个失误没有任何后悔药。4. 实操中容易踩的坑与排查方法4.1 找不到配置头文件最常见的报错长这样include/configs/myboard_evb.h:30:24: fatal error: configs/refboard.h: No such file or directory原因一般是引用的参考板头文件不在include/configs/下或者名称打错。参考板头文件在拷贝后仍然保留着按理说不该报错但有一种情况参考板头文件被Kconfig里另一个SYS_CONFIG_NAME指向了别的名字。所以这个报错出现时先回头检查Kconfig里的SYS_CONFIG_NAME是否和头文件实际名字一致再检查头文件里的#include路径大小写和文件名是否完全一致。4.2 选择列表里没有自己的板卡执行make menuconfig后在板卡选择列表里看不到MyBoard EVB或者编译时不认识myboard_evb_defconfig。这种时候先从三个地方排查加Kconfig的choice是否漏了depends on条件source路径是否已经引入configs/目录下是否有对应defconfig。还有一个隐蔽问题Kconfig里整个板卡选择列表可能被某个总的if包裹比如if SPL或if ARM如果你的目标板卡所在的SoC没有开启对应总开关整个列表都不会显示。解决方法是打开参考板的defconfig对比看它有没有额外定义一些全局select项。4.3 链接阶段符号缺失新板卡编译走到链接阶段报undefined reference最常见的几个符号是board_init、board_late_init、dram_init。原因基本是迁板文件时漏了关键函数或者C文件改名后没同步把函数导出。U-Boot里很多板级函数是弱符号但板级文件里存在强符号时就会覆盖它。如果参考板C文件定义了一堆#ifdef CONFIG_FOO的函数而你新板卡defconfig里没开对应配置这些函数就不会被编译链接时自然找不到。排查手段是先把参考板defconfig复制过来不做任何裁剪确认能完整编译后再逐步精简不要一上来就大刀阔斧地删功能。4.4 设备树编译不过设备树相关报错通常会出现在make的dtb阶段。最典型的问题有三个dtsi include路径错误、dts里引用节点别名但dtsi没定义、dtc版本太老。Ubuntu 20.04自带的dtc版本偏低但通常够用如果报错涉及新版dts语法特性就手动升级sudo apt install device-tree-compiler设备树语法检查不过时可以先只编译dtb验证不用每次全量编译U-Boot。命令类似make myboard_evb.dtb把dts语法和引用错误先解决干净再回到整体编译效率会高很多。4.5 上电后串口没有输出这是最让新手崩溃的坑U-Boot编译没问题烧进板子就是毫无反应。先按从硬件到软件的顺序排查。第一步用示波器或者逻辑分析仪测量串口TX引脚的波形上电时有信号翻转说明代码至少跑到了串口初始化的位置完全没波形优先怀疑供电、晶振、串口电平。第二步确认调试串口的物理编号和U-Boot配置一致SoC芯片上往往有多个UARTUART0和UART1的引脚复用不同配置错了自然没输出。第三步确认波特率绝大部分调试串口默认115200但如果参考板用的是别的波特率U-Boot头和板级文件里没对齐也会出乱码。曾经遇到过一次很无语的情况新板卡硬件上把UART TX/RX接反了示波器测出来CONFIG_CONS_INDEX根本没写错纯粹是硬件工程师把线序画反了。所以“串口无输出”这个坑硬件因素占据的比例比很多人想象的大得多。5. 后续维护与版本管理心得一套能编译、能跑起来的板级支持只是个起点。我个人的习惯是把参考板目录原样保留在仓库里不要删。原因很实际参考板是你的“差分基准”后续无论是升级U-Boot版本还是排查某个外设问题都需要拿参考板和自己的板子对比代码两个目录都在git diff就能直接看出你改了什么、为什么这么改。如果只留下新板卡目录时间久了很容易忘记当初动了哪些地方。还有一个建议是每完成一个可编译的里程碑就提交一次git提交信息里写明“能编译通过”还是“板子引导到U-Boot命令行”或者是“能加载Linux内核”这几个状态之间的差距非常大靠提交记录保留下来后续回溯问题会省非常多时间。适配自己的开发板这件事真正难的不是改配置本身而是理解U-Boot对一块板卡的认知模型由哪些部分组成。参考板的代码、配置头文件、设备树、defconfig这四件套的关系理清了剩下的就是对着原理图填参数。等这一步做通再往后面走就是Linux内核的设备树对接和驱动裁剪那条路就比U-Boot这边顺畅多了。