4. 网络接口层移植:netif结构体初始化、底层数据收发函数实现、链路层地址解析

好,咱们进入移植工作的核心地带——网络接口层。说白了,这一层就是让协议栈跟你的硬件网卡“握手”。你想想看,TCP/IP 协议栈再牛,数据也得从网线或者天线里进出吧?这个进出的大门,就是 netif 结构体。

我刚开始做移植时,以为这步就是填几个函数指针。结果呢?板子 ping 不通,查了三天,发现是 netif 的 input 函数注册错了地方。嗯,这里面的坑,咱们一个个填。

4.1 netif 结构体:协议栈的“网卡身份证”

在 lwIP 里,每个网络接口都对应一个 struct netif。它记录了网卡的所有关键信息。我个人习惯把它叫做“网卡身份证”。

先看核心字段:

字段名 类型 作用
ip_addr ip4_addr_t 本机 IPv4 地址
netmask ip4_addr_t 子网掩码
gw ip4_addr_t 默认网关
hwaddr unsigned char[] MAC 地址(6字节)
mtu u16_t 最大传输单元
flags u8_t 接口状态标志(如 NETIF_FLAG_UP)
output 函数指针 IP 层调用,发送数据
linkoutput 函数指针 底层直接发送原始数据帧
input 函数指针 网卡收到数据后,交给协议栈的入口
state void * 指向底层硬件描述结构,由你定义

关键点: output 和 linkoutput 的区别。output 是 IP 层调用的,它会先查路由、做分片,再调用 linkoutput。而 linkoutput 是直接发原始帧的。如果你的网卡不支持硬件校验和,记得在 netif 的 flags 里设置 NETIF_FLAG_HWCHECKSUM。

4.2 初始化流程:从零到一

初始化 netif 的代码,我建议放在一个专门的函数里,比如 netif_eth_init()。别偷懒写在 main 里,后面维护起来你会哭的。

static struct netif g_netif;
static struct eth_device g_eth_dev; // 你的硬件描述结构

err_t netif_eth_init(struct netif *netif)
{
    // 1. 设置 MAC 地址
    netif->hwaddr_len = 6;
    netif->hwaddr[0] = 0x00;
    netif->hwaddr[1] = 0x11;
    netif->hwaddr[2] = 0x22;
    netif->hwaddr[3] = 0x33;
    netif->hwaddr[4] = 0x44;
    netif->hwaddr[5] = 0x55;

    // 2. 设置 MTU,以太网通常是 1500
    netif->mtu = 1500;

    // 3. 注册底层函数
    netif->output = etharp_output;    // lwIP 自带 ARP 封装
    netif->linkoutput = low_level_output; // 你的硬件发送函数

    // 4. 关联硬件设备
    netif->state = &g_eth_dev;

    // 5. 设置接口标志
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;

    // 6. 初始化硬件
    eth_device_init(&g_eth_dev);

    return ERR_OK;
}

我的习惯: 在 netif->state 里放一个自定义结构,包含网卡寄存器基地址、中断号、信号量等。这样底层函数里直接 struct eth_device *dev = netif->state; 就能拿到所有硬件信息,清爽得很。

4.3 底层数据收发:硬碰硬的活儿

收发函数是移植里最“接地气”的部分。你得直接操作 DMA 描述符、FIFO 或者 SPI 寄存器。我做过一个项目,网卡芯片的 DMA 描述符是 16 字节对齐的,我忘了,结果数据一直错位。

4.3.1 发送函数:low_level_output

这个函数要做的事很明确:把 lwIP 给你的 struct pbuf *p 里的数据,一股脑塞进网卡的发送 FIFO 或 DMA 链里。

static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
    struct eth_device *dev = netif->state;
    struct pbuf *q;
    u8_t *buf_ptr;
    u32_t len = 0;

    // 1. 计算总长度
    for (q = p; q != NULL; q = q->next) {
        len += q->len;
    }

    // 2. 从 pbuf 链中拷贝数据到发送缓冲区
    buf_ptr = dev->tx_buffer;
    for (q = p; q != NULL; q = q->next) {
        MEMCPY(buf_ptr, q->payload, q->len);
        buf_ptr += q->len;
    }

    // 3. 触发硬件发送
    eth_send_frame(dev, dev->tx_buffer, len);

    return ERR_OK;
}

我曾经踩过的坑: pbuf 是链表结构,不能直接 memcpy 整个 p->payload。一定要遍历链表。另外,如果网卡要求 4 字节对齐,记得在分配 pbuf 时用 PBUF_POOL 或 PBUF_RAM 并注意对齐属性。

4.3.2 接收函数:中断处理与 input 回调

接收通常走中断。网卡收到数据后,触发中断,你在中断服务程序里把数据从硬件 FIFO 搬到 pbuf 里,然后调用 netif->input() 交给协议栈。

void eth_isr_handler(void)
{
    struct netif *netif = &g_netif;
    struct eth_device *dev = netif->state;
    struct pbuf *p;
    u32_t len;

    // 1. 检查中断源,确认是接收事件
    if (eth_get_int_status(dev) & ETH_INT_RX) {
        // 2. 获取接收数据长度
        len = eth_get_rx_len(dev);

        // 3. 分配 pbuf
        p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
        if (p != NULL) {
            // 4. 从硬件读取数据到 pbuf
            eth_read_frame(dev, p->payload, len);
            // 5. 交给协议栈
            netif->input(p, netif);
        } else {
            // 内存不足,丢弃数据
            eth_drop_frame(dev);
        }
    }
}

这里有个细节:netif->input 在 lwIP 里默认是 ethernet_input,它会解析以太网头,根据 EtherType 分发给 ARP 或 IP 模块。你不需要自己写。

4.4 链路层地址解析:ARP 的幕后工作

ARP 说白了就是“我知道你 IP,告诉我你 MAC”。在移植中,你基本不需要自己实现 ARP 协议——lwIP 的 etharp_output 已经帮你搞定了。但你要确保底层能正确处理 ARP 请求和应答。

具体来说,etharp_output 会做这几件事:

  1. 检查目标 IP 是否在同一子网。不在的话,用网关 IP 代替。
  2. 查 ARP 缓存表。有记录就直接发。
  3. 没有记录?广播 ARP 请求,然后把数据包排队等待。
  4. 收到 ARP 应答后,更新缓存,发送排队的包。

你只需要确保你的 low_level_output 能发送广播帧(目标 MAC 全 F),并且接收中断能正确处理 ARP 包(EtherType = 0x0806)。

避坑指南: 我曾经在一个项目里发现 ARP 总是超时。查了半天,原来是网卡驱动在发送广播帧时,把目标 MAC 地址填错了。硬件手册上写的是“广播地址为全 1”,我写成了全 0。嗯,低级错误,但调了整整一天。

4.5 本章知识体系

下面这张图,把 netif 初始化、收发、ARP 解析的流程串起来了。你可以把它当作移植时的检查清单。

网络接口层移植核心流程 1. netif 结构体初始化 2. 注册 output / linkoutput / input 3. 发送:low_level_output 遍历 pbuf 链表 → 拷贝到 TX FIFO 3. 接收:中断 + input 分配 pbuf → 从 RX FIFO 读取 → 调用 input 4. ARP 解析(etharp_output 自动处理) 确保支持广播帧发送 正确处理 EtherType=0x0806

这张图从左到右、从上到下,把初始化、注册、收发、ARP 串了一遍。你移植时,按这个顺序检查,基本不会漏。

最后说一句: 网络接口层移植,最忌讳的就是“想当然”。每个硬件都有自己的脾气——DMA 对齐、中断优先级、FIFO 深度。我建议你先把网卡的裸机驱动调通,能收能发了,再跟 lwIP 对接。分步验证,比一口气写完再 debug 要快得多。