
简介这是一份面向STM32嵌入式网络开发者的LWIPW5500组合参考工程。W5500内置硬件TCP/IP协议栈LWIP负责上层协议管理两者结合可在资源受限MCU上快速搭建TCP/UDP以太网通信适用于物联网网关、工业数据采集等场景适合已有一定STM32基础、希望学习轻量协议栈移植的开发者。压缩包共188个文件大小3.48MB以C源文件、H头文件为主体配套Keil工程配置、启动汇编、MAP/LST辅助文件及PDF/TXT文档其中.uvproj/.uvopt可直接打开工程.hex/.bin为固件输出.o/.axf等为编译中间文件有助于完整复现构建过程目录结构较为完整。已有1012人浏览学习。工程内包含W5500 SPI底层驱动、LWIP网络接口对接与基础示例可对照学习MAC地址配置、IP/子网掩码/网关设置、TCP/UDP连接建立流程同时保留链接脚本与编译中间产物便于复现构建过程或二次开发对理解硬件协议栈与软件协议栈的分工很有帮助。1. 为什么STM32以太网方案里W5500比软件协议栈更省心做嵌入式网络开发最尴尬的不是写不出应用层逻辑而是底层TCP/IP协议栈占掉了宝贵的Flash和RAM还要在MCU中断里频繁处理报文重传、超时计时、分片重组这些脏活。很多工程师在STM32F103上跑裸机LWIP配合DM9000或ENC28J60时都会遇到同一个问题主频72MHz的M3核既要跑控制逻辑又要扛协议栈稍微有点并发数据量CPU占用率就直奔50%以上。而W5500的核心思路完全不同它把TCP/IP协议栈做成了硬件逻辑固化在芯片内部MCU只需要通过SPI读写socket缓冲区即可完成收发CPU占用率通常能控制在10%以内。这个项目的价值在于它提供了一套完整的STM32 W5500 LWIP组合参考工程且文件列表里能看到os_core.c、os_cpu_a.asm这类RTOS内核文件意味着这套代码在uC/OS-II下做过适配LWIP是以操作系统封装层sys_arch的方式跑在RTOS之上。与裸机轮询方式不同这种架构下每个TCP连接可以由独立任务维护阻塞式API调用也不会卡死整个系统。适合的对象是需要在STM32平台上快速落地TCP/UDP通信、但不想从零调协议栈的开发者以及准备评估硬件协议栈和软件协议栈在资源占用、实时性、吞吐量方面差异的工程师。2. SPI驱动层W5500寄存器访问与可变长度数据帧2.1 W5500的SPI帧格式与三个关键区段W5500的SPI接口与普通SPI从机最大的区别在于它采用三元组帧结构地址段、控制段、数据段。地址段占16位控制段占8位数据段长度由控制段中的读写标志和块选择决定。读取和写入的区别只在于控制段的Bit2读为1写为0但传输时序上读操作需要先发送地址和控制段再拉低CS持续接收数据字节而写操作则是在地址和控制段之后紧跟数据字节。uint8_t w5500_read_byte(uint16_t addr) { uint8_t tx_buf[3], rx_buf[2]; tx_buf[0] addr 8; tx_buf[1] addr 0xFF; tx_buf[2] (0x00 2) | (0x01 1) | 0x01; // BSB000, RWB1, OM01 // HAL_SPI_TransmitReceive阻塞模式CS由片选引脚控制 }控制段0x01在读取场景下表示块选择BSB为000即通用寄存器区读标志位RWB置1偏移量OM固定为01B。如果访问的是socket寄存器区BSB需要相应调整例如访问socket 0的寄存器时BSB的低5位对应socket编号00000。忽略OM字段的取值可能导致读取量与预期不符特别是当需要读取socket RX buffer中的数据时。2.2 变长数据帧模式VDM的时序处理W5500的socket收发缓冲区访问并不走通用寄存器地址而是通过Socket TX/RX Buffer地址映射。硬件上支持两种访问模式固定数据帧模式FDM和可变数据帧模式VDM后者是吞吐量优化的关键。FDM模式适合访问寄存器这类长度固定的数据而VDM模式允许在控制段中附加16位数据长度使一次SPI传输就能读写长达64KB的数据。void w5500_send_data(SOCKET s, uint8_t *buf, uint16_t len) { uint16_t free_size; // 先读Sn_TX_FSR确认发送缓冲区剩余空间 free_size w5500_read_socket_reg(s, Sn_TX_FSR); // 构造VDM帧头地址段为Sn_TX_BASE控制段使能VDM并指定socket编号 uint8_t header[6]; header[0] (Sn_TX_BASE(s) 8) 0xFF; header[1] Sn_TX_BASE(s) 0xFF; header[2] 0x14; // BSB101(表示TX buffer), RWB0, OM10(VDM) header[3] (len 8) 0xFF; header[4] len 0xFF; }header[2]的0x14拆开看bit7-3为BSB段值10100对应socket 0的TX bufferbit2为RWB写标志bit1-0为OM值10B即VDM模式。此时帧尾紧跟两个字节的数据长度值。使用VDM模式有一个坑硬件不检查写入长度是否超出Sn_TX_FSR剩余空间一旦溢出会覆盖其他socket的缓冲区数据且不产生任何错误标志必须在软件层预先比较待发送长度和剩余空间大小。2.3 复位时序与PHY配置要点W5500上电后需要等待至少10ms让内部PLL稳定然后通过写模式寄存器MR的RST位触发软复位。复位完成后必须重新写入PHY配置常见做法是将PHYCFGR的RST位清0然后设置OPMDC字段选择10M/100M全双工自动协商模式。不少开发者碰到链路不通的问题其实就是没有等待PHY链接寄存器PHYSR的LINK位变为1就匆忙对socket执行listen或connect操作。3. LWIP移植netif接口对接W5500驱动3.1 lwipopts.h关键配置项移植LWIP到STM32 W5500组合时lwipopts.h的配置直接决定后续开发顺不顺畅。重点不是堆内存大小而是协议栈内部描述性的结构体消耗。对于W5500这种硬件协议卸载方案LWIP实际只保留IP层以上的逻辑TCP分包重排序、校验和计算全部由硬件完成因此内存池可以相对保守。#define MEM_ALIGNMENT 4 #define MEM_SIZE (32 * 1024) #define MEMP_NUM_PBUF 16 #define MEMP_NUM_TCP_SEG 32 #define PBUF_POOL_SIZE 32 #define PBUF_POOL_BUFSIZE 1512 #define LWIP_DHCP 1 #define LWIP_NETCONN 1 #define LWIP_SOCKET 1 #define NO_SYS 0 // 与RTOS配合时置0 #define SYS_LIGHTWEIGHT_PROT 1PBUF_POOL_BUFSIZE设置成1512而不是1500是因为以太网帧最大载荷为1500字节但netif结构需要额外的14字节头部空间用于MAC地址解封装。NO_SYS必须与工程实际使用的RTOS匹配如果源码里有os_core.c却把NO_SYS置1直接编译就会报sys_now未定义错误。3.2 netif结构体与low_level_output实现LWIP的netif结构是协议栈和底层驱动的桥梁驱动侧需要实现两个核心回调low_level_init负责初始化接口并填充netif的MAC地址、MTU、硬件地址长度等字段low_level_output则在协议栈有报文要发送时被回调。报文在协议栈内部以pbuf链形式存在驱动需要把链中每个节点按顺序拼成一个完整帧写入W5500的TX ring buffer。static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint16_t total_len 0; uint8_t *tx_buf malloc(p-tot_len); // 遍历pbuf链表将各节点拷贝到连续缓冲区 for (q p; q ! NULL; q q-next) { memcpy(tx_buf total_len, q-payload, q-len); total_len q-len; } // 写W5500发送缓冲区并触发发送命令 w5500_send_data(0, tx_buf, total_len); w5500_socket_cmd(0, SEND); }这段代码把多段pbuf拼接成连续数据再发送好处是W5500不需要支持分散/聚集DMA坏处是存在一次额外的memcpy。对大部分数据包小于512字节的MQTT或Modbus TCP场景性能影响可以忽略。如果追求极致吞吐可以改写w5500_send_data支持多段写入逐段调用SPI传输并不释放CS这样省掉拼接缓冲但代码复杂度明显上升。3.3 裸机轮询模型与RTOS信号量模型的取舍这套参考工程里同时存在裸机和RTOS两套调度可能。裸机方案下主循环需要周期调用ethernetif_poll函数内部检查W5500的socket interrupt寄存器收到数据就往LWIP的tcpip_input传包。RTOS方案下驱动在中断里只置位信号量由一个专用网络任务等待信号量后调用tcpip_input这能降低主循环轮询带来的延迟抖动。// RTOS方案SPI中断中仅做事件标记 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { osSemaphoreRelease(netif_semaphore); // uC/OS-II的信号量 } }uC/OS-II下信号量释放操作可能触发任务调度需注意不能在中断服务里调用可能导致阻塞的API。LWIP要求裸机下tcpip_input在锁保护下调用RTOS下则必须通过邮箱或信号量跨越任务边界直接把报文指针作为消息传给网络任务而不是在中断上下文中直接调用协议栈函数。很多不稳定现象如随机丢包、偶尔死机多半是中断上下文调用协议栈API造成的临界区竞争。4. Socket配置实战TCP Server与UDP收发调通4.1 三路socket初始化的代码骨架W5500内部有8个独立的socket每个可以独立配置为TCP客户端、TCP服务端或UDP模式。常见做法是socket 0用作TCP Server监听socket 1用作UDP透传socket 2预留作为TCP Client主动上报。初始化时先统一软复位再逐个socket写模式寄存器。void network_init(void) { // 复位W5500并等待PHY链接 w5500_soft_reset(); while (!(w5500_read_phy_reg(PHYSR) 0x01)); // LINK位 // 配置MAC和IP地址 w5500_write_reg(SHAR, mac_addr, 6); w5500_write_reg(SIPR, ip_addr, 4); // socket 0作为TCP Server监听8080端口 w5500_socket_init(0, Sn_MR_TCP, 8080, Sn_MR_SOCK_INT); w5500_socket_cmd(0, LISTEN); // socket 1作为UDP绑定5000端口 w5500_socket_init(1, Sn_MR_UDP, 5000, Sn_MR_SOCK_INT); }while轮询PHYSR寄存器是个简单粗暴但有效的方式超时保护不可少否则PHY芯片异常时会卡死整个初始化流程。建议加一个计数上限比如循环500次约50ms后跳出并打印错误日志。Sn_MR_SOCK_INT位决定是否使能socket中断实际使用中如果走轮询模式可以关掉中断节省资源。4.2 TCP分段接收与断开重连处理TCP数据到达W5500的RX buffer后读取Sn_RX_RSR可以拿到已接收但未被应用读取的字节数。关键在于一次读取不一定拿到完整应用层报文TCP是字节流协议上层报文可能被拆成多个TCP段分次到达。因此必须维护一个应用层接收缓冲区把每次读到的数据追加进去再根据自定义协议帧格式做解帧。uint16_t tcp_recv_handler(SOCKET s, uint8_t *buf, uint16_t max_len) { uint16_t rx_size, ret 0; rx_size w5500_read_socket_reg(s, Sn_RX_RSR); if (rx_size 0) { uint16_t read_len (rx_size max_len) ? max_len : rx_size; w5500_recv_data(s, buf, read_len); w5500_socket_cmd(s, RECV); // 告知硬件缓冲区已释放 ret read_len; } return ret; }RECV命令必须在数据读取完成后立即发送否则W5500认为接收缓冲区仍被占用后续收不到新报文。断开检测通过Sn_SR状态寄存器判断当状态变为SOCK_CLOSE_WAIT或SOCK_CLOSED时需要主动调用DISCON和CLOSE命令再重新执行LISTEN进入监听状态。这里有个容易踩的坑LISTEN命令不适用于已有连接的socket必须先把socket切换到SOCK_INIT状态。4.3 与STM32集成的调试手段逻辑分析仪与wireshark最有效的联调方法是用USB转以太网模块把W5500接到PCwireshark抓包看三层交互是否正常同时用逻辑分析仪抓SPI信号确认底层时序。SPI时序方面重点看CS拉低期间SCLK的时钟个数是否符合预期写寄存器场景是16位地址8位控制8位数据共32个时钟VDM发送模式下则是16816长度N字节数据。逻辑分析仪如果抓到的时钟个数不匹配优先检查SPI的CPOL/CPHA配置W5500要求CPOL1、CPHA1SPI模式3与很多低速传感器默认的模式0不同初始化SPI时稍不留神就是全乱码。5. 性能调优与实测从吞吐量到内存占用的验证方法5.1 内存预算分析LWIP协议栈吃掉了多少RAM用Keil的map文件统计内存占用是最直接的方法。稳定运行后通过串口把mallinfo或自定义统计信息打印出来能看到堆剩余量和各内存池水位。LWIP的MEMP_NUM_TCP_SEG乘上MEMP_SIZE加上PBUF_POOL_SIZE乘上PBUF_POOL_BUFSIZE就是核心内存消耗。以第3章的配置估算仅这两块就需约64KB。// 在串口调试任务中周期性打印内存水位 struct mem_stats_t { uint16_t heap_free; uint16_t pbuf_used; };实际经验是STM32F103ZET6的64KB RAM跑满TCP UDP部分功能时剩余空间约8~10KB。如果剩余空间低于4KB建议主动削减PBUF_POOL_SIZE和MEMP_NUM_TCP_SEG虽然会降低并发连接数但保证长时间运行的稳定性更重要。LWIP频繁重建和释放连接时若内存不足会直接返回ERR_MEM导致connect失败或listen拒绝。5.2 吞吐量验证iperf与TCP窗口大小的关系W5500的硬件TCP/IP卸载虽然减轻了CPU负担但吞吐量的上限受限于SPI时钟和socket缓冲区大小。实测72MHz主频下SPI时钟配到18MHz单TCP连接通过iperf测得的吞吐量大约在8~10Mbps。如果调高SPI到36MHz部分芯片在长线传输时可能出现数据错位需要加长CS拉低之前的建立时间。参数影响范围建议值SPI时钟直接影响传输速率18MHz稳定/ 36MHz需验证Socket TX/RX buffer大小决定TCP窗口上限8KB/8KB 或 16KB/16KBLWIP TCP_WND协议栈接收窗口与RX buffer匹配8 * TCP_MSSMEMP_NUM_TCP_SEG并发TCP段数量16~32TCP_WND和W5500的RX buffer大小如果不匹配会出现奇怪的现象pc发送数据永远只有满窗口大小而应用层明明没有及时读取。这是因为W5500硬件收到数据后放在RX buffer里LWIP的TCP窗口通告由netif驱动读取数据并交给协议栈后才能扩窗如果RX buffer比TCP_WND小硬件层面就已经丢包了。5.3 裸机向RTOS迁移时的时间关键点最后说一个容易被忽略的细节关闭SPI的CRC校验。STM32的SPI外设默认CRC功能关闭但一旦在CubeMX中误开了CRC每个数据帧尾部会多出两个字节的CRCW5500不识别这类帧结构表现为寄存器读写偶尔错位且无固定规律。排查方法是对同一寄存器连续读三次看返回值是否一致。LWIP下的UDP发送如果带宽要求高建议把W5500 socket的发送缓冲区开大同时用macOS的Network Link Conditioner模拟弱网环境测试丢包重传。硬件协议栈的重传机制完全由W5500内部状态机负责与LWIP的RTO定时器无关这既是个优点也是个约束重传策略不可调但确定性很好。基于这套工程做产品时建议把W5500中断引脚接到STM32的EXTI通过事件驱动代替主动轮询RTOS下可以节省约1%的CPU占用裸机则能显著降低主循环延迟。本文还有配套的精品资源点击获取