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 本章知识体系
下面这张图,是我梳理的硬件初始化优化路径。你可以对照自己的板子,看看哪一步还有优化空间。
嗯,硬件初始化优化,说白了就是跟时间赛跑。时钟、DDR、存储控制器,这三块优化好了,你的 U-Boot 启动速度至少能提升 30% 以上。我做过一个极端案例,把启动时间从 2.8 秒压到了 0.9 秒,靠的就是这几招。