第四章 Bootloader原理与U-Boot移植

Bootloader是嵌入式Linux系统启动的第一段软件,它负责完成从硬件上电到操作系统内核接管之间的全部初始化工作。在ARM嵌入式开发中,U-Boot(Universal Boot Loader)是事实上的标准Bootloader,几乎所有的主流SoC(如NXP i.MX系列、TI AM335x、Rockchip RK3568等)都提供官方或社区维护的U-Boot支持。理解Bootloader的工作原理、U-Boot的源码组织方式以及板级移植的完整流程,是进行系统裁剪与定制的前提。本章将从Bootloader的职责边界出发,深入剖析U-Boot的源码结构,并给出一个可复用的板级移植操作框架。

U-Boot 板级移植工程链路 ① 硬件初始化 时钟/内存/串口 GPIO/存储控制器 中断/看门狗关闭 ② U-Boot自举 SPL/TPL加载 重定位到DDR 环境变量初始化 ③ 板级配置 设备树加载 外设驱动绑定 网络/存储就绪 ④ 内核加载 bootm/booti命令 校验/解压内核 跳转执行 移植核心文件: include/configs/xxx.h arch/arm/dts/xxx.dts board/vendor/xxx/ defconfig 板级移植关键配置项 配置项 作用 典型值/示例 CONFIG_SYS_TEXT_BASE U-Boot链接地址 0x87800000 (i.MX6ULL) CONFIG_SYS_MALLOC_LEN 动态内存池大小 16MB (0x1000000) CONFIG_BOOTCOMMAND 默认启动命令 run distro_bootcmd CONFIG_DEFAULT_FDT_FILE 默认设备树文件名 imx6ull-14x14-evk.dtb CONFIG_SYS_UBOOT_BASE U-Boot烧写地址 0x2FF00000 (eMMC)

4.1 Bootloader的核心作用与启动流程

Bootloader的本质是一个运行在裸机环境下的C语言程序,它必须在操作系统启动之前完成硬件初始化、内存映射建立、存储介质访问和内核镜像加载等任务。在ARM架构中,Bootloader通常分为两个阶段:第一阶段(SPL/TPL)负责最基础的CPU初始化和DDR控制器配置,第二阶段(U-Boot主程序)负责完整的板级初始化和内核引导。这种两阶段设计是为了解决"DDR尚未初始化时,代码无法在内存中运行"的鸡生蛋问题——SPL通常运行在芯片内部SRAM中,其代码量被严格限制在几十KB以内。

从系统启动的完整链路来看,Bootloader处于ROM固化代码(BootROM)与Linux内核之间。以典型的NXP i.MX6ULL为例,芯片上电后BootROM首先执行,根据BOOT_CFG引脚的电平状态选择启动介质(SD卡、eMMC、NAND或USB),然后将SPL从介质头部加载到内部SRAM(0x00900000地址)并跳转执行。SPL完成DDR初始化后,将完整的U-Boot镜像从存储介质复制到DDR的CONFIG_SYS_TEXT_BASE地址,随后U-Boot主程序接管控制权,加载设备树(.dtb)和内核镜像(zImage/Image),最终通过bootz或booti命令跳转到内核入口。

核心原理要点:Bootloader的职责边界非常清晰——它不负责管理硬件资源(这是内核的工作),也不负责提供用户交互界面(这是应用程序的工作)。它的核心使命只有三个:初始化硬件到可用状态、建立内存映射、加载并跳转到内核。理解这个边界,有助于在移植时避免过度设计。

4.2 U-Boot源码结构与关键目录

U-Boot的源码结构经过多年演进,已经形成了清晰的分层架构。从顶层目录来看,arch/目录存放与CPU架构相关的代码,其中arch/arm/下又按SoC厂商和型号进一步细分,例如arch/arm/mach-imx/存放i.MX系列SoC的底层初始化代码,arch/arm/cpu/armv7/存放ARMv7架构通用的cache、MMU和中断控制代码。board/目录则存放具体的板级代码,每个板子一个子目录,例如board/freescale/mx6ullevk/就是i.MX6ULL官方评估板的板级支持包。

对于移植工作而言,最需要关注的是以下几个关键目录和文件:

  • include/configs/:存放板级配置头文件,如mx6ullevk.h,定义了内存大小、环境变量、启动命令等宏配置。
  • arch/arm/dts/:存放设备树源文件(.dts)和编译生成的设备树二进制(.dtb),U-Boot使用设备树来描述板级硬件资源。
  • configs/:存放defconfig文件,这是U-Boot的默认配置入口,通过make xxx_defconfig生成.config。
  • drivers/:包含各种外设驱动,如drivers/net/(网络)、drivers/mmc/(存储)、drivers/mtd/(NAND/NOR Flash)。
  • include/dt-bindings/:存放设备树绑定的头文件,定义了GPIO、时钟、中断等宏。

U-Boot的配置系统基于Kbuild(与Linux内核相同),通过Kconfig文件实现菜单式配置。板级移植时,通常不需要从零编写Kconfig,而是基于相近的参考板修改。例如,如果你的目标板基于i.MX6ULL但使用了不同的DDR型号和网络PHY,可以复制mx6ullevk的defconfig和头文件,然后修改内存时序参数和PHY地址即可。

目录/文件 作用 移植时修改频率
arch/arm/mach-imx/ SoC级初始化(时钟、DDR、IOMUX) 低(通常不需要改)
board/<vendor>/<board>/ 板级初始化(LED、按键、电源管理) 高(新增板级文件)
include/configs/<board>.h 板级宏配置(内存、环境变量、启动参数) 高(核心修改点)
arch/arm/dts/<board>.dts 设备树描述(外设地址、中断、引脚复用) 高(新增或修改)
configs/<board>_defconfig 默认配置入口(使能驱动、命令) 中(按需调整)

4.3 U-Boot配置编译与板级移植流程

U-Boot的配置编译流程遵循标准的Kbuild体系,但针对ARM嵌入式场景有一些特殊步骤。完整的移植流程可以分为五个阶段:环境准备、参考板选择、板级文件创建、配置编译、烧写验证。下面以基于i.MX6ULL的自制板卡为例,详细说明每个阶段的操作要点。

阶段一:环境准备与参考板选择

首先需要准备交叉编译工具链,推荐使用Linaro GCC 7.5或更高版本,并设置环境变量:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin

然后从官方仓库获取U-Boot源码,建议使用与你的内核版本匹配的U-Boot版本。例如,如果你的内核是Linux 5.4,推荐使用U-Boot 2020.04或2021.04。选择参考板时,优先选择与目标板使用相同SoC和相近外设的官方评估板,这样可以最大程度复用已有的初始化代码。

阶段二:创建板级文件

假设你的目标板名为myboard,基于i.MX6ULL,DDR容量为512MB,使用RMII接口的LAN8720A网络PHY。首先复制参考板文件:

cp -r board/freescale/mx6ullevk board/freescale/myboard
cp include/configs/mx6ullevk.h include/configs/myboard.h
cp arch/arm/dts/imx6ull-14x14-evk.dts arch/arm/dts/imx6ull-myboard.dts
cp configs/mx6ullevk_defconfig configs/myboard_defconfig

然后修改myboard.h中的关键配置:将CONFIG_SYS_SDRAM_SIZE改为(512 * 1024 * 1024),修改CONFIG_BOOTARGS中的mem=512M,并调整网络PHY的地址(LAN8720A的默认地址是0,但需要确认硬件上的PHYAD引脚配置)。同时,在myboard.c中需要修改板级初始化函数,例如board_init()中的LED GPIO定义、board_eth_init()中的PHY复位引脚等。

阶段三:设备树修改

设备树是U-Boot与内核共享硬件信息的桥梁。在imx6ull-myboard.dts中,需要修改以下内容:

&fec1 {
    pinctrl-names = "default";
    pinctrl-0 = &pinctrl_enet1;
    phy-mode = "rmii";
    phy-handle = &ethphy0;
    status = "okay";
};

&mdio {
    #address-cells = <1>;
    #size-cells = <0>;
    ethphy0: ethernet-phy@0 {
        reg = <0>;
        reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
        reset-assert-us = <10000>;
        reset-deassert-us = <20000>;
    };
};

这里的关键是reg = <0>必须与PHY的实际地址一致,reset-gpios需要根据硬件原理图确认复位引脚。如果PHY地址不对,网络驱动将无法识别PHY,导致U-Boot启动时网络不可用。

阶段四:配置编译

完成文件修改后,执行配置和编译:

make myboard_defconfig
make -j8

编译产物包括u-boot.bin(完整镜像)、SPL(第一阶段引导程序)和u-boot.dtb(设备树)。对于i.MX6ULL,还需要使用mkimage工具将U-Boot转换为可烧写的格式:

tools/mkimage -n board/freescale/myboard/myboard.cfg -T imximage -e 0x87800000 -d u-boot.bin u-boot.imx

其中myboard.cfg是DDR初始化脚本,通常从参考板复制并修改时序参数。

阶段五:烧写与验证

将生成的SPL和u-boot.imx烧写到SD卡的特定偏移位置(i.MX6ULL的SPL偏移为1KB,U-Boot偏移为69KB),然后从SD卡启动。在串口终端上观察启动日志,重点检查以下信息:

  • DDR初始化是否成功(日志中应显示"DRAM: 512 MiB")
  • 网络PHY是否识别(执行mdio list命令)
  • eMMC/SD卡是否识别(执行mmc list命令)
  • 环境变量是否正常加载(执行printenv命令)

常见风险提示:DDR初始化失败是移植初期最常见的问题,表现为U-Boot启动后无任何输出或输出乱码。此时应首先检查myboard.cfg中的DDR时序参数是否与硬件实际使用的DDR颗粒匹配,特别是CAS延迟、突发长度和刷新周期。建议使用SoC厂商提供的DDR调试工具(如NXP的DDR Stress Test)来验证时序参数。

4.4 实战案例:从零移植U-Boot到自制i.MX6ULL板卡

下面通过一个完整的案例来串联上述流程。假设我们设计了一块基于i.MX6ULL的工业控制板,硬件配置如下:512MB DDR3L(型号MT41K256M16HA-125)、10/100M以太网(LAN8720A)、4GB eMMC、USB OTG、3路UART、2路CAN。

初始现象:参考官方EVK板的配置编译后烧写,串口无任何输出,板卡完全无响应。

分析过程:首先使用万用表测量核心电压(VDD_ARM=1.2V、VDD_SOC=1.2V、DDR_VREF=0.75V),确认电源正常。然后用示波器检查24MHz晶振是否起振,发现晶振正常。接着检查BOOT_CFG引脚,发现BOOT_MODE[1:0]被硬件拉高到10(即从eMMC启动),但我们的板卡eMMC尚未烧写任何数据,因此BootROM无法加载SPL。将BOOT_MODE改为01(从SD卡启动)后,串口开始输出SPL日志,但随后卡在"DDR initialization failed"。

关键参数与调试:通过对比EVK板原理图,发现我们的DDR3L芯片的列地址宽度为10bit(A0-A9),而EVK板使用的是11bit列地址。在myboard.cfg中,DDR_PDR寄存器的BL(Burst Length)和COL(Column Width)字段需要相应调整。修改后重新编译烧写,DDR初始化通过,但网络仍然不通。

网络问题排查:执行mdio list命令无输出,说明MDIO总线无法访问PHY。检查设备树中的reset-gpios,发现我们使用的GPIO5_IO07在硬件上被复用为JTAG功能,需要在IOMUX配置中将其改为GPIO模式。修改imx6ull-myboard.dts中的pinctrl_enet1节点,添加正确的引脚复用配置后,网络恢复正常。

结果对比:最终U-Boot启动日志显示:

U-Boot 2021.04 (Jan 15 2025 - 10:30:00 +0800)
CPU:   Freescale i.MX6ULL rev1.1 at 528MHz
DRAM:  512 MiB
MMC:   FSL_SDHC: 0, FSL_SDHC: 1
Net:   eth0: ethernet@2188000
Hit any key to stop autoboot:  0

复盘经验:本次移植耗时约2天,主要时间花在DDR时序调试和引脚复用排查上。总结出三条经验:第一,DDR参数必须根据实际芯片数据手册逐项核对,不能盲目复制参考板;第二,IOMUX引脚复用是ARM移植的高频出错点,建议在硬件设计阶段就整理一份完整的引脚复用表;第三,U-Boot的调试手段有限,善用md(内存查看)、mw(内存写入)和i2c probe等命令可以快速定位硬件问题。

经验提醒:在开始移植之前,务必先确认SoC的启动模式(Boot Mode)和启动介质。很多"U-Boot无输出"的问题,根源在于BootROM根本没有找到可执行的SPL。建议在硬件设计时预留一组拨码开关或跳线,方便切换启动介质。

4.5 常见误区与总结

在U-Boot移植过程中,工程师容易陷入以下几个误区:

  • 误区一:盲目使用最新版本U-Boot。最新版本可能包含尚未稳定的驱动或配置变更,建议选择与内核版本匹配的长期支持版本。
  • 误区二:忽略SPL的独立配置。SPL和U-Boot主程序是两套独立的配置,SPL的CONFIG_SPL_*选项需要单独设置,例如CONFIG_SPL_TEXT_BASE必须指向SRAM地址。
  • 误区三:设备树与内核不匹配。U-Boot加载的设备树必须与内核版本兼容,否则内核启动时会报"FDT: Failed to reserve memory"等错误。
  • 误区四:环境变量存储位置设置错误。如果CONFIG_ENV_IS_IN_MMC和CONFIG_ENV_OFFSET设置不当,可能导致环境变量无法保存或覆盖其他分区数据。

总结而言,U-Boot移植是一项系统工程,它要求开发者同时具备硬件原理图阅读能力、ARM体系结构知识和Linux设备树技能。本章所讲的流程和方法,适用于绝大多数ARM SoC的U-Boot移植场景。在实际项目中,建议按照"最小系统优先"的原则推进——先确保串口和DDR正常工作,再逐步添加网络、存储等外设支持,这样可以将问题隔离在可控范围内,提高调试效率。