4. LwIP协议栈移植与裁剪
在STM32上跑通以太网,光有MAC和PHY还不够,真正让TCP/IP通信可用的是LwIP协议栈。LwIP(Lightweight IP)是一套为嵌入式系统设计的开源TCP/IP协议栈,它把ARP、IP、ICMP、UDP、TCP、DHCP、DNS等协议压缩到几十KB的RAM和ROM占用内,同时保留完整的socket和raw API接口。本节围绕“怎么把LwIP装进STM32工程并让它稳定工作”展开,重点讲清CubeMX配置、网卡驱动对接、内存池划分和线程模型四个环节。
一、CubeMX中的LwIP配置要点
在STM32CubeMX中启用ETH外设后,Middleware栏会出现LwIP选项。配置时最关键的几项是:
- PHY地址与接口模式:RMII模式下PHY地址通常为0或1,需与硬件原理图一致;LAN8720常用地址0,DP83848常用地址1。
- DHCP与静态IP:调试阶段建议先用静态IP(如192.168.1.100/255.255.255.0/192.168.1.1),避免DHCP超时导致启动阻塞;产品阶段再启用DHCP。
- 内存配置:MEM_SIZE决定堆大小,默认约1600字节,跑TCP服务端时建议调到4KB~8KB;PBUF_POOL_SIZE决定pbuf池数量,默认4~16,高吞吐场景需增大。
- 协议裁剪:只保留实际用到的协议。例如仅做UDP采集时,可关闭TCP、DNS、SNMP,节省ROM和RAM。
CubeMX生成的代码会调用MX_LWIP_Init(),它完成三件事:初始化网卡驱动、初始化LwIP内核、启动网络接口。理解这三步的先后顺序,是后续排查问题的关键。
二、网卡驱动对接:ethernetif.c的角色
LwIP本身不直接操作STM32的ETH寄存器,中间靠一个网卡驱动层ethernetif.c衔接。它实现了LwIP要求的netif结构体,核心是两个函数:
low_level_init():设置netif的MAC地址、MTU、flags,并启动ETH外设和DMA描述符链。low_level_output():把LwIP的pbuf数据拷贝到ETH发送DMA缓冲区,触发发送。
接收方向则由ETH中断或轮询触发,在ethernetif_input()中把DMA收到的帧封装成pbuf,再交给netif->input()送入LwIP内核。这里最容易出问题的是DMA描述符的对齐和Cache一致性:在带D-Cache的STM32F7/H7上,发送前要SCB_CleanDCache_by_Addr(),接收后要SCB_InvalidateDCache_by_Addr(),否则会出现“ping通但TCP数据错乱”的典型现象。
三、内存池与lwipopts.h裁剪
LwIP的内存管理有两套机制:内存堆(mem heap)和内存池(memp pool)。堆用于变长分配,如pbuf数据区;池用于定长结构体,如TCP PCB、UDP PCB。CubeMX生成的lwipopts.h是裁剪的核心文件,几个关键参数如下:
| 参数 | 默认值 | 作用 | 调整建议 |
|---|---|---|---|
| MEM_SIZE | 1600 | 内存堆总大小 | TCP服务端调至4096~8192 |
| PBUF_POOL_SIZE | 16 | pbuf池数量 | 高吞吐场景调至32 |
| MEMP_NUM_TCP_PCB | 5 | 同时活跃TCP连接数 | 按最大并发连接数设置 |
| TCP_SND_BUF | 2560 | TCP发送缓冲区 | 大文件传输调至5840 |
| TCP_WND | 2048 | TCP接收窗口 | 与TCP_SND_BUF匹配 |
裁剪的原则是“按需保留”。例如一个只做Modbus TCP从站的设备,只需要TCP、IP、ICMP、ARP,可以关闭DHCP、DNS、SNMP、PPP,ROM占用能从约60KB降到35KB左右。但要注意:关闭LWIP_NETCONN后就不能用socket API,只能使用raw API,这会改变应用层的编程方式。
四、线程模型:tcpip_thread与应用的协作
LwIP默认采用tcpip_thread单线程模型:所有协议处理都在这个线程内完成,应用任务通过消息队列与它通信。这种设计避免了协议栈内部加锁,但要求应用任务不能直接操作netif或pbuf。具体协作方式有两种:
- Socket API(NETCONN):应用任务调用
netconn_new()、netconn_recv()等函数,这些函数内部通过消息队列把请求发给tcpip_thread,并阻塞等待结果。编程简单,但每次调用都有一次上下文切换开销。 - Raw API:应用直接注册回调函数(如
tcp_recv()),回调在tcpip_thread上下文中执行。效率高,但回调中不能调用阻塞函数,否则会卡死整个协议栈。
实际工程中,常见的做法是:创建一个优先级低于tcpip_thread的应用任务,用socket API处理业务逻辑;ETH中断只负责触发接收信号量,不做任何协议处理。这样既保证了实时性,又避免了竞态。
HAL_Delay()或等待信号量,会导致tcpip_thread阻塞,表现为ping延迟骤增甚至断网。回调中只做数据拷贝和标志置位,耗时操作交给应用任务。
五、一个可复现的配置示例
假设目标是用STM32F407+LAN8720做一个TCP Echo服务器,静态IP 192.168.1.100,端口7。CubeMX配置如下:
/* lwipopts.h 关键片段 */
#define MEM_SIZE (6*1024)
#define PBUF_POOL_SIZE 24
#define MEMP_NUM_TCP_PCB 8
#define TCP_SND_BUF (4*TCP_MSS)
#define TCP_WND (4*TCP_MSS)
#define LWIP_NETCONN 1
#define LWIP_SOCKET 0
#define LWIP_DHCP 0
#define LWIP_DNS 0
#define LWIP_ICMP 1
#define LWIP_UDP 1
#define LWIP_TCP 1
应用任务中创建socket并监听:
int sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(7);
addr.sin_addr.s_addr = INADDR_ANY;
lwip_bind(sock, (struct sockaddr*)&addr, sizeof(addr));
lwip_listen(sock, 2);
/* 后续 accept/recv/send 循环 */
验证时,先在PC上ping 192.168.1.100,确认ARP和ICMP正常;再用telnet 192.168.1.100 7或nc发送字符串,观察是否原样返回。如果ping通但TCP连不上,优先检查MEMP_NUM_TCP_PCB是否耗尽、MEM_SIZE是否过小导致pbuf分配失败。如果TCP能连但数据错乱,检查D-Cache维护是否遗漏。
整个移植过程的核心逻辑是:CubeMX负责生成框架和默认参数,ethernetif.c负责硬件对接,lwipopts.h负责资源裁剪,线程模型决定应用如何安全地使用协议栈。把这四件事理清,LwIP在STM32上的运行就从一个黑盒变成了可控的工程组件。