3. 网络接口与链路层:网络接口管理、以太网驱动框架、MAC地址配置、链路层调试

好,咱们进入第三章。这一章讲的是网络协议栈最底层的东西——网络接口和链路层。说白了,就是数据从芯片管脚出去之前,最后一道关卡。很多性能问题,追根溯源都卡在这一层。我这些年调过的TCP/IP性能问题,至少有三成是链路层配置不当导致的。

3.1 网络接口管理:从iface说起

在Zephyr里,每个网络接口对应一个 struct net_if 对象。你可以把它理解成操作系统里的“网卡抽象”。不管底层是以太网、Wi-Fi还是LoRa,上层都通过这个结构体来操作。

我个人习惯,在项目初始化阶段,第一件事就是打印出所有网络接口的信息。你想想看,如果接口都没起来,后面调传输层就是白费功夫。

核心数据结构:

struct net_if {
    struct net_if_config config;      // 接口配置(IP、MAC等)
    const struct net_if_api *if_api;  // 驱动API接口
    struct net_linkaddr linkaddr;     // 链路层地址(MAC)
    struct net_if_stats stats;        // 接口统计信息
    ...
};

接口的生命周期管理,我归纳为三步:

  1. 注册:驱动初始化时调用 net_if_register(),将接口挂到全局链表。
  2. 启动:调用 net_if_up(),使接口进入工作状态。
  3. 监控:通过 net_if_stats 观察收发包计数、错误计数。

我的经验: 调试时多用 net_if_get_first_by_type() 获取接口指针。我曾经在一个多网口项目里,因为拿错了接口句柄,导致数据一直发不出去,查了两天才发现是iface索引搞混了。

3.2 以太网驱动框架:Zephyr怎么管网卡

Zephyr的以太网驱动框架,设计得挺巧妙。它没有让驱动开发者从头造轮子,而是提供了一套标准模板。你只需要填充几个关键函数指针就行。

驱动框架的核心是 struct eth_driver_api:

struct eth_driver_api {
    int (*iface_init)(struct net_if *iface);
    int (*send)(struct net_if *iface, struct net_pkt *pkt);
    int (*recv)(struct net_if *iface, struct net_pkt *pkt);
    int (*set_config)(struct net_if *iface, enum eth_config_type type,
                      const struct ethernet_config *config);
};

这里我重点说两个函数:

  • send:发送函数。Zephyr把数据封装在 struct net_pkt 里,驱动要做的就是把这个pkt里的数据搬进DMA描述符,然后触发发送。
  • recv:接收函数。通常是中断触发,驱动从DMA缓冲区把数据取出来,塞进一个 net_pkt,然后交给协议栈。

避坑指南: 我曾经在移植一个新网卡驱动时,发现发送速度上不去。后来定位到是 send 函数里每次都要等待上一次发送完成才返回。改成支持多描述符的环形队列后,吞吐量直接翻倍。记住:驱动层不要做不必要的同步等待。

3.3 MAC地址配置:别小看这6个字节

MAC地址配置,看起来简单,但坑不少。Zephyr支持三种方式:

方式 配置方法 适用场景
硬件内置 驱动从芯片寄存器读取 大多数以太网控制器
设备树指定 在dts中设置 local-mac-address 开发板调试、固定MAC
运行时生成 调用 net_if_set_linkaddr() 需要动态分配MAC的场景

我个人推荐的做法是:优先使用硬件内置MAC。如果硬件没有(比如某些USB网卡),就在设备树里写死。运行时生成只适合原型验证,因为随机MAC会导致网络冲突。

设备树配置示例:

ð0 {
    compatible = "st,stm32-ethernet";
    reg = <0x40028000 0x400>;
    local-mac-address = [00 80 E1 00 00 01];
    phy-handle = <&phy0>;
};

嗯,这里要注意:MAC地址的字节序。Zephyr内部存储是网络字节序(大端),但有些驱动在写入硬件寄存器时需要转换。我见过一个案例,因为字节序没处理好,导致MAC地址变成了“乱码”,网络包根本发不出去。

3.4 链路层调试:三板斧

链路层出问题,表现往往是“能ping通但丢包严重”或者“完全不通”。我总结了三板斧:

第一板斧:检查链路状态

用 net_if_is_operational() 判断接口是否真正“在线”。有些驱动在 iface_init 里只做了寄存器初始化,但PHY还没完成自协商。这时候接口状态是 NET_IF_OPER_DOWN。

if (!net_if_is_operational(iface)) {
    printk("Interface is not operational!\n");
    // 检查PHY状态,等待自协商完成
}

第二板斧:抓原始包

Zephyr内置了 net_pkt 的打印功能。在驱动层加打印,看数据是否真的从DMA送出去了:

static int eth_send(struct net_if *iface, struct net_pkt *pkt) {
    // 打印前64字节
    net_hexdump("eth send", pkt->buffer->data, 
                min(64, net_pkt_get_len(pkt)));
    // ... 实际发送逻辑
}

我的习惯: 在驱动层加一个条件编译开关 CONFIG_ETH_DEBUG,平时关掉,调试时打开。这样不会影响性能。

第三板斧:看统计计数器

struct net_if_stats 里有很多有用信息:

  • pkts_tx / pkts_rx:收发总包数
  • bytes_tx / bytes_rx:收发总字节数
  • errors_tx / errors_rx:错误计数
  • dropped_tx / dropped_rx:丢包计数

如果 errors_tx 持续增长,说明物理层有问题——可能是网线接触不良、PHY配置不对。如果 dropped_rx 很高,说明接收缓冲区不够或者中断处理太慢。

3.5 链路层知识体系

我把这一章的核心逻辑画成了图,方便你理解各模块之间的关系:

Zephyr 网络接口与链路层架构 网络协议栈(IP/TCP/UDP) struct net_if(网络接口管理层) 接口注册 / 启动 / 监控 / 统计 struct eth_driver_api(以太网驱动框架) iface_init | send | recv | set_config MAC地址管理 硬件内置 / 设备树 / 运行时配置 链路层调试 状态检查 / 原始包抓取 / 统计计数 硬件层(以太网控制器 / PHY / DMA) 链路层是协议栈与硬件的桥梁,调试时从底层向上逐层排查

这张图把链路层的几个关键模块串起来了。从上往下看:协议栈调用 net_if 接口,net_if 调用驱动框架,驱动框架再操作硬件。调试的时候,我习惯从最底层开始查——先确认硬件有没有收到包,再看驱动有没有把包交给协议栈。

总结一下本章要点:

  • 网络接口管理:掌握 net_if 的注册、启动、监控流程
  • 以太网驱动框架:理解 eth_driver_api 四个核心函数
  • MAC地址配置:优先硬件内置,其次设备树,避免运行时随机生成
  • 链路层调试:状态检查、原始包抓取、统计计数器,三板斧够用

嗯,链路层的内容就这些。下一层一层往上走,后面我们会聊到IP层和传输层的调优。但记住:基础不牢,地动山摇。链路层没调好,上层再优化也是白搭。