3、Bootloader移植(下):网络驱动移植(FEC/ENET)、eMMC/SD卡驱动移植、fastboot功能实现、u-boot环境变量定制

好,咱们接着往下聊。上一节我们把 Bootloader 的启动流程和时钟、DDR 这些基础模块搞定了。这一节,我打算把剩下的几个硬骨头啃下来——网络、存储、fastboot 还有环境变量。这几个模块,说白了就是让板子能“联网、存东西、被刷机”。

我个人习惯,做移植的时候,先把网络和存储调通。为什么呢?因为这两个是后续调试的“生命线”。网络不通,你没法通过网络加载内核;存储不行,系统装不进去。fastboot 则是 Android 刷机的命根子,必须稳。

核心要点: 本章节聚焦于 i.MX8M 系列(特别是 i.MX8M Mini/Nano/Plus)的 Bootloader 移植下半部分。我们将深入 FEC/ENET 网络控制器、eMMC/SDHC 控制器、fastboot 协议实现以及 u-boot 环境变量的定制策略。

3.1 网络驱动移植:FEC 与 ENET

i.MX8M 系列集成了两个以太网控制器:一个是 Fast Ethernet Controller (FEC),另一个是 Gigabit Ethernet Controller (ENET)。FEC 通常接百兆 PHY,ENET 接千兆 PHY。我在项目中遇到过,很多人搞混了这两个控制器的设备树节点,导致网络死活不通。

先看设备树。以 i.MX8M Mini 为例,ENET 的节点通常在 arch/arm/dts/imx8mm.dtsi 中定义。你需要确认以下几点:

  • 时钟源: ENET 需要两个时钟——一个给 MAC 本身(ipg_clk),一个给 RGMII 接口(enet_clk)。时钟频率必须与 PHY 匹配。
  • 复位引脚: PHY 的复位 GPIO 必须在设备树中正确配置。我曾经因为复位时序不对,PHY 一直处于复位状态,查了两天才发现。
  • MDIO 总线: 确保 MDIO 引脚功能正确,否则读不到 PHY 的 ID。

下面是一个典型的 ENET 设备树配置片段:

&fec1 {
    pinctrl-names = "default";
    pinctrl-0 = &pinctrl_fec1;
    phy-mode = "rgmii-id";
    phy-handle = <&ethphy0>;
    status = "okay";

    mdio {
        #address-cells = <1>;
        #size-cells = <0>;

        ethphy0: ethernet-phy@0 {
            compatible = "ethernet-phy-ieee802.3-c22";
            reg = <0>;
            reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
            reset-assert-us = <10000>;
            reset-deassert-us = <80000>;
        };
    };
};
避坑指南: 我曾经在配置 RGMII 时序时,忽略了 phy-mode = "rgmii-id" 中的 "id" 后缀。这个后缀告诉 MAC 内部添加 TX 和 RX 的延迟。如果 PHY 本身已经加了延迟,这里就要用 "rgmii" 不带 "id"。否则信号会错位,丢包率极高。

在 u-boot 驱动层面,FEC/ENET 的驱动代码位于 drivers/net/fec_mxc.c。你需要检查:

  • DMA 描述符对齐: FEC 要求 DMA 描述符必须 64 字节对齐。如果内存分配不对齐,会导致 DMA 传输错误。
  • PHY 驱动: 确保 u-boot 中包含了你的 PHY 芯片驱动(如 RTL8211、AR8031 等)。可以在 drivers/net/phy/ 下找到。
  • MAC 地址: 如果板子没有 EEPROM 存储 MAC 地址,u-boot 会使用随机 MAC。这会导致网络冲突。我建议在 board_late_init() 中从 OTP 或固定值读取 MAC。

3.2 eMMC/SD 卡驱动移植

存储驱动,说白了就是让 u-boot 能读写 eMMC 和 SD 卡。i.MX8M 使用 u-boot 的 drivers/mmc/fsl_esdhc_imx.c 驱动。这个驱动比较成熟,但移植时仍有几个关键点。

首先,设备树中的 MMC 节点需要配置:

&usdhc2 {
    pinctrl-names = "default", "state_100mhz", "state_200mhz";
    pinctrl-0 = &pinctrl_usdhc2;
    pinctrl-1 = &pinctrl_usdhc2_100mhz;
    pinctrl-2 = &pinctrl_usdhc2_200mhz;
    bus-width = <4>;
    non-removable;
    status = "okay";
};

这里要注意:

  • bus-width: eMMC 通常用 8 位,SD 卡用 4 位。别搞错了。
  • non-removable: 对于 eMMC,必须加上这个标志,否则 u-boot 会认为它是可插拔设备,影响启动流程。
  • pinctrl 状态: 100MHz 和 200MHz 的引脚配置必须准确。这是为了支持 DDR 模式和 HS200/HS400 模式。

在驱动代码中,我建议你关注以下几点:

  1. 时钟频率: u-boot 初始化 MMC 时,会从 400kHz 开始,然后切换到高频。确保你的时钟树配置正确,否则高频下会出错。
  2. 电压切换: SD 卡需要 3.3V 到 1.8V 的电压切换。这个切换由 GPIO 控制,必须在驱动中实现。我遇到过因为电压切换时序不对,导致 SD 卡在高速模式下无法识别。
  3. DMA 支持: 如果启用了 DMA,要确保 DMA 缓冲区是 cache 一致的。否则会出现数据损坏。
注意: eMMC 的 boot 分区和用户数据分区是分开的。在 u-boot 中,你可以通过 mmc dev 0 切换到 eMMC,然后使用 mmc partconf 命令配置 boot 分区。如果你要从 eMMC 启动,必须确保 boot 分区使能且包含正确的 bootloader 镜像。

3.3 fastboot 功能实现

fastboot 是 Android 刷机的核心协议。在 u-boot 中实现 fastboot,说白了就是让 u-boot 能通过 USB 与 PC 通信,执行刷写命令。

i.MX8M 的 fastboot 实现主要依赖 drivers/usb/gadget/f_fastboot.c。你需要确保以下几点:

  • USB Gadget 驱动: i.MX8M 使用 DWC3 USB 控制器。确保 CONFIG_USB_GADGETCONFIG_USB_GADGET_DWC3 已启用。
  • fastboot 命令支持:include/configs/imx8mm_evk.h 中,添加 CONFIG_CMD_FASTBOOT
  • 分区表: fastboot 需要知道分区布局。你可以在 CONFIG_FASTBOOT_FLASH 中定义分区名和偏移。

下面是一个典型的分区配置:

#define CONFIG_FASTBOOT_FLASH
#define CONFIG_FASTBOOT_FLASH_MMC_DEV 0
#define CONFIG_FASTBOOT_BUF_SIZE (64 * 1024 * 1024)

#define CONFIG_FASTBOOT_GPT_NAME "gpt"
#define CONFIG_FASTBOOT_MISC_NAME "misc"
#define CONFIG_FASTBOOT_BOOT_NAME "boot"
#define CONFIG_FASTBOOT_SYSTEM_NAME "system"
#define CONFIG_FASTBOOT_VENDOR_NAME "vendor"
实战经验: 我在调试 fastboot 时,发现 PC 端总是提示 "FAILED (remote: 'unknown command')"。后来发现是 u-boot 的 fastboot 命令列表中没有包含 flash 命令。你需要确保 CONFIG_FASTBOOT_FLASHCONFIG_FASTBOOT_FLASH_MMC_DEV 正确配置。另外,如果使用 GPT 分区表,记得启用 CONFIG_EFI_PARTITION

fastboot 的流程其实不复杂:

  1. u-boot 启动后,检测到按键(如音量减键)进入 fastboot 模式。
  2. 初始化 USB Gadget,等待 PC 连接。
  3. PC 发送命令(如 fastboot flash boot boot.img)。
  4. u-boot 解析命令,将数据写入指定分区。

这里有个坑:u-boot 的 fastboot 默认使用 download 缓冲区。如果镜像太大(比如 system.img 几个 GB),缓冲区可能不够。我建议将 CONFIG_FASTBOOT_BUF_SIZE 设置为 64MB 或更大,或者实现流式写入。

3.4 u-boot 环境变量定制

环境变量是 u-boot 的灵魂。它决定了启动参数、网络配置、存储路径等。对于 Android Automotive,环境变量的定制尤为重要。

默认情况下,u-boot 的环境变量存储在 eMMC 的特定区域(通常是 mmc 0:0 的某个偏移)。你可以通过 CONFIG_ENV_OFFSETCONFIG_ENV_SIZE 配置。

我建议你定制以下几类环境变量:

类别 变量名 说明
启动参数 bootargs 传递给内核的命令行参数。包括 console、rootfs 类型、DMA 大小等。
网络配置 ipaddr, serverip, netmask 用于网络启动时的 IP 配置。
存储路径 loadaddr, fdt_addr, kernel_addr_r 内核和设备树在内存中的加载地址。
启动命令 bootcmd 默认的启动命令序列。通常是从 eMMC 读取内核并启动。
Android 专用 slot_suffix, androidboot.slot_suffix 用于 A/B 分区的槽位选择。

下面是一个典型的 bootcmd 示例:

bootcmd=run findfdt; run loadfdt; run loadkernel; run bootkernel

loadkernel=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}
loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr} ${fdt_file}
bootkernel=booti ${loadaddr} - ${fdt_addr}
关键点: 对于 Android Automotive,我强烈建议使用 A/B 分区方案。这意味着你需要定义 slot_suffix 变量,并在 bootcmd 中根据当前槽位动态选择分区。例如:setenv mmcpart ${slot_suffix}。这样,系统更新时可以无缝切换槽位。

环境变量的存储位置也要注意。如果使用 eMMC,我建议将环境变量放在 mmc 0:0CONFIG_ENV_OFFSET 处。这个偏移量必须避开分区表和其他关键数据。通常放在 8MB 或 16MB 偏移处。

嗯,这里还要提一下冗余环境变量。u-boot 支持两个环境变量副本(redundant env)。如果一个损坏,自动使用另一个。这在产品中非常有用。配置方法:

#define CONFIG_ENV_IS_IN_MMC
#define CONFIG_ENV_OFFSET (8 * 1024 * 1024)
#define CONFIG_ENV_SIZE (8 * 1024)
#define CONFIG_ENV_OFFSET_REDUND (16 * 1024 * 1024)
#define CONFIG_SYS_REDUNDAND_ENVIRONMENT
警告: 不要将环境变量放在 eMMC 的 boot 分区内。boot 分区是只读的(除非你特意使能写操作)。我曾经犯过这个错误,导致环境变量无法保存,每次重启都恢复默认值。正确的做法是放在用户数据分区的起始位置之前。

3.5 本章小结

这一节的内容,说白了就是让板子“活起来”。网络驱动让板子能联网,存储驱动让板子能读写数据,fastboot 让板子能被刷机,环境变量则让这一切变得可配置。

我个人觉得,移植过程中最容易被忽视的是细节——时钟频率、引脚复用、对齐要求、电压切换。这些细节,任何一个出错,都会导致功能异常。我的建议是:每移植一个模块,就用对应的 u-boot 命令测试一下。网络用 ping,存储用 mmc read,fastboot 用 PC 端工具验证。这样能快速定位问题。

最后,别忘了环境变量的持久化。没有正确的环境变量,你的板子可能连内核都启动不了。

Bootloader移植(下)核心模块关系图 Bootloader 移植 网络驱动 FEC / ENET 存储驱动 eMMC / SD卡 fastboot USB刷机协议 环境变量 启动参数定制 四个模块相互依赖:网络和存储是基础,fastboot依赖存储,环境变量贯穿全局

公众号:蓝海资料掘金营,微信deep3321