☰
uboot移植实战:从源码索引到DTS配置,构建开发板引导全流程
2026/10/3 22:14:46 网站建设 项目流程

刚拿到开发板,uboot移植这件事我都是先做索引再做代码

先给同样被uboot折腾过的朋友说句实在话:uboot移植的难点从来不在“改代码”本身,而在于面对一块新板子时,你不知道该从哪里下手、哪些文件动不得、哪些配置改了之后要连带改哪里。我做了这么多年嵌入式,总结下来,uboot移植本质上就是一次“信息索引”的建立过程——你要在庞大的源码树、芯片手册、厂商BSP和网络热词堆里,把跟自己板子相关的信号线、时钟、内存、外设驱动全部找出来,打成一张可查的对照表。这篇内容就是聊这件事,适合正在做uboot移植的嵌入式工程师、学生,以及手上有一块不常见开发板却想跑起Linux引导的人。

我第一次拿到一款国产主控的评估板时,串口完全没有输出,点灯程序勉强能跑,可一进引导就黑屏。后来发现,问题根本不是电路图错了,而是我压根没把DTS里的内存节点、串口引脚和实际板子的走线对应上。这就是uboot移植里最典型的“信息盲区”。所以这篇文章的核心内容,就是教你怎样快速建立自己的移植索引:从源码梳理、DTS调整、板级初始化,到交叉编译、烧录和调试,每一步都讲清楚“为什么这样做”,而不是只给你一组命令。

别看网上搜“uboot移植”能出来一堆教程,真到了你的板子上,十有八九都对不上号。原因很简单:uboot的版本差异、芯片差异、板级配置差异,导致每一套代码都是“半定制”。你能用的,只能是思路和方法。这篇文章就是我把这些通用思路和方法整理成一份可复用的“索引表”,希望能帮你少走几个月的弯路。

1. 移植前先建索引:整体思路与准备工作

1.1 选对uboot版本,比改代码更重要

很多新手拿到板子之后第一件事就是去找“最新版”的uboot源码,然后开始改。这其实是移植路上第一个坑。uboot并不是越新越好,越是新的版本,代码结构变化越大,很多老板子的厂商补丁根本打不进去。我自己做移植的时候,更倾向于选择一个“稳定且资料多”的版本,比如u-boot 2018系列,或者厂商BSP自带的版本,而不是盲目追主线。

有个很现实的例子,网上常有人问“wr703n刷uboot用什么版本”“itop4412移植uboot2017怎么弄”,这些问题的共性就是版本和板子强相关。u-boot 2018这个版本之所以推荐,是因为它的设备树(DTS)机制已经非常成熟,不像早期版本要填一堆平台宏定义。你可以直接把厂商提供的dts文件丢进去,改起来清晰得多。就算后面要升级到更新的uboot,先基于2018把硬件驱动跑通,再研究差异点,也是可行的路线。

选版本的时候,建议你先查三样东西:

  • 厂商BSP是否自带uboot源码,如果带了,优先用它,因为驱动适配都是验证过的。
  • 主线uboot中是否已经包含你主控芯片的SoC支持,如果没有,要么找厂商补丁,要么移植相近SoC的配置。
  • 你手上的Linux内核版本,因为uboot和内核之间通过设备树传递硬件信息,两者的dts最好来自同一套体系,避免节点格式对不上。

1.2 建立“交叉工具链 + 手册 + 源码”的铁三角索引

uboot移植要准备的东西,其实不算多,但每一样都可能成为卡壳点。我给自己立了一个标配清单,每次移植前都会确认一遍:

  • 交叉编译器:比如arm-linux-gnueabihf-gcc,版本别太老,至少支持gnu11标准,否则有些dts编译脚本会报错。
  • 源码树:uboot源码本体,配上厂商提供的补丁或BSP分支。
  • 芯片手册:至少要有SoC的datasheet、开发板原理图、DDR颗粒手册、PHY芯片手册。没有原理图做移植等于盲人摸象。
  • 调试工具:串口转USB模块(TTL电平)、J-Link或厂商烧录器、网线(用于tftp下载内核)、SD卡读卡器。

提示:严格来说,一个能出u-boot.bin的交叉工具链就够了。但我强烈建议你把cscope或ctags建立好源码符号索引,直接用source insight或者vscode的“全局搜索”也行。grep源码符号的效率,决定了你排查问题的速度。我见过太多人还在用文本编辑器逐个目录翻,翻到吐。

1.3 把“索引”思维用到源码里:config、dts、board三层定位

uboot移植涉及的文件多,但真正需要你动手的只有三层:

第一层是config,也就是板级配置头文件,比如include/configs/myboard.h。这里面定义了内存大小、启动参数、命令行环境变量等。第二层是dts,也就是设备树源文件,描述硬件拓扑。第三层是board,也就是board/vendor/myboard/目录下的板级初始化代码,包含board_init、board_late_init等函数。

我自己的习惯是,拿到一份uboot源码后,不急着看代码,而是先在文档里画一张表,把自己板子的关键硬件和源码中的对应关系列出来。比如:

硬件模块原理图标注源码搜索关键字涉及文件
串口UART0,TX/RX引脚PE8/PE9CONFIG_CONS_INDEX,uart0include/configs、arch/arm/mach-xxx
DDRDDR3颗粒,容量512MBCONFIG_NR_DRAM_BANKS,SDRAM0board/xxx/ddr.c
以太网PHY地址0x01CONFIG_PHY_ADDR,phy_iddrivers/net/phy
存储SD卡,SDMMC1CONFIG_MMC,sdmmcdrivers/mmc

做完这张表,你已经建立了自己的“移植索引”骨架。后面的时间,基本就是在往这张表格里填充细节,而不是毫无头绪地改代码。这一步看似费时间,实际上能帮你省掉至少一周的迷茫期。

2. 移植核心流程与实操步骤

2.1 从“最接近的配置”起步,而不是从零开始

uboot移植最快的方法就是找一块“已经能跑”的板子做参照。如果你的SoC跟某个开发板同系列,那就直接复制这块板的defconfig改一改。几乎没有一个成功的uboot移植是从空目录开始写起的,行业内都是“改配置 + 改dts + 补驱动”。

具体操作是这样的:

  1. 在configs/目录下找跟你的板子最接近的defconfig,比如xxx_defconfig。
  2. 复制一份,改成你自己的板级名称,比如myboard_defconfig。
  3. 打开这个文件看关键配置项,比如CONFIG_SYS_SOC、CONFIG_SYS_BOARD、CONFIG_SYS_CONFIG_NAME,这些决定了uboot会去include/configs/目录下找哪个头文件。
  4. 执行make myboard_defconfig,然后make,先看看能不能编译通过。

我第一次做移植是拿一款老平台wr703n的配置作为起点,它的代码量小,结构简单,非常适合理解uboot的编译流程。后来的itop4412移植项目,我也是借助厂商原有config开始,而不是直接去追主线,几次尝试之后确认是把uboot2017跑通了。

编译之前,记得先配置环境变量:

export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make myboard_defconfig make -j8

如果defconfig里的名字没改对,make的时候会出现找不到配置项的错误。不要慌,打开configs/myboard_defconfig,把CONFIG_SYS_CONFIG_NAME改成你在include/configs下创建的头文件名,这是最常见的问题。

2.2 DTS设备树调整:uboot 2018时代的核心工作

从u-boot 2016开始,DTS已经是uboot的标准配置方式。你打开arch/arm/dts/目录,会看到一票以SoC型号命名的dts文件,比如hi3798m100.dtsi、zynq-zc702.dts等。uboot移植过程中,dts是你改动最频繁的文件。

DTS结构分三层:dtsi(SoC级)、dts(板级)、dtsi的include链。SoC级dtsi里定义的是这个芯片的所有外设节点,比如uart、mmc、ehci、mac,它们默认是disabled的,板级dts中把用到的节点改为okay,并补充引脚、时钟、复位等属性。

下面是我移植时基本都会改的几个节点,写出来给你参考:

&uart0 { status = "okay"; clocks = <&clk_uart0>; pinctrl-names = "default"; pinctrl-0 = <&uart0_pins_a>; }; &mmc0 { status = "okay"; bus-width = <4>; cd-gpios = <&pio 4 0 GPIO_ACTIVE_LOW>; }; &mdio { phy0: ethernet-phy@0 { reg = <0>; }; }; &gmac { status = "okay"; phy-mode = "rgmii"; phy-handle = <&phy0>; };

这里有几个容易踩的坑:

  • pinctrl节点名必须和dtsi中pinctrl定义的子节点名一致,否则uboot虽然能编译,但引脚不会复用。
  • clocks属性里的clk_uart0这些宏,要去dtsi或者include/dt-bindings/clock/下确认定义,写错数字会导致驱动初始化失败。
  • 以太网的phy-mode具体值要跟电路实际连接一致,rgmii还是rmii,千万不能凭感觉填。

注意:DTS编译的报错信息在uboot里比较“抽象”,常见的是“syntax error”或者“FDT_ERR_BADMAGIC”。前者是格式问题,后者通常是dtsi被include进了两次。我建议每次改完dts之后,先用make dtbs单独编译设备树,不要直接整包编译。

2.3 板级初始化:把“点灯”跑通再说

uboot移植是否成功的第一个里程碑,不是看到命令行提示符,而是能控制一个GPIO点灯。这个目标很小,但它验证了时钟、引脚复用、GPIO控制器驱动三个最基础的模块,全都正常。

板级初始化代码通常放在board/myboard/myboard.c里。你需要实现以下几个关键函数:

  • board_init:初始化串口、GPIO、时钟等。
  • board_late_init:在uboot启动后期做额外配置。
  • dram_init:告诉uboot板上有多少内存、分几个bank。
  • dram_init_banksize:填写gd->bd->bi_bankstart,供后续内存分配使用。

dram_init是个重灾区。很多板子明明有512MB内存,uboot却只识别到128MB,登录后free一片混乱。常见原因是configured using CONFIG_NR_DRAM_BANKS不对,或者DDR控制器OPM(操作模式)设置错误。我一般会在dram_init里临时加一句printf打印gd->ram_size,用它来对照理论计算值。

这里也提一下“移植freertos移植lvgl”这类gpio相关项目。很多人做完uboot之后会继续做GUI或RTOS,其实底层都是引脚初始化那套逻辑,思路完全相通。你要做的就是先在uboot阶段把GPIO读写函数跑通,上层随便怎么玩。

2.4 编译和烧录:用起来像样的镜像

uboot编译产物有u-boot.bin、u-boot.img、u-boot-spl.bin等。具体烧哪个,取决于你的启动方式。如果你的SoC内有ROM引导,一般会把SPL放到偏移地址,然后SPL把主uboot加载到DDR里。如果你的板子是直接从NOR或SD卡启动,那可能只需要一个完整的u-boot.bin。

我用得最多的烧录方式有两种:

  • SD卡启动:把生成的u-boot.bin写入SD卡特定扇区偏移。
  • tftp下载:在uboot命令行中用tftp命令加载镜像。

SD卡写入的典型命令:

sudo dd if=u-boot.bin of=/dev/sdb bs=512 seek=1 conv=fsync

注意那个seek=1,很多板子是跳过了第一个扇区的MBR。至于烧到eMMC的地址,要参考厂商的烧写脚本,每家的偏移都不一样,不要照搬。

烧录完成后,上电看串口log。如果串口没有任何输出,优先检查三件事:串口工具波特率是否正确、uboot里配置的调试串口引脚是否对、u-boot.bin有没有真的写到启动介质上。八成问题出在这三个的某一个里。

3. 调试方法与工程实录:从“点不亮”到“跑通系统”

3.1 串口无log的排查逻辑

uboot移植第一个拦路虎,就是串口无输出。有些板子还好,至少能看到ROM code打印的乱码,有些干脆全黑。我把排查顺序总结成一个固定流程:

  1. 先用示波器或逻辑分析仪测uart TX引脚,看有没有波形。没有波形,就说明uboot根本没执行到串口初始化。
  2. 有波形但全是乱码,检查波特率。uboot默认115200还是厂商改成了别的值,你要跟bootrom的打印保持一致。
  3. 有波形也有像样的log,但停在某一行不动,那才是uboot代码运行阶段的bug。

这里要特别提一句“linux摄像头移植教程”这些热词里常见的调试手法:先把串口作为最基本硬件资源,它的初始化要放到board_early_init_f阶段,比任何驱动都早。如果你在board_init里才开串口,那前面的SPL阶段日志全丢,调试会很吃力。

对于带MMU的SoC,还要确认一个细节:页面映射是否覆盖了UART寄存器物理地址。如果dcache开启而UART寄存器没被map到虚拟地址中,写寄存器可能被缓存吞掉,表现就是串口偶尔输出、偶尔不输出。

3.2 网络调试:tftp下载是移植中期的最强工具

uboot阶段没有文件系统,跟主机交互最常用的方式就是网络。启动Linux内核时,我们常常用uboot的tftp命令把内核镜像和设备树拉到DDR里,再bootm启动。这样反复调试内核,都不用反复拔插SD卡。

网络驱动的调试,多半卡在PHY芯片上。PHY的地址、是否内置、MAC与PHY的连接模式(MII/RMII/RGMII),全靠板级dts中的phy-handle和phy-mode字段控制。我在一块板子上遇到过ping不通,查了半天发现是PHY地址写错了,芯片实际是地址0x03,我代码里写的0x01,导致每次都是在一个不存在的MDIO设备上读ID。

调网络的时候,可以先用mii命令看PHY状态,比如:

mii device # 查看当前MDIO总线上探测到的PHY mii info # 读取PHY ID和链路状态

如果mii info显示PHY ID全是ffff,说明MDIO总线没通,多半是引脚或时钟问题,不是uboot配置问题。

3.3 从tftp到启动Linux:验证uboot对内核传递真的没问题

uboot移植的成功标准,最后还是要落到“能引导Linux内核”上。这里有个跟热词“zynq移植busybox”很搭的经典组合:uboot加载好内核跟文件系统,再用bootargs指定root=/dev/nfs或者root=/dev/ram0。我自己调试时经常用RAM版本的busybox根文件系统作为最小验证环境,这样不需要碰存储介质,排查起来最干净。

启动命令大致是:

setenv ipaddr 192.168.1.10 setenv serverip 192.168.1.1 setenv bootargs 'console=ttyS0,115200 root=/dev/ram0 rw' tftp 0x40000000 zImage tftp 0x44000000 myboard.dtb bootz 0x40000000 - 0x44000000

有内核起来,说明你的uboot已经把内存大小、设备树地址、串口参数正确传递给了内核。如果内核起来后又panic在“Unable to handle kernel paging request”,那就要回头检查uboot的dram_bank配置,很可能是内存大小没传对。

3.4 git diff是最好的移植日志

移植过程中改了多少文件、哪些配置动了,如果不做记录,两周之后你自己都记不住。我强烈建议每完成一个功能模块就提交一次commit,commit message写清楚改动点和验证方法。比如:

arm: myboard: enable Ethernet PHY support - add PHY address for myboard - set phy-mode to rgmii - verified with tftp download Signed-off-by: yourname <you@example.com>

这样整个移植过程的diff就成了一份天然的“索引文件”,后续换人维护、升级uboot版本、移植到其他板子,都能快速定位影响范围。我自己后来回看几个月前的移植项目,就是靠commit log和数据手册建索引,比重新翻代码高效太多。

4. 常见问题排查速查表与避坑建议

4.1 我把移植过程中踩过的坑整理成了一张速查表

问题现象可能原因排查思路
编译报undefined reference缺少board目录下的.o文件执行make mrproper后重新make myboard_defconfig
DTS节点编译报syntax error缺少逗号或引号不匹配用dtc单独编译dts文件定位报错行
u-boot烧录后无串口log串口引脚复用配置错误检查dts的pinctrl节点是否对应当前板子
内存识别只有一半dram_init里gd->ram_size设置不对打印gd->ram_size,对照芯片手册的片选映射
tftp下载超时网线没插好或PHY驱动没起来用mii info命令查看PHY状态
启动内核后乱码内核与uboot串口波特率不一致检查bootargs里的console参数
保存env后重启丢失未定义CONFIG_ENV_IS_IN_MMC/NOR按启动介质选择环境变量存储驱动
启动时卡在Starting kernel...设备树地址不在RAM内确认bootm/bootz后跟的dtb地址落在RAM范围且未被覆写

这张表我自己每次移植都会翻一遍。上面这些坑,我至少各踩过两回。尤其是“DTS节点编译报错”和“启动时卡在Starting kernel”这两个,排查起来最耗时,因为它们的问题往往不是表面报错位置,而是前一个环节的信息错误。

4.2 避坑建议:加打印、做备份、拆步骤

我自己总结出的三条避坑经验,每一条都来自切实的教训:

第一,不要贸然删除打印信息。uboot里大量printf看起来“没用”,但它们能告诉你当前执行到哪个函数。移植早期我建议把debug等级提升到最高,比如在board_init_f处加#define DEBUG,让系统把每个initcall打印出来。这样你能用最小的成本掌握uboot的执行流。

第二,烧录前备份原始优盘镜像。有的开发板出厂自带一个能用的bootloader,我见过不少人一上来就覆盖烧写,结果uboot没调通,旧的也丢了,只能拆芯片上编程器。正确做法是先把原始镜像用dd完整备份到电脑,再开始折腾。

第三,把一个大目标拆成三个小里程碑。第一个小里程碑是板级点灯,第二个是串口打印uboot命令行,第三个是tftp引导内核。每完成一个就验证一次,出了问题能明确知道是哪个阶段引入的,不会一堆问题混在一起无法定位。

4.3 一套可以在多块板子上复用的“索引模板”

做完几次移植之后,我发现自己不再需要从头翻代码了。原因是手头有了一套通用的索引模板,每次只需要往里填新板子的信息。这里分享一个精简版:

  • 硬件资源索引:记录每一位引脚连接的模块,GPIO号、控制器base地址、中断号。
  • 外设驱动索引:记录每个外设(UART、MMC、GMAC、USB)在dts中的status节点和对应驱动文件路径。
  • 编译打印索引:记录哪些defconfig关键宏会影响编译过程,哪些宏影响运行行为。
  • 调试记录索引:记录每次故障的现象、根因、修改方案,按日期排列。

这套索引一建立,原本要花一两个月的uboot移植工作,基本可以压缩到一周内完成大半。特别是换板子的时候,对比新旧索引,差异一目了然,能快速确定哪些配置可以沿用,哪些必须重做。建立在“信息可检索”基础上的操作,才是可复现的工程经验,而不是一次性手艺活。

我个人的体会是,uboot移植这件事做多了,你会慢慢发现它不再可怕,甚至有点“无聊”——因为所有问题都有迹可循,所有坑都有文档记录。困难的地方不在于敲多少命令,而在于你是否系统地整理过手上的信息。每次动工前,先把索引建好,你就成功了一半。

最后再分享一个小技巧:组里如果有多个人同时做不同板子的移植,可以在共享文档里维护一份公共问题库,谁踩了坑就把现象和解决方案贴进去。这个库积累起来之后,新同事上手速度会快非常多。毕竟uboot移植不像应用编程,很多报错信息反人类,靠的就是“前面有人趟过路”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询