4. U-Boot 移植实战:时钟与 PLL 配置、DDR 初始化、串口驱动适配、网络驱动调试

U-Boot 移植,说白了就是让 Bootloader 在你的板子上「活过来」。我做了这么多年,最深的体会是:时钟和 DDR 是门槛,串口是眼睛,网络是手脚。这四关过不了,后面全是白搭。

今天咱们就按实际移植的顺序,把这四个核心模块拆开揉碎了讲。我会把我在项目里踩过的坑、总结的经验都放进来。

4.1 时钟与 PLL 配置:让芯片先跑起来

芯片上电后,默认时钟通常很低。你得先配置 PLL,把 CPU、总线、外设的时钟提上去。这是所有后续操作的基础。

4.1.1 时钟树分析

拿到一个 SoC 的参考手册,第一件事就是找时钟树图。你得搞清楚:

  • 输入时钟源:板子上是 24MHz 晶振还是 25MHz?
  • PLL 类型:ARM PLL、DDR PLL、PERI PLL…… 它们分别给谁供电?
  • 分频链:从 PLL 输出到最终模块时钟,中间经过几级分频?

我个人习惯,先把时钟树画成一张表,对照着写代码,不容易乱。

关键原则:配置 PLL 时,一定要先确认输入时钟稳定,再操作 PLL 锁定。锁定后,等一段时间(通常几百微秒),再切换时钟源。顺序错了,芯片直接死给你看。

4.1.2 PLL 配置实战

以某款 ARMv8 芯片为例,配置 ARM PLL 到 1.2GHz 的典型步骤:

  1. 读取当前 PLL 状态,确保未锁定。
  2. 写 PLL 控制寄存器,设置倍频系数(比如 1200MHz / 24MHz = 50 倍)。
  3. 等待 PLL 锁定标志位置位。
  4. 切换 CPU 时钟源到新 PLL。
/* 伪代码示例:配置 ARM PLL */
void pll_config_arm(void) {
    // 1. 进入 PLL 配置模式
    writel(0x2, &pll->con);
    // 2. 设置倍频系数 (MDIV=50, PDIV=1, SDIV=0)
    writel((50 << 16) | (1 << 8) | (0 << 0), &pll->con);
    // 3. 等待锁定
    while (!(readl(&pll->status) & LOCK_BIT));
    // 4. 切换时钟源
    clk_set_parent(clk_get("arm_clk"), clk_get("arm_pll"));
}

我曾经在一个项目里,因为没等 PLL 锁定就切了时钟源,结果 CPU 跑飞,串口打印全是乱码。查了整整一天才找到原因。从那以后,我每次都在 PLL 锁定后加一个 10ms 的延时,确保万无一失。

4.2 DDR 初始化:内存是系统的基石

DDR 初始化是 U-Boot 移植里最复杂、最容易出问题的环节。没有内存,你连 printf 都跑不起来。

4.2.1 DDR 控制器配置要点

DDR 控制器的寄存器多如牛毛,但核心参数就几个:

参数 说明 常见坑点
时序参数 (tRCD, tRP, tRAS...) 从 DDR 颗粒 datasheet 获取 不同频率下时序不同,别照搬
刷新率 (REFI) DDR 需要定期刷新 刷新间隔太短会降低性能
ODT (片上端接) 信号完整性关键 ODT 值不对,高频下会出错
ZQ 校准 输出驱动强度校准 必须等 DDR 初始化完成后做

你想想看,这些参数任何一个错了,DDR 要么初始化失败,要么跑起来后随机死机。调试起来非常痛苦。

4.2.2 初始化流程

标准的 DDR 初始化流程,我总结为「五步走」:

  1. 上电与复位:给 DDR 颗粒供电,释放复位信号。
  2. 时钟使能:提供差分时钟 (CK/CK#)。
  3. 控制器配置:写时序、刷新、ODT 等参数。
  4. DDR 初始化序列:发送 MRS (模式寄存器设置) 命令。
  5. ZQ 校准:完成校准后,DDR 才可用。
/* DDR 初始化核心代码片段 */
void ddr_init(void) {
    // 1. 配置 DDR 控制器时序
    ddr_set_timing(tRCD, tRP, tRAS, tWR);
    // 2. 设置刷新率
    ddr_set_refresh(7.8); // 7.8us 刷新周期
    // 3. 发送 MRS 命令
    ddr_send_mrs(MR0, DDR_BL8 | DDR_CL_11);
    ddr_send_mrs(MR1, DDR_ODT_40OHM);
    // 4. 等待 ZQ 校准完成
    while (!(readl(&ddr->status) & ZQ_DONE));
}

调试技巧:如果 DDR 初始化失败,先检查时钟是否稳定,再检查时序参数。我习惯在 DDR 初始化后,写一个简单的内存测试(比如写 0xAA、0x55 模式),读回来验证。能快速定位问题。

4.3 串口驱动适配:让 U-Boot 开口说话

串口是 U-Boot 的「眼睛」。没有串口输出,你根本不知道系统跑到哪一步了。

4.3.1 串口硬件初始化

串口驱动适配,核心就是配置 UART 控制器的波特率、数据位、停止位、校验位。以 115200 波特率为例:

/* 串口初始化 */
void uart_init(void) {
    // 1. 使能 UART 时钟
    clk_enable("uart0_clk");
    // 2. 配置引脚复用
    pinmux_set(PIN_UART0_TX, MODE_ALT1);
    pinmux_set(PIN_UART0_RX, MODE_ALT1);
    // 3. 设置波特率 (假设时钟 50MHz)
    uart_set_baudrate(115200, 50000000);
    // 4. 使能发送和接收
    writel(UART_CTRL_TXE | UART_CTRL_RXE, &uart->ctrl);
}

嗯,这里要注意:波特率计算一定要精确。如果时钟频率不是整数倍,实际波特率会有误差。误差超过 3%,串口就可能丢数据。

4.3.2 调试输出函数

U-Boot 的 putc() 函数是串口驱动的核心。它负责发送一个字符。实现很简单:

void uart_putc(char c) {
    // 等待发送缓冲区空
    while (!(readl(&uart->status) & TX_EMPTY));
    // 写数据
    writel(c, &uart->data);
    // 如果是换行符,加一个回车
    if (c == '\n')
        uart_putc('\r');
}

我曾经在一个项目里,串口能输出,但输入死活不行。查了半天,发现是 RX 引脚的中断没使能。U-Boot 的串口输入通常用轮询方式,但有些 SoC 需要额外配置接收触发条件。这个坑,我印象很深。

4.4 网络驱动调试:让 U-Boot 联网

网络驱动是 U-Boot 移植的「高阶关卡」。有了网络,你才能通过网络下载内核、文件系统,调试效率翻倍。

4.4.1 网络驱动架构

U-Boot 的网络驱动架构,说白了就是三层:

  • 底层:MAC 控制器驱动,负责收发数据帧。
  • 中间层:PHY 驱动,负责管理物理层。
  • 上层:协议栈,负责解析 ARP、IP、UDP 等协议。

移植时,你主要关注底层和中间层。上层协议栈 U-Boot 已经帮你做好了。

4.4.2 PHY 驱动适配

PHY 芯片种类繁多,但接口是标准的 MDIO。适配 PHY 驱动,核心是:

  1. 通过 MDIO 总线读写 PHY 寄存器。
  2. 读取 PHY ID,确定 PHY 型号。
  3. 配置 PHY 的工作模式(如 1000Mbps、全双工)。
  4. 等待链路建立。
/* PHY 初始化示例 */
int phy_init(void) {
    u16 phy_id;
    // 1. 读取 PHY ID
    phy_id = phy_read(PHY_ADDR, PHY_ID_REG);
    if (phy_id != EXPECTED_PHY_ID) {
        printf("PHY ID mismatch: 0x%04x\n", phy_id);
        return -1;
    }
    // 2. 配置自动协商
    phy_write(PHY_ADDR, PHY_ANAR, AN_1000BASE_T_FULL | AN_100BASE_TX_FULL);
    phy_write(PHY_ADDR, PHY_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
    // 3. 等待链路建立
    while (!(phy_read(PHY_ADDR, PHY_BMSR) & BMSR_LINK_STATUS));
    return 0;
}

4.4.3 常见调试问题

网络驱动调试,我遇到过最多的问题就是「ping 不通」。原因通常有:

现象 可能原因 排查方法
PHY 无法建立链路 PHY 地址错误、复位引脚未拉高 用示波器看 MDIO 波形
能 ping 通,但丢包严重 MAC 的 DMA 描述符配置错误 检查描述符链表的地址对齐
能收到包,但发不出去 TX 描述符状态未正确更新 检查中断处理或轮询逻辑
ARP 请求无响应 MAC 地址未正确设置 检查环境变量 ethaddr

调试技巧:我习惯在 U-Boot 里加一个「网络回环测试」命令。让板子自己给自己发包,验证 MAC 的收发通路是否正常。能快速排除 PHY 和链路的问题。

4.5 本章知识体系

下面这张图,把本章四个核心模块的关系画清楚了。你可以对照着看,理解它们之间的依赖顺序。

U-Boot 移植核心模块关系图 U-Boot 移植核心模块关系图 时钟与 PLL 配置 DDR 初始化 串口驱动适配 网络驱动调试 U-Boot 启动成功 说明:时钟和 DDR 是基础依赖,必须先完成;串口和网络可并行调试,但都依赖 DDR 初始化成功。

好了,这一章的内容就到这儿。时钟、DDR、串口、网络,这四个模块是 U-Boot 移植的「四座大山」。翻过它们,你的板子就能跑起来了。剩下的,就是根据具体硬件做微调。