#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 为开发者提供了一个高效、可靠的网络协议栈解决方案。

相关推荐
时序数据说44 分钟前
时序数据库市场前景分析
大数据·数据库·物联网·开源·时序数据库
scilwb8 小时前
RoboCon考核题——scilwb
单片机
点灯小铭9 小时前
基于STM32单片机智能RFID刷卡汽车位锁桩设计
stm32·单片机·汽车·毕业设计·课程设计
TDengine (老段)10 小时前
TDengine IDMP 高级功能(4. 元素引用)
大数据·数据库·人工智能·物联网·数据分析·时序数据库·tdengine
bai54593610 小时前
STM32 软件I2C读写MPU6050
stm32·单片机·嵌入式硬件
逼子格13 小时前
AT89C52单片机介绍
单片机·嵌入式硬件·51单片机·硬件工程师·硬件工程师真题·at89c52·器件手册
David WangYang15 小时前
基于 IOT 的安全系统,带有使用 ESP8266 的语音消息
物联网·安全·语音识别
竹照煜_ysn17 小时前
STM32——软硬件I2C
stm32·嵌入式硬件·mongodb
Wallace Zhang19 小时前
STM32 - Embedded IDE - GCC - 显著减少固件的体积
stm32·单片机·嵌入式硬件
fengfuyao9851 天前
STM32如何定位HardFault错误,一种实用方法
stm32·单片机·嵌入式硬件