3. Bootloader裁剪与配置:U-Boot源码获取、配置与编译、裁剪不必要的驱动与命令、适配自定义硬件

Bootloader 是系统上电后跑的第一段代码。说白了,它负责把内核从 Flash 搬到内存,然后跳过去执行。U-Boot 是目前嵌入式 Linux 用得最多的 Bootloader,没有之一。我这些年经手的项目,从 ARM9 到 Cortex-A72,几乎都在用 U-Boot。

但 U-Boot 默认配置很臃肿。你想想看,它要支持几十种开发板、几百种驱动、上千条命令。而我们实际产品只需要其中一小部分。所以裁剪是必须的。

3.1 U-Boot 源码获取与版本选择

U-Boot 官方源码托管在 https://source.denx.de/u-boot/u-boot。我个人习惯用 Git 拉取,方便切分支和打补丁。

git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
git checkout v2024.01  # 选一个长期支持版本
版本选择建议: 别追最新版。我一般选 LTS 或发布超过 3 个月的稳定版。新版本可能有未暴露的坑,老版本又缺关键修复。v2023.04、v2024.01 都是不错的选择。

版本选好后,先看看 configs/ 目录下有没有接近你硬件的 defconfig。比如我用的是 i.MX6ULL,那就找 mx6ull_14x14_evk_defconfig。没有完全匹配的,就选最接近的,后面再改。

3.2 配置与编译:从 defconfig 到 u-boot.bin

U-Boot 的配置系统跟 Linux 内核很像,都是 Kconfig + Makefile。流程很简单:

  1. 加载默认配置:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig
  2. 菜单配置:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
  3. 编译:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4

编译完成后,当前目录下会生成 u-boot.bin、u-boot-dtb.bin(带设备树)、u-boot.imx(i.MX 专用格式)等文件。具体生成哪些,取决于你的平台。

交叉编译器版本要匹配: 我曾经在一个项目里用了太新的 gcc 12 编译 U-Boot v2022.10,结果链接阶段报了一堆莫名其妙的重定义错误。后来换成 Linaro 7.5 就过了。建议用 U-Boot 文档里推荐的编译器版本范围。

3.3 裁剪不必要的驱动与命令

U-Boot 的裁剪主要分两块:驱动 和 命令。驱动占 Flash 空间,命令占内存和 Flash。我一般按以下步骤来:

3.3.1 裁剪驱动

驱动裁剪在 Device Drivers 菜单里。以下是我常用的裁剪清单:

驱动类别 裁剪建议 说明
串口驱动 保留板级对应 UART 至少留一个调试串口
MMC/SD 驱动 保留实际使用的控制器 如果从 eMMC 启动,必须保留
NAND/SPI Flash 驱动 按实际存储介质保留 用 SPI NOR 就关掉 NAND
网络驱动 保留板载 PHY 对应驱动 调试阶段建议保留,量产可关
USB 驱动 量产阶段可全部关闭 除非你需要 USB 烧录
显示/GPU 驱动 无显示需求直接关 能省不少空间
PCIe 驱动 不用 PCIe 就关 很多 ARM 平台用不到

具体操作:进入 Device Drivers,把不需要的驱动前面的 [*] 改成 [ ]。注意有些驱动有依赖关系,关掉后会自动关掉子选项。

3.3.2 裁剪命令

命令裁剪在 Command line interface 菜单里。U-Boot 默认开了上百条命令,很多你一辈子都用不上。我一般这样处理:

  • 必留命令:bootm、bootz、go、load、mmc、sf、setenv、saveenv、printenv、reset
  • 调试阶段保留:md、mw、cp、cmp、ping、tftp
  • 量产可关:fat、ext4、ubi、jffs2、usb、pxe、dhcp、bootp
  • 直接关掉:ide、scsi、sata、virtio、cramfs、squashfs
裁剪效果参考: 以 i.MX6ULL 为例,默认配置编译出来约 450KB。经过驱动和命令裁剪后,可以压到 280KB 左右。如果再去掉设备树里不用的节点,能到 250KB 以下。

3.4 适配自定义硬件

适配自定义硬件,核心是改 设备树 和 板级文件。U-Boot 从 v2016 开始全面使用设备树来描述硬件,所以大部分适配工作都在 .dts 里完成。

3.4.1 设备树修改

设备树文件在 arch/arm/dts/ 目录下。找到你板子对应的 .dts,比如 imx6ull-myboard.dts。主要修改点:

  • 内存节点:修改 reg = <0x80000000 0x10000000>; 匹配实际 DDR 大小
  • 串口节点:确认引脚复用和时钟配置
  • MMC 节点:调整 bus-width、no-1-8-v 等属性
  • 网络节点:修改 PHY 地址、复位 GPIO
  • GPIO/LED/按键:按原理图添加
// 示例:修改内存大小为 512MB
memory {
    reg = <0x80000000 0x20000000>;
};

// 修改 FEC 网卡的 PHY 地址
&fec1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_enet1>;
    phy-mode = "rmii";
    phy-handle = <ðphy0>;
    status = "okay";
};

&mdio {
    ethphy0: ethernet-phy@1 {
        reg = <1>;  // PHY 地址改为 1
    };
};

3.4.2 板级文件修改

如果设备树搞不定(比如特殊的上电时序、GPIO 初始化顺序),就需要改板级 C 文件。文件在 board/<厂商>/<板子名>/ 目录下。常见的修改点:

  • board_init():早期硬件初始化,比如配置 PMIC、设置时钟
  • board_late_init():后期初始化,比如设置环境变量默认值
  • board_mmc_init():MMC 控制器初始化
  • board_eth_init():以太网初始化
注意: 改板级文件要谨慎。我遇到过一个问题:在 board_init() 里加了一段 GPIO 操作,结果导致 DDR 初始化失败,U-Boot 连串口都没打印。后来发现是 GPIO 时钟还没开。所以改硬件相关代码时,一定要确认时钟和电源域已经就绪。

3.5 裁剪与适配的完整流程

下面这张图总结了从拿到 U-Boot 源码到生成最终镜像的完整流程:

U-Boot 裁剪与适配完整流程 获取源码 选择 defconfig make menuconfig 裁剪驱动 裁剪命令 适配设备树 编译 烧录验证 迭代优化 裁剪和适配是迭代过程。烧录验证发现问题后,返回 menuconfig 或设备树继续调整,直到稳定运行。

3.6 避坑指南

做 U-Boot 裁剪这几年,我踩过不少坑。挑几个典型的说说:

  • 裁剪后无法启动:我曾经把 CONFIG_CMD_BOOTM 关了,结果内核启动时直接 hang 住。后来才发现 bootm 是启动内核的核心命令,不能关。所以裁剪前先搞清楚每条命令的作用。
  • 设备树编译不过:U-Boot 的设备树语法跟 Linux 内核完全一样,但有些属性是 U-Boot 特有的。比如 u-boot,dm-pre-reloc,不加的话驱动在 SPL 阶段不会初始化。我遇到过网卡在 SPL 里不工作,查了两天才发现是少了这个属性。
  • 环境变量保存失败:裁剪时如果把 CONFIG_ENV_IS_IN_MMC 或 CONFIG_ENV_IS_IN_SPI_FLASH 搞错了,环境变量就存不进去。我建议先确认你的存储介质,再选对应的环境变量后端。
  • SPL 太大放不下:很多芯片的 SPL 有大小限制,比如 i.MX6ULL 的 SPL 不能超过 68KB。如果裁剪不彻底,SPL 编译出来超了,就会覆盖后面的区域。用 make size 可以提前检查。
我的调试习惯: 每次裁剪后,先编译通过,再烧录到板子上看串口输出。如果 U-Boot 能正常打印版本号和提示符,说明裁剪没伤到筋骨。然后逐个测试关键功能:读 eMMC、加载内核、启动内核。这样能快速定位问题。

U-Boot 裁剪说难不难,说简单也不简单。核心就三点:知道要什么、知道怎么关、知道怎么测。把这三点吃透了,你就能根据任何硬件定制出最精简的 Bootloader。