4. U-Boot 移植实战:从源码编译 U-Boot、添加新板级支持、配置 DDR 与时钟参数

U-Boot 是 ARM Linux 系统启动的第一阶段软件,负责初始化硬件、加载内核并传递设备树。本章将带领你完成从源码编译 U-Boot、为新板卡添加板级支持,以及精确配置 DDR 和时钟参数的完整流程。掌握这些技能是后续内核移植的基础。

核心目标: 理解 U-Boot 的板级初始化流程,掌握通过修改设备树和配置文件适配新硬件平台的方法。

4.1 U-Boot 源码结构与编译系统

U-Boot 采用 Kconfig / Makefile 构建系统,与 Linux 内核类似。其顶层目录结构如下:

目录/文件 功能说明
arch/ 体系结构相关代码,ARM 代码位于 arch/arm/
board/ 板级特定代码,每个板卡一个子目录
configs/ 默认配置文件(defconfig),用于快速配置
dts/ 设备树源文件(.dts / .dtsi),描述硬件拓扑
drivers/ 驱动框架,包括串口、DDR、网络等
include/configs/ 板级配置头文件(旧式配置方式)

编译 U-Boot 的基本命令序列如下:

# 清理之前的编译产物
make distclean

# 加载目标板卡的默认配置(以 qemu-arm 为例)
make qemu_arm_defconfig

# 进入菜单配置界面(可选)
make menuconfig

# 开始编译,-jN 指定并行任务数
make -j4
提示: 对于新板卡,通常从最接近的参考板卡复制 defconfig 和板级目录,然后逐步修改。不要从零开始编写所有文件。

4.2 添加新板级支持:从参考板到定制板

添加新板级支持的核心步骤包括:创建板级目录、编写板级初始化代码、配置 DDR 时序、生成设备树文件。以下流程图展示了整体流程:

添加新板级支持流程图 1. 选择参考板卡 2. 复制板级目录 board/<vendor>/<board> 3. 修改 Kconfig / Makefile 4. 配置 DDR 参数(时序/地址映射) 5. 编写/修改设备树文件 6. 生成 defconfig 并编译验证 关键:DDR 参数错误会导致 U-Boot 无法启动,必须根据芯片手册和 PCB 布局精确计算。

4.2.1 创建板级目录与配置文件

假设我们要为新板卡 myboard 添加支持,基于参考板 vexpress:

# 1. 复制板级目录
cp -r board/armltd/vexpress board/armltd/myboard

# 2. 复制默认配置
cp configs/vexpress_ca9x4_defconfig configs/myboard_defconfig

# 3. 修改 board/armltd/myboard/Kconfig
#    将 VEXPRESS 相关选项改为 MYBOARD
#    添加新的提示文本

# 4. 修改 board/armltd/myboard/Makefile
#    将目标文件从 vexpress.o 改为 myboard.o
注意: 如果板卡使用不同的 SoC 或 DDR 控制器,不能直接复制板级目录。必须从使用相同 SoC 系列的板卡开始,否则 DDR 和时钟初始化代码完全不兼容。

4.2.2 板级初始化函数调用链

U-Boot 启动时,板级初始化函数按以下顺序调用:

阶段 函数 职责
SPL / TPL board_init_f() 早期初始化:时钟、DDR、堆栈
SPL board_init_r() 重定位后初始化:串口、存储设备
U-Boot 主阶段 board_init() 板级外设初始化:GPIO、网络、MMC
U-Boot 主阶段 misc_init_r() 杂项初始化:读取 EEPROM、设置 MAC

4.3 配置 DDR 参数

DDR 配置是移植中最关键也最容易出错的环节。参数错误会导致系统无法启动、随机崩溃或数据损坏。DDR 配置主要包含以下参数:

  • 时序参数(Timing Parameters): tRCD、tRP、tRAS、tWR、tRFC 等,由 DDR 芯片数据手册提供。
  • 地址映射(Address Mapping): 行列地址位数、Bank 数量、芯片选择。
  • 驱动强度(Drive Strength): ODT(片上端接)配置、输出阻抗。
  • 刷新率(Refresh Rate): 根据温度范围调整刷新周期。

以下是一个典型的 DDR 配置结构体示例(以 DDR3 为例):

/* board/armltd/myboard/ddr.c */
struct ddr3_timing myboard_ddr_timing = {
    .tRCD   = 10,    /* RAS to CAS 延迟 */
    .tRP    = 10,    /* 预充电时间 */
    .tRAS   = 30,    /* 行激活时间 */
    .tWR    = 12,    /* 写入恢复时间 */
    .tRFC   = 160,   /* 刷新周期 */
    .tWTR   = 6,     /* 写入到读取延迟 */
    .tRRD   = 4,     /* 行激活到行激活延迟 */
    .tFAW   = 24,    /* 四窗口激活时间 */
};

/* DDR 控制器配置 */
struct ddr_ctrl_config myboard_ddr_ctrl = {
    .ddr_type       = DDR_TYPE_DDR3,
    .data_width     = 16,        /* 16-bit 数据总线 */
    .bank_count     = 8,         /* 8 个 Bank */
    .row_bits       = 15,        /* 行地址 15 位 */
    .col_bits       = 10,        /* 列地址 10 位 */
    .frequency_mhz  = 533,       /* DDR 时钟频率 533 MHz */
    .odt_enable     = 1,         /* 启用片上端接 */
};
调试技巧: 如果 U-Boot 在 DDR 初始化后卡死,可以尝试降低 DDR 频率(如从 533 MHz 降至 400 MHz),或放宽时序参数(增加 tRCD、tRP 值)。稳定后再逐步优化。

4.4 配置时钟参数

ARM SoC 的时钟系统通常包含 PLL(锁相环)、分频器和多路选择器。U-Boot 需要正确配置 PLL 以生成 CPU、总线、外设所需的时钟频率。

时钟配置的关键步骤:

  1. 确定参考时钟源: 通常是外部晶振(如 24 MHz 或 50 MHz)。
  2. 配置 PLL: 设置倍频因子(M)、分频因子(N)和后分频(P)。
  3. 配置分频器: 为 CPU、AXI、AHB、APB 总线设置合适的分频比。
  4. 使能时钟输出: 确保 DDR、串口、网络等外设时钟已使能。

以下是一个时钟配置的代码片段:

/* board/armltd/myboard/clock.c */
void myboard_clock_init(void)
{
    /* 假设参考时钟为 24 MHz */
    const unsigned long ref_clk = 24000000;

    /* 配置 ARM PLL:输出 1.2 GHz */
    /* PLL 公式:VCO = ref_clk * M / N, 输出 = VCO / P */
    writel(0x00000001, &pll_base->arm_pll_cfg);  /* 复位 PLL */
    writel((120 << 16) | (1 << 8) | 1, &pll_base->arm_pll_cfg);
    /* M=120, N=1, P=1 => 24 MHz * 120 / 1 / 1 = 2880 MHz? 需要调整 */

    /* 等待 PLL 锁定 */
    while (!(readl(&pll_base->arm_pll_stat) & 0x1));

    /* 配置 DDR PLL:输出 533 MHz */
    writel((533 << 16) | (24 << 8) | 1, &pll_base->ddr_pll_cfg);
    /* M=533, N=24, P=1 => 24 MHz * 533 / 24 / 1 = 533 MHz */

    while (!(readl(&pll_base->ddr_pll_stat) & 0x1));

    /* 设置 CPU 时钟分频器:CPU 时钟 = PLL 输出 / 1 */
    writel(0x0, &clk_base->cpu_div);

    /* 设置 AXI 总线分频器:AXI 时钟 = PLL 输出 / 2 */
    writel(0x1, &clk_base->axi_div);
}
重要: 时钟配置顺序必须正确。通常先配置 PLL,再配置分频器,最后使能外设时钟。错误的顺序可能导致 SoC 进入未知状态。

4.5 设备树文件编写与集成

U-Boot 使用设备树来描述板级硬件。对于新板卡,需要创建 .dts 文件并包含正确的 .dtsi(SoC 级描述)。

设备树文件的基本结构:

/* arch/arm/dts/myboard.dts */
/dts-v1/;

#include "vexpress-v2p-ca9.dtsi"  /* 包含 SoC 级设备树 */

/ {
    model = "MyBoard based on ARM Cortex-A9";
    compatible = "armltd,myboard", "armltd,vexpress";

    memory@60000000 {
        device_type = "memory";
        reg = <0x60000000 0x20000000>;  /* 512 MB DDR */
    };

    chosen {
        stdout-path = &serial0;
        bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rw";
    };

    &serial0 {
        status = "okay";
        clock-frequency = <24000000>;
    };

    &mmc0 {
        status = "okay";
        bus-width = <4>;
        max-frequency = <50000000>;
    };
};

将设备树文件集成到 U-Boot 构建中:

# 1. 将 .dts 文件放入 arch/arm/dts/
# 2. 修改 arch/arm/dts/Makefile,添加:
#    dtb-$(CONFIG_TARGET_MYBOARD) += myboard.dtb

# 3. 在板级 Kconfig 中设置默认设备树:
#    config DEFAULT_DEVICE_TREE
#        string "Default Device Tree"
#        default "myboard" if TARGET_MYBOARD

4.6 编译与验证

完成上述配置后,执行编译并验证:

# 配置新板卡
make myboard_defconfig

# 编译 U-Boot 和 SPL
make -j4

# 检查生成的镜像
ls -lh u-boot.bin u-boot.img spl/u-boot-spl.bin

# 检查设备树是否编译成功
ls -lh arch/arm/dts/myboard.dtb
验证清单:
  • U-Boot SPL 能否正常初始化 DDR?
  • 串口是否输出启动信息?
  • 能否通过 tftp 或 mmc 加载内核?
  • 设备树中的内存大小与实际 DDR 容量是否一致?

4.7 常见问题与调试方法

问题现象 可能原因 解决方案
SPL 无任何输出 DDR 初始化失败、时钟未使能 检查 DDR 时序参数、PLL 配置、串口 GPIO 复用
U-Boot 启动后随机崩溃 DDR 时序过紧、ODT 配置错误 放宽时序参数、调整 ODT 阻抗
设备树中内存与实际不符 设备树 reg 属性错误 核对 PCB 原理图,确认 DDR 地址映射
网络无法工作 时钟频率错误、PHY 地址不对 检查 MAC 时钟、PHY 地址、MDIO 总线

调试 U-Boot 时,可以使用以下方法:

  • 增加调试输出: 在关键函数中添加 printf() 或 debug() 语句。
  • 使用 JTAG 调试器: 连接 ARM 调试接口,单步执行 DDR 初始化代码。
  • 对比参考板: 使用逻辑分析仪抓取参考板和新板的 DDR 时钟/命令信号。
经验之谈: 在首次移植时,建议先让 U-Boot 在 DDR 中运行(跳过 SPL),验证串口和时钟正确后,再回头调试 SPL 的 DDR 初始化。这样可以缩小问题范围。

通过本章的学习,你已经掌握了 U-Boot 移植的核心技能:从源码编译、添加板级支持、配置 DDR 和时钟参数,到编写设备树文件。这些知识将直接应用于后续的内核移植和设备树调试中。