4. LwIP协议栈移植(下):内存管理配置与网络接口测试

好,咱们接着上回聊。上次我们把LwIP的底层接口和时钟滴答都搞定了,这次要动真格的了——内存管理配置、TCP/UDP协议栈初始化,还有网络接口的测试。说实话,这部分是移植过程中最容易出幺蛾子的地方。我见过太多人卡在这里,一调就是好几天。

4.1 内存管理三剑客:PBUF、MEMP、MEM

LwIP的内存管理,说白了就三个核心概念:PBUF、MEMP和MEM。很多人一开始搞混,我当年也是。咱们一个一个说清楚。

4.1.1 PBUF:数据包的“快递箱”

PBUF是LwIP里最基础的数据结构。每个网络数据包,从网卡收上来,到协议栈处理,再到应用层读取,全程都装在PBUF里。你可以把它想象成一个快递箱——箱子本身是固定的,但里面的东西可以灵活组合。

PBUF有四种类型:PBUF_RAM、PBUF_ROM、PBUF_REF和PBUF_POOL。实际项目中,我最常用的是PBUF_POOL和PBUF_RAM。

  • PBUF_POOL:固定大小的内存池分配。速度快,适合中断上下文。我习惯在接收路径上用这个。
  • PBUF_RAM:动态内存分配。灵活,但可能慢。发送大数据包时我会选它。

核心配置项:在lwipopts.h中,PBUF_POOL_SIZE和PBUF_POOL_BUFSIZE决定了接收缓冲区的数量和大小。我一般这样设:

// 接收缓冲区数量,根据实际吞吐量调整
#define PBUF_POOL_SIZE          16
// 每个缓冲区大小,必须能容纳一个完整以太网帧
#define PBUF_POOL_BUFSIZE       1524

嗯,这里要注意:PBUF_POOL_BUFSIZE至少要比你的MTU大。以太网MTU是1500字节,加上14字节以太网头、4字节CRC,1524是个安全值。我曾经见过有人设成1514,结果跑大包时疯狂丢包,查了两天才发现是这里的问题。

4.1.2 MEMP:静态内存池,快就一个字

MEMP是LwIP的静态内存池管理器。它预先分配好固定大小的内存块,比如PCB结构、网络接口结构等。为什么用静态池?因为动态分配在嵌入式系统里容易产生碎片,而且慢。

MEMP的配置在memp_std.h里,但实际项目中我们通过lwipopts.h来覆盖默认值。我个人习惯这样配:

// 最大TCP连接数
#define MEMP_NUM_TCP_PCB        8
// 最大UDP连接数
#define MEMP_NUM_UDP_PCB        4
// 最大同时ARP表项
#define MEMP_NUM_ARP_QUEUE      8

这些值怎么定?看你的应用场景。如果只是做简单的数据采集,TCP连接数4个就够了。但如果是做网关,可能要16个甚至更多。我建议先设大一点,跑起来后用memp_stats查看实际使用情况,再调小。

4.1.3 MEM:动态内存分配,灵活但小心

MEM是LwIP的通用动态内存分配器,类似于C标准库的malloc/free。但它是在一个固定大小的数组上实现的,所以不会产生系统堆的碎片问题。

配置很简单:

// 动态内存池大小,单位字节
#define MEM_SIZE                (16 * 1024)

16KB够不够?看情况。如果你的应用需要频繁分配大块内存,比如HTTP服务器要缓存页面,那可能得32KB甚至64KB。我做过一个项目,设备需要同时处理多个TCP连接,MEM_SIZE设成32KB才稳定。

避坑指南:MEM_SIZE不是越大越好。它占的是RAM,嵌入式系统的RAM本来就金贵。我曾经在一个只有64KB RAM的MCU上,把MEM_SIZE设成48KB,结果系统直接起不来——因为其他模块也要用内存。建议MEM_SIZE不超过总RAM的50%。

4.2 TCP/UDP协议栈初始化

内存配置好了,接下来就是协议栈的初始化。这部分代码其实不多,但顺序很重要。我习惯这样写:

void lwip_init(void)
{
    // 1. 先初始化内存管理
    mem_init();
    memp_init();
    
    // 2. 再初始化网络接口
    netif_init();
    
    // 3. 然后初始化IP层
    ip_init();
    
    // 4. 最后初始化传输层
    tcp_init();
    udp_init();
}

为什么是这个顺序?因为TCP和UDP的PCB结构需要从MEMP池里分配,而MEMP池又依赖MEM。所以必须从底层往上层初始化。这个顺序我踩过坑——有一次我把tcp_init放到了mem_init前面,结果系统直接hardfault。

TCP初始化时,有几个参数值得关注:

  • TCP_MSS:最大分段大小。以太网环境下,MSS = MTU - IP头 - TCP头 = 1460字节。这个值影响吞吐量。
  • TCP_WND:TCP窗口大小。决定了发送端能连续发送多少数据而不等待确认。我一般设成4倍MSS,也就是5840字节。
  • TCP_SND_BUF:发送缓冲区大小。如果应用层写入速度比网络发送速度快,这个缓冲区就很重要。

个人经验:调试TCP性能时,我习惯先检查TCP_WND和TCP_SND_BUF。很多吞吐量上不去的问题,都是这两个值设得太小。比如TCP_WND只有1460,那发送端每发一个包就要等ACK,效率极低。

4.3 网络接口配置与测试

协议栈初始化完了,但还没完——你得告诉LwIP你的网卡长什么样。这就是netif_add要做的事。

struct netif g_netif;

// 添加网络接口
netif_add(&g_netif, 
          &ipaddr,    // IP地址
          &netmask,   // 子网掩码
          &gw,        // 网关
          NULL,       // 状态参数,一般传NULL
          ethernetif_init,  // 网卡初始化函数
          ethernet_input);  // 网卡输入函数

// 设置为默认接口
netif_set_default(&g_netif);

// 启动网络接口
netif_set_up(&g_netif);

这里有个细节:ethernetif_initethernet_input是我们自己实现的。前者负责初始化网卡硬件、注册MAC地址;后者负责把网卡收到的数据包喂给LwIP协议栈。

配置完成后,怎么测试?我一般分三步走:

  1. 链路层测试:用ping命令,看能不能通。如果ping不通,先检查网卡驱动和PHY芯片。
  2. IP层测试:用netif->ip_addr确认IP地址是否正确。我习惯在代码里加个打印,把IP地址打出来。
  3. 传输层测试:写一个简单的TCP echo server,用telnet连上去,看数据能不能回显。

调试小技巧:如果ping不通,别急着怀疑协议栈。先检查网卡的中断服务函数有没有正确调用ethernet_input。我遇到过好几次,中断里忘了调用这个函数,结果数据包永远到不了协议栈。

4.4 性能调优:从能用到好用

移植完成只是第一步。要让系统跑得又快又稳,还得调优。我总结了几条经验:

参数 默认值 推荐值 说明
PBUF_POOL_SIZE 16 32~64 接收缓冲区数量,高吞吐场景需要更多
MEMP_NUM_TCP_PCB 8 16~32 并发连接数,根据实际需求调整
TCP_WND 2048 5840~11680 窗口越大,吞吐量越高
TCP_SND_BUF 2048 4096~8192 发送缓冲区,防止应用层阻塞

调优不是一蹴而就的。我习惯先跑一个基准测试,记录吞吐量和延迟。然后每次改一个参数,再跑一次,对比效果。这样能清楚知道每个参数的影响。

最后提醒一句:别为了追求极致性能,把所有内存都塞给LwIP。系统还有其他任务要跑。我见过一个项目,工程师把MEM_SIZE设成64KB,结果系统任务栈不够,频繁死机。平衡才是王道。

好了,这一章的内容就到这儿。下一章我们会聊LwIP的Socket API编程,到时候会写一个完整的TCP客户端和服务器。有什么问题,欢迎在评论区交流。