3、LwIP协议栈移植(上):LwIP源码结构解析、操作系统抽象层(sys_arch)移植、网卡驱动接口实现

好,咱们今天正式开始啃LwIP移植这块硬骨头。说实话,很多工程师一提到协议栈移植就头大,觉得那是底层大牛才干的事。其实不然,你只要把LwIP的骨架摸清楚,移植就是个按部就班的体力活。

我个人习惯把LwIP移植拆成三个核心步骤:源码结构梳理 → 操作系统抽象层适配 → 网卡驱动对接。今天这上半部分,咱们先把前两步讲透,顺便把驱动接口的框架搭起来。

3.1 LwIP源码结构解析——别被文件夹吓到

我第一次打开LwIP源码包时,说实话也有点懵。一堆文件夹,几百个文件,从哪下手?后来我总结了一个规律:你只需要关注三个目录

目录 作用 移植时改不改?
src/core/ 协议栈核心,TCP/IP、UDP、ICMP等协议实现 不动,这是灵魂
src/netif/ 网卡接口层,包含ethernetif.c模板 要改,这是你的网卡驱动
src/include/lwip/ 头文件,各种配置宏定义 要改,主要是opt.h和lwipopts.h

嗯,这里要注意:src/arch/目录是空的,需要你自己创建。这个目录放的就是操作系统抽象层——sys_arch的实现。说白了,LwIP本身不依赖任何OS,但它留了一堆钩子函数,让你把OS的线程、信号量、邮箱这些东西挂上去。

核心原则:LwIP的core层你一个字都不要改。所有平台相关的东西,都通过宏定义和回调函数来隔离。这是嵌入式软件设计的精髓——分层隔离。

3.2 操作系统抽象层(sys_arch)移植——让LwIP跑在RTOS上

为什么需要sys_arch?你想想看,LwIP内部需要创建线程(比如tcpip_thread)、需要同步机制(比如信号量保护临界区)、需要消息传递(比如邮箱传递网络事件)。这些在裸机上没法直接做,必须借助RTOS。

sys_arch说白了就是一张适配表,把LwIP需要的OS服务,映射到你实际用的RTOS API上。我以FreeRTOS为例,给你看看核心的几个接口怎么实现。

3.2.1 信号量(Semaphore)实现

LwIP内部大量使用信号量来做同步。比如网卡收到数据后,通过信号量唤醒tcpip_thread来处理。

// sys_arch.c 中的信号量实现
#include "FreeRTOS.h"
#include "semphr.h"
#include "arch/sys_arch.h"

// 创建信号量
err_t sys_sem_new(sys_sem_t *sem, u8_t count) {
    // 我习惯用二值信号量,初始值为0
    *sem = xSemaphoreCreateBinary();
    if (*sem == NULL) {
        return ERR_MEM;
    }
    // 如果count不为0,释放一次
    if (count > 0) {
        xSemaphoreGive(*sem);
    }
    return ERR_OK;
}

// 释放信号量
void sys_sem_signal(sys_sem_t *sem) {
    xSemaphoreGive(*sem);
}

// 等待信号量,带超时
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout) {
    TickType_t ticks;
    BaseType_t ret;

    if (timeout == 0) {
        // 永久等待
        ret = xSemaphoreTake(*sem, portMAX_DELAY);
        return SYS_ARCH_TIMEOUT;  // 实际上不会超时
    } else {
        // 毫秒转Tick
        ticks = pdMS_TO_TICKS(timeout);
        ret = xSemaphoreTake(*sem, ticks);
        if (ret == pdTRUE) {
            return 0;  // 成功,返回等待时间(简化处理)
        } else {
            return SYS_ARCH_TIMEOUT;
        }
    }
}

我的经验:信号量超时处理是个坑。很多新手直接把timeout参数忽略,全部用永久等待。这在网络负载高的时候,会导致tcpip_thread被长时间阻塞,其他任务饿死。我建议至少给tcpip_thread的邮箱等待设置一个100ms的超时。

3.2.2 邮箱(Mailbox)实现

邮箱是LwIP消息传递的核心机制。tcpip_thread通过邮箱接收来自各个协议层和网卡的消息。在FreeRTOS里,我一般用队列来实现。

// 邮箱实现
err_t sys_mbox_new(sys_mbox_t *mbox, int size) {
    // 队列长度建议设置为:TCP_MSS * 2 + 一些余量
    *mbox = xQueueCreate(size, sizeof(void *));
    if (*mbox == NULL) {
        return ERR_MEM;
    }
    return ERR_OK;
}

void sys_mbox_free(sys_mbox_t *mbox) {
    vQueueDelete(*mbox);
}

void sys_mbox_post(sys_mbox_t *mbox, void *msg) {
    // 这个函数不能在中断中调用
    xQueueSend(*mbox, &msg, portMAX_DELAY);
}

u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout) {
    TickType_t ticks;
    BaseType_t ret;

    if (timeout == 0) {
        ret = xQueueReceive(*mbox, msg, portMAX_DELAY);
    } else {
        ticks = pdMS_TO_TICKS(timeout);
        ret = xQueueReceive(*mbox, msg, ticks);
        if (ret != pdTRUE) {
            return SYS_ARCH_TIMEOUT;
        }
    }
    return 0;
}

曾经踩过的坑:邮箱队列长度设置太小。有一次我在一个项目里把邮箱队列设成了4,结果网络一忙,消息堆积,tcpip_thread处理不过来,直接导致协议栈死锁。后来我改成TCP_MSS * 2,再也没出过问题。记住:邮箱队列宁可大一点,不要省那几百字节的RAM

3.2.3 线程创建与临界区保护

LwIP需要创建至少一个线程——tcpip_thread。如果你用了PPP或DHCP,可能还需要额外的线程。

// 创建线程
sys_thread_t sys_thread_new(const char *name, 
                            lwip_thread_fn thread, 
                            void *arg, 
                            int stacksize, 
                            int prio) {
    TaskHandle_t handle = NULL;
    
    // 注意:FreeRTOS的优先级是数值越大优先级越高
    // LwIP的优先级定义可能相反,需要做映射
    UBaseType_t freertos_prio = LWIP_TO_FREERTOS_PRIO(prio);
    
    xTaskCreate(thread, name, stacksize, arg, freertos_prio, &handle);
    return (sys_thread_t)handle;
}

// 临界区保护
sys_prot_t sys_arch_protect(void) {
    // 对于单核MCU,关中断即可
    taskENTER_CRITICAL();
    return 0;  // 返回值用于恢复,这里简化处理
}

void sys_arch_unprotect(sys_prot_t pval) {
    taskEXIT_CRITICAL();
}

3.3 网卡驱动接口实现——让数据流动起来

网卡驱动是LwIP和物理硬件的桥梁。LwIP定义了一个netif结构体,你需要实现它的outputinputlinkoutput等回调函数。

3.3.1 netif结构体初始化

// 网卡初始化函数
err_t ethernetif_init(struct netif *netif) {
    // 设置网卡硬件地址(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;
    
    // 设置最大传输单元
    netif->mtu = 1500;
    
    // 注册回调函数
    netif->output = etharp_output;    // IP层发包,走ARP
    netif->linkoutput = low_level_output;  // 底层实际发送
    
    // 设置网卡状态
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
    
    // 初始化底层硬件
    low_level_init(netif);
    
    return ERR_OK;
}

3.3.2 数据发送与接收

发送函数相对简单:把pbuf链中的数据,通过DMA或轮询方式写入网卡寄存器。

static err_t low_level_output(struct netif *netif, struct pbuf *p) {
    struct pbuf *q;
    u8_t *buffer;
    int len = 0;
    
    // 计算总长度
    for (q = p; q != NULL; q = q->next) {
        len += q->len;
    }
    
    // 分配发送缓冲区(从网卡硬件获取)
    buffer = get_tx_buffer();
    if (buffer == NULL) {
        return ERR_BUF;
    }
    
    // 将pbuf链拷贝到连续缓冲区
    len = 0;
    for (q = p; q != NULL; q = q->next) {
        MEMCPY(buffer + len, q->payload, q->len);
        len += q->len;
    }
    
    // 启动DMA发送
    start_tx_dma(buffer, len);
    
    return ERR_OK;
}

接收函数稍微复杂一点。它通常在中端服务程序中被调用,或者由轮询线程调用。

// 在网卡接收中断中调用
void ethernetif_input(struct netif *netif) {
    struct pbuf *p;
    u8_t *rx_buffer;
    int len;
    
    // 检查是否有新数据
    if (!has_rx_data()) {
        return;
    }
    
    // 获取接收缓冲区
    rx_buffer = get_rx_buffer();
    len = get_rx_length();
    
    // 分配pbuf,注意使用PBUF_RAM类型
    p = pbuf_alloc(PBUF_RAW, len, PBUF_RAM);
    if (p != NULL) {
        // 拷贝数据到pbuf
        pbuf_take(p, rx_buffer, len);
        
        // 将数据交给LwIP协议栈处理
        if (netif->input(p, netif) != ERR_OK) {
            pbuf_free(p);
        }
    }
    
    // 释放硬件接收缓冲区
    free_rx_buffer();
}

关键点:接收中断中不要做太多事情。我见过有人直接在中断里调用netif->input,结果中断服务时间过长,导致其他中断丢失。正确的做法是:中断里只做数据拷贝和标志位设置,然后通过信号量或消息通知tcpip_thread来处理

3.4 移植检查清单

好了,理论讲完了。我给你列个清单,移植的时候对着检查,能省不少调试时间。

检查项 说明 常见问题
sys_arch信号量 创建、释放、等待(带超时) 超时参数被忽略,导致死等
sys_arch邮箱 创建、发送、接收(带超时) 队列长度太小,导致消息丢失
sys_arch线程 创建tcpip_thread,栈空间足够 栈溢出,表现为随机死机
网卡发送 pbuf链转连续缓冲区,DMA发送 pbuf释放时机不对,内存泄漏
网卡接收 中断中拷贝数据,通知协议栈 中断中调用协议栈函数,导致重入
lwipopts.h配置 内存池大小、超时时间、调试开关 MEM_SIZE太小,分配失败

嗯,今天就先到这儿。下半部分我们会深入调试技巧、性能调优,以及一些高级特性(比如零拷贝、checksum卸载)的移植方法。到时候我会分享一个我实际项目中遇到的性能瓶颈案例——那个问题折腾了我整整三天,最后发现是一个宏定义没开。

记住:LwIP移植不是写代码,而是搭积木。你把sys_arch和网卡驱动这两块积木搭好了,剩下的就是配置和调试了。下节课见。