4. SPL 裁剪与加速:最小化SPL代码量

SPL 这玩意儿,说白了就是 U-Boot 的「迷你版」。它负责把完整的 U-Boot 从 Flash 里搬到 DDR 里。但问题来了——SPL 本身跑在 SRAM 里,空间就那么一丁点。我见过不少板子,SPL 空间只有 64KB 甚至 32KB。你想想看,这点空间要完成初始化、加载、跳转,代码稍微一膨胀就爆了。

所以,SPL 裁剪的核心目标就一个:在满足功能的前提下,把代码体积压到最小。体积小了,加载就快,启动自然就加速了。

4.1 为什么 SPL 需要裁剪?

先看一组我实际项目中的数据。某款 ARM 平台,默认 SPL 编译出来 128KB,但 SRAM 只有 96KB。结果呢?启动直接挂掉。后来我花了三天时间裁剪,最终压到 52KB,启动时间从 1.2 秒降到了 0.4 秒。

优化项 优化前体积 优化后体积 节省空间
默认 SPL 128 KB 52 KB 59%
驱动裁剪 45 KB 18 KB 60%
功能裁剪 30 KB 10 KB 67%
链接优化 53 KB 24 KB 55%

你看,每个环节都能砍掉一大半。关键是,你得知道从哪下手。

4.2 裁剪的三大方向

我个人习惯把 SPL 裁剪分成三个维度:功能裁剪、驱动裁剪、链接裁剪。一个一个说。

4.2.1 功能裁剪:关掉不需要的模块

U-Boot 的 Kconfig 里有一堆 SPL 相关的配置项。很多默认是打开的,但你的板子根本用不上。比如:

  • SPL_FAT_SUPPORT:如果你从 RAW NAND 启动,FAT 文件系统支持就是多余的。
  • SPL_PARTITIONS:分区表解析,只有从 MMC 或 SD 卡启动时才需要。
  • SPL_NET_SUPPORT:网络启动?SPL 阶段基本用不到。
  • SPL_USB_SUPPORT:USB 下载模式,调试时有用,量产时可以关掉。

我在项目中遇到过一件事:某款产品 SPL 死活放不下,最后发现是 SPL_CMD_BOOT 这个命令集没关。它包含了 bootm、bootz 等一堆命令,SPL 根本不需要。关掉后,体积直接少了 12KB。

核心原则:SPL 只做两件事——初始化硬件、加载 U-Boot。其他功能一律砍掉。

4.2.2 驱动裁剪:只保留必要的驱动

SPL 里的驱动,能少就少。比如:

  • 串口驱动:调试用,量产时可以关掉 CONFIG_SPL_SERIAL。
  • 时钟驱动:只保留 PLL 和关键外设时钟,其他外设时钟等 U-Boot 阶段再初始化。
  • 存储驱动:只保留你用的那一种。比如用 NAND 启动,就把 MMC、SPI Flash 驱动全关掉。

嗯,这里要注意:驱动裁剪不能盲目。我曾经把 SPL_I2C 关掉了,结果 DDR 初始化失败——因为 DDR 的 SPD 信息需要通过 I2C 读取。所以,裁剪前一定要搞清楚硬件依赖链。

我的经验:先编译一次 SPL,用 size 命令看各段体积。然后用 nm 或 objdump 找出最大的函数,优先优化它们。

4.2.3 链接裁剪:优化代码布局

链接阶段的优化,很多人会忽略。其实这里能省不少空间。

  • 使用 LTO(Link Time Optimization):GCC 的 -flto 选项可以跨文件优化,去掉未使用的函数。我实测能省 5%~10%。
  • 调整链接脚本:把 .text、.data、.bss 段紧凑排列,减少填充字节。
  • 去掉调试符号:CONFIG_SPL_SYS_THUMB_BUILD 如果平台支持,用 Thumb 指令集能省 30% 空间。
// 链接脚本示例:紧凑排列各段
SECTIONS
{
    . = ALIGN(4);
    .text : {
        *(.text*)
    }
    . = ALIGN(4);
    .rodata : {
        *(.rodata*)
    }
    . = ALIGN(4);
    .data : {
        *(.data*)
    }
    . = ALIGN(4);
    .bss : {
        *(.bss*)
    }
}

4.3 裁剪后的验证

裁剪完了,别急着高兴。一定要验证功能是否正常。我吃过这个亏:有一次把 SPL_DM(驱动模型)关掉了,结果 U-Boot 阶段加载时找不到设备树,直接 panic。

验证步骤很简单:

  1. 编译 SPL,检查体积是否在 SRAM 限制内。
  2. 烧录到板子,看能否正常打印 SPL 日志。
  3. 检查能否正确加载 U-Boot 并跳转。
  4. 用示波器或逻辑分析仪测量 SPL 阶段耗时。

警告:不要一次性关掉太多配置。每关一个,编译一次,测试一次。否则出了问题,你根本不知道是哪个配置导致的。

4.4 实战:一个裁剪案例

拿我最近做的一个 i.MX6ULL 项目举例。原始 SPL 配置如下:

CONFIG_SPL_FAT_SUPPORT=y
CONFIG_SPL_PARTITIONS=y
CONFIG_SPL_NET_SUPPORT=y
CONFIG_SPL_USB_SUPPORT=y
CONFIG_SPL_CMD_BOOT=y
CONFIG_SPL_SERIAL=y
CONFIG_SPL_I2C=y
CONFIG_SPL_MMC=y
CONFIG_SPL_NAND=y

裁剪后:

CONFIG_SPL_FAT_SUPPORT=n
CONFIG_SPL_PARTITIONS=n
CONFIG_SPL_NET_SUPPORT=n
CONFIG_SPL_USB_SUPPORT=n
CONFIG_SPL_CMD_BOOT=n
CONFIG_SPL_SERIAL=y    // 保留,调试用
CONFIG_SPL_I2C=y       // 保留,DDR 需要
CONFIG_SPL_MMC=y       // 保留,从 MMC 启动
CONFIG_SPL_NAND=n

体积从 98KB 降到了 41KB。启动时间从 0.8 秒降到了 0.3 秒。

4.5 SPL 裁剪知识体系

下面这张图,是我总结的 SPL 裁剪知识体系。你可以对照着检查自己的项目。

SPL 裁剪知识体系 SPL 裁剪知识体系 SPL 裁剪 功能裁剪 驱动裁剪 链接裁剪 关掉命令集 关掉文件系统 关掉网络/USB 只留必要外设 关掉调试串口 精简时钟驱动 启用 LTO 调整链接脚本 Thumb 指令集 裁剪核心原则 SPL 只做两件事:初始化硬件 + 加载 U-Boot 其他功能一律砍掉,每砍一个配置都要验证

4.6 总结

SPL 裁剪,说白了就是「做减法」。你想想看,SPL 的生命周期就那么几百毫秒,跑完就没了。没必要把 U-Boot 的完整功能塞进去。

我个人习惯是:先关掉所有非必要功能,然后逐个打开必须的。每开一个,编译一次,测试一次。这样既能保证体积最小,又能确保功能完整。

最后提醒一句:裁剪后的 SPL,一定要做压力测试。我曾经遇到过裁剪后 SPL 启动正常,但 U-Boot 加载时因为缺少某个初始化步骤而崩溃。嗯,这种问题最难排查。