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 时序、生成设备树文件。以下流程图展示了整体流程:
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、总线、外设所需的时钟频率。
时钟配置的关键步骤:
- 确定参考时钟源: 通常是外部晶振(如 24 MHz 或 50 MHz)。
- 配置 PLL: 设置倍频因子(M)、分频因子(N)和后分频(P)。
- 配置分频器: 为 CPU、AXI、AHB、APB 总线设置合适的分频比。
- 使能时钟输出: 确保 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 和时钟参数,到编写设备树文件。这些知识将直接应用于后续的内核移植和设备树调试中。