1. 为什么要在国产 MCU 上折腾 LwIP
第一次把 LwIP 往国产 MCU 上搬,是在一个工业网关项目里。客户指定用 GD32F4 系列,要求带一路以太网,能跑 Modbus TCP 和 MQTT,成本还得压到最低。当时团队里有人提议直接上带硬件协议栈的芯片,或者干脆用 Linux 方案,但成本和功耗都过不了关。最后绕了一圈,还是回到 LwIP 这条路上。
LwIP 这个名字,做嵌入式网络的人基本都听过。全称是 Lightweight IP,一个开源的轻量级 TCP/IP 协议栈,最初由瑞典计算机科学院的 Adam Dunkels 开发。它的核心卖点就一个:在资源极其有限的嵌入式设备上,实现一套功能相对完整的网络协议。RAM 占用可以压到几十 KB,ROM 占用几十 KB 到一百多 KB,这个量级放在国产 MCU 上刚好能接受。
但“能跑”和“跑得稳”是两码事。我在 GD32、兆易创新、华大、极海这几家的 MCU 上都做过 LwIP 的移植,踩过的坑能写满一个笔记本。国产 MCU 和 ST 的芯片在以太网外设上虽然寄存器层面高度相似,但时钟树配置、DMA 描述符对齐、中断优先级分组这些细节上,差异足以让一个跑通的工程在换芯片后直接罢工。更别说 LwIP 本身的配置参数,默认值在 PC 上没问题,放到 MCU 上就是各种超时、丢包、内存耗尽。
这篇内容适合两类人看:一类是刚接触嵌入式网络、准备在国产 MCU 上跑 LwIP 的开发者,另一类是在 LwIP 上已经跑通但被稳定性问题折磨的工程师。我会从协议栈的裁剪配置讲起,一直讲到网络稳定性的实战调优,中间穿插大量我在实际项目里验证过的参数和排查方法。所有内容都基于真实项目经验,不是从文档里抄来的理论。
2. LwIP 协议栈在 MCU 上的裁剪与配置策略
2.1 内存池与内存堆的取舍逻辑
LwIP 的内存管理有两套机制:内存池(Pool)和内存堆(Heap)。这两个东西用不好,系统跑几分钟就死给你看。
内存池是预分配的固定大小块,每种大小的块有一个独立的池。比如 PBUF_POOL 用于网络数据包的 pbuf 结构,MEMP_NUM_PBUF 控制池里有多少个块。内存池的优点是分配和释放都是 O(1) 操作,不会产生碎片,缺点是大小固定,用不完就浪费。内存堆则是动态分配,灵活但会产生碎片,在长时间运行的设备上是隐患。
我的经验是:所有 LwIP 内部用到的结构,全部走内存池;只有应用层需要动态申请的大块数据,才考虑用内存堆,而且最好自己管理,不要频繁 malloc/free。
具体到 lwipopts.h 里的配置,几个关键参数必须根据实际场景算:
#define MEMP_NUM_PBUF 16 #define MEMP_NUM_UDP_PCB 6 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_PCB_LISTEN 4 #define MEMP_NUM_TCP_SEG 32 #define MEMP_NUM_REASSDATA 4 #define MEMP_NUM_ARP_QUEUE 8 #define PBUF_POOL_SIZE 24 #define PBUF_POOL_BUFSIZE 1528PBUF_POOL_BUFSIZE 设成 1528 是有讲究的。以太网帧最大 1518 字节,加上 LwIP 内部可能需要的对齐和头部空间,1528 是一个经过验证的安全值。设小了会导致大包被拆分到多个 pbuf,增加处理开销;设大了浪费 RAM。
MEMP_NUM_TCP_SEG 这个参数最容易被忽略。它控制同时存在的 TCP 分段数量。如果你发现 TCP 吞吐量上不去,或者大量重传,先检查这个值。计算公式大致是:(TCP_SND_BUF / TCP_MSS) * 并发连接数 * 1.5。比如 TCP_SND_BUF 设 5840,TCP_MSS 是 1460,单个连接需要 4 个分段,两个并发连接就是 8 个,留 50% 余量就是 12 个。我一般直接设 32,给足余量。
2.2 国产 MCU 的以太网外设适配要点
国产 MCU 的以太网外设,寄存器层面基本是参考 ST 的 ETH 模块设计的,但有几个地方必须手动确认。
第一是 DMA 描述符的对齐。ST 的 HAL 库默认把描述符放在普通 RAM 里,但很多国产 MCU 的以太网 DMA 要求描述符地址 4 字节对齐,甚至有些要求 8 字节对齐。我遇到过 GD32F4 上描述符没对齐导致 DMA 传输随机失败的情况,现象是 ping 通几包就断,断了又自己恢复。解决办法是在链接脚本里给描述符单独分配一个对齐的段,或者用__attribute__((aligned(4)))强制对齐。
第二是时钟配置。国产 MCU 的 ETH 时钟来源可能和 ST 不同。STM32F4 的 ETH 时钟来自 PLL,而某些国产型号需要单独使能 ETH 时钟分频器。如果 RMII 接口的 50MHz 参考时钟不对,PHY 芯片根本不会 link up。我一般先用示波器量 REF_CLK 引脚,确认是 50MHz 且占空比接近 50%,再往下调。
第三是中断优先级。LwIP 的收发依赖中断,如果以太网中断优先级低于其他高频中断,会出现接收溢出。在 Cortex-M4 上,我通常把 ETH 中断设为抢占优先级 5(数值越小优先级越高),子优先级 0。不要设成最高,否则会影响系统 tick 和其他关键中断。
2.3 lwipopts.h 里必须改的十几个参数
LwIP 的默认配置是给桌面环境用的,直接拿来在 MCU 上跑,RAM 瞬间就爆了。下面这些参数是我在每个项目里都会重新审视的:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| MEM_LIBC_MALLOC | 0 | 0 | 用 LwIP 自带内存管理 |
| MEMP_MEM_MALLOC | 0 | 0 | 内存池不用 malloc 实现 |
| MEM_ALIGNMENT | 4 | 4 | Cortex-M 是 4 字节对齐 |
| MEM_SIZE | 1600 | 4096~8192 | 内存堆大小,按需调整 |
| TCP_SND_BUF | 256 | 2920~5840 | 发送缓冲区,影响吞吐 |
| TCP_WND | 2048 | 2920~5840 | 接收窗口,影响吞吐 |
| TCP_MSS | 536 | 1460 | 最大分段大小 |
| LWIP_NETIF_TX_SINGLE_PBUF | 0 | 1 | 单 pbuf 发送,减少拷贝 |
| CHECKSUM_GEN_IP | 1 | 1 | 硬件校验和可关 |
| CHECKSUM_GEN_TCP | 1 | 1 | 同上 |
| LWIP_STATS | 1 | 0 | 发布版本关掉统计 |
| LWIP_DEBUG | 1 | 0 | 发布版本关掉调试 |
TCP_MSS 设 1460 的前提是你的网络 MTU 是 1500,且中间没有 PPPoE 之类的额外封装。如果设备要过路由器,有些路由器 MTU 是 1492,这时候 TCP_MSS 要相应降到 1452。我一般先用 1460,如果发现大包传输有问题,再往下调。
LWIP_NETIF_TX_SINGLE_PBUF 设成 1 之后,LwIP 会尽量把要发送的数据放在一个 pbuf 里,减少 DMA 描述符的数量。这个选项在国产 MCU 上特别有用,因为有些芯片的 DMA 描述符数量有限,多 pbuf 发送容易触发描述符耗尽。
3. 从零搭建 LwIP 工程的完整实操流程
3.1 硬件确认与 PHY 芯片选型
在写代码之前,先把硬件确认清楚。国产 MCU 的以太网接口一般是 RMII 模式,需要外接 PHY 芯片。常用的 PHY 有 LAN8720、DP83848、YT8512 等。LAN8720 最便宜,但要注意它的 REF_CLK 输出需要配置,有些模块默认是输入模式,需要改电阻。
我一般按这个顺序检查硬件:
- 量 MCU 的 ETH_REF_CLK 引脚,确认有 50MHz 时钟。
- 量 PHY 的复位引脚,确认上电后有复位脉冲。
- 读 PHY 的 BCR 寄存器(地址 0x00),确认能读到 0x1140 之类的值。
- 读 PHY 的 BSR 寄存器(地址 0x01),确认 link 状态和速度。
如果第 3 步读不到值,先查 MDIO 和 MDC 两根线。MDC 频率不能超过 2.5MHz,我一般设到 1.5MHz 左右。MDIO 需要上拉电阻,通常是 1.5kΩ 到 4.7kΩ。
3.2 以太网外设初始化与 DMA 描述符配置
以太网初始化的核心是 DMA 描述符环的建立。下面是我在 GD32F4 上用的描述符定义:
typedef struct { volatile uint32_t Status; uint32_t ControlBufferSize; uint8_t *Buffer1Addr; uint8_t *Buffer2NextDescAddr; } ETH_DMADescTypeDef; __attribute__((aligned(4))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB]; __attribute__((aligned(4))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB]; __attribute__((aligned(4))) uint8_t Rx_Buff[ETH_RXBUFNB][ETH_MAX_PACKET_SIZE]; __attribute__((aligned(4))) uint8_t Tx_Buff[ETH_TXBUFNB][ETH_MAX_PACKET_SIZE];ETH_RXBUFNB 和 ETH_TXBUFNB 我一般设 4 和 4。设多了浪费 RAM,设少了容易丢包。ETH_MAX_PACKET_SIZE 是 1524。
描述符初始化的时候,要把 Rx 描述符的 Buffer1Addr 指向 Rx_Buff 对应的行,把 Status 设成 ETH_DMARXDESC_OWN。Tx 描述符类似,但 OWN 位在发送时才置位。
这里有个坑:国产 MCU 的 DMA 描述符在 Cache 使能的情况下需要做 Cache 一致性维护。如果你的 MCU 有 D-Cache,在读取 Rx 描述符状态之前要 invalidate 对应的 Cache 行,在填充 Tx 描述符之后要 clean 对应的 Cache 行。我见过一个项目因为没做 Cache 维护,ping 通但 TCP 传文件全是乱码。
3.3 LwIP 的初始化顺序与网络接口注册
LwIP 的初始化顺序不能乱,我一般按这个流程:
// 1. 初始化 LwIP 内核 lwip_init(); // 2. 初始化以太网外设和 DMA ETH_Init(); // 3. 配置网络接口 struct netif gnetif; 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); // 4. 添加网络接口 netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input); // 5. 设置为默认接口 netif_set_default(&gnetif); // 6. 启动接口 netif_set_up(&gnetif); // 7. 启动 DHCP(如果需要) // dhcp_start(&gnetif);ethernetif_init 这个函数是移植的重点,它要完成 low_level_init 的调用,设置 netif 的 flags、output 回调、linkoutput 回调等。low_level_init 里要初始化以太网外设、创建信号量、启动接收线程。
我习惯把 LwIP 的收发放在一个独立的线程里,优先级比应用线程高一级。接收线程用信号量等待以太网中断释放,然后调用 ethernetif_input 处理数据包。
3.4 中断处理与数据收发的线程模型
以太网中断里只做最少的事:清除中断标志,释放信号量。真正的数据包处理放在线程里做。这样做的原因是 LwIP 的 ethernetif_input 可能会调用上层回调,如果在中断里执行,栈空间和中断延迟都不可控。
void ETH_IRQHandler(void) { if (ETH_GetDMAFlagStatus(ETH_DMA_FLAG_R) == SET) { ETH_DMAClearITPendingBit(ETH_DMA_IT_R); // 释放信号量,通知接收线程 osSemaphoreRelease(sem_rx); } if (ETH_GetDMAFlagStatus(ETH_DMA_FLAG_T) == SET) { ETH_DMAClearITPendingBit(ETH_DMA_IT_T); // 释放信号量,通知发送完成 osSemaphoreRelease(sem_tx); } }接收线程的伪代码:
void ethernetif_thread(void *arg) { while (1) { if (osSemaphoreWait(sem_rx, osWaitForever) == osOK) { ethernetif_input(&gnetif); } } }这里有个细节:ethernetif_input 里会调用 low_level_input 从 DMA 描述符里取数据,然后封装成 pbuf,再调用 netif->input。如果 netif->input 返回 ERR_MEM,说明 LwIP 内存不够,这时候要释放 pbuf 并记录错误。
4. 网络稳定性问题的排查与调优实录
4.1 丢包与超时问题的定位思路
网络不稳定,第一件事是确认问题出在哪一层。我一般按这个顺序排查:
物理层:用示波器看 RMII 的 TX_EN、TXD0、TXD1 波形,确认发送时有时钟和数据。如果波形畸变严重,检查 PCB 走线是否等长,终端电阻是否匹配。
MAC 层:读以太网外设的错误计数器,看是否有 CRC 错误、对齐错误、溢出错误。GD32 的 ETH_MMCRFCECR 寄存器记录 CRC 错误数,ETH_MMCRFAECR 记录对齐错误数。如果这两个数在涨,说明物理层有问题。
协议栈层:打开 LwIP 的统计功能,看 memp 的 err 计数、pbuf 的 alloc 失败计数。如果 pbuf alloc 失败频繁,说明 PBUF_POOL_SIZE 太小。
应用层:用 ping 测试基础连通性,用 iperf 测试吞吐量,用长时间 ping 测试稳定性。我一般会跑一个 12 小时的 ping,看丢包率是否低于 0.1%。
4.2 内存耗尽与 pbuf 泄漏的典型场景
pbuf 泄漏是 LwIP 上最常见的稳定性问题。典型现象是设备运行几小时后网络卡死,重启后恢复。用 LwIP 的 stats 功能可以看到 pbuf 的 used 计数一直涨,最终达到 PBUF_POOL_SIZE 后分配失败。
泄漏的原因通常有几个:
第一,接收路径上没有释放 pbuf。比如在应用层回调里,收到数据后没有调用 pbuf_free。我见过一个项目,TCP 接收回调里把数据拷贝到自己的缓冲区后,忘了释放 pbuf,结果每收一包泄漏一个 pbuf。
第二,发送失败后没有释放 pbuf。tcp_write 返回 ERR_MEM 时,pbuf 还在应用层手里,需要自己释放。如果直接丢弃,就泄漏了。
第三,ARP 队列溢出。当目标 IP 的 MAC 地址还没解析出来时,LwIP 会把数据包挂在 ARP 队列上。如果队列满了,新包会被丢弃,但已经挂上去的包如果一直解析不出来,就会一直占着 pbuf。MEMP_NUM_ARP_QUEUE 设大一点可以缓解,但根本解决办法是确保 ARP 能正常解析。
我的做法是在应用层加一个 pbuf 使用计数,定期打印。如果发现计数只涨不跌,就重点检查接收和发送路径。
4.3 长时间运行下的连接保持策略
工业设备要求 7x24 小时运行,TCP 连接可能几天都不动。这时候需要处理几个问题:
Keepalive 配置。LwIP 支持 TCP keepalive,在 lwipopts.h 里打开:
#define LWIP_TCP_KEEPALIVE 1 #define TCP_KEEPIDLE_DEFAULT 7200000 // 2小时 #define TCP_KEEPINTVL_DEFAULT 75000 // 75秒 #define TCP_KEEPCNT_DEFAULT 9但 keepalive 只能检测连接是否还活着,不能防止中间路由器断开空闲连接。我一般会在应用层加心跳包,每 30 秒发一次,确保 NAT 表项不过期。
重连机制。如果连接断了,应用层要能自动重连。我一般用状态机实现:连接断开后,等待 5 秒重连,连续失败 3 次后等待 30 秒,再失败等待 60 秒,最多退避到 5 分钟。
内存碎片防护。长时间运行后,内存堆可能产生碎片。如果应用层频繁 malloc/free 不同大小的块,碎片会越来越严重。我的做法是应用层尽量用静态分配,或者用固定大小的内存池。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| ping 不通 | PHY 未 link | 读 BSR 寄存器 | 检查时钟、复位、网线 |
| ping 通但丢包严重 | DMA 描述符不足 | 看 ETH 错误计数器 | 增加 RXBUFNB |
| TCP 传大文件卡死 | TCP_SND_BUF 太小 | 看 tcp sndbuf 统计 | 增大 TCP_SND_BUF |
| 运行几小时后死机 | pbuf 泄漏 | 看 pbuf used 计数 | 检查释放路径 |
| 连接频繁断开 | keepalive 未开 | 抓包看是否有 RST | 开启 keepalive |
| 吞吐量只有几 Mbps | TCP_WND 太小 | 计算带宽时延积 | 增大 TCP_WND |
| 换芯片后不工作 | 时钟配置不同 | 量 REF_CLK | 调整时钟树 |
| 中断里死机 | 栈溢出 | 看 MSP 值 | 增大中断栈 |
5. 性能优化与吞吐量提升的实战技巧
5.1 零拷贝发送的实现方法
LwIP 默认的发送路径会做一次数据拷贝:应用层的数据先拷贝到 LwIP 的 pbuf,再拷贝到 DMA 缓冲区。两次拷贝在高速传输时很浪费 CPU。
零拷贝的思路是让应用层的数据直接进入 DMA 缓冲区。LwIP 提供了netif->output的自定义实现,可以绕过 pbuf 拷贝。具体做法是:
- 应用层申请一个 pbuf,类型是 PBUF_REF,指向自己的数据缓冲区。
- 调用 tcp_write 时用这个 pbuf。
- 在 low_level_output 里,直接把 pbuf 的数据地址填到 DMA 描述符里。
但 PBUF_REF 有个限制:数据缓冲区在发送完成前不能被修改。所以应用层需要等发送完成信号才能复用缓冲区。我一般用双缓冲:两个缓冲区交替使用,一个在发送时,另一个在填充。
5.2 TCP 窗口与缓冲区大小的计算
TCP 吞吐量的理论上限是窗口大小 / RTT。假设 RTT 是 10ms,窗口是 5840 字节,吞吐量上限就是 5840 / 0.01 = 584KB/s,约 4.7Mbps。如果要跑到 50Mbps,窗口需要 50Mbps * 10ms / 8 = 62.5KB。
但 MCU 的 RAM 有限,不可能开 64KB 的窗口。这时候需要权衡:要么降低 RTT(用局域网),要么接受较低的吞吐量。我的经验是,在 100Mbps 局域网里,TCP_WND 设 5840 到 11680 之间,吞吐量能到 20 到 40Mbps,足够大多数工业场景。
UDP 没有窗口限制,但要注意接收缓冲区。如果接收速度跟不上发送速度,UDP 包会被丢弃。我一般把 UDP 接收缓冲区设成 4 到 8 个包的大小。
5.3 中断与轮询的混合模式
纯中断模式下,每个数据包都触发一次中断,高吞吐时中断开销很大。纯轮询模式又浪费 CPU。混合模式的做法是:低负载时用中断,高负载时切换到轮询。
具体实现:在接收中断里判断,如果连续收到 N 个包(比如 10 个),就关闭接收中断,切换到轮询模式。轮询线程处理完所有包后,再打开中断。这样在高吞吐时减少中断次数,低吞吐时不影响实时性。
我在 GD32F4 上实测,混合模式比纯中断模式的吞吐量高 15% 左右,CPU 占用低 10%。
5.4 实测数据与调优前后对比
在一个 GD32F450 平台上,我用 iperf 做了对比测试:
| 配置 | TCP 吞吐量 | UDP 吞吐量 | CPU 占用 |
|---|---|---|---|
| 默认配置 | 8.2 Mbps | 12 Mbps | 45% |
| 调优后 | 38 Mbps | 65 Mbps | 62% |
| 零拷贝+混合模式 | 52 Mbps | 78 Mbps | 55% |
调优的主要改动:TCP_WND 从 2048 增到 11680,TCP_SND_BUF 从 2560 增到 11680,PBUF_POOL_SIZE 从 16 增到 32,开启 LWIP_NETIF_TX_SINGLE_PBUF,启用混合中断轮询。
CPU 占用上升是因为吞吐量上去了,处理的数据包多了。零拷贝模式下 CPU 占用反而下降,因为减少了拷贝开销。
6. 国产 MCU 平台差异与移植避坑指南
6.1 GD32 与 STM32 的以太网外设差异
GD32F4 的以太网外设和 STM32F4 高度相似,但有几个寄存器地址和位定义不同。最典型的是 DMA 状态寄存器的位定义,GD32 的 ETH_DMASR 寄存器和 ST 的略有差异。直接拿 ST 的 HAL 库往 GD32 上搬,可能会在中断处理里读不到正确的标志位。
我的做法是:不直接用 ST 的 HAL 以太网驱动,而是自己写一个薄层,把寄存器操作封装起来。这样换芯片时只需要改薄层,LwIP 部分不动。
另外,GD32 的 ETH 时钟使能位在 RCU 寄存器里,和 ST 的 RCC 不同。这个在初始化时要注意。
6.2 华大与极海 MCU 的适配经验
华大 HC32F4A0 的以太网外设也是 RMII 接口,但它的 DMA 描述符要求 8 字节对齐,比 ST 的 4 字节更严格。我一开始没注意,描述符只做了 4 字节对齐,结果 DMA 传输随机失败。后来在链接脚本里加了. = ALIGN(8);才解决。
极海 APM32F4 的以太网外设和 ST 基本兼容,但 PHY 地址默认是 1,不是 0。如果 PHY 芯片的地址引脚没接对,MDIO 读不到。我一般先用扫描的方式,遍历 0 到 31 的 PHY 地址,找到能读回正确 ID 的那个。
6.3 移植过程中的编译与链接问题
LwIP 的源码在国产 MCU 的编译器上编译,可能会遇到几个问题:
第一,__packed关键字。LwIP 里有些结构体用了PACK_STRUCT_BEGIN和PACK_STRUCT_END,在 Keil 和 IAR 里需要正确定义。我一般在 lwipopts.h 里加上:
#define PACK_STRUCT_USE_INCLUDES 1然后在 cc.h 里包含编译器的 packed 定义。
第二,字节序。Cortex-M 是小端,LwIP 默认也是小端,一般不用改。但如果你的 MCU 是大端,需要定义BYTE_ORDER为BIG_ENDIAN。
第三,链接脚本。LwIP 的 pbuf 池和内存堆需要放在 RAM 里,如果 RAM 不够,可以把不常用的段放到外部 RAM。但要注意外部 RAM 的访问速度,DMA 描述符最好放在内部 RAM。
6.4 工具链与调试手段的选择
调试 LwIP 网络问题,光靠 printf 不够。我一般用这几个工具:
Wireshark:在 PC 上抓包,看 MCU 发出的包是否正常。如果 MCU 发的包有 CRC 错误,Wireshark 会标红。
LwIP 的 stats:打开 LWIP_STATS,定期打印 memp、pbuf、tcp 的统计信息。这个在排查内存泄漏时特别有用。
J-Link 的 RTT:用 SEGGER RTT 输出日志,比串口快,不占用 UART。我一般在关键路径上加 RTT 打印,记录每个数据包的处理时间。
逻辑分析仪:抓 RMII 的时序,确认时钟和数据的关系。如果 TX_EN 和 TXD 的建立时间不够,PHY 会收到错误数据。
7. 项目实战中的经验沉淀
7.1 一个工业网关项目的完整复盘
去年做的一个工业网关,用的是 GD32F450 + LAN8720,跑 LwIP 2.1.2,上面跑 Modbus TCP 服务器和 MQTT 客户端。项目要求 7x24 小时运行,丢包率低于 0.01%。
开发过程中遇到的最大问题是:设备运行 4 到 6 小时后,MQTT 连接断开,重连失败。抓包发现,断开时 MCU 发出的 TCP 包序号乱了,像是发送缓冲区被破坏。
排查了三天,最后定位到是 TCP_SND_BUF 和 MEM_SIZE 的配置冲突。TCP_SND_BUF 设了 5840,但 MEM_SIZE 只有 4096,LwIP 在分配发送缓冲区时内存不够,导致部分数据没发出去,序号就乱了。把 MEM_SIZE 增到 8192 后问题解决。
这个坑的教训是:LwIP 的各个内存参数是有关联的,不能单独调。TCP_SND_BUF 加上 TCP_WND 加上其他开销,不能超过 MEM_SIZE 的 70%。
7.2 从失败案例中总结的配置原则
经过多个项目的积累,我总结了几条 LwIP 配置原则:
原则一:内存池优先。所有 LwIP 内部结构用内存池,应用层数据用静态分配或固定大小内存池。
原则二:参数联动。TCP_SND_BUF、TCP_WND、MEM_SIZE、PBUF_POOL_SIZE 要一起调,调完跑压力测试。
原则三:留足余量。所有计数类参数(MEMP_NUM_*)至少留 50% 余量,防止突发流量导致分配失败。
原则四:发布版本关调试。LWIP_DEBUG 和 LWIP_STATS 在发布版本里关掉,省 ROM 和 RAM。
原则五:压力测试不可少。至少跑 24 小时 ping 和 iperf,观察内存和错误计数。
7.3 后续可扩展的方向
LwIP 跑通之后,还可以往几个方向扩展:
IPv6 支持。LwIP 支持 IPv6,但需要额外的 RAM。如果项目有需求,可以打开 LWIP_IPV6,配置地址和路由。
TLS 加密。用 mbedTLS 和 LwIP 配合,实现 HTTPS 或 MQTT over TLS。但 TLS 握手需要大量 RAM,国产 MCU 上要谨慎评估。
多网口支持。有些国产 MCU 有两个以太网接口,LwIP 可以同时管理多个 netif,实现冗余或分流。
低功耗优化。在电池供电场景下,可以让 LwIP 在空闲时进入低功耗模式,收到包再唤醒。这需要和 MCU 的低功耗模式配合。
我在实际项目里最深的体会是:LwIP 在国产 MCU 上跑通不难,难的是跑稳。而跑稳的关键,不在于协议栈本身,而在于你对内存、中断、DMA 这些底层机制的理解。每次出问题,最后查到的原因都是某个参数没配对,或者某个资源没释放。所以,与其花时间研究 LwIP 的源码,不如先把 lwipopts.h 里的每个参数搞清楚,把内存和中断的账算明白。