#lwIP 的 Raw API 使用指南

1. 简介

lwIP(Lightweight IP)是一个为嵌入式系统设计的开源轻量级 TCP/IP 协议栈。它旨在提供尽可能小的内存占用和高效的性能,适用于资源受限的设备,如物联网设备、路由器和工业控制系统。lwIP 支持多种协议,包括 IPv4、IPv6、TCP、UDP、ICMP 等,并提供多种 API 以适应不同的应用需求。

2. lwIP 提供的 API 接口

lwIP 提供三种应用程序接口(API)供程序使用,以与 TCP/IP 代码进行通信:

  1. 低级别 "核心" / "回调" 或 "Raw" API。
  2. 较高级别的 "顺序" API。
  3. 类似 BSD 的 Socket API。

2.1 顺序 API

顺序 API 为普通的、顺序执行的程序提供了一种使用 lwIP 栈的方式。它与 BSD socket API 非常相似。执行模型基于阻塞的打开-读取-写入-关闭范式。由于 TCP/IP 栈本质上是基于事件的,TCP/IP 代码和应用程序必须运行在不同的执行上下文(线程)中。

2.2 Socket API

Socket API 是一个兼容性 API,适用于现有的应用程序,目前构建在顺序 API 之上。它旨在为在其他平台(例如 Unix / Windows 等)上运行的 Socket API 应用程序提供所需的所有功能。然而,由于该 API 规范的限制,可能存在不兼容性,可能需要对现有程序进行小幅修改。

2.3 Raw API

Raw API 允许应用程序更好地与 TCP/IP 代码集成。通过在 TCP/IP 代码内部调用回调函数来实现事件驱动的程序执行。TCP/IP 代码和应用程序都在同一线程中运行。与顺序 API 相比,Raw API 在代码执行时间方面更快,内存占用也更少。但其缺点是编程开发较为复杂,使用 Raw API 编写的应用程序更难理解。然而,对于希望在代码大小和内存使用上保持小型的应用程序来说,这是首选方法。

3. 线程模型

lwIP 最初针对单线程环境设计。在添加多线程支持时,没有选择使核心线程安全,而是采用了另一种方法:运行 lwIP 核心的主线程(也称为 "tcpip_thread")。Raw API 只能在这个主线程中使用! 使用顺序 API 或 Socket API 的应用线程通过消息传递与主线程通信。

因此,仅有限的函数列表可以从其他线程或中断服务程序(ISR)中调用!只有以下 API 头文件中的函数是线程安全的:

  • api.h
  • netbuf.h
  • netdb.h
  • netifapi.h
  • sockets.h
  • sys.h

此外,内存(分配和释放)函数可以在多个线程中调用(但不能在 ISR 中),前提是 NO_SYS=0,因为它们受 SYS_LIGHTWEIGHT_PROT 和/或信号量的保护。

从版本 1.3.0 开始,如果设置了 SYS_LIGHTWEIGHT_PROT=1LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT=1pbuf_free() 也可以从另一个线程或 ISR 中调用(仅在 PBUF_RAM 使用时,mem_free 可以从 ISR 中调用;否则,堆仅通过信号量保护)。

4. Raw API 详解

Raw TCP/IP 接口允许应用程序更紧密地与 TCP/IP 代码集成。通过在 TCP/IP 代码内部调用回调函数实现事件驱动的程序执行。TCP/IP 代码和应用程序都在同一线程中运行。

4.1 回调机制

程序执行由回调驱动。每个回调都是一个普通的 C 函数,由 TCP/IP 代码内部调用。每个回调函数会传递当前 TCP 或 UDP 连接状态作为参数。此外,为了能够保持程序特定的状态,回调函数会传递一个由程序指定的、独立于 TCP/IP 状态的参数。

设置应用连接状态
c 复制代码
void tcp_arg(struct tcp_pcb *pcb, void *arg);
  • 功能:设置传递给所有其他回调函数的程序特定状态。
  • 参数
    • pcb:当前的 TCP 连接控制块。
    • arg:将传递给回调函数的参数。

4.2 TCP 连接设置

TCP 连接的设置函数类似于顺序 API 和 BSD Socket API。使用 tcp_new() 函数创建一个新的 TCP 连接标识符(即协议控制块 PCB)。然后,可以将其设置为监听新传入连接或显式连接到另一个主机。

创建新的 TCP PCB
c 复制代码
struct tcp_pcb *tcp_new(void);
  • 功能:创建一个新的连接标识符(PCB)。
  • 返回值 :如果没有可用内存,返回 NULL,否则返回新的 PCB 指针。
绑定 PCB 到本地地址和端口
c 复制代码
err_t tcp_bind(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port);
  • 功能 :将 PCB 绑定到本地 IP 地址和端口号。IP 地址可以设置为 IP_ADDR_ANY,以绑定到所有本地 IP 地址。
  • 返回值
    • ERR_USE:如果另一个连接已绑定到相同端口。
    • ERR_OK:绑定成功。
开始监听传入连接
c 复制代码
struct tcp_pcb *tcp_listen(struct tcp_pcb *pcb);
  • 功能 :将 PCB 设置为监听传入连接。当有传入连接被接受时,将调用通过 tcp_accept() 设置的回调函数。
  • 返回值 :返回一个新的连接标识符,用于监听连接。如果没有足够的内存,返回 NULL
  • 注意:会释放传入的 PCB 所占用的内存,并分配一个新的更小的 PCB 用于监听连接。
设置接受回调函数
c 复制代码
void tcp_accept(struct tcp_pcb *pcb, err_t (* accept)(void *arg, struct tcp_pcb *newpcb, err_t err));
  • 功能:设置当有新的连接到达时调用的回调函数。
  • 参数
    • pcb:监听的 PCB。
    • accept:回调函数,接收新连接时调用。
连接到远程主机
c 复制代码
err_t tcp_connect(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port, err_t (* connected)(void *arg, struct tcp_pcb *tpcb, err_t err));
  • 功能:设置 PCB 以连接到远程主机,并发送初始的 SYN 段以打开连接。
  • 返回值
    • ERR_OK:如果 SYN 段成功入队。
    • ERR_MEM:如果内存不足以入队 SYN 段。
  • 注意tcp_connect() 函数立即返回,不会等待连接建立。连接建立后,将调用传递的 connected 回调函数。如果连接无法建立,将调用通过 tcp_err() 设置的错误回调函数。

4.3 发送 TCP 数据

TCP 数据的发送通过调用 tcp_write() 将数据入队。当数据成功发送到远程主机后,应用程序将通过指定的回调函数接收到通知。

发送数据
c 复制代码
err_t tcp_write(struct tcp_pcb *pcb, void *dataptr, u16_t len, u8_t copy);
  • 功能 :将 dataptr 指向的数据入队,长度为 len 字节。copy 参数为 0 或 1,指示是否为数据分配新内存并复制。
    • copy = 1:分配新内存并复制数据。
    • copy = 0:不分配新内存,直接引用数据指针。
  • 返回值
    • ERR_MEM:如果数据长度超过当前发送缓冲区大小或发送队列超出 lwipopts.h 中定义的上限。
    • ERR_OK:数据成功入队。
  • 注意
    • 正确使用方法是调用 tcp_write() 时,最多传递 tcp_sndbuf() 字节的数据。
    • 如果返回 ERR_MEM,应用程序应等待部分已入队数据被远程主机确认后再重试。
设置已发送回调函数
c 复制代码
void tcp_sent(struct tcp_pcb *pcb, err_t (* sent)(void *arg, struct tcp_pcb *tpcb, u16_t len));
  • 功能:设置当数据被远程主机确认时调用的回调函数。
  • 参数
    • pcb:连接的 PCB。
    • sent:回调函数,接收确认的字节数。

4.4 接收 TCP 数据

TCP 数据的接收是基于回调的。当新数据到达时,将调用应用程序指定的回调函数。当应用程序处理完数据后,必须调用 tcp_recved() 函数,以便 TCP 可以增加接收窗口。

设置接收回调函数
c 复制代码
void tcp_recv(struct tcp_pcb *pcb, err_t (* recv)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err));
  • 功能 :设置当有新数据到达时调用的回调函数。如果远程主机关闭连接,将传递一个 NULL 的 pbuf。
  • 参数
    • pcb:连接的 PCB。
    • recv:回调函数,接收新数据时调用。
  • 注意
    • 如果没有错误且回调函数返回 ERR_OK,则必须释放 pbuf。
    • 否则,不能释放 pbuf,让 lwIP 核心代码保存它。
通知 lwIP 已接收数据
c 复制代码
void tcp_recved(struct tcp_pcb *pcb, u16_t len);
  • 功能:当应用程序接收到数据时调用,通知 lwIP 可以增加接收窗口。
  • 参数
    • pcb:连接的 PCB。
    • len:接收数据的长度。

4.5 应用程序轮询

当连接处于空闲状态(即没有数据传输或接收)时,lwIP 会通过调用指定的回调函数来轮询应用程序。这可以用作看门狗定时器,以终止长时间空闲的连接,或者作为等待内存变得可用的方法。例如,如果 tcp_write() 返回内存不足,应用程序可以使用轮询功能在连接空闲一段时间后再次尝试调用 tcp_write()

设置轮询回调函数
c 复制代码
void tcp_poll(struct tcp_pcb *pcb, err_t (* poll)(void *arg, struct tcp_pcb *tpcb), u8_t interval);
  • 功能:设置轮询间隔和回调函数,当连接空闲时调用回调函数。
  • 参数
    • pcb:连接的 PCB。
    • poll:回调函数,每隔 interval 个 TCP 粗粒度定时器周期调用一次。
    • interval:轮询间隔,以 TCP 粗粒度定时器周期数表示,通常每秒两次。比如,interval = 10 表示每 5 秒轮询一次。

4.6 关闭和中止连接

关闭连接
c 复制代码
err_t tcp_close(struct tcp_pcb *pcb);
  • 功能:关闭连接。
  • 返回值
    • ERR_OK:关闭成功。
    • ERR_MEM:内存不足,无法关闭连接。
  • 注意
    • 如果关闭成功,函数返回 ERR_OK,并且 PCB 将被自动释放。
    • 如果内存不足,应用程序应等待并通过确认回调或轮询功能重试。
中止连接
c 复制代码
void tcp_abort(struct tcp_pcb *pcb);
  • 功能:通过发送 RST(重置)段来中止连接,并释放 PCB。
  • 注意
    • 永远不会失败。
    • 在 TCP 回调中调用时,确保返回 ERR_ABRT,否则可能导致访问已释放的内存或内存泄漏。

4.7 设置错误回调函数

c 复制代码
void tcp_err(struct tcp_pcb *pcb, void (* err)(void *arg, err_t err));
  • 功能:设置当连接因错误而中止时调用的回调函数。
  • 参数
    • pcb:连接的 PCB。
    • err:回调函数,不会接收 PCB 作为参数,因为 PCB 可能已经被释放。

5. UDP 接口

UDP 接口类似于 TCP,但由于 UDP 的低复杂性,接口显著更简单。

5.1 创建 UDP PCB

c 复制代码
struct udp_pcb *udp_new(void);
  • 功能:创建一个新的 UDP PCB,用于 UDP 通信。
  • 返回值 :新的 UDP PCB 指针,若内存不足则返回 NULL

5.2 移除 UDP PCB

c 复制代码
void udp_remove(struct udp_pcb *pcb);
  • 功能:移除并释放 UDP PCB。

5.3 绑定 UDP PCB 到本地地址和端口

c 复制代码
err_t udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port);
  • 功能 :将 PCB 绑定到本地地址和端口。IP 地址可以设置为 IP_ADDR_ANY,表示绑定到所有本地 IP 地址。
  • 返回值 :当前总是返回 ERR_OK

5.4 连接 UDP PCB 到远程地址和端口

c 复制代码
err_t udp_connect(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port);
  • 功能:设置 PCB 的远程端。这不会产生任何网络流量,只是设置 PCB 的远程地址。

5.5 断开 UDP 连接

c 复制代码
err_t udp_disconnect(struct udp_pcb *pcb);
  • 功能:移除 PCB 的远程端。不会产生任何网络流量,只是移除 PCB 的远程地址。

5.6 发送 UDP 数据

c 复制代码
err_t udp_send(struct udp_pcb *pcb, struct pbuf *p);
  • 功能 :发送 pbuf p。不会释放 pbuf。

5.7 设置接收回调函数

c 复制代码
void udp_recv(struct udp_pcb *pcb, void (* recv)(void *arg, struct udp_pcb *upcb, struct pbuf *p, struct ip_addr *addr, u16_t port), void *recv_arg);
  • 功能:设置当收到 UDP 数据报时调用的回调函数。
  • 参数
    • pcb:连接的 PCB。
    • recv:回调函数,当收到数据报时调用。
    • recv_arg:传递给回调函数的参数。

6. 系统初始化

系统初始化的完整和通用序列取决于构建配置(lwipopts.h)及运行时环境的其他初始化(例如定时器)。以下是基于单一以太网接口、UDP 和 TCP 传输层、IPv4 和 DHCP 客户端的 Raw API 使用流程。

6.1 初始化步骤

按照以下顺序调用这些函数:

  1. 初始化统计信息

    c 复制代码
    stats_init();
    • 清除用于收集运行时统计信息的结构。
  2. 初始化系统

    c 复制代码
    sys_init();
    • 如果在 lwipopts.h 中设置了 NO_SYS=1,此函数用于简化配置更改。
  3. 初始化内存管理

    c 复制代码
    mem_init();
    • 初始化由 MEM_SIZE 定义的动态内存堆。
  4. 初始化内存池

    c 复制代码
    memp_init();
    • 初始化由 MEMP_NUM_x 定义的内存池。
  5. 初始化 pbuf 内存池

    c 复制代码
    pbuf_init();
    • 初始化由 PBUF_POOL_SIZE 定义的 pbuf 内存池。
  6. 初始化 ARP

    c 复制代码
    etharp_init();
    • 初始化 ARP 表和队列。
    • 注意 :必须每隔 ARP_TMR_INTERVAL(5 秒)调用 etharp_tmr()
  7. 初始化 IP 层

    c 复制代码
    ip_init();
    • 目前没有太大作用,但应在后续可能的更改时调用。
  8. 初始化 UDP

    c 复制代码
    udp_init();
    • 清空 UDP PCB 列表。
  9. 初始化 TCP

    c 复制代码
    tcp_init();
    • 清空 TCP PCB 列表并清除一些内部 TCP 定时器。
    • 注意 :必须在此初始化后,定期调用 tcp_fasttmr()tcp_slowtmr()
  10. 添加网络接口

    c 复制代码
    netif_add(struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask, struct ip_addr *gw, void *state, err_t (* init)(struct netif *netif), err_t (* input)(struct pbuf *p, struct netif *netif));
    • 将你的网络接口添加到 netif_list
    • 分配一个 struct netif 并传递指向该结构的指针作为第一个参数。
    • 如果使用 DHCP,传递已清零的 ip_addr 结构;否则,填充为合理的数值。
    • init 函数指针必须指向以太网网络接口的初始化函数。示例如下:
    c 复制代码
    err_t netif_if_init(struct netif *netif) {
        u8_t i;
        
        for(i = 0; i < ETHARP_HWADDR_LEN; i++) netif->hwaddr[i] = some_eth_addr[i];
        init_my_eth_device();
        return ERR_OK;
    }
    • 对于以太网驱动,输入函数指针必须指向 lwIP 的 ethernet_input() 函数(在 netif/etharp.h 中声明)。
    • 对于其他驱动,必须使用 ip_input() 函数(在 lwip/ip.h 中声明)。
  11. 设置默认网络接口

    c 复制代码
    netif_set_default(struct netif *netif);
    • 注册默认网络接口。
  12. 启用网络接口

    c 复制代码
    netif_set_up(struct netif *netif);
    • 当网络接口完全配置后,必须调用此函数。
  13. 启动 DHCP 客户端

    c 复制代码
    dhcp_start(struct netif *netif);
    • 为此接口创建一个新的 DHCP 客户端。
    • 注意 :必须之后定期调用 dhcp_fine_tmr()dhcp_coarse_tmr()

6.2 示例:初始化 lwIP 网络接口

c 复制代码
#include "lwip/netif.h"
#include "lwip/tcpip.h"
#include "lwip/dhcp.h"

/* 定义网络接口结构体 */
struct netif netif0;

/* 初始化网络接口 */
void lwip_network_init(void) {
    ip4_addr_t ipaddr, netmask, gw;

    /* 设置初始IP地址为0.0.0.0 */
    IP4_ADDR(&ipaddr, 0, 0, 0, 0);
    IP4_ADDR(&netmask, 0, 0, 0, 0);
    IP4_ADDR(&gw, 0, 0, 0, 0);

    /* 初始化TCP/IP堆栈 */
    tcpip_init(NULL, NULL);

    /* 添加网络接口 */
    netif_add(&netif0, &ipaddr, &netmask, &gw, NULL, low_level_init, tcpip_input);

    /* 设置为默认网络接口 */
    netif_set_default(&netif0);

    /* 启动DHCP客户端 */
    dhcp_start(&netif0);
}

7. lwIP API 使用

lwIP 提供了多种 API 以适应不同的应用层需求。以下主要介绍 Socket APIRaw API 的使用方法。

7.1 Socket API

Socket API 类似于 BSD Socket,适用于网络应用开发,如 HTTP 服务器、TCP 客户端等。

创建 TCP 服务器
c 复制代码
#include "lwip/sockets.h"

void vTCPServer(void *pvParameters) {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024];
    int n;

    /* 创建套接字 */
    listen_fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        /* 处理错误 */
    }

    /* 配置服务器地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /* 绑定套接字 */
    if (lwip_bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        /* 处理错误 */
    }

    /* 监听连接 */
    if (lwip_listen(listen_fd, 5) < 0) {
        /* 处理错误 */
    }

    /* 接受客户端连接 */
    while (1) {
        conn_fd = lwip_accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
        if (conn_fd < 0) {
            /* 处理错误 */
            continue;
        }

        /* 处理客户端请求 */
        while ((n = lwip_recv(conn_fd, buffer, sizeof(buffer), 0)) > 0) {
            lwip_send(conn_fd, buffer, n, 0);
        }

        lwip_close(conn_fd);
    }
}
创建 TCP 客户端
c 复制代码
#include "lwip/sockets.h"

void vTCPClient(void *pvParameters) {
    int sock_fd;
    struct sockaddr_in server_addr;
    char *message = "Hello, Server!";
    char buffer[1024];
    int n;

    /* 创建套接字 */
    sock_fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        /* 处理错误 */
    }

    /* 配置服务器地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");

    /* 连接服务器 */
    if (lwip_connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        /* 处理错误 */
    }

    /* 发送消息 */
    lwip_send(sock_fd, message, strlen(message), 0);

    /* 接收响应 */
    if ((n = lwip_recv(sock_fd, buffer, sizeof(buffer), 0)) > 0) {
        /* 处理接收到的数据 */
    }

    lwip_close(sock_fd);
}

7.2 Raw API

Raw API 提供更底层的接口,适用于对性能要求高或需要自定义协议处理的应用。

创建一个简单的 TCP 客户端
c 复制代码
#include "lwip/tcp.h"

struct tcp_pcb *client_pcb;
struct pbuf *p_tx;

static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err) {
    if (err == ERR_OK) {
        /* 连接成功,发送数据 */
        const char *data = "Hello, Raw TCP!";
        tcp_write(tpcb, data, strlen(data), TCP_WRITE_FLAG_COPY);
        tcp_output(tpcb);

        /* 关闭连接 */
        tcp_close(tpcb);
    }
    return ERR_OK;
}

void vRawTCPClient(void *pvParameters) {
    /* 创建 TCP PCB */
    client_pcb = tcp_new();
    if (client_pcb != NULL) {
        /* 连接到服务器 */
        ip_addr_t server_ip;
        IP4_ADDR(&server_ip, 192, 168, 1, 100);
        err_t err = tcp_connect(client_pcb, &server_ip, 8080, tcp_client_connected);
        if (err != ERR_OK) {
            /* 处理连接错误 */
        }
    }
}

8. 常见配置选项

lwIP 的功能通过 lwipopts.h 文件中的宏定义进行配置。以下是一些常用的配置选项:

内存管理

c 复制代码
#define MEM_SIZE                   (16 * 1024)
#define MEMP_NUM_PBUF              128
#define MEMP_NUM_TCP_PCB           32
#define MEMP_NUM_TCP_SEG           256
#define MEMP_NUM_UDP_PCB           16
#define MEMP_NUM_REASSDATA         10

协议选项

c 复制代码
#define LWIP_TCP                   1
#define LWIP_UDP                   1
#define LWIP_IPV4                  1
#define LWIP_IPV6                  0

调试与跟踪

c 复制代码
#define LWIP_DEBUG                 0
#define LWIP_STATS                 1

任务配置(与 FreeRTOS 集成时)

c 复制代码
#define TCPIP_THREAD_NAME          "TCP/IP"
#define TCPIP_THREAD_STACKSIZE      512
#define TCPIP_MBOX_SIZE            32
#define DEFAULT_TCP_RECVMBOX_SIZE  32
#define DEFAULT_UDP_RECVMBOX_SIZE  32

9. 使用示例

以下是一个完整的示例,展示如何在 FreeRTOS 环境中集成 lwIP,并创建一个简单的 TCP 服务器。

9.1 初始化 lwIP 网络接口

c 复制代码
#include "lwip/netif.h"
#include "lwip/tcpip.h"
#include "lwip/dhcp.h"

/* 定义网络接口结构体 */
struct netif netif0;

/* 初始化网络接口 */
void lwip_network_init(void) {
    ip4_addr_t ipaddr, netmask, gw;

    /* 设置初始IP地址为0.0.0.0 */
    IP4_ADDR(&ipaddr, 0, 0, 0, 0);
    IP4_ADDR(&netmask, 0, 0, 0, 0);
    IP4_ADDR(&gw, 0, 0, 0, 0);

    /* 初始化TCP/IP堆栈 */
    tcpip_init(NULL, NULL);

    /* 添加网络接口 */
    netif_add(&netif0, &ipaddr, &netmask, &gw, NULL, low_level_init, tcpip_input);

    /* 设置为默认网络接口 */
    netif_set_default(&netif0);

    /* 启动DHCP客户端 */
    dhcp_start(&netif0);
}

9.2 创建 TCP 服务器任务

c 复制代码
#include "lwip/sockets.h"
#include "FreeRTOS.h"
#include "task.h"

void vTCPServerTask(void *pvParameters) {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024];
    int n;

    /* 创建套接字 */
    listen_fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        /* 处理错误 */
        vTaskDelete(NULL);
    }

    /* 配置服务器地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /* 绑定套接字 */
    if (lwip_bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        /* 处理错误 */
        lwip_close(listen_fd);
        vTaskDelete(NULL);
    }

    /* 监听连接 */
    if (lwip_listen(listen_fd, 5) < 0) {
        /* 处理错误 */
        lwip_close(listen_fd);
        vTaskDelete(NULL);
    }

    /* 接受并处理客户端连接 */
    while (1) {
        conn_fd = lwip_accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
        if (conn_fd < 0) {
            /* 处理错误 */
            continue;
        }

        /* 处理客户端请求 */
        while ((n = lwip_recv(conn_fd, buffer, sizeof(buffer), 0)) > 0) {
            /* 回显接收到的数据 */
            lwip_send(conn_fd, buffer, n, 0);
        }

        /* 关闭连接 */
        lwip_close(conn_fd);
    }
}

9.3 主函数

c 复制代码
#include "lwip_network_interface.h"
#include "tcp_server_task.c"
#include "FreeRTOS.h"
#include "task.h"

int main(void) {
    /* 系统初始化代码,如硬件初始化 */

    /* 初始化网络接口 */
    lwip_network_init();

    /* 创建 TCP 服务器任务 */
    xTaskCreate(vTCPServerTask, "TCPServer", 1024, NULL, tskIDLE_PRIORITY + 1, NULL);

    /* 启动调度器 */
    vTaskStartScheduler();

    /* 程序不应到达这里 */
    for (;;) {}
}

10. 优化建议

10.1 优化校验和

首先要优化的是 lwip_standard_checksum() 函数,该函数位于 src/core/inet.c。可以通过以下方式覆盖标准校验和函数,以提高性能:

c 复制代码
#define LWIP_CHKSUM <your_checksum_routine>

可以参考 inet.c 中的 C 示例,或者编写一个汇编函数符合 RFC1071 规范。

10.2 优化字节序转换

如果使用小端架构,可以通过提供汇编或内联的 htons()htonl() 函数进行优化:

c 复制代码
#define LWIP_PLATFORM_BYTESWAP 1
#define LWIP_PLATFORM_HTONS(x) <your_htons>
#define LWIP_PLATFORM_HTONL(x) <your_htonl>

10.3 调整网络接口驱动

检查网络接口驱动是否以高于最大线速的速度读取数据。如果硬件没有被及时服务,可能会导致缓冲区溢出。举例来说,如果使用 cs8900 驱动,应尽可能频繁调用 cs8900if_service(ethif)。在使用 RTOS 时,可以让 cs8900 中断唤醒一个高优先级的任务,通过二值信号量或事件标志服务驱动。

10.4 关闭统计信息

对于生产发布版本,建议将 LWIP_STATS 设置为 0。注意,简单地设置较高的内存选项值并不会显著影响速度性能。

10.5 零拷贝 MAC

为了实现发送数据的零拷贝,传递给 Raw API 的数据必须保持不变,直到发送完成。具体要求如下:

  • 对于 PBUF_RAMPBUF_POOL 的 pbuf,应用程序在数据被入队后不应修改数据,除非其引用计数为 1。
  • 对于 PBUF_ROMPBUF_REF,数据也必须保持不变,但栈/驱动程序会复制 PBUF_REF 的数据进行入队,而 PBUF_ROM 的 pbuf 则直接入队(因期望 ROM 数据永不更改)。
  • 使用 tcp_write() 时,如果 copy 标志为 0,传递的数据指针对应的数据不能被修改。

11. 关键注意事项

  1. 内存管理 :确保 lwipopts.h 中的内存配置适合你的应用需求,避免内存不足或浪费。
  2. 多线程安全 :在多任务环境下,lwIP 的 API 调用应确保线程安全,通常通过配置 NO_SYS 和启用任务锁来实现。
  3. 网络接口驱动:正确实现网络接口驱动是确保 lwIP 正常工作的关键,需根据具体硬件平台进行定制。
  4. 调试与监控:启用调试和跟踪功能,有助于开发过程中问题的排查与性能优化。

12. 总结

lwIP 是一个功能强大且灵活的轻量级 TCP/IP 协议栈,适用于各种嵌入式系统。通过正确的配置和集成,可以实现高效的网络通信功能。在 FreeRTOS 等 RTOS 环境中使用 lwIP,可以充分利用 RTOS 的任务调度能力,提高系统的实时性和响应速度。掌握 lwIP 的基本使用方法和关键配置选项,是开发网络嵌入式应用的基础。

参考资料

致谢

感谢 Adam Dunkels 为嵌入式网络通信所做的贡献,lwIP 为开发者提供了一个高效、可靠的网络协议栈解决方案。

相关推荐
小菜鸟学代码··41 分钟前
STM32文件详解
stm32·单片机·嵌入式硬件
马浩同学2 小时前
【GD32】从零开始学GD32单片机 | DAC数模转换器 + 三角波输出例程
c语言·单片机·嵌入式硬件·mcu
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
最后一个bug5 小时前
STM32MP1linux根文件系统目录作用
linux·c语言·arm开发·单片机·嵌入式硬件
wenchm5 小时前
细说STM32F407单片机IIC总线基础知识
stm32·单片机·嵌入式硬件
嵌入式lover6 小时前
STM32项目之环境空气质量检测系统软件设计
stm32·单片机·嵌入式硬件
kenwblack7 小时前
STM32 SPI读取SD卡
stm32·单片机
兰_博7 小时前
51单片机驱动1602液晶显示
单片机·嵌入式硬件·51单片机
深圳市青牛科技实业有限公司 小芋圆7 小时前
开关电源特点、分类、工作方式
前端·科技·单片机·物联网·分类·数据挖掘·新能源
我qq不是451516527 小时前
单片机优先级
单片机·嵌入式硬件