☰
U-Boot移植实战:从启动链路到DDR与设备树适配
2026/10/8 9:38:14 网站建设 项目流程

1. 项目概述

1.1 为什么大家都卡在U-Boot移植这一步

干嵌入式这行,尤其是做板级开发和BSP的工程师,几乎没人能绕过U-Boot移植这道坎。很多朋友拿到一块新的开发板或者自己画的板子,第一件事就是想把U-Boot跑起来——因为它是整个系统启动链路的第一环,也是一切上层应用(包括那些热词里提到的FreeRTOS移植、LVGL移植、LWIP移植)能跑起来的前提。

先说清楚U-Boot是什么。它本质上是一个bootloader,一段上电后最先执行的用户代码(在片内ROM代码之后),负责把硬件环境初始化到一个"可用"的状态,然后从存储介质里把内核或者应用镜像加载到内存,最后跳转执行。听起来很简单,但实际做起来要命的地方在于:每一块板子的硬件配置都不同,DDR容量和时序不同、引脚复用不同、外设时钟不同、存储介质不同,而U-Boot的源码树又极其庞大,支持的架构从ARM到RISC-V到x86应有尽有,想要让它在你的板子上跑起来,不是配置一下defconfig就能搞定的。

这个话题适合谁来参考?如果你是刚接触嵌入式底层、手头有一块不常见的开发板需要适配、或者在做项目选型时被要求评估某个SoC的BSP完整度,这一篇的内容应该能帮你省下不少瞎折腾的时间。我会把整个移植过程拆成几个大块:启动链路分析、最小系统点灯、驱动适配顺序、以及最常见的坑和排查方法。偏向实操,理论部分点到为止,但关键的"为什么"我会讲清楚。

1.2 移植U-Boot之前必须先想明白的三件事

第一件事:你的启动介质是什么?这决定了你要改的第一段代码是SPL还是更偏后的board_init,也决定了你在调试初期能不能用JTAG救回来。常见的启动方式有SD卡、eMMC、SPI NOR Flash、NAND、USB,不同的SoC有不同的BootROM支持顺序,选错介质可能导致你花了几个小时调试一个根本不会走的流程。

第二件事:你手上有没有参考板?这是整个移植过程里最重要的资源。几乎所有的SoC厂商(ST、NXP、Rockchip、Allwinner、全志、瑞萨等)都会在官方U-Boot仓库里维护一个或者多个参考板的board目录,你要做的事情本质上不是从零写代码,而是"把参考板适配到你的目标板",找到相近的board目录就等于成功了一半。我在后面第3节会具体讲怎么利用这些目录。

第三件事:你的调试手段是什么?U-Boot移植的调试周期里,最珍贵的输出口就是串口。我遇到过一些项目,硬件工程师说串口引脚接了但实际走线到SoC那边错了一根,结果BootROM代码根本没跑起来,处理器完全变砖。所以焊接调试串口之前,务必对着原理图把TX、RX、GND三根线反复确认,尤其是TX/RX不要接反,电平不要搞错(3.3V和1.8V的区别很大)。

这三件事想清楚,你再往下推进的时候心里就有底了。接下来我们正式进入整条移植链路。

2. 移植前的环境准备与关键源码结构

2.1 拿到SoC后的第一件事:读懂BootROM启动流程图

每一颗SoC内部都有一段固化在硅片上的BootROM代码,上电后CPU从复位向量执行的第一条指令就在那里。这段代码负责根据fuse位、引脚电平、eFuse配置等条件,决定从哪里加载下一段启动代码(也就是SPL或者U-Boot本身的镜像)。所以你第一步不是急着编译,而是花半小时把芯片手册里的"Boot Process"章节反复看明白。

以典型的ARM Cortex-A系列SoC为例,BootROM大致的流程是:上电后初始化SRAM、时钟、串口(有的SoC支持BootROM里直接打印启动信息,比如Rockchip的loader会打印“DDR Version”),然后读启动介质上特定偏移位置的签名数据块,校验通过后拷贝到SRAM执行。这个"特定偏移位置"至关重要,你烧写的镜像必须放在那里,否则BootROM找不到合法的引导头,就会回退到下一启动介质或者USB下载模式。

我来举个例子。NXP的i.MX系列,BootROM会查找启动设备头(IVT,Image Vector Table),放在SD卡第1K字节偏移处;Rockchip的RK3288/RK3399,则是查找IDB(ID Block)里的“RKFW”标记;全志的SoC(比如V3s、F1C100s)是从SD卡8K偏移找“eGON.BT0”签名。这些标记和烧写地址都是硬规定,不搞清楚,编译出来的U-Boot就算镜像完全正确也启动不了。

所以实操上我的习惯是:拿到一颗不熟的SoC,先看三样东西——数据手册的Boot章节、SDK里烧录工具的配置文件(比如Rockchip的parameter分区表、NXP的uuu脚本或者mfgtool的ucl2.xml)、以及SDK自带的U-Boot源码里对应board的README。三样对照着看,基本能在半天内搞清楚存储布局和烧录流程。

2.2 确认你手里的U-Boot版本和源码分支

U-Boot的官方仓库在source.denx.de/git/u-boot.git,但实际项目里我们几乎不会直接用主线代码去适配一颗厂商的SoC——因为厂商一般会把自己的BSP提前推送到主线,或者维护一个长期分支。主线代码的好处是安卓设备树(FDT)支持很完善,而且社区修复bug很快;坏处是如果你选的SoC比较新,主线的支持可能还不完整,甚至根本没有这个SoC的defconfig。

我的建议是:第一步先看厂商SDK里附带的U-Boot版本,用那个版本起步。很多时候厂商在release SDK里的U-Boot虽然老,但是已经把DDR初始化代码、存储介质驱动这些最痛苦的部分调通了,你在这个地基上加自己的板级支持,工作量会小很多。当整体功能稳定之后,再考虑要不要升级到主线U-Boot。

这里顺带提一句编译工具链的选择。32位ARM用arm-linux-gnueabihf-或者arm-none-eabi-都行,但如果SoC是Cortex-A7/A9这种老架构,arm-none-eabi-在某些版本的gcc上编译U-Boot会出问题(比如内联汇编语法不兼容),我踩过这个坑。64位ARM必须用aarch64-linux-gnu-工具链。建议直接用芯片厂商SDK里绑定的工具链,或者用Linaro的release版本,稳定性优先,别一上来就追最新版gcc。

2.3 源码树结构里哪些东西必须改

U-Boot源码树展开之后会看到一堆目录,移植时真正需要动手的其实就几个核心部分:

  • arch/arm/mach-*/:芯片相关的初始化代码,包括时钟、复位、L2缓存、DDR控制器初始化。一般SoC厂商已经写好了,你在适配新板子时基本不用动,但如果你的板子改了DDR颗粒型号,要回来改这里。
  • board/<厂商>/<板子>/:板级代码的所在地,包括board_init、board_late_init、引脚复用配置、网卡的MAC地址读取逻辑等。
  • configs/<板子>_defconfig:构建配置入口,决定了Makefile怎么编译、哪些驱动会被编进去、文本环境变量和FIT镜像的默认配置是什么。
  • arch/arm/dts/*.dts:设备树源文件。如果你用FDT方式传参给内核,这里就是要花最多精力的地方。U-Boot自身也依赖DTS来描述板载硬件(甚至U-Boot的驱动模型DM也通过设备树展开)。

我给新手一个很实用的建议:移植初期,在拿到第一版能串口打印之前,不要碰DTS。DTS的内容会影响驱动是否probe成功,但如果你连printch都没有,准备工作再充分也没有意义。先把最小系统跑通,再逐步打开外设节点。

3. 最小系统启动:从零到串口打印

3.1 第一版编译配置:仿照参考板,不要自作聪明

现在我们把目标缩小到一件事:编译出一个U-Boot镜像,烧到板子上,串口能看到启动log,哪怕后面卡死在DDR初始化或者找不到存储介质都没关系,只要能在串口输出东西,就说明CPU core、时钟、串口、DDR(或者至少SRAM阶段)都在工作。

首选路径是找到参考板的defconfig。假设你用的是ST的STM32MP157,那就先看configs/stm32mp15_defconfig或者更细的stm32mp15-dk1_defconfig这种;如果你是全志的V3s,那就是licheepi_zero_defconfig一类的。我的操作方法是:把参考板的defconfig复制一份,改名为myboard_defconfig,然后只改最必要的差异化配置,比如CONFIG_SYS_TEXT_BASE(代码链接地址)、CONFIG_SYS_LOAD_ADDR(内核加载地址)、CONFIG_BOOTCOMMAND(默认启动命令)、CONFIG_BOOTARGS(默认内核cmdline)等,其他一律保持原样。

为什么不要大改?因为一个defconfig里每个CONFIG项背后都对应一串代码路径。你改一个CONFIG_SYS_EXTRA_OPTIONS配错,可能导致编译出来的SPL直接从错误的介质启动;你把CONFIG_DM_SERIAL关掉,串口驱动可能就不加载了,连log都没有。所以我的原则是:第一版求同不求异,参考板怎么配,你就怎么配,最大程度减少变量。

3.2 SPL和U-Boot两段式启动,你该关注哪个

现在大部分的U-Boot都是两段式启动:BootROM加载SPL(Secondary Program Loader,通常很小,几十KB到一两百KB)到SRAM,SPL初始化DDR、时钟等基本外设,然后把完整的U-Boot镜像从存储介质加载到DDR,跳转进去执行。三段式也常见:BootROM -> SPL -> TPL -> U-Boot,TPL一般更小,用来初始化DDR,SPL再加载主U-Boot。

调试顺序必须是先SPL后U-Boot。因为SPL是你能看到输出的第一个自己编译的程序。很多SoC的BootROM本身可以打印一个版本号,那个是芯片出厂固件,等于给你一个"我还活着"的信号;再往后如果你在SPL里打开了CONFIG_SPL_SERIAL_SUPPORT和CONFIG_SPL_DRIVERS_MISC_SUPPORT,那么SPL阶段就能从串口打印log了,哪怕它打印到一半死掉,你也知道问题在DDR初始化之前还是之后。

我强烈建议在SPL阶段打开CONFIG_SPL_SERIAL_SUPPORT、CONFIG_SPL_DM_SERIAL(如果SoC支持DM模型)、CONFIG_SPL_PRINTF和CONFIG_SPL_LIBCOMMON_SUPPORT。没有这些,你连“卡在哪一步”都不知道。踩坑记录里最常见的是:有人改了一个defconfig,编译通过,烧进去发现卡死,但因为没有开SPL串口输出,根本判断不了卡在初始化时钟还是DDR还是存储驱动,只能盲试,浪费时间。

3.3 时钟和DDR初始化:整个移植里风险最高的环节

先讨论时钟。SoC厂商的SDK里一般会在arch/arm/mach-xxx/clock.c里面写好一套从24MHz或者其它晶振频率倍频到CPU/总线/DDR频率的逻辑,你不需要重新推导锁相环公式——除非你的板子换了晶振。这是很多人移植时忽略的坑:参考板用24MHz晶振,你基于成本考虑换成25MHz,厂家默认配置还是24MHz倍频到DDR、AXI、AHB、APB各bus,结果DDR跑在错误的频率上,表现为“SPL偶尔能启动、偶尔起不来”、“有log但到DDR calibration就卡死”、“DDR init passed但跑memtester崩掉”。

所以拿到板子第一步,用万用表或者示波器确认晶振频率,再对照SoC手册的Clock Tree章节,去clock.c、board_clock_init或者DTS的clocks节点里把频率改准确。别跳过这一步,DDR对频率异常非常敏感,而且报错信息往往不直观。

再谈DDR初始化。DDR IP一般分成两大类:一类是芯片内部集成DDR控制器(比如Synopsys DesignWare、Cadence、Arasan),U-Boot里通过厂商的DDR training库初始化;另一类是SoC不带PHY,需要外挂DDR PHY。绝大多数消费级SoC是前者,所以你的工作集中在DDR参数配置上。

DDR配置文件的常见路径有:全志在arch/arm/mach-sunxi/dram_sunxi_dw.c里维护一个dram_params表,里面按dram类型、频率、tCK、tRCD、tRP这些时序参数排布,你在board_sunxi的board头文件里选一组即可;NXP i.MX由board/freescale/imx8mp_evk/lpddr4_timing.c这类文件直接内嵌了寄存器初始化序列,通常是一长串寄存器地址和值,非常难手工修改。

实操经验:如果你只是更换DDR容量(比如从1GB DDP换到2GB双片选),不需要动时序参数,只需要改CONFIG_NR_DRAM_BANKS、行列地址bit数、片选数量这些配置;如果你换了DDR颗粒型号(比如从DDR3L 1866换到DDR3L 1600),最好让厂商提供他们验证过的时序参数表,自己硬配基本不可行。

3.4 从波特率到引脚复用:串口驱动的细枝末节

串口驱动其实是相对简单的,U-Boot的串口驱动基于serial_ops结构体,一般只需要实现putc、getc、pending三个函数。但移植时出问题多半在两个地方。

第一是引脚复用。很多SoC的UART引脚默认是GPIO功能,必须在board_init里做pinmux配置,让UART引脚切换到UART功能。以i.MX为例,需要调用imx_iomuxc_set_pad;全志是sunxi_gpio_set_pinmux。如果你在SPL阶段就想起串口,还要确认SPL里有没有执行板级pinmux初始化,因为SPL和U-Boot主程序的board_init可能是分开的,SPL阶段可能根本没跑到设置pinmux的那段代码。

第二是波特率。U-Boot默认波特率一般是115200,但如果你改了DTS里chosen的stdout-path,或者手动设置了CONFIG_BAUDRATE,要确保和终端软件一致。这个太基础了,但每次都有人栽在上面——我遇到过SPL已经正常输出DDR版本信息了,客户却说串口没有输出,一问,他终端开的是57600。

4. 添加板级支持:Board目录与设备树实操

4.1 复制参考板还是从零创建?两种方案的取舍

当你确认参考板能跑通之后,就该考虑怎么把这块板子的代码变成你自己板子的代码了。两种思路:复制修改和从零创建。

复制修改是绝大多数人的选择,因为它快、能少踩坑。具体操作是cp -r board/vendor/refboard board/vendor/myboard,然后把目录里的.c、Kconfig、MAINTAINERS文件里的板子名替换成新的,接着在board/vendor/myboard/Kconfig里把SYS_VENDOR、SYS_BOARD改成你的板子名字。之后就可以把参考板的defconfig复制过来改改用。这么做最大的优势是保留了一份肉眼可见的差异对照:你的board目录和参考板目录的diff就是你的板级改动清单,后续review和排查都很直观。

从零创建适合那些需要深度定制、复用参考板意义不大的场景。但即便从零创建,工程里还是会大量引用参考板的底层驱动代码,完全从空文件写板级初始化在U-Boot里是没有必要的,因为U-Boot只要求你实现board_init、dram_init这些特定接口,其他逻辑都在通用层。我见过有一些新手试图把board目录里所有函数全部重写,结果只是把厂商调好的代码改成了一堆bug。

4.2 设备树:U-Boot怎么用它描述你的板子

从U-Boot 2014年左右开始,设备树就成了必选项。新的U-Boot驱动模型(DM)让驱动的probe基于设备树节点,所以你的板子有哪些外设、对应什么驱动、引脚配置怎么样,全反映在arch/arm/dts/myboard.dts里。

U-Boot的设备树和Linux内核的设备树是同一套语法,而且U-Boot通常会复用内核的dts(通过ARCH_MISC配置里的CONFIG_OF_LIST指定)。所以在U-Boot单独维护一个dts,不如直接到内核源码树的arch/arm/boot/dts/里找参考板dts,然后基于它修改出你的板子dts,再拷到U-Boot的arch/arm/dts/目录。这样内核启动的时候用同一份dts描述硬件,不会出现U-Boot和内核对同一块外设理解不一致的情况。

DTS里需要重点核对的内容包括:model和compatible要改成你板子的标识;memory节点要跟上一步DDR配置一致(起始地址和大小);chosen的stdout-path指向你的调试串口;各外设节点的status要设为okay;GPIO的pinctrl-0里引脚号要与板子实际走线一致。

我见过最典型的错误是:U-Boot能启动、串口也打印了,但网卡不工作,查了半天发现是dts里网卡节点的reset-gpio指向了一个不存在的GPIO,导致驱动probe的时候去操作一个未初始化的引脚,崩了。

4.3 Kconfig和Makfile:把板级文件挂进编译体系

光有文件还不够,你得让U-Boot的构建系统认识你的板子。三处地方要动:

第一,arch/arm/mach-xxx/Kconfig里应该有config TARGET_MYBOARD这样的选择项,里面select依赖的SoC或board选项,然后help信息里写板子描述。第二步是board/vendor/myboard/Kconfig里配置config SYS_BOARD为myboard、SYS_VENDOR为vendor、SYS_CONFIG_NAME为myboard——这个SYS_CONFIG_NAME会被用来寻找include/configs/myboard.h头文件,所以你的头文件名字必须和它一致。第三,board/vendor/myboard/Makefile要列出编译哪些源文件,通常至少是obj-y += myboard.o。

这里有一个非常常见的坑:编译时提示找不到<configs/myboard.h>,但你的文件明明在include/configs/目录下。原因多半是SYS_CONFIG_NAME拼写和文件名不一致,或者Kconfig里漏了select SYS_CONFIG_NAME。U-Boot的Kconfig体系是"选择即生效"的,不select,相关配置项不会被展开,头文件自然找不到。

4.4 实战演练:给一块STM32MP157工控板添加板级支持

以STM32MP157为例走一遍完整流程,方便你对照自己的操作。假设我的目标板叫myboard,参考板是官方DK2。

第一步,创建目录并复制文件:

cd u-boot cp -r board/st/stm32mp1 board/st/myboard cd board/st/myboard # 修改Makefile里obj-y目标名 sed -i 's/stm32mp1/myboard/g' Makefile sed -i 's/stm32mp1/myboard/g' myboard.c

第二步,创建defconfig和头文件:

cp configs/stm32mp15_dk2_defconfig configs/myboard_defconfig cp include/configs/stm32mp15.h include/configs/myboard.h

然后打开myboard_defconfig,把CONFIG_TARGET_STM32MP15_DK2这类target项替换成CONFIG_TARGET_MYBOARD(需要在arch/arm/mach-stm32mp/Kconfig里增加对应项),再检查CONFIG_DEFAULT_DEVICE_TREE改成myboard。

第三步,创建设备树:

cp arch/arm/dts/stm32mp157c-dk2.dts arch/arm/dts/myboard.dts

修改model、compatible、memory及外设节点。同时arch/arm/dts/Makefile里把stm32mp157c-dk2.dtb加上或者新增myboard.dtb,确保编译时生成对应dtb。

第四步,编译:

make myboard_defconfig make -j$(nproc) DEVICE_TREE=myboard

纯文本编译阶段如果通过了,接着就烧写测试。从复制到编译通过,熟练的话一个小时以内可以搞定,剩下的时间全在调试硬件上。

5. 网络与存储外设适配

5.1 网卡驱动验证:为什么网络是移植是否成功的试金石

很多嵌入式Linux系统的开发流程都依赖网络:通过tftp下载内核、通过nfs挂载根文件系统、通过ssh把编译产物拷到板子上。所以U-Boot移植完成后,网卡能不能正常工作直接决定了后续的“开发体验”和“调试效率”——如果网卡起不来,你每次调试都要拔SD卡或者用USB烧录器,那种折磨谁用谁知道。

网卡适配的技术点通常在列几个方面:是SoC内部集成MAC(比如全志的EMAC、i.MX的FEC)还是外挂独立PHY芯片(常见的有YT8512、RTL8211F、KSZ9031);PHY的地址是什么(地址由PHY芯片的ADDR引脚电平决定,一般是0到31之间);MDIO读写引脚是哪些GPIO(有些设计把MDIO复用到了GPIO上需要手动配);PHY的复位引脚是哪个GPIO,大概率还需要一个上电后的延时等待。

实际操作时,验证网卡的方式是先ping通PC:

setenv ipaddr 192.168.1.10 setenv serverip 192.168.1.100 ping 192.168.1.100

如果ping不通,从这几个维度查:mdio总线是否能detect到PHY(mii info命令会列出PHY的ID和状态);PHY的link状态是否up(mii status或者mdio read);MAC和PHY之间时钟是否正常(很多RMII设计需要给PHY提供50MHz参考时钟);以及发送数据时网卡硬件的TXD引脚信号对不对。

我强烈建议在正式切入内核之前,先在U-Boot里把网络测通。因为U-Boot环境相对简单,没有复杂的协议栈,问题如果出在网络这一层,U-Boot阶段就能暴露出来,排查会直观得多。

5.2 MAC地址与EEPROM:板级信息的管理方式

MAC地址是个不起眼但很容易影响网络功能的细节。有些SoC内部有固定的MAC(比如出厂烧录的fuse位),U-Boot直接读出来用就完事。更多的板子需要在EEPROM里保存MAC地址,U-Boot从i2c总线读取。如果你的板子没有EEPROM,那就只能在环境变量里预置ethaddr,但风险是环境变量被擦除后MAC就丢了,未设置MAC的网卡在Linux里可能出现设备无法启动的毛病。

我遇到过的情况:一块板子刷了官方出厂固件能上网,我们自己移植的U-Boot启动后Linux里网卡出现Invalid MAC address或者直接用随机MAC。原因就是U-Boot环境变量里没有ethaddr,而U-Boot启动时试图获取MAC失败,没有往后传递有效的local-mac-address到设备树,Linux只能生成一个随机MAC。

解决方式很简单,在启动环境里预先设置好一个合法的MAC:

setenv ethaddr 00:11:22:33:44:55 saveenv

如果板子有eMMC或者EEPROM,更规范的做法是在board_late_init函数里,去i2c总线上读固定偏移的MAC,校验有效性后写入环境变量。这个逻辑可以在board代码里自己写,也可以在DTS里加一个nvmem节点,让U-Boot的eth驱动去读。

5.3 存储介质支持:SD卡、eMMC与SPI NOR的启动配置

存储介质适配是启动能否持久的根本。不同介质的驱动在U-Boot里的实现路径差异很大:

SD卡和eMMC走的是MMC子系统,由drivers/mmc/下面的平台驱动(比如sdhci、dw_mmc、sunxi_mmc)加通用层组成。最常遇到的问题有两个:一是CONFIG_MMC和具体控制器宏有没有开启;二是SD卡CD(card detect)引脚的使用是否正确。很多板子在设计时没有接CD引脚,如果你不关掉CONFIG_DM_MMC里的CD管理,驱动会一直认为没有插卡,导致卡永远扫描不到。常见操作是在DTS的mmc节点里设置cd-gpios为不存在的GPIO,或者直接broken-cd;属性来禁用卡检测。

SPI NOR走的是SPI子系统和MTD子系统。这里是另外一个高频坑:SPI NOR Flash的JEDEC ID不在U-Boot的spi_flash_probe支持列表里,导致驱动报unrecognized JEDEC id。临时的解法是把这个JEDEC ID加到drivers/mtd/spi/spi-nor-ids.c里的表格里,指定对应的mfr和name;长期的做法是让硬件选型时优先选U-Boot已经支持的型号(比如w25q128、GD25Q128这些很常见的)。

NAND Flash的适配最麻烦,因为它需要坏块管理和ECC,不同厂商的NAND排列方式差异很大。除非必须,否则不建议在移植初期就碰NAND。先用SD卡或者eMMC把系统跑起来,NAND相关的支持放到后面再补。

5.4 环境变量保存位置:又一个极容易被忽略的坑

U-Boot环境变量默认保存在一个固定的存储位置,由CONFIG_ENV_IS_IN_*决定,比如CONFIG_ENV_IS_IN_MMC、CONFIG_ENV_IS_IN_SPI_FLASH、CONFIG_ENV_IS_IN_FAT等。如果这个配置和你的实际启动介质不匹配,会出现两种诡异现象:saveenv保存成功了,但下次重启时环境变量丢失;或者启动时提示Warning: failed to validate saved environment。

问题根源在于:你把环境变量存在了MMC的某个分区偏移,但那个偏移上恰好是文件系统的数据区,写几次之后把文件系统搞坏了;或者你的环境变量存在SPI Flash的某个扇区,但该扇区恰好在bootloader镜像附近,升级固件时覆盖了环境变量。

规范的操作方式:SD/eMMC启动的话,建议把环境变量放在一个独立的专用分区或者MMC的末尾区域(比如CONFIG_ENV_OFFSET设为某个大于bootloader镜像大小的值),同时CONFIG_ENV_SIZE设置为至少32KB(默认一般是8KB到64KB,取整页对齐最稳妥)。SPI NOR的话,放在离bootloader结束位置往后至少留出一个扇区的位置,避免互相影响。

还有一个非常山寨但是很多开发板上的默认做法:环境变量直接放BSS段后面(CONFIG_ENV_IS_NOWHERE)。如果实在没有存储介质可以存,那就打开环境变量默认值表兜底,但每次重启都要重新设IP、设bootcmd,开发初期能忍,量产别这么干。

6. 常见问题与排查技巧实录

6.1 上电卡在BootROM:片内代码没起来的排查方向

表现:串口完全无输出、电流仅有待机值、CPU core不工作。这种情况下U-Boot根本没有被执行,问题基本是在更底层。排查顺序是:

  • 电源是否稳定:核对各路电源的时序(power sequence)是否满足SoC要求。很多SoC要求先核心供电再IO供电,间隔几百us,时序反了或者间隔不对,CPU是起不来的。用示波器抓上电波形,对照手册里的时序图,这步最基础。
  • 复位脚电平:确认复位脚释放时间是否足够长,有些SoC要求复位拉低至少几十毫秒后才允许释放。
  • 时钟是否起振:用示波器或频率计量晶振引脚,确认有正确的振荡频率。最怕的是晶振虚焊或者匹配电容过大导致不振。
  • Boot模式引脚:检查BootROM的启动模式选择引脚电平是否对上你的启动介质。很多SoC上电时靠几个GPIO的电平决定从SD还是USB还是SPI启动,这些引脚被外部上下拉电阻固定,但如果你画板时没接或者接错,BootROM就会走错流程。

这个阶段没有任何软件可以调试,全靠硬件测量。所以前面说的“调试串口要正确连接”之外,万用表、示波器也是必备工具。

6.2 SPL卡死:用jtag之外的办法确定卡在哪个函数

如果串口有输出但卡在某一行log之后,通常问题就出在log之后马上执行的那段代码。U-Boot的SPL代码里有很多debug级别的打印信息,默认不显示,你可以打开CONFIG_DEBUG_UART或者CONFIG_SPL_DEBUG来打印更多细节。

更硬核的办法是在怀疑卡死的函数里手动加打印串口输出——很多人觉得改源码加print很麻烦,但这是工作量最小、定位最准的方法。比如怀疑DDR初始化卡死,在dram_init函数入口加一行printf("enter dram_init\n"),出口加一行printf("leave dram_init\n"),如果只打印enter不打印leave,问题一定在这个函数内部。

另外强烈推荐在SPL阶段打开CONFIG_SPL_FIT_IMAGE_POST_PROCESS(如果有)和CONFIG_SPL_LOAD_FIT相关的打印,确认SPL是否成功从存储介质读取了U-Boot主镜像。这能区分“SPL自身运行到了末尾”和“镜像读出来但跳转失败”。

6.3 DDR初始化失败:数据总线位宽与地址线错误最常见

DDR初始化失败的表现多种多样:卡在DDR clock初始化、提示DDR training fail、或者干脆重启变砖(有些SoC在DDR失败后强制系统复位,看起来像不断重启)。

经验之谈,第一件事检查DDR数据总线位宽。参考板是32位DDR,你的板子如果做16位,那么CONFIG_SYS_FSL_DDR3或对应SoC的DDR配置头文件里的DATA_BUS_WIDTH必须改成16。这个配置不对,DDR training百分之百失败。第二件事检查行列地址宽度。DDR颗粒的地址线数(Row bits、Column bits、Bank bits)不同,对应到DDR控制器的配置也是固定的,改粒度之前多核对颗粒datasheet。

还有一类隐蔽问题:DDR电压不对。DDR3L是1.35V,DDR3是1.5V,如果你的电源设计用了1.5V的LDO但颗粒是DDR3L,偶尔能启动但跑memtester报错,问题不在代码而在硬件。先排除硬件问题再调配置。

6.4 网卡识别不到PHY或ping不通

网卡问题我在5.1里提了主要方向,这里补充两个常见的隐蔽细节:

第一个是MDIO时钟问题。MDIO总线的时钟源来自MAC,默认可能是2.5MHz,但有些PHY对MDIO时序有上限要求,时钟太快会让PHY无响应。你可以在驱动初始化里设置MDC divider,或者检查CONFIG_PHY_GIGE这个配置项,如果你的PHY不是千兆的,但驱动按千兆去初始化,可能会在自协商阶段卡住。

第二个是TX_CLK延时问题。RMII接口的TX_CLK信号经常要求PHY提供一个50MHz时钟给MAC,此时对PCB走线长度非常敏感。有些PHY内置了clock delay的配置项(通过寄存器或者外部引脚),但默认值是“no delay”,如果你的板子在PHY芯片端做了等长却没在寄存器里开delay,就会出现网络不稳定的情况,丢包率极高。排查时可以先用mii info看link状态,如果link up但ping丢包,多半就是这个原因。

6.5 启动Linux时挂载根文件系统失败

U-Boot本身启动ok了,但传到内核时bootargs写错,导致Linux起不来,这类问题也算在U-Boot移植的调试范围内。

最常见的错法是bootargs里的root=参数写的设备节点不对。比如SD卡根文件系统应该写root=/dev/mmcblk0p2,但写成root=/dev/mmcblk1p2,或者没有指定rootwait导致内核在SD卡设备还没ready时就尝试挂载。排查方法是在U-Boot启动后打印printenv bootargs,确认传给内核的cmdline。如果发现root参数不对,去改环境变量,或者修改CONFIG_BOOTARGS。

还有一个极其常见的坑:设备树里内存节点和实际DDR配置不一致,导致内核态看到的物理内存比实际小,或者映射到了不存在的地址。内核启动后会疯狂报Unable to handle kernel NULL pointer dereference也就是所谓的内核panic,位置随机、信息难读,最后往往发现是dts里memory的reg属性填错了起始地址或大小。

7. 移植完成之后的收敛与经验

7.1 稳定之后必做的两件事:长时间拷机与代码review

当U-Boot能在你的板子上稳定启动并进入Linux之后,不要急着宣布“移植完成”。我的经验是,至少做一轮72小时不关机的稳定性验证,让板子在U-Boot的shell里反复重启、反复执行tftp下载、mmc read/write、mtest(内存测试),确认没有偶发性的死机或者数据错误。

同时,把你在移植过程中做过的所有代码修改整理成一份patch清单,diff出来逐条review。我踩过的教训是:调试过程中为了定位问题,改了一些看似无关的配置(比如把CONFIG_SYS_MALLOC_LEN加大、把某个延迟加长),这些临时改动如果不过滤就提交,后期会让U-Boot行为变得“说不清为什么”,影响极坏。

7.2 回头看看那些热搜词:移植的系统观

开篇提到的那串热搜词——FreeRTOS移植、LVGL移植、portmaster移植游戏——看起来都是各不相干的技术话题,但站在U-Boot移植的角度回头看,它们其实共享同一个逻辑:任何软件移植的第一步都是搞清目标硬件与参考硬件之间的差异。U-Boot是硬件差异最大的那一层,因为它离寄存器最近。U-Boot跑通了,等于你把板子的引脚、时钟、DDR、存储、网卡摸了一遍底,后续FreeRTOS里写个外设驱动、LVGL里适配显示接口、甚至移植一个游戏引擎,都能复用你对这块板子的理解。

这也是为什么我一直觉得嵌入式移植最耗时间的不是写代码,而是把“硬件是什么样”搞清楚。所以如果你在做的是应用层的移植,卡住了先别怀疑应用代码,回头看看Bootloader这层有没有把硬件摸底做好。链路是环环相扣的。

根据我个人经验,U-Boot移植这件事,最大的收获往往不在最终跑通的那一刻,而在于中间每一次“卡住—分析—突破”的过程。你把一块陌生板子从完全黑盒调到能看到串口log、能ping通PC,那种掌握感是任何现成BSP都给不了的。所以如果你正卡在某一步,稳住心态,按启动链路逐层排查,别乱改配置,上手也只是时间问题。等你跑通第一版之后,再回头优化DTS、裁剪驱动、调启动速度,那是后面的事情了。

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

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

立即咨询