3、Bootloader移植(下):网络驱动移植(FEC/ENET)、eMMC/SD卡驱动移植、fastboot功能实现、u-boot环境变量定制
好,咱们接着往下聊。上一节我们把 Bootloader 的启动流程和时钟、DDR 这些基础模块搞定了。这一节,我打算把剩下的几个硬骨头啃下来——网络、存储、fastboot 还有环境变量。这几个模块,说白了就是让板子能“联网、存东西、被刷机”。
我个人习惯,做移植的时候,先把网络和存储调通。为什么呢?因为这两个是后续调试的“生命线”。网络不通,你没法通过网络加载内核;存储不行,系统装不进去。fastboot 则是 Android 刷机的命根子,必须稳。
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 = <ðphy0>;
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>;
};
};
};
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 模式。
在驱动代码中,我建议你关注以下几点:
- 时钟频率: u-boot 初始化 MMC 时,会从 400kHz 开始,然后切换到高频。确保你的时钟树配置正确,否则高频下会出错。
- 电压切换: SD 卡需要 3.3V 到 1.8V 的电压切换。这个切换由 GPIO 控制,必须在驱动中实现。我遇到过因为电压切换时序不对,导致 SD 卡在高速模式下无法识别。
- DMA 支持: 如果启用了 DMA,要确保 DMA 缓冲区是 cache 一致的。否则会出现数据损坏。
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_GADGET和CONFIG_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"
flash 命令。你需要确保 CONFIG_FASTBOOT_FLASH 和 CONFIG_FASTBOOT_FLASH_MMC_DEV 正确配置。另外,如果使用 GPT 分区表,记得启用 CONFIG_EFI_PARTITION。
fastboot 的流程其实不复杂:
- u-boot 启动后,检测到按键(如音量减键)进入 fastboot 模式。
- 初始化 USB Gadget,等待 PC 连接。
- PC 发送命令(如
fastboot flash boot boot.img)。 - 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_OFFSET 和 CONFIG_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}
slot_suffix 变量,并在 bootcmd 中根据当前槽位动态选择分区。例如:setenv mmcpart ${slot_suffix}。这样,系统更新时可以无缝切换槽位。
环境变量的存储位置也要注意。如果使用 eMMC,我建议将环境变量放在 mmc 0:0 的 CONFIG_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
3.5 本章小结
这一节的内容,说白了就是让板子“活起来”。网络驱动让板子能联网,存储驱动让板子能读写数据,fastboot 让板子能被刷机,环境变量则让这一切变得可配置。
我个人觉得,移植过程中最容易被忽视的是细节——时钟频率、引脚复用、对齐要求、电压切换。这些细节,任何一个出错,都会导致功能异常。我的建议是:每移植一个模块,就用对应的 u-boot 命令测试一下。网络用 ping,存储用 mmc read,fastboot 用 PC 端工具验证。这样能快速定位问题。
最后,别忘了环境变量的持久化。没有正确的环境变量,你的板子可能连内核都启动不了。