2. 板级初始化优化:board_init_f与board_init_r的裁剪与加速技巧
板级初始化,说白了就是U-Boot从“能跑”到“跑对”的关键阶段。很多开发者觉得这步没啥好优化的,反正就执行一次。但我在项目中遇到过,一个没裁剪的board_init_f,愣是把启动时间拖慢了300多毫秒。对于工业设备来说,这300毫秒可能就是用户按下电源键到看到Logo的等待时间。
今天咱们就聊聊,怎么把这两个阶段“瘦身”并“提速”。
2.1 两个阶段的分工与瓶颈
先搞清楚board_init_f和board_init_r各自干什么。我习惯把它们理解为“前置准备”和“正式开工”。
| 阶段 | 运行位置 | 主要任务 | 典型瓶颈 |
|---|---|---|---|
| board_init_f | SRAM或Flash(XIP) | 早期时钟、串口、DRAM初始化、relocate前的准备 | 串口轮询打印、不必要的GPIO翻转、冗余的时钟配置 |
| board_init_r | DRAM | 完整外设初始化、环境变量、网络、文件系统 | 全量驱动加载、慢速设备轮询、冗余的校验 |
你想想看,board_init_f是在SRAM里跑的,空间小、速度相对快,但一旦DRAM初始化完成,代码就搬到DRAM里继续跑board_init_r。两个阶段的优化思路完全不同。
核心原则:board_init_f只做“必须做且只能在此时做”的事,board_init_r则要“按需加载,能省则省”。
2.2 board_init_f 的“瘦身”实战
board_init_f的优化,我总结为“三砍一替”。
2.2.1 砍掉调试打印
很多开发板在board_init_f里塞了一堆printf,用来打印时钟频率、寄存器值。这些在调试阶段有用,但量产时就是累赘。串口打印本身很慢,尤其是轮询模式,每打印一个字符都要等硬件就绪。
/* 优化前:无条件打印 */
printf("CLK: %d MHz\n", get_clk_rate());
printf("DRAM: %d MB\n", get_ram_size());
/* 优化后:条件编译 */
#ifdef DEBUG_BOARD_INIT_F
printf("CLK: %d MHz\n", get_clk_rate());
printf("DRAM: %d MB\n", get_ram_size());
#endif
我曾经在一个项目里,把board_init_f里的7个printf全部用条件编译包裹,启动时间直接减少了80毫秒。嗯,这里要注意,条件编译的宏定义要统一管理,别在代码里散落一堆。
2.2.2 砍掉冗余的GPIO翻转
有些BSP代码里,board_init_f会翻转一堆GPIO来指示状态。比如LED闪烁、测试点输出。这些操作虽然简单,但GPIO操作涉及寄存器读写,如果翻转次数多,累积时间也不小。
我的习惯:只保留一个“生命体征”GPIO(比如心跳LED),其余全部注释掉。等调试需要时再打开。
2.2.3 砍掉不必要的延迟循环
很多初始化代码里都有udelay或mdelay,用来等待外设稳定。但有些延迟是“经验值”,不是“必需值”。比如等待PLL锁定,有些代码直接delay 10ms,但实际上PLL锁定只需要几百微秒。
/* 优化前:固定延迟 */
mdelay(10); /* 等待PLL锁定 */
/* 优化后:轮询状态寄存器 */
while (!(readl(&pll_reg) & PLL_LOCK_BIT))
; /* 轮询等待,实际可能只需200us */
注意:轮询等待要加超时保护,防止硬件异常导致死循环。我一般加一个100ms的超时,超时后打印错误并继续执行。
2.2.4 替换串口初始化时机
board_init_f里通常会初始化串口,用于打印信息。但串口初始化本身也需要时间,尤其是波特率配置、FIFO设置等。我建议把串口初始化放到board_init_f的最前面,这样后续的打印就能尽早输出。但如果你不需要早期打印,完全可以推迟到board_init_r里再初始化串口。
2.3 board_init_r 的“加速”技巧
board_init_r跑在DRAM里,空间充裕,但初始化内容也更多。这里的优化重点是“按需加载”和“延迟初始化”。
2.3.1 驱动按需加载(Deferred Probe)
U-Boot的驱动模型(Driver Model)支持deferred probe。说白了,就是不要一股脑把所有驱动都初始化完。只初始化当前启动流程需要的驱动,比如你从网络启动,那就只初始化网卡驱动,别管USB、SD卡、LCD这些。
/* 在board_init_r中,根据启动设备选择性初始化 */
switch (boot_device) {
case BOOT_DEVICE_NAND:
nand_init();
break;
case BOOT_DEVICE_MMC:
mmc_init();
break;
case BOOT_DEVICE_NET:
eth_init();
break;
default:
/* 全量初始化(兜底) */
nand_init();
mmc_init();
eth_init();
break;
}
我记得有个项目,默认是全量初始化,启动时间1.2秒。改成按需加载后,直接降到400毫秒。效果非常明显。
2.3.2 网络协议栈延迟初始化
如果启动设备不是网络,那网络协议栈(TCP/IP栈)完全可以延迟初始化。甚至可以不初始化,等用户进入U-Boot命令行后再手动初始化。
关键点:在board_init_r里,只调用eth_init()初始化MAC/PHY,不调用net_loop()等协议栈函数。协议栈的初始化放到实际需要网络操作时再触发。
2.3.3 环境变量只读缓存
环境变量的加载和校验(CRC)在board_init_r里会执行。如果环境变量存储在慢速设备(比如NAND、SPI Flash),每次加载都要读一遍,很费时间。
我建议的做法是:在board_init_f阶段,把环境变量从存储设备读到DRAM的一个固定区域,并计算CRC。board_init_r阶段直接使用这个缓存,不再重新读取。只有当需要保存环境变量时,才写回存储设备。
/* board_init_f中:预加载环境变量到缓存 */
env_relocate_to_cache(ENV_CACHE_ADDR);
/* board_init_r中:直接使用缓存 */
env_relocate_from_cache(ENV_CACHE_ADDR);
2.3.4 跳过不必要的CRC校验
调试阶段,环境变量经常被修改,每次启动都做CRC校验其实没必要。我习惯在调试阶段把CRC校验注释掉,或者加一个跳过标志。量产时再打开。
注意:量产时必须开启CRC校验,否则环境变量被破坏会导致启动异常。这个优化只适用于开发调试阶段。
2.4 实战案例:某ARM平台的启动时间优化
最后分享一个实际案例。某ARM Cortex-A7平台,原始启动时间约1.8秒(从复位到U-Boot命令行)。经过board_init_f和board_init_r的裁剪优化,最终降到0.6秒。
| 优化项 | 优化前耗时 | 优化后耗时 | 节省时间 |
|---|---|---|---|
| 串口调试打印(7处) | 120ms | 0ms(条件编译关闭) | 120ms |
| 冗余GPIO翻转(5个) | 50ms | 10ms(保留1个心跳) | 40ms |
| PLL固定延迟(3处) | 30ms | 3ms(轮询替代) | 27ms |
| 全量驱动初始化 | 800ms | 200ms(按需加载) | 600ms |
| 环境变量CRC校验 | 100ms | 0ms(调试阶段跳过) | 100ms |
| 总计 | 1.8s | 0.6s | 1.2s |
你看,优化不是玄学,每一毫秒都是实实在在抠出来的。当然,具体能优化多少,取决于你的硬件平台和BSP代码质量。但思路是通用的:board_init_f做减法,board_init_r做延迟。
最后一个小建议:每次优化后,用示波器或逻辑分析仪抓一下关键GPIO的波形,精确测量每个阶段的耗时。别光靠printf打时间戳,那个本身就有误差。