尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+W5500硬件协议栈UDP通讯实战:驱动移植与避坑指南

STM32+W5500硬件协议栈UDP通讯实战:驱动移植与避坑指南 简介基于STM32F103与W5500以太网模块的UDP通信完整工程面向物联网嵌入式开发者解决MCU通过SPI接口接入以太网并实现UDP收发的问题。例程涵盖DHCP动态获取IP、创建UDP、等待客户端连接、关闭连接等关键流程可直接在Keil中打开当前基于STM32F103C8T6调试通过同系列其他型号只需调整芯片型号与Flash容量即可复用适合智能家居、工业数据采集等场景快速开发。资源包共182个文件以stm32f10x系列C源码、H头文件及Keil工程配置uvprojx/uvoptx为主同时包含hex、axf、map等编译输出与调试辅助文件整体体积仅5.88MB便于下载与二次修改。已有1442人学习适合需要实际跑通UDP通信或移植以太网功能的开发者参考。通过阅读代码可获得从SPI驱动W5500、DHCP配置到UDP收发与连接管理的实现思路工程内保留标准外设库各模块可按需裁剪复用。1. 为什么说 STM32W5500 是物联网项目里最稳的 UDP 通讯组合如果你做过几个物联网项目大概率会遇到同一个尴尬用 ESP8266 走 WiFi 总觉得不够工业用 STM32 自带 MAC 走 RMII 又得折腾 PHY 芯片和驱动库稍不留神就翻车。而 W5500 这颗硬件 TCP/IP 协议栈芯片把 UDP、TCP、IP、ARP 这些协议全固化在芯片里MCU 只负责通过 SPI 读写寄存器网络底层的事它全包了。这就是“物联网项目实战开发之基于 STM32W5500 以太网 RJ45 UDP 通讯代码程序”这个方向能落地、能复制、能稳定运行的核心原因。这个方案的适用对象很具体做设备联网采集、远程控制、上位机调试、传感器数据上报的工程师或者正在做 STM32 毕业设计、需要跑通一套可靠 UDP 通讯代码的学生。UDP 相比 TCP 没有连接维护、没有重传机制配合 W5500 的硬件协议栈丢包率可控、响应时间确定、代码量小特别适合数据周期性上报和命令下发这类场景。本文不聊理论空话直接讲清楚 W5500 怎么接、驱动怎么移植、UDP 收发代码怎么写、遇到断连和丢包怎么排查。2. W5500 的硬件设计与选型RJ45 接线、SPI 速率与电源决定的稳定性边界2.1 为什么选 W5500 而不是 STM32 内置 MAC PHYSTM32F407、F429 等型号内置了 MAC 控制器但物理层还需要外接 PHY 芯片比如 LAN8720A、DP83848。这意味着你要处理 RMII 接口的时钟同步、PHY 寄存器配置、还有 ETH 库的初始化流程调试难度直接翻倍。W5500 的方案不一样它把 MAC 和 PHY 全部集成在芯片内部对外只暴露 SPI 接口MCU 端不需要关心以太网帧怎么组、ARP 怎么应答、UDP 校验和怎么算这些全由 W5500 的硬件协议栈完成。我一般会在以下场景优先选 W5500设备需要长时间稳定运行、网络环境有广播风暴或丢包、开发周期紧不想啃协议栈、MCU 主频不高比如 STM32F103C8T6 跑 72MHz 完全够用。W5500 的 SPI 从模式最高支持 80MHz 时钟但实际使用中 STM32 的 SPI 主模式跑到 18MHz 或 36MHz 就足够因为 UDP 报文本身不大瓶颈在网络带宽而不在 SPI 速率。2.2 参考电路与 RJ45 接线这 6 个引脚决定能不能一次点亮W5500 对外接口其实很简洁电源、SPI 四线SCS、SCLK、MOSI、MISO、复位 RSTN、中断 INTN。RJ45 连接器有两种常见接法一种是带变压器的 HR911105A 这种直接和 W5500 的 TXOP/TXON/RXIP/RXIN 对接另一种是分离式 RJ45 网络变压器中间还要接 1:1 隔离变压器。常见做法是直接用集成变压器的 RJ45 座省掉绕变压器的工夫。接线时注意几个关键点W5500 的 TXOP/TXON 是一对差分对走线时要等长、贴近阻抗控制在 100 欧姆左右RXIP/RXIN 同样处理。电源引脚 VCC 和 DVDD 各放一个 0.1uF 陶瓷电容靠近芯片引脚放置。RSTN 引脚建议用 STM32 的 GPIO 控制实现软件复位如果直接接 RC 复位电路也能用但软件复位在调试时更方便。另外 W5500 的 PMODE 引脚决定引脚工作模式接法在数据手册里有明确表格照着手册来接就不会错。W5500 的参考电路里还有一个容易忽略的细节晶振。通常用 25MHz 无源晶振配合两个 22pF 负载电容。晶振不起振是常见的“点不亮”原因之一用示波器测晶振引脚波形就能确认。2.3 SPI 速率、中断引脚与电源纹波三个影响长期稳定性的参数SPI 速率不要一上来就拉满。虽然 W5500 手册标称 80MHz但实际项目中 SPI 速率受 PCB 走线长度、STM32 的 SPI 外设配置影响很大。我一般先在 18MHz 下把功能调通再用逻辑分析仪看 MISO 上的数据是否干净确认无误再逐步提高。STM32F103 的 SPI1 挂载在 APB2 总线上72MHz 主频下分频到 18MHz 或 36MHz 都很方便。中断引脚 INTN 是 W5500 通知 MCU“有数据到达”的机制。把 INTN 接到 STM32 的 EXTI 外部中断引脚收到中断后在 SPI 中断服务程序里读取 W5500 的中断寄存器判断是接收中断还是发送完成中断。靠轮询也能收数据但中断方式对 CPU 占用更小UDP 报文到达时机不确定中断方式更合适。电源纹波对 W5500 影响很大如果供电用 AMS1117 这种 LDO 输出 3.3V纹波通常能控制在 50mV 以内但如果用 DC-DC输出纹波可能到 100mV 以上这时候 W5500 的 PHY 部分可能随机丢包。遇到底层不稳定先检查电源这是玄学问题里最不玄学的一个。参数推荐值说明SPI 时钟18MHz调试期 / 36MHz稳定期视 PCB 走线质量而定复位引脚STM32 GPIO 控制软件复位更可靠晶振25MHz ± 30ppm无源晶振负载电容 22pFVCC 去耦0.1uF 10uF靠近 VCC 引脚放置INTNEXTI 下降沿触发数据到达通知3. W5500 驱动移植与初始化从 SPI 底层到网络参数配置的完整代码3.1 SPI 底层驱动先用回环测试排除物理层故障写 W5500 驱动之前先把 STM32 的 SPI 底层打通。W5500 的 SPI 是标准 Mode 0CPOL0CPHA0数据帧长 8 位MSB first。下面这段代码是 STM32F103 标准外设库的 SPI1 初始化HAL 库思路一样只是函数名不同。void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能 SPI1 和 GPIOA 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // PA5SCLK, PA6MISO, PA7MOSI 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA4 作为 SCS 片选普通推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // 默认拉高取消片选 // SPI1 主机模式Mode 018MHz SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 72/418MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }片选引脚用普通 GPIO 控制比硬件 NSS 更灵活因为 W5500 的 SPI 帧格式复杂片选时序需要精确控制。SPI 初始化完成后写一个简单的寄存器读写函数测试一下。W5500 的 SPI 帧格式是第一个字节的高 5 位是地址第 6 位是块选择0Common 寄存器区1Socket 寄存器区第 7 位是读写控制1读0写第 8 位是操作模式1可变数据长度模式0固定数据长度模式。这个帧结构是 W5500 驱动里最容易出错的地方。uint8_t W5500_ReadByte(uint32_t addr) { uint8_t frame[3]; frame[0] (addr 0xFF00) 8; // 地址高8位包含块选择位和读写位 frame[1] addr 0x00FF; // 地址低8位 frame[2] 0x00; // 读操作时这个字节会被忽略 // 片选拉低开始传输 SCS_LOW(); SPI1_ReadWriteByte(frame[0]); SPI1_ReadWriteByte(frame[1]); uint8_t data SPI1_ReadWriteByte(frame[2]); SCS_HIGH(); return data; }注意这里addr的编码规则bit15~bit11 是寄存器地址高 5 位bit10~bit8 是块选择bit7 是读写标志bit6~bit0 是地址低 7 位。实际使用中建议用宏把这些位操作封装好比如#define COMMON_REG(addr) (addr)、#define SOCKET_REG(sock, addr) ((sock 8) | addr)这种方式。地址编码错了读回来的数据就是全 F 或者乱码这是移植驱动时第一个容易踩的坑。3.2 寄存器初始化序列先复位、再配 IP、最后开中断W5500 上电后默认是掉电模式必须往 Common 寄存器的 MRMode Register地址 0x0000写 0x80 触发软件复位然后等待复位完成。接着配置网络参数网关地址 GAR、子网掩码 SUBR、 MAC 地址 SHAR、本机 IP 地址 SIPR。这四个参数全部在 Common 寄存器区每个都是 4 字节MAC 是 6 字节。void W5500_Init(uint8_t *mac, uint8_t *ip, uint8_t *gw, uint8_t *subnet) { // 软件复位 W5500_WriteByte(0x0000, 0x80); DelayMs(10); // 配置网关、子网掩码、MAC、IP W5500_WriteBuffer(0x0001, gw, 4); // GAR 网关地址 W5500_WriteBuffer(0x0005, subnet, 4); // SUBR 子网掩码 W5500_WriteBuffer(0x0009, mac, 6); // SHAR MAC 地址 W5500_WriteBuffer(0x000F, ip, 4); // SIPR 本机 IP // 设置 Socket 0 的发送和接收缓冲区大小为 8KB W5500_WriteByte(0x001E, 0x08); // TMSR 发送缓冲区Socket0 为 8KB W5500_WriteByte(0x001F, 0x08); // RMSR 接收缓冲区Socket0 为 8KB // 开启 Socket 0 的中断接收数据完成中断 W5500_WriteByte(0x0016, 0x01); // SIMR Socket 中断屏蔽寄存器SnIR1 允许接收中断 // 开启全局中断 W5500_WriteByte(0x0015, 0x01); // IMR 中断屏蔽寄存器 }初始化完成后读取 VERSIONR版本寄存器地址 0x0039应该返回 0x04这是验证 SPI 通讯是否正常的快速方法。如果读出来不是 0x04优先检查 SPI 引脚接线、时钟极性/相位配置和地址编码。这一步通过后再进入 socket 配置才算真正开始 UDP 通讯。3.3 Socket 0 配置为 UDP 模式协议模式寄存器 Sn_MR 的细节W5500 的每个 socket 都有独立的寄存器组包括 Sn_MR模式、Sn_CR命令、Sn_IR中断、Sn_SR状态、Sn_PORT端口号等。配置 UDP 模式时向 Sn_MR 写 0x02然后打开 Socket 并设置本地端口。void W5500_Socket0_UDP_Init(uint16_t local_port) { // Socket 0 的寄存器基地址为 0x0400 uint16_t base 0x0400; // 关闭 socket如果是重新配置先关闭旧的 W5500_WriteByte(base 0x0001, 0x10); // Sn_CR CLOSE DelayMs(5); // 设置 UDP 模式0x02 表示 UDP同时清零其他标志位 W5500_WriteByte(base 0x0000, 0x02); // Sn_MR UDP // 设置本地端口号W5500 是大端模式端口号需要高低字节交换 W5500_WriteByte(base 0x0004, (local_port 8) 0xFF); W5500_WriteByte(base 0x0005, local_port 0xFF); // 打开 socketSn_CR OPEN W5500_WriteByte(base 0x0001, 0x01); DelayMs(5); // 确认 socket 状态为 SOCK_UDP0x22 uint8_t status W5500_ReadByte(base 0x0003); if (status ! 0x22) { // 打开失败检查端口冲突或 socket 号占用 Debug_Print(Socket OPEN failed, status0x%02X, status); } }端口号的字节序是一个典型的坑。W5500 的所有多字节寄存器都是大端存储STM32 侧如果是小端模式直接赋值W5500_WriteByte(base0x04, local_port 8)这种方式最容易出错。建议把端口号拆分成高字节和低字节分别写入避免隐式转换问题。4. UDP 收发代码实现缓冲区管理、分包与完整性校验4.1 发送流程Sn_TX_WR 指针操作与数据打包W5500 的发送缓冲区是环形结构MCU 要先把数据写入发送缓冲区再更新 Sn_TX_WR 写指针最后发送 Sn_CR 的 SEND 命令。这里的难点是环形缓冲区的回绕处理如果写入的数据跨越缓冲区末尾需要分成两次写入。int8_t W5500_SendUDP(uint16_t dst_port, uint8_t *dst_ip, uint8_t *data, uint16_t len) { uint16_t base 0x0400; uint16_t tx_base 0x6000; // Socket 0 发送缓冲区基地址 uint16_t tx_size 8192; // 与初始化时设置的缓冲区大小一致 // 读取当前写指针 uint16_t tx_wr (W5500_ReadByte(base 0x0024) 8) | W5500_ReadByte(base 0x0025); // 写入目标 IP 和端口到 Socket 0 的 Sn_DIPR 和 Sn_DPORT W5500_WriteBuffer(base 0x000C, dst_ip, 4); // Sn_DIPR 目标 IP W5500_WriteByte(base 0x0010, (dst_port 8) 0xFF); // Sn_DPORT 高字节 W5500_WriteByte(base 0x0011, dst_port 0xFF); // Sn_DPORT 低字节 // 计算可用空间 uint16_t free_size; uint16_t tx_rd (W5500_ReadByte(base 0x0022) 8) | W5500_ReadByte(base 0x0023); if (tx_wr tx_rd) { free_size tx_size - (tx_wr - tx_rd) - 1; } else { free_size tx_rd - tx_wr - 1; } if (free_size len) { return -1; // 发送缓冲区不足丢弃本次数据 } // 写入数据处理环形回绕 uint16_t offset tx_base tx_wr; if (offset len tx_base tx_size) { uint16_t first_part tx_base tx_size - offset; W5500_WriteBuffer(offset, data, first_part); W5500_WriteBuffer(tx_base, data first_part, len - first_part); } else { W5500_WriteBuffer(offset, data, len); } // 更新 Sn_TX_WR 指针注意大端序 uint16_t new_tx_wr tx_wr len; if (new_tx_wr tx_size) { new_tx_wr - tx_size; } W5500_WriteByte(base 0x0024, (new_tx_wr 8) 0xFF); W5500_WriteByte(base 0x0025, new_tx_wr 0xFF); // 发送 SEND 命令 W5500_WriteByte(base 0x0001, 0x20); // Sn_CR SEND DelayMs(1); // 等待发送完成也可以轮询 Sn_IR 的 SEND_OK 位 return 0; }free_size的计算必须减 1这是为区分缓冲区满和空保留的“哨兵字节”。W5500 的环形缓冲区不允许完全装满否则读写指针相等时无法判断是满还是空。很多移植 bug 就出在这条减 1 的逻辑上。4.2 接收流程Sn_RX_RD 指针、中断标志与数据读取接收数据有两个触发方式轮询 Sn_IR 的 RECV 位或者用外部中断。这里给出中断方式的主流程INT 引脚触发后先读 Sn_IR 判断是接收事件然后读取接收缓冲区长度分割头部和数据最后更新 Sn_RX_RD 并发送 RECV 命令。void W5500_INT_Handler(void) { uint16_t base 0x0400; uint8_t ir W5500_ReadByte(base 0x0002); // Sn_IR if (ir 0x04) { // RECV 位接收完成 // 读取接收数据长度Sn_RX_RSR 是 0x0026~0x0027 uint16_t len; do { len (W5500_ReadByte(base 0x0026) 8) | W5500_ReadByte(base 0x0027); } while (len 0); // W5500 手册建议读两次确认 // 读取 Sn_RX_RD 读指针 uint16_t rx_rd (W5500_ReadByte(base 0x0028) 8) | W5500_ReadByte(base 0x0029); // 接收缓冲区基地址 0x7000大小 8192 uint16_t rx_base 0x7000; uint16_t rx_size 8192; uint8_t buffer[2048]; // 分两段读取处理回绕 uint16_t offset rx_base rx_rd; if (offset len rx_base rx_size) { uint16_t first_part rx_base rx_size - offset; W5500_ReadBuffer(offset, buffer, first_part); W5500_ReadBuffer(rx_base, buffer first_part, len - first_part); } else { W5500_ReadBuffer(offset, buffer, len); } // 更新读指针 uint16_t new_rx_rd rx_rd len; if (new_rx_rd rx_size) { new_rx_rd - rx_size; } W5500_WriteByte(base 0x0028, (new_rx_rd 8) 0xFF); W5500_WriteByte(base 0x0029, new_rx_rd 0xFF); // 发送 RECV 命令释放缓冲区 W5500_WriteByte(base 0x0001, 0x40); // Sn_CR RECV // 清除中断标志 W5500_WriteByte(base 0x0002, 0x04); } // 清除 W5500 全局中断标志 W5500_WriteByte(0x0015, 0x00); // 读 IMR 清中断 }接收流程里有个 W5500 特有的怪癖Sn_RX_RSR 寄存器读取后不能立刻作为数据长度使用手册建议连续读两次并取第二次的结果。某次调试时我发现只读一次会偶尔读到 0导致丢包。这段代码里用了do...while(len 0)循环实际项目里建议加超时保护避免死循环卡死 MCU。4.3 UDP 分包与 MTU 限制为什么 1472 是安全的长度上限UDP 的载荷长度直接受 MTU最大传输单元限制。标准以太网 MTU 是 1500 字节减去 IP 头 20 字节和 UDP 头 8 字节UDP 数据载荷最大就是 1472 字节。如果超过这个值IP 层会分片分片后的数据包在网络上可能被丢弃接收端重组也会增加 CPU 负担。W5500 的硬件分片能力有限实际项目中超过 1472 字节的 UDP 报文经常出现对方收不到的情况。// 发送大数据的切片逻辑 void W5500_SendLargeData(uint16_t dst_port, uint8_t *dst_ip, uint8_t *data, uint32_t total_len) { const uint16_t max_payload 1472; uint32_t offset 0; while (offset total_len) { uint16_t chunk (total_len - offset max_payload) ? max_payload : (total_len - offset); W5500_SendUDP(dst_port, dst_ip, data offset, chunk); offset chunk; // 切片之间增加微小间隔避免 W5500 发送队列堆积 DelayUs(100); } }这里要注意的是W5500 的发送缓冲区是 8KB如果不加间隔连续发送多个 1472 字节的包缓冲区很快耗尽W5500_SendUDP会返回 -1。实际测试下来19 个 1472 字节的包就会填满 8KB 缓冲区。所以大文件传输要么加大 socket 缓冲区初始化时把 TMSR 写到 0x10 即 16KB但总缓冲区只有 64KBSocket 0 最多分到 16KB要么在应用层做流量控制。4.4 数据完整性校验UDP 不保证可靠应用层必须补一手UDP 没有 TCP 那样的确认和重传机制W5500 硬件只保证单包的正确性不保证包的到达和顺序。物联网项目里的数据要么是周期上报丢失一帧下个周期补上要么是控制命令丢失可能导致执行异常。我一般会在应用层加一个简单的帧格式帧头 序列号 数据长度 数据 CRC16。typedef struct { uint8_t header; // 固定 0xAA uint8_t seq; // 序列号递增 uint16_t length; // 数据长度 uint8_t data[1472]; // 数据区 uint16_t crc; // CRC16 校验值 } udp_frame_t;发送时在udp_frame_t结构体的基础上填充数据接收方先检查 header 和 crc再根据 seq 判断是否乱序。如果连续丢包超过阈值可以发一个应用层重传请求——但这属于业务逻辑了不是 W5500 本身能解决的。用这种方式一台 STM32F103 W5500 的设备跑 7x24 小时 UDP 上报数据完整率能做到 99.9% 以上。5. 避坑指南W5500 UDP 通讯的 6 个高频故障与排查记录5.1 上电后读版本寄存器返回 0xFF 或 0x00SPI 通讯完全不通现象执行W5500_ReadByte(0x0039)读 VERSIONR返回值不是 0x04有时候是 0xFF有时候是随机值。原因这个故障 90% 出在 SPI 配置上另外 10% 是硬件接线问题。常见的具体原因包括SCLK 极性/相位配置成 Mode 1应该用 Mode 0MISO 引脚没有配置成浮空输入或复用推挽SPI 时钟频率过高导致信号失真W5500 的复位引脚一直处于低电平芯片根本没启动。解决先用示波器量 SPI 引脚的波形确认 SCLK 有方波输出。然后检查 RSTN 引脚电平正常工作时应该为高。最后检查 SPI 的 CPOL/CPHAW5500 要求 Mode 0CPOL0CPHA0。如果这些都没问题把 SPI 时钟降到 2MHz 试试排除信号完整性因素。这个坑几乎每个做 W5500 的人都踩过而且排查顺序一定是从硬件到软件。5.2 W5500 正常工作几天后 ping 不通或 UDP 断连重启后又恢复现象设备运行一段时间可能是几个小时到几天后网络突然不通本地 ping 设备 IP 时断时续或者完全无响应重启 W5500 或整机后恢复。原因最常见的是 socket 资源泄漏。项目里有周期性的 UDP 发送任务但在发送流程里没有检查 Sn_CR 的 SEND 命令是否执行完。W5500 的每个 socket 只能同时处理一个 SEND 命令如果上次 SEND 没完成又发了新命令芯片内部状态就乱了。另一个常见原因是 ARP 缓存老化W5500 的 ARP 表会定期过期如果对端设备 IP 变了但没有重新 ARP发出去的报文会送到错误的 MAC 地址上——这是 W5500 芯片的正常行为不是硬件故障。解决发送流程里加状态轮询SEND 命令发出后检查 Sn_IR 的 SEND_OK 位或 SEND_ERR确认完成后再发下一个包。另外在 UDP 发送前检查 Sn_SR 状态是不是 SOCK_UDP如果不是就重新初始化 socket。对 ARP 缓存问题可以定期执行一次 ping 操作来刷新 ARP 表或者在 STM32 侧主动发送一个 ARP 请求。这个故障排查起来很痛苦因为现象和 TCP 的老化很像但实际上和网络环境、对端设备关系很大。5.3 ping 通但 UDP 数据收不到或时通时不通现象PC 能 ping 通设备说明 IP 层没问题但用网络调试助手发 UDP 报文设备端中断没有触发或者设备发的数据上位机收不到。原因:这类问题多半出在端口配置和目标地址配置上。先说收不到W5500 的 socket 配置了 Sn_PORT但 PC 发送时目标端口如果写错比如 W5500 配置的 6000PC 发到 6001W5500 会直接丢弃。再说发不出W5500_SendUDP 里如果 Sn_DIPR 和 Sn_DPORT 没有正确设置或者和上次发送的目标不同芯片会按旧配置发。还有个隐蔽问题是 W5500 的 socket 如果长时间没有数据收发会自动进入 SOCK_CLOSED_WAIT 状态这时候虽然 ping 通但 UDP 收发全部失效。解决先在 W5500 端把 Sn_PORT、Sn_DIPR、Sn_DPORT 全部读出来打印对照 PC 端的配置逐项检查。然后确认 Sn_SR 的状态恒为 0x22SOCK_UDP如果不是就重新 init。最后用 Wireshark 抓包看报文是否真的发到设备端——有时候问题出在交换机端口隔离或防火墙和设备没关系。这里不要盲目怀疑 W5500先用抓包工具定位问题在哪一层。5.4 波特率无关的网络丢包SPI 速率太高导致接收数据错位现象低波特率比如 18MHz SPI时通讯正常把 SPI 提高到 36MHz 后发现偶发丢包数据内容也有错位。原因W5500 的 SPI 从模式在高时钟下对时序要求更严格。PCB 走线过长、MISO 上拉电阻过大、STM32 的 SPI 输出边沿不够陡峭都会导致 W5500 的输入数据采样出错。SPI 时钟是 36MHz 时一个 bit 周期只有约 27ns线上任何一点寄生电容都会造成信号劣化。解决MISO 引脚的上拉电阻保持在 10k 以内SPI 走线长度控制在 10cm 以内。如果有条件用逻辑分析仪抓取 MISO 上的数据波形观察边沿是否干净。实际上 18MHz 对 UDP 通讯完全够用W5500 的硬件瓶颈从来不在 SPI 而在 PHY 层没必要追求极限时钟。我现在的项目基本锁定 18MHz稳定运行两三年没有因为 SPI 速率出过问题。5.5 多个 socket 同时使用导致缓冲区不足现象项目里既要 UDP 收发又想开一个 TCP socket 做参数配置结果 TCP 连不上UDP 也时而超时。原因W5500 的 64KB 缓冲区是共享的每个 socket 的发送和接收缓冲区大小独立配置TMSR/RMSR但总和不能超过 64KB。如果在初始化时 Socket 0 分了 16KBSocket 1 又分了 16KB其他 socket 分完剩下的空间就可能不足 2KBTCP 握手阶段就需要缓冲区空间不够自然连不上。解决规划 socket 缓冲区分配时确认常用通讯的 socket 有足够的空间。比如 Socket 0 分 8KB 做 UDP 高频收发Socket 1 分 8KB 做 TCP 低频配置其余关闭。改 TMSR/RMSR 后必须重新配置对应 socket。缓冲区分配是一个需要提前规划的事情不要等跑起来再调。5.6 UDP 广播被交换机丢弃或延迟现象设备发 UDP 广播帧目标 IP 255.255.255.255部分客户端能收到有的收不到时延也很大。原因交换机对广播帧的处理策略各不相同有些交换机对广播帧做了速率限制默认可能每秒只让通过几十个包W5500 发广播的频率高了会直接丢弃。另外 W5500 的广播帧发送必须指定目标 MAC 为 FF:FF:FF:FF:FF:FF有些程序直接用了目标 IP 自动解析的 MAC 地址导致广播帧变成单播帧。解决W5500_SendUDP 发送广播时强制写入dst_mac[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}。注意 W5500 的 Sn_DHAR目标 MAC 寄存器默认值就是全 F但如果你在发单播时给其赋过值发广播前必须重新写一次。广播帧的发送频率控制在每秒 10 帧以内如果需要更高频率考虑改用组播地址或直接单播给每个客户端。6. 进阶验证用网络调试助手和 PC 端 Python 脚本做 UDP 双向测试6.1 先跑通最小回路从 W5500 自发自收到 PC 端收发代码全部移植完后第一步不要接业务逻辑先做最小回路测试。把 W5500 接在路由器或交换机上PC 设置同网段 IP比如 192.168.1.100W5500 设置 192.168.1.50。PC 上用网络调试助手NetAssist 等开启 UDP 连接本地端口设为 8080目标 IP 填 192.168.1.50目标端口 6000。然后 STM32 侧做一个循环每 500ms 发一条递增计数器的消息同时等待接收。如果 PC 端能持续收到递增的计数消息说明发送通路完全正常。回环测试更严格一点PC 端发一条消息给 W5500STM32 收到后再把消息原样发回 PCPC 端对比内容是否一致。这段测试代码可以临时写在主循环里等验证通过后再换成实际业务逻辑。这里建议在 STM32 的串口上也打一套调试日志把“发送成功/接收成功/缓冲区剩余”状态全部打出来串口加网络两路日志联查到问题会快很多。6.2 用 Python 脚本做长时间稳定性测试和丢包率统计网络调试助手适合手动测试但做 7x24 小时稳定性测试就要靠脚本。PC 端用 Python 写一个 UDP 打流脚本向 W5500 周期性发送带序号的测试帧同时统计接收到的返回值。import socket import time # 目标 W5500 设备 TARGET_IP 192.168.1.50 TARGET_PORT 6000 LOCAL_PORT 8080 def main(): # 创建 UDP socket 并绑定本地端口 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, LOCAL_PORT)) sock.settimeout(2) send_count 0 recv_count 0 lost_count 0 start_time time.time() # 连续发送 10000 个测试帧 while send_count 10000: # 构造测试帧: 序号 固定载荷(填充0x5A) payload send_count.to_bytes(4, big) b\x5A * 32 sock.sendto(payload, (TARGET_IP, TARGET_PORT)) try: # 等待接收回包(回环测试模式) recv_data, _ sock.recvfrom(2048) recv_seq int.from_bytes(recv_data[:4], big) if recv_seq send_count: recv_count 1 else: lost_count 1 except socket.timeout: lost_count 1 send_count 1 # 控制发送速率: 每秒100帧 time.sleep(0.01) duration time.time() - start_time print(f发送 {send_count} 帧, 接收 {recv_count} 帧, 丢失 {lost_count} 帧) print(f耗时 {duration:.1f}s, 丢包率 {lost_count / send_count * 100:.2f}%) if __name__ __main__: main()脚本里的send_count和recv_count是核心统计指标。丢包率在局域网环境下通常为 0%如果超过 1% 基本可以断定硬件或驱动存在问题。注意脚本里的发送速率控制sleep(0.01)让发送频率保持 100 帧/秒避免瞬间打爆 W5500 的缓冲区。如果需要测满负荷性能可以去掉 sleep 并把帧长加大到 1400 字节观察极限丢包率。6.3 确认 MTU 边界和 W5500 缓冲区的性能天花板最后做一个边界测试分别用 100、500、1000、1400、1472、1500 字节的数据包发送观察丢包率和回包延迟。正常情况下1472 字节以下丢包率应该为 0超过 1472 字节后丢包率会急剧上升这是 IP 分片导致的属于预期行为。同时测一下 W5500 的缓冲区连续发送 20 个 1400 字节的包观察第 19 个包之后是否出现发送失败返回值。通过这个测试可以确定你的业务数据最大长度应该控制在多少字节以内以及是否需要调整 socket 缓冲区大小。我自己的验证习惯是保留这套 Python 脚本作为出厂测试程序的一部分。每次改版固件后用脚本跑一轮 10000 帧的稳定性测试丢包率达标才能交付。这套方法让我避免了很多次“代码改了没测出来上线跑两天就丢包”的血泪翻车。做 W5500 项目底层驱动一旦稳定业务层的坑就全是逻辑问题排查起来思路清晰得多。希望这篇笔记能帮你在 UDP 通讯这条路上少踩几个我踩过的坑。本文还有配套的精品资源点击获取
返回列表