4. lwIP移植接口详解:sys_arch层、netif与OSAL适配

好,咱们进入正题。lwIP 的移植,说白了就是给它配三样东西:操作系统抽象层(sys_arch)、底层网络接口(netif),以及操作系统抽象层(OSAL)的适配。这三样搞定了,lwIP 就能在你的硬件上跑起来。

我个人习惯把这三层比作「三驾马车」。缺了哪一匹,协议栈都跑不稳。今天咱们就一匹一匹来驯。

核心观点:移植不是重写,是填空。lwIP 已经给你画好了框,你只需要把框里的函数填对。

4.1 sys_arch 层:操作系统的「翻译官」

sys_arch 层是 lwIP 和 RTOS 之间的桥梁。lwIP 本身不依赖任何特定操作系统,它通过 sys_arch 接口来调用操作系统的信号量、邮箱、互斥锁、线程创建等功能。

说白了,你用的 FreeRTOS、RT-Thread、uC/OS,都得通过这层「翻译」给 lwIP 用。

4.1.1 必须实现的接口

我列一下最核心的几个接口,你在移植时一个都不能少:

接口函数 功能描述 典型实现
sys_sem_new 创建信号量 调用 RTOS 的 semaphore create API
sys_sem_signal 释放信号量 调用 RTOS 的 semaphore give API
sys_arch_sem_wait 等待信号量(带超时) 调用 RTOS 的 semaphore take 并处理超时
sys_mbox_new 创建邮箱 用 RTOS 的消息队列实现
sys_mbox_post 发送消息(非阻塞) 调用 RTOS 的 queue send
sys_arch_mbox_fetch 接收消息(带超时) 调用 RTOS 的 queue receive
sys_thread_new 创建线程 调用 RTOS 的 task create
sys_prot / sys_unprot 临界区保护 关/开中断或使用互斥锁
我的经验:sys_arch_sem_wait 的超时处理是坑最多的。很多 RTOS 的 semaphore take 不支持 timeout,你得自己用定时器+信号量组合实现。我曾经在这上面折腾了两天,最后发现 lwIP 的 sys_arch_timeouts 机制可以帮你省不少事。

4.1.2 一个典型的 sys_arch.c 片段

以 FreeRTOS 为例,信号量创建的实现大概是这样的:

// sys_arch.c - FreeRTOS 实现示例
#include "lwip/sys.h"
#include "FreeRTOS.h"
#include "semphr.h"

err_t sys_sem_new(sys_sem_t *sem, u8_t count) {
    // 创建二值信号量
    *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 = timeout / portTICK_PERIOD_MS;
        ret = xSemaphoreTake(*sem, ticks);
        if (ret == pdTRUE) {
            // 返回实际等待时间(毫秒)
            return timeout - (ticks * portTICK_PERIOD_MS);
        } else {
            return SYS_ARCH_TIMEOUT;
        }
    }
}
注意:邮箱的实现不要直接用 RTOS 的队列,因为 lwIP 的邮箱允许 NULL 消息(表示关闭),而很多 RTOS 队列不允许发送 NULL 指针。我建议用信号量+环形缓冲区自己封装一个。

4.2 底层网络接口(netif):让协议栈「看见」硬件

netif 层是 lwIP 和网卡驱动之间的接口。每个网络接口(以太网、Wi-Fi、PPP)都需要注册一个 struct netif 结构体,并提供 input 和 output 函数。

4.2.1 netif 结构体核心字段

字段 说明 你该填什么
ip_addr IP 地址 静态配置或 DHCP 获取
netmask 子网掩码 根据网络环境设置
gw 默认网关 路由器地址
output IP 层发送回调 指向你的发送函数
linkoutput 链路层发送回调 指向你的 MAC 发送函数
input 接收数据入口 驱动收到数据后调用
state 私有数据指针 指向你的驱动结构体

4.2.2 netif_add 注册流程

你想想看,网卡驱动初始化完成后,需要调用 netif_add 把接口注册到 lwIP。流程是这样的:

  1. 分配并初始化 struct netif 结构体
  2. 调用 netif_add 注册,传入 init 回调
  3. 在 init 回调中设置 output 和 linkoutput 函数指针
  4. 调用 netif_set_default 设为默认接口(如果只有一个网口)
  5. 调用 netif_set_up 使能接口
// 网卡初始化函数示例
static err_t ethernetif_init(struct netif *netif) {
    // 设置输出函数
    netif->output = etharp_output;      // 使用 ARP 封装
    netif->linkoutput = low_level_output; // 你的 MAC 发送函数

    // 设置 MAC 地址
    netif->hwaddr_len = 6;
    memcpy(netif->hwaddr, my_mac_addr, 6);

    // 设置 MTU
    netif->mtu = 1500;

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

    return ERR_OK;
}

// 注册接口
struct netif my_netif;
ip4_addr_t ipaddr, netmask, gw;
IP4_ADDR(&ipaddr, 192, 168, 1, 100);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);

netif_add(&my_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input);
netif_set_default(&my_netif);
netif_set_up(&my_netif);
关键点:output 和 linkoutput 的区别。output 是 IP 层调用的,会经过 ARP 封装;linkoutput 是 ARP 层调用的,直接发原始数据帧。如果你用 NO_SYS=1 模式,output 可以直接指向 etharp_output。

4.3 操作系统抽象层(OSAL):让 lwIP 融入你的系统

OSAL 是比 sys_arch 更高一层的抽象。它定义了 lwIP 如何与操作系统交互,包括:

  • 线程模型:tcpip_thread 主线程、ethernet_input 接收线程
  • 同步机制:信号量、邮箱、互斥锁的封装
  • 内存管理:lwIP 内部的内存池和堆管理
  • 超时机制:sys_timeout 和 sys_untimeout

4.3.1 两种运行模式

模式 宏定义 适用场景 特点
无操作系统 NO_SYS=1 裸机、简单 MCU 所有代码在同一个上下文运行,需要轮询
有操作系统 NO_SYS=0 RTOS、Linux 多线程运行,tcpip_thread 处理协议栈

我个人建议,只要你的项目用了 RTOS,就老老实实把 NO_SYS 设为 0。为什么?因为多线程模式下,tcpip_thread 可以独立处理网络事件,不会阻塞你的应用任务。我在一个项目里试过 NO_SYS 模式,结果主循环里既要处理传感器数据又要轮询网络,最后网络延迟高得离谱。

4.3.2 tcpip_thread 的启动

在 NO_SYS=0 模式下,你需要调用 tcpip_init 来启动协议栈线程:

// main.c
#include "lwip/tcpip.h"

void my_network_init(void) {
    // 初始化 tcpip 线程
    tcpip_init(NULL, NULL);

    // 等待 tcpip 线程就绪(可以用信号量同步)
    // ...

    // 添加网络接口
    netif_add(&my_netif, ...);
    netif_set_default(&my_netif);
    netif_set_up(&my_netif);
}
避坑指南:我曾经在 tcpip_init 之后立即调用 netif_add,结果发现 netif 的 init 回调在 tcpip_thread 里执行,而我的 init 回调里用了一个还没初始化的硬件外设。解决方案是:在 tcpip_init 的回调参数里传递一个信号量,等 tcpip_thread 启动完成后再添加接口。

4.4 知识结构总览

下面这张图把三者的关系画清楚了。你对照着看,心里就有谱了:

lwIP 移植三层架构图 lwIP 移植三层架构 应用层(Socket API / Netconn API) lwIP 核心协议栈(TCP/IP 层) 移植接口层 sys_arch 接口 | netif 接口 | OSAL 适配 sys_arch 实现 信号量/邮箱/线程 netif 驱动 MAC 发送/接收 OSAL 适配 内存/超时/临界区 移植工作量分布 sys_arch 约 40% | netif 约 35% | OSAL 约 25%

4.5 移植检查清单

最后,我给你一份自检清单。每次移植完,对着打勾:

  • sys_sem_new/signal/wait 全部实现,超时处理正确
  • sys_mbox_new/post/fetch 实现,支持 NULL 消息
  • sys_thread_new 创建 tcpip_thread 成功
  • netif_add 注册成功,init 回调设置 output/linkoutput
  • 网卡中断接收调用 netif->input 或 tcpip_input
  • NO_SYS 宏定义与你的运行模式一致
  • MEM_SIZE / PBUF_POOL_SIZE 等内存配置合理
  • 临界区保护(sys_prot/sys_unprot)正确实现
最后提醒:移植完成后,先用 ping 测试基本连通性,再用 iperf 或类似工具测吞吐量。如果 ping 通但 iperf 性能差,多半是 sys_arch 层的信号量/邮箱实现效率低,或者 netif 的发送函数没有做 DMA 优化。别问我怎么知道的——我调了整整一周才发现是邮箱的 memcpy 太频繁了。