4、Bootloader深度解析:U-Boot启动流程、DDR初始化、分区表设计、fastboot与recovery模式

Bootloader,说白了就是上电后第一个跑起来的程序。它负责把内核从闪存里捞出来,放到内存里,然后跳过去执行。U-Boot是目前机顶盒领域最主流的Bootloader,没有之一。我这些年经手的项目,从海思到晶晨,从瑞芯微到全志,底层清一色都是U-Boot。

这一章,咱们就把它彻底扒开看看。

4.1 U-Boot的启动流程:从Reset向量到命令行

U-Boot的启动,大致分两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么搞这么复杂?因为早期的NOR Flash可以直接XIP(原地执行),但现在的NAND/eMMC不行,片内SRAM又太小,装不下完整的U-Boot。所以得有个“小个子”先把“大块头”请进DDR。

第一阶段:SPL(约4KB~32KB)

  • CPU上电,从ROM里固定的地址取指令(比如全志的BROM)。
  • BROM检查启动介质顺序:SD卡、eMMC、NAND、SPI NOR……
  • 加载SPL到SRAM,跳转执行。
  • SPL初始化最基本的硬件:时钟、串口、DDR控制器。
  • 从存储介质加载U-Boot proper到DDR,跳转。

嗯,这里有个坑。SPL的代码必须非常精简,因为它能用的SRAM空间极其有限。我曾在某款低端芯片上,SPL只有8KB可用,连个完整的printf都塞不进去,调试全靠点LED灯。

第二阶段:U-Boot proper(约200KB~500KB)

  • 完整的驱动框架:MMC、NAND、USB、网络、文件系统。
  • 解析设备树(FDT),根据硬件配置初始化外设。
  • 执行环境变量脚本(bootcmd),加载内核。
  • 进入命令行交互(如果设置了bootdelay)。

为什么会这样设计?说白了就是“分步加载,各司其职”。SPL负责把DDR搞起来,U-Boot proper负责把内核搞起来。

4.2 DDR初始化:最关键的“硬骨头”

DDR初始化是Bootloader里最敏感、最容易翻车的地方。我见过太多项目,U-Boot跑起来了,一加载内核就死机,最后发现是DDR时序参数配错了。

DDR初始化的核心步骤:

  1. PLL配置:给DDR控制器提供稳定的时钟源。
  2. DDR控制器复位:让控制器进入已知状态。
  3. DDR PHY配置:设置ODT(片上端接)、驱动强度、ZQ校准。
  4. 时序参数写入:tRCD、tCL、tRP、tRAS……这些值必须和DDR颗粒的datasheet严格对应。
  5. 训练(Training):读写DQS门控、Vref校准、写平衡。
  6. 验证:写入特定pattern并回读,确认无误。

我的经验:DDR训练这一步,千万别偷懒用默认值。不同批次的内存颗粒,甚至同一颗芯片在不同温度下,训练结果都可能不一样。我习惯在量产固件里保留完整的训练流程,而不是把训练好的参数写死。

代码层面,U-Boot里DDR初始化通常放在board_init_f()或专门的ddr_init()函数里。以全志H3为例:

// 简化的DDR初始化流程
void ddr_init(void)
{
    // 1. 设置DDR时钟
    clock_set_pll(CLK_PLL_DDR, 408);  // 408MHz

    // 2. 复位DDR控制器
    ddr_ctrl_reset();

    // 3. 配置PHY
    ddr_phy_config(&ddr_params);

    // 4. 写入时序
    ddr_timing_set(&ddr_timing);

    // 5. 执行训练
    ddr_training();

    // 6. 验证
    if (ddr_verify() != 0) {
        printf("DDR init failed!\n");
        hang();
    }
}

注意:DDR初始化失败后,千万不要直接重启了事。我建议在串口打印出具体的失败原因(比如“DQS gate training timeout”),这对产线排查问题至关重要。

4.3 分区表设计:别让存储空间变成一团乱麻

分区表决定了你的固件、数据、配置怎么摆放。机顶盒里常见的分区结构:

分区名 起始地址 大小 用途
bootloader 0x00000000 2MB U-Boot proper + SPL
env 0x00200000 128KB U-Boot环境变量
boot 0x00220000 16MB 内核 + dtb
system 0x01220000 512MB Android/Linux系统
data 0x21220000 剩余空间 用户数据
cache 0x41220000 256MB OTA升级缓存
recovery 0x51220000 32MB Recovery内核

设计分区表时,有几点要特别注意:

  • 对齐:所有分区的起始地址必须是擦除块大小(通常128KB或256KB)的整数倍。否则擦除时会波及相邻分区。
  • 冗余:环境变量分区最好保留两个备份(env、env_redund)。我曾经遇到过eMMC坏块导致环境变量丢失,整批设备变砖的惨案。
  • 预留:给bootloader分区留足空间。U-Boot功能越加越多,体积膨胀很快。我习惯留2MB以上。

避坑指南:分区表一旦发布,尽量不要改动。如果非要改,一定要在U-Boot里做兼容处理——检测到旧分区表时自动迁移数据。否则用户升级固件后,数据全没了,那可就麻烦了。

4.4 Fastboot模式:刷机救砖的“瑞士军刀”

Fastboot是Google定义的Android刷机协议,通过USB传输数据。U-Boot里实现Fastboot,说白了就是实现一个USB设备端,然后解析Fastboot命令。

常见的Fastboot命令:

  • fastboot flash boot boot.img —— 刷写boot分区
  • fastboot erase system —— 擦除system分区
  • fastboot reboot —— 重启设备
  • fastboot getvar product —— 获取设备信息

U-Boot里启用Fastboot很简单,配置好USB gadget驱动,然后使能CONFIG_CMD_FASTBOOT即可。但要注意,Fastboot模式下,USB中断处理不能阻塞太久,否则PC端会超时报错。

我的习惯:在Fastboot模式下,我会把串口日志级别调到最高,同时打印出每次刷写的分区名和大小。这样一旦刷写失败,能立刻知道是哪个分区出了问题。

4.5 Recovery模式:系统最后的“救命稻草”

Recovery模式和Fastboot不同。Fastboot依赖USB连接PC,而Recovery是一个独立的小系统,可以运行在设备本地。它通常包含一个精简的Linux内核和一套恢复工具。

Recovery的启动流程:

  1. U-Boot检测到特定按键组合(比如音量+加电源键),或者读取到“boot-recovery”标志。
  2. 从recovery分区加载内核和ramdisk。
  3. Recovery系统启动,显示菜单:重启、清除数据、应用更新。
  4. 执行用户选择的操作。

Recovery模式下最常用的功能是OTA升级。系统会把升级包下载到cache分区,然后重启进入Recovery,由Recovery负责解包、校验、刷写。

重要提醒:Recovery内核必须足够稳定。它是在主系统已经崩溃的情况下才启用的,如果Recovery本身也有bug,那设备就彻底变砖了。我建议Recovery内核只包含最基础的驱动:eMMC、显示、触摸(或按键)、USB。其他花里胡哨的功能一概不要。

嗯,说到Recovery,我想起一个案例。某次量产,客户反馈设备升级后无法开机。排查发现,Recovery里的eMMC驱动版本和主系统不一致,导致刷写时写入了错误的数据。从那以后,我要求Recovery和主系统共用同一套驱动代码,编译时通过宏区分。

4.6 小结

Bootloader看似简单,实则处处是坑。从SPL的尺寸控制,到DDR的时序调优,再到分区表的布局设计,每一步都影响着产品的稳定性和可维护性。Fastboot和Recovery模式,则是设备出厂后最重要的两道防线。

我个人建议,在项目初期就把Bootloader的调试手段做足。串口打印、内存dump、GPIO点灯,这些看似原始的方法,在关键时刻往往比JTAG仿真器更管用。你想想看,产线上几百台设备同时烧录,哪有功夫给你一个个接仿真器?

下一章,咱们聊聊内核移植和设备树。那又是另一片江湖了。