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,你会发现它其实不大。核心函数就那么几个,但每个都承担着关键职责。我把它梳理成了一张图,你先看个全貌:
这张图你看懂了吗?上层协议(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 启动时,网卡是怎么被发现的?流程大致如下:
- 设备树解析:U-Boot 读取设备树(.dts),找到所有 compatible 匹配的以太网节点。
- 驱动绑定:DM 框架根据 compatible 字符串,找到对应的
U_BOOT_DRIVER,调用probe函数。 - probe 函数:驱动在这里分配私有数据、映射寄存器、复位 PHY。
- eth_init 调用:当上层协议需要网络时,调用
eth_init(),它遍历所有已 probe 的网卡,调用eth_ops->start()。 - 设置当前设备:如果有多个网卡,通过
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 框架。