2. U-Boot网络协议栈基础:网络驱动模型与 net/eth.c 核心结构解析

做嵌入式网络升级,第一步不是写应用层代码,而是先把底层的网卡驱动跑通。U-Boot 的网络驱动模型,说白了就是一套统一的接口规范。它让上层协议(比如 TFTP、NFS)不用关心底层用的是 DM9000 还是 RTL8169,只管调用 eth_recv() 和 eth_send() 就行。

我个人习惯,拿到一块新板子,第一件事就是看 net/eth.c。这个文件是整个网络协议栈的“调度中心”。今天我们就把它拆开,看看里面到底藏了什么玄机。

2.1 驱动模型概览:从“杂牌军”到“正规军”

U-Boot 的网络驱动模型经历过一次大重构。早期版本里,每个网卡驱动自己注册自己,调用关系混乱。后来引入了 Driver Model (DM),也就是驱动模型。现在主流版本都基于 DM。

你想想看,如果没有统一模型,你要同时支持以太网和 USB 网卡,代码得写成什么样?每个驱动自己实现一套 probe、init、send、recv,上层调用时得写一堆 #ifdef。有了 DM 之后,所有网卡都遵循同一个框架:

  • UCLASS_ETH:网卡设备类,所有以太网设备都属于这个类。
  • struct eth_ops:操作函数集,定义了 start、send、recv、stop 等接口。
  • struct udevice:设备实例,每个网卡对应一个。

核心思想:驱动只负责实现 eth_ops 里的函数,剩下的设备枚举、初始化顺序、回调管理,全交给 DM 框架。这就是“正规军”的打法。

2.2 net/eth.c 核心结构:调度中心长什么样?

打开 net/eth.c,你会发现它其实不大。核心函数就那么几个,但每个都承担着关键职责。我把它梳理成了一张图,你先看个全貌:

U-Boot net/eth.c 核心结构图 net/eth.c 核心结构图 上层协议(TFTP / NFS / Ping) eth.c 核心调度层 eth_init() eth_send() / eth_recv() eth_rx() 轮询 驱动层(各网卡具体实现) struct eth_ops start / send / recv / stop struct udevice 网卡设备实例 UCLASS_ETH 设备类标识 硬件(MAC / PHY / MDIO) 说明:上层协议不直接操作硬件,所有调用都经过 eth.c 的调度函数,再转发给具体驱动的 eth_ops

这张图你看懂了吗?上层协议(TFTP、Ping)调用 eth_send(),eth.c 找到当前活动的网卡设备,再调用它的 eth_ops->send()。反过来,数据接收走 eth_rx() 轮询。整个过程,驱动层对上层完全透明。

2.3 核心数据结构:struct eth_ops 和 struct eth_device

我们先看 struct eth_ops。这是每个网卡驱动必须实现的“契约”:

// include/net.h
struct eth_ops {
    int (*start)(struct udevice *dev);
    int (*send)(struct udevice *dev, void *packet, int length);
    int (*recv)(struct udevice *dev, int flags, uchar **packetp);
    int (*free_pkt)(struct udevice *dev, uchar *packet, int length);
    int (*stop)(struct udevice *dev);
    int (*mcast)(struct udevice *dev, const u8 *mac, int add);
    int (*write_hwaddr)(struct udevice *dev);
    int (*read_rom_hwaddr)(struct udevice *dev);
};

这里我重点说几个:

  • start / stop:打开和关闭网卡。start 里通常要做 PHY 初始化、MAC 配置、开启 DMA。
  • send:发送一个数据包。注意,U-Boot 没有协议栈的“分片”概念,一次 send 就是一个完整的以太网帧。
  • recv:接收数据包。它返回一个指向接收缓冲区的指针 packetp。驱动需要自己管理缓冲区,要么用静态数组,要么用 DMA 环。
  • free_pkt:接收完成后释放缓冲区。这个函数很容易被忽略,但如果不实现,接收缓冲区很快就会耗尽。

我的经验: 写驱动时,recv 和 free_pkt 一定要配对实现。我曾经在一个项目中只实现了 recv,没实现 free_pkt,结果连续接收几十个包后,系统就再也收不到新数据了。查了两天才发现是缓冲区没释放。

再看 struct eth_device(旧版)和 struct udevice(DM 版)。在 DM 框架下,每个网卡设备都是一个 struct udevice,通过 dev_get_uclass_priv() 获取驱动私有数据。举个例子:

// drivers/net/dwmac.c (简化)
static int dwmac_start(struct udevice *dev)
{
    struct dwmac_priv *priv = dev_get_uclass_priv(dev);
    // 初始化 DMA、MAC、PHY
    dwmac_init(priv->regs, priv->phy_addr);
    return 0;
}

static const struct eth_ops dwmac_ops = {
    .start  = dwmac_start,
    .send   = dwmac_send,
    .recv   = dwmac_recv,
    .free_pkt = dwmac_free_pkt,
    .stop   = dwmac_stop,
};

U_BOOT_DRIVER(dwmac) = {
    .name   = "dwmac",
    .id     = UCLASS_ETH,
    .ops    = &dwmac_ops,
    .priv_auto = sizeof(struct dwmac_priv),
    .probe  = dwmac_probe,
};

看到没?驱动只需要定义 eth_ops 和 U_BOOT_DRIVER 宏,剩下的注册、匹配、初始化顺序,全由 DM 框架搞定。

2.4 初始化流程:从 U_BOOT_DRIVER 到 eth_init

U-Boot 启动时,网卡是怎么被发现的?流程大致如下:

  1. 设备树解析:U-Boot 读取设备树(.dts),找到所有 compatible 匹配的以太网节点。
  2. 驱动绑定:DM 框架根据 compatible 字符串,找到对应的 U_BOOT_DRIVER,调用 probe 函数。
  3. probe 函数:驱动在这里分配私有数据、映射寄存器、复位 PHY。
  4. eth_init 调用:当上层协议需要网络时,调用 eth_init(),它遍历所有已 probe 的网卡,调用 eth_ops->start()。
  5. 设置当前设备:如果有多个网卡,通过 eth_set_current() 选择活动设备。

注意: eth_init() 不是只调用一次。每次网络操作前(比如每次 TFTP 重试),U-Boot 都会调用 eth_init()。所以你的 start 函数必须是幂等的——多次调用不会导致资源泄漏或状态错乱。

2.5 数据收发路径:send 和 recv 的完整链路

我们以发送为例,看看数据是怎么从应用层走到网卡的:

步骤 函数调用 说明
1 tftp_loop() → eth_send() TFTP 协议构造好 UDP 包,调用 eth_send
2 eth_send() → ops->send() eth.c 找到当前设备,调用驱动 send
3 dwmac_send() → 写 DMA 描述符 驱动将数据拷贝到 DMA 缓冲区,触发发送
4 MAC 控制器 → PHY → 网线 硬件完成帧发送

接收路径稍微复杂一点,因为 U-Boot 没有中断。它采用 轮询模式:

// net/eth.c
int eth_rx(void)
{
    struct udevice *current = eth_current;
    struct eth_ops *ops = eth_get_ops(current);
    uchar *packet;
    int ret;
    
    ret = ops->recv(current, 0, &packet);
    if (ret > 0) {
        // 交给上层协议处理
        net_process_received_packet(packet, ret);
        ops->free_pkt(current, packet, ret);
    }
    return ret;
}

这个函数在主循环中被反复调用。每次调用,驱动检查是否有新数据到达。有就交给 net_process_received_packet() 处理,然后释放缓冲区。

关键点: U-Boot 的接收是“一次一包”。eth_rx() 每次最多处理一个包。如果你一次收到多个包,需要多次调用 eth_rx()。这也是为什么主循环里会连续调用 eth_rx() 直到返回 0。

2.6 避坑指南:我踩过的几个坑

做网络驱动,有些坑是共通的。我把自己踩过的列出来,你遇到了可以少走弯路:

  • PHY 复位时序:有些 PHY 芯片复位后需要等待几毫秒才能访问。我遇到过一上电就读 PHY ID 返回 0xFFFF,后来加了 delay 就好了。
  • MAC 地址未设置:很多网卡驱动在 start 里没有写 MAC 地址,导致发送的包源 MAC 全是 0。交换机直接丢弃。记得在 start 里调用 ops->write_hwaddr()。
  • DMA 对齐问题:有些 DMA 控制器要求缓冲区地址 4 字节对齐。如果上层传下来的包不对齐,需要拷贝。我建议在驱动里统一用 cache-aligned 的缓冲区。
  • MDIO 时序:访问 PHY 的 MDIO 总线,时钟频率不能太高。我见过有人把 MDC 配到 25MHz,结果 PHY 完全不响应。降到 2.5MHz 就好了。

2.7 小结

U-Boot 的网络驱动模型,说白了就是“上层定接口,下层做实现”。net/eth.c 是中间的调度层,它不关心你用的是 SPI 接口的 enc28j60 还是 GMII 接口的 RTL8211,它只负责把上层的调用转发给正确的驱动函数。

理解了这个模型,你再看任何网卡驱动代码,都会觉得“哦,原来都是套路”。下一层一层往下拆,你会发现每个驱动都在做同样的事:初始化硬件、实现 eth_ops、注册到 DM 框架。