2. 硬件初始化优化:时钟、DDR、存储控制器的快速配置技巧

硬件初始化,说白了就是给芯片“通电唤醒”的过程。很多工程师觉得这步就是跑个默认流程,能进系统就行。但我告诉你,这里头的水很深。U-Boot 启动慢,十有八九是硬件初始化阶段在拖后腿。

我个人习惯,拿到一块新板子,第一件事不是看代码,而是先理清硬件初始化路径。时钟、DDR、存储控制器,这三个是启动速度的“三座大山”。翻过它们,你的 U-Boot 就能快上一大截。

2.1 时钟配置:别让 PLL 等你

时钟是芯片的心脏。但很多默认的时钟配置,其实是在“等”。等 PLL 锁定,等分频器稳定。这些等待时间,加起来可能几百毫秒。

核心思路:能并行就别串行,能提前就别等到用时。

2.1.1 提前配置 PLL

我见过不少代码,在 board_init_f 里先初始化串口,再配 GPIO,最后才去配 PLL。你想想看,串口初始化的时候,CPU 跑在低速时钟上,那速度慢得让人着急。

正确的做法是:上电后第一件事,就把 CPU 主频 PLL 和总线 PLL 配好。这样后续所有初始化,都在高速下执行。

/* 不好的做法:先做别的,最后配时钟 */
void board_init_f(void)
{
    serial_init();      /* 低速时钟下跑,慢! */
    gpio_init();        /* 还是低速 */
    clock_init();       /* 最后才提频 */
}

/* 好的做法:上来就提频 */
void board_init_f(void)
{
    clock_init();       /* 先提频,后面全快 */
    serial_init();      /* 高速下跑 */
    gpio_init();        /* 也快 */
}

2.1.2 减少 PLL 锁定等待

PLL 锁定需要时间,这是硬件决定的。但我们可以“骗”过它。什么意思?

很多芯片手册说 PLL 锁定需要 200us。但实际测试下来,可能 100us 就锁定了。我曾经在一个项目里,把等待时间从 200us 砍到 120us,板子跑得稳稳的。省了 80us,虽然不多,但积少成多。

我的经验:不要盲目相信手册上的“典型值”。拿示波器实测 PLL 锁定时间,然后取一个安全余量(比如 1.5 倍)。这样既安全,又最快。

2.1.3 关闭不必要的时钟

有些外设,U-Boot 阶段根本用不上。比如 USB、Ethernet、GPU。它们的时钟开着,除了浪费电,还拖慢启动速度?其实不会拖慢速度,但会浪费功耗。不过,如果你在调试阶段,建议先关掉它们,减少干扰。

/* 关闭不需要的外设时钟 */
static void disable_unused_clocks(void)
{
    clk_disable(CLK_USB);       /* U-Boot 不用 USB */
    clk_disable(CLK_GPU);       /* 更不用 GPU */
    clk_disable(CLK_ETH);       /* 网络初始化前再开 */
}

2.2 DDR 初始化:最耗时的环节

DDR 初始化,是 U-Boot 启动中最慢的一步。没有之一。DDR 需要训练、校准、写 Leveling。一套流程下来,几百毫秒甚至一秒多都很正常。

为什么会这么慢?因为 DDR 颗粒的电气特性,每块板子都不一样。控制器必须通过“训练”来找到最佳时序。

2.2.1 使用预置参数,跳过训练

如果你的硬件设计是固定的(比如量产板),DDR 走线长度、颗粒型号都不变。那恭喜你,训练结果每次都是一样的。

我建议的做法是:第一次启动时,让 DDR 做完整训练。然后把训练好的参数 dump 出来,写死到代码里。后续启动直接加载这些参数,跳过训练。

注意:跳过训练的前提是硬件完全一致。如果换了 PCB 版本、换了 DDR 颗粒批次,必须重新训练。我曾经吃过这个亏,跳过训练后板子跑几天就随机死机,查了半个月才发现是 DDR 时序漂移了。

2.2.2 精简训练流程

有些 DDR 控制器支持“快速训练”模式。只做必要的步骤,跳过冗余校准。比如:

  • 写 Leveling:必须做,影响数据写入
  • 读 Leveling:必须做,影响数据读取
  • Vref 校准:如果电压稳定,可以跳过
  • ZQ 校准:每次启动做一次即可,不用反复做
训练步骤 耗时(典型) 能否跳过? 风险
写 Leveling 50-200ms 不能 数据写错
读 Leveling 50-200ms 不能 数据读错
Vref 校准 20-50ms 可以(硬件稳定时) 噪声容限降低
ZQ 校准 10-30ms 可以(每次启动一次) 驱动强度不准

2.2.3 降低 DDR 频率做快速启动

这是一个取舍。如果你对启动速度有极致要求,可以先用低频初始化 DDR(比如 400MHz),等系统启动后再切到高频(比如 1600MHz)。

低频下,DDR 训练时间会缩短。因为时序裕量更大,训练算法收敛更快。我试过,从 1600MHz 降到 800MHz 训练,时间能省 40% 左右。

/* 先低频初始化,快速启动 */
ddr_init(800);      /* 800MHz 快速训练,耗时 100ms */
load_kernel();      /* 加载内核 */
ddr_set_freq(1600); /* 启动后切到高频 */

小技巧:如果你的 SoC 支持 DDR 频率动态切换,这个方案很香。如果不支持,那就老老实实用高频训练,然后保存参数。

2.3 存储控制器初始化:别让 Flash 拖后腿

U-Boot 要从 Flash(NAND、eMMC、SPI NOR)里读取内核和设备树。存储控制器的初始化速度,直接影响加载时间。

2.3.1 使用 DMA 模式

很多默认配置用的是 PIO 模式,CPU 一个一个字节地读。那速度,慢得让人抓狂。一定要开启 DMA,让数据自己飞。

/* 开启 DMA 模式 */
static void nand_init_dma(void)
{
    nand_ctrl->dma_enable = 1;
    nand_ctrl->dma_mode = DMA_BURST_16; /* 16 字节突发 */
}

我见过一个项目,NAND 读取速度从 2MB/s 提升到 25MB/s,就靠开了 DMA。启动时间直接少了 200ms。

2.3.2 提高 SPI 时钟频率

SPI NOR Flash 的读取速度,直接受 SPI 时钟频率影响。很多板子默认用 20MHz,但 Flash 颗粒可能支持 80MHz 甚至 104MHz。

查一下 Flash 的数据手册,找到它支持的最高频率。然后配到 SPI 控制器里。注意,要确保 PCB 走线能支持这个频率,否则数据会出错。

/* 提高 SPI 频率 */
static void spi_flash_init(void)
{
    /* 默认 20MHz */
    spi_set_clk(20 * 1000 * 1000);
    
    /* 查手册后,改为 80MHz */
    spi_set_clk(80 * 1000 * 1000);
    
    /* 注意:如果走线太长,80MHz 可能不稳定 */
}

避坑:提高 SPI 频率后,一定要做压力测试。我曾经把频率从 30MHz 提到 60MHz,读数据没问题,但写数据偶尔出错。后来发现是 Flash 的写操作对时序更敏感。最后降到 50MHz,稳了。

2.3.3 配置 Cache 预取

如果你的 SoC 支持 Cache 预取,一定要打开。这样 CPU 读 Flash 时,Cache 会自动把后面的数据也拉进来。顺序读取时,速度能翻倍。

/* 开启 Cache 预取 */
static void enable_cache_prefetch(void)
{
    uint32_t val;
    
    val = readl(SYS_CACHE_CTRL);
    val |= (1 << 3);  /* 预取使能位 */
    writel(val, SYS_CACHE_CTRL);
}

2.4 本章知识体系

下面这张图,是我梳理的硬件初始化优化路径。你可以对照自己的板子,看看哪一步还有优化空间。

硬件初始化优化路径 硬件初始化优化路径 上电复位 时钟优化 提前配PLL · 减少锁定等待 · 关无用时钟 DDR 优化 预置参数 · 精简训练 · 低频快速启动 存储控制器优化 DMA模式 · 提高SPI频率 · Cache预取 快速启动完成 每步优化可节省 50ms - 300ms

嗯,硬件初始化优化,说白了就是跟时间赛跑。时钟、DDR、存储控制器,这三块优化好了,你的 U-Boot 启动速度至少能提升 30% 以上。我做过一个极端案例,把启动时间从 2.8 秒压到了 0.9 秒,靠的就是这几招。