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 会做这几件事:
- 检查目标 IP 是否在同一子网。不在的话,用网关 IP 代替。
- 查 ARP 缓存表。有记录就直接发。
- 没有记录?广播 ARP 请求,然后把数据包排队等待。
- 收到 ARP 应答后,更新缓存,发送排队的包。
你只需要确保你的 low_level_output 能发送广播帧(目标 MAC 全 F),并且接收中断能正确处理 ARP 包(EtherType = 0x0806)。
避坑指南: 我曾经在一个项目里发现 ARP 总是超时。查了半天,原来是网卡驱动在发送广播帧时,把目标 MAC 地址填错了。硬件手册上写的是“广播地址为全 1”,我写成了全 0。嗯,低级错误,但调了整整一天。
4.5 本章知识体系
下面这张图,把 netif 初始化、收发、ARP 解析的流程串起来了。你可以把它当作移植时的检查清单。
这张图从左到右、从上到下,把初始化、注册、收发、ARP 串了一遍。你移植时,按这个顺序检查,基本不会漏。
最后说一句: 网络接口层移植,最忌讳的就是“想当然”。每个硬件都有自己的脾气——DMA 对齐、中断优先级、FIFO 深度。我建议你先把网卡的裸机驱动调通,能收能发了,再跟 lwIP 对接。分步验证,比一口气写完再 debug 要快得多。