STM32+W5500 UDP以太网通讯实战:SPI驱动、收发实现与排障

发布时间:2026/10/31 1:10:29
STM32+W5500 UDP以太网通讯实战:SPI驱动、收发实现与排障

简介:面向物联网项目实战的STM32+W5500以太网UDP通讯完整代码工程,适合嵌入式开发者和物联网初学者参考。工程基于STM32F103通过SPI接口驱动W5500,完整演示了DHCP动态获取IP、创建UDP服务、等待客户端连接、收发数据及关闭连接等核心流程,代码在STM32F103C8T6上验证运行,其他F103型号可通过修改KEIL芯片型号与FLASH容量快速适配。压缩包共182个文件,大小约5.88MB,包含44个.h头文件、42个.c源文件以及.uvprojx工程文件、.hex烧录文件、.axf调试文件、.uvoptx工程选项文件等,可直接用KEIL打开编译,标准外设库的定时器、Flash、RCC、ADC等配置均集成在内,便于完整复现实验环境。已有1442人学习下载。资源附带了JLINK/STLINK两种下载器选择提示,并保留了工程配置细节,尤其适合需要快速上手以太网UDP通信、并希望深入了解SPI驱动W5500底层机制的开发者查阅。

1. 物联网设备要把数据送上网,为什么我最后落在 STM32 + W5500 跑 UDP

手头这块板子要采集 12 路传感器数据,每隔 200ms 往工业网关推一次,中间还要响应上位机的参数下发。串口距离不够,CAN 要加网关,Wi-Fi 在产线里干扰太大,最后只能走有线的 RJ45 以太网。方案选型时先试过 STM32 直接跑 LwIP,驱动 DM9000 这类 MAC 芯片,结果裸机下 eth 中断、协议栈调度、内存池管理全压在主循环里,调了半个月还是会在高负载时复位。换 W5500 之后思路完全变了:TCP/IP 协议栈烧在 W5500 内部,DHCP、ARP、ICMP、UDP、TCP 全是硬件自己处理,MCU 只通过 SPI 读写寄存器,丢包率再也没有因为协议栈占用 CPU 而波动。这篇笔记就是把基于 STM32 + W5500 的 UDP 通讯从硬件接线到驱动封装、收发实现和排障经验完整讲透,适合正在做设备联网上报、上位机以太网通讯或者毕设里做网络通信的同学。

2. 从 CubeMX 到 W5500 上电:硬件接线、SPI 初始化与版本自检

2.1 模块选型:带不带网络变压器的 RJ45 差别很大

市面上的 W5500 模块分成两类,一类是纯 W5500 芯片加引脚排针,RJ45 接口需要自己外接带网络变压器的座子,另一类是把 W5500、网络变压器、RJ45 座子做到一块 PCB 上的集成模块。做产品我建议用集成模块,因为 W5500 是 3.3V 电平、RJ45 座子内部变压器的中心抽头要按数据手册接电阻电容,这一块布局走线没做好就会出现“能 ping 通但一加压就掉线”的玄学问题。做实验和毕设就不用纠结,直接选带 RJ45 的模块,省去变压器外围电路。

接线上一共 6 根线,STM32F103 和 W5500 都是 3.3V 电平,可以直接互连,不需要电平转换。标准接法是把 W5500 挂在 SPI1 上,因为 SPI1 的引脚 AFIO 映射在默认的 PA5/PA6/PA7,片上外设配置最简单:

STM32 引脚方向接 W5500说明
PA5 (SPI1_SCK)输出SCLK时钟,最高不要超过 33.3MHz
PA6 (SPI1_MISO)输入MISOW5500 输出,STM32 读数据
PA7 (SPI1_MOSI)输出MOSISTM32 写寄存器/数据
PA4 (GPIO)输出SCS片选,低有效
PB0 (GPIO)输出RST复位,低有效
PB1 (GPIO)输入INT中断引脚,轮询模式下可不接

提示:PA4 默认是 SPI1_NSS,要把它配置成普通 GPIO 输出并置高,绝不能交给 SPI 外设的硬件 NSS 管理,否则片选信号会和 SPI 时序打架。

2.2 用 CubeMX 把 SPI1 参数固定下来再动代码

很多教程一上来就贴 HAL_SPI_TransmitReceive 代码,但忽略了 CubeMX 里的初始化参数,导致后面通讯不稳定。W5500 的 SPI 接口支持模式 0 和模式 3,我常用的是模式 0,即 CPOL = Low、CPHA = 1Edge。数据帧长度固定 8 位,高位在前。这块在配置时就按下表选:

参数项选择值原因
ModeFull-Duplex Master主机模式,W5500 是从机
Data Size8 bit寄存器按字节寻址
First BitMSB FirstW5500 要求高位先行
Clock PolarityLowCPOL=0
Clock Phase1 EdgeCPHA=0,即 SPI Mode 0
Prescaler16(4.5MHz)先把速率降下来,跑通后再往上加

Prescaler 是最容易被忽略的参数。STM32F103 的 SPI1 挂在 APB2 上,时钟 72MHz,分频 16 后是 4.5MHz。很多新手在这里直接选 2 分频也就是 36MHz,虽然 W5500 标称支持 33.3MHz,但实际飞线连接、杜邦线过长时高速 SPI 的时序余量很小,读回版本号经常出错。先用 4.5MHz 把链路跑通,调优阶段再往上拉是更稳的顺序。

CubeMX 配置好后生成的初始化代码关键段长这样:

SPI_HandleTypeDef hspi1; hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);

这里 NSS 必须选 SOFT,PA4 保持普通 GPIO 输出。MISO 和 MOSI 的引脚复用由 CubeMX 自动配置,不需要手写 GPIO_InitStruct。

2.3 上电后第一件事:复位时序和读 VERSIONR 自检

W5500 没有上电自动复位逻辑,必须由 MCU 给出复位脉冲。常见做法是上电后把 RST 拉低至少 500us,再拉高,之后等待 10ms 让内部 PLL 稳定。有些模块的 RST 引脚已经接了 RC 复位电路,可以省掉这一步,但无论如何在初始化代码里做一次软复位更稳妥。

void w5500_hard_reset(void) { HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(50); /* 复位结束,等待芯片内部稳定,再访问寄存器 */ }

复位后第一件事是读 VERSIONR 寄存器,地址 0x0039,正常值应该是 0x04。这个自检通过了再往下配 MAC、IP,否则后面所有寄存器操作都是白搭。SPI 线序接错、时钟极性选反、片选没拉低都会导致读回的值是 0xFF 或者乱跳,这段自检代码就是第一道防线:

uint8_t w5500_check_version(void) { uint8_t version = w5500_read_reg(0x0039); return (version == 0x04) ? 0 : -1; }

如果 version 不是 0x04,优先检查 SPI 初始化的参数,其次用示波器抓 SCLK 和 MOSI 的空闲电平,确认 CPOL 是否匹配。

3. 把 W5500 驱动拆开:SPI 帧结构、寄存器读写与网络参数配置

3.1 W5500 的 SPI 帧结构:一次看懂为什么要封装底层函数

W5500 的 SPI 通信和普通 SPI 存储器不同,每一次访问都要先发送一个帧头,帧头由地址段和控制段组成。地址段告诉 W5500 要访问哪个寄存器或者哪块缓冲区,控制段告诉 W5500 这次是读还是写、操作哪个 Socket。常见的驱动库都把帧头封装在底层的 w5500_read_reg 和 w5500_write_reg 里,应用层只用传 16 位寄存器地址。

我采用的封装方式是:先传地址高字节、再传地址低字节,第三个字节是控制段。控制段低 2 位表示操作模式,0x01 是读、0x02 是写,高位用于区分通用寄存器、Socket 寄存器和收发缓冲区。这块封装一次,后面所有代码都基于它,不要在上层到处拼字节。

static uint8_t w5500_spi_xfer(uint8_t byte) { uint8_t rx = 0; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, HAL_MAX_DELAY); return rx; } void w5500_write_reg(uint16_t addr, uint8_t val) { w5500_cs_low(); w5500_spi_xfer((uint8_t)(addr >> 8)); w5500_spi_xfer((uint8_t)(addr & 0xFF)); w5500_spi_xfer(0x02); /* 控制段:写模式 */ w5500_spi_xfer(val); w5500_cs_high(); } uint8_t w5500_read_reg(uint16_t addr) { uint8_t val; w5500_cs_low(); w5500_spi_xfer((uint8_t)(addr >> 8)); w5500_spi_xfer((uint8_t)(addr & 0xFF)); w5500_spi_xfer(0x01); /* 控制段:读模式 */ val = w5500_spi_xfer(0xFF); w5500_cs_high(); return val; }

为什么要单独抽出 cs_low 和 cs_high?因为 W5500 对片选非常敏感,每次访问必须保证 CS 在整个帧过程中保持低电平,中途拉高再拉低会被芯片认为是新的一次访问。用 HAL 库自带的 HAL_SPI_Transmit 一次只发一个字节的话,如果每字节都手动切换 CS,帧头会被拆散,寄存器读写必然出错。上面的封装保证了 CS 在完整帧范围内唯一拉低一次。

3.2 网络参数初始化:把 MAC、IP、网关、掩码写进 W5500 的寄存器

W5500 内部有完整的 TCP/IP 协议栈,MCU 要做的只是告诉它本机在网络里的身份。需要配置四个信息:本机 MAC、本机 IP、默认网关、子网掩码。对应寄存器如下表,注意 W5500 多字节寄存器按大端存储,高位在前:

参数寄存器起始地址长度示例
网关 Gateway0x00014 字节192.168.1.1
子网掩码 Subnet0x00054 字节255.255.255.0
本机 MAC0x00096 字节00:08:DC:11:22:33
本机 IP0x000F4 字节192.168.1.100

初始化函数如下,多字节寄存器的写入要按地址单调递增的顺序一次写完,不要分开写高低字节,否则中间掉电会有半写状态:

void w5500_network_init(uint8_t *mac, uint8_t *ip, uint8_t *gw, uint8_t *mask) { w5500_write_buf(0x0001, gw, 4); /* 网关 */ w5500_write_buf(0x0005, mask, 4); /* 子网掩码 */ w5500_write_buf(0x0009, mac, 6); /* 本地 MAC */ w5500_write_buf(0x000F, ip, 4); /* 本地 IP */ }

w5500_write_buf 是前面 write_reg 的批量版本,作用是连续写多个字节,内部同样保证 CS 只拉低一次。这里写 MAC 地址时需要注意,W5500 芯片本身不带 MAC,出厂也没有全球唯一编号,必须软件赋值。很多设备出问题是因为多台板子烧了同一个 MAC,在局域网内导致 ARP 表互相覆盖,UDP 报文时通时不通。如果是做产品,MAC 地址要从厂商申请或者至少用单片机唯一的 ID 号动态生成。

3.3 为什么缓冲页要单独划分:Socket 发收缓冲的配平

W5500 内部有 16KB 缓冲,8KB 给发送,8KB 给接收,由 Sn_RXBUF_SIZE 和 Sn_TXBUF_SIZE 寄存器控制每个 Socket 分到的页数。寄存器地址是 0x001E 和 0x001F,每两个 bit 代表一个 Socket,值就是对应 Socket 分到的 2KB 页数量。

做 UDP 通讯时大部分场景同时只用一个 Socket,我一般把 Socket0 的发送和接收都设为 2KB,也就是 TXBUF_SIZE 的低两位写 1,RXBUF_SIZE 的低两位写 1。如果业务上需要一边上报传感器数据、一边下发配置命令,建议发送缓冲给大一点,比如接收 1KB、发送 3KB。缓冲区分配会影响后面的发送丢包率,UDP 虽然无连接,但 W5500 内部发送队列满了之后新的 SEND 命令会被丢弃,这是应用层最容易踩的坑。

void w5500_buffer_init(void) { /* 0x001E: TXBUF_SIZE,bit1:0 对应 Socket0,值 01 表示 2KB */ w5500_write_reg(0x001E, 0x01); /* 0x001F: RXBUF_SIZE,同样给 Socket0 分配 2KB */ w5500_write_reg(0x001F, 0x01); }

这个配置要在打开 Socket 之前完成,改完缓冲区大小后必须对 W5500 做一次软复位,否则新的大小不会生效。软复位的方式是把 0x0000 地址的 MR 寄存器最高位置 1,然后等待复位完成。

4. UDP 通讯落地:从 Socket 初始化到收发包状态机

4.1 Socket 0 初始化成 UDP 模式:序列、命令与状态检查

W5500 的 Socket 操作本质上是一个命令状态机。每一个 Socket 都有一组独立寄存器,Socket0 的寄存器起始地址是 0x0000,Sn_MR 在偏移 0,Sn_CR 在偏移 1,Sn_SR 在偏移 3。初始化流程分三步:

  • 写 Sn_MR 设置协议类型,UDP 是 0x02
  • 写本地端口 Sn_PORT
  • 往 Sn_CR 写 OPEN 命令 0x10,然后读 Sn_SR 确认进入 SOCK_UDP 状态 0x42
#define SOCK_UDP 0x42 #define SOCK_OPEN 0x10 void w5500_udp_init(uint8_t sock, uint16_t local_port) { /* Sn_MR = 0x02 (UDP) */ w5500_write_reg(sock_base(sock) + 0x00, 0x02); /* Sn_PORT 是 16 位,高低字节连续写 */ w5500_write_buf(sock_base(sock) + 0x04, &local_port, 2); /* 发送 OPEN 命令 */ w5500_write_reg(sock_base(sock) + 0x01, SOCK_OPEN); /* 等待进入 UDP 就绪状态,带超时保护 */ uint16_t timeout = 1000; while (w5500_read_reg(sock_base(sock) + 0x03) != SOCK_UDP) { HAL_Delay(1); if (--timeout == 0) break; } }

判断 Socket 是否真的打开有两种方式,一种是轮询 Sn_SR,另一种是查询 Sn_IR 中断寄存器的 SOCK_OPEN 位。在裸机项目里轮询 Sn_SR 是最直观的,只要带超时保护就不会卡死主循环。初始化完成后,W5500 就已经在监听本地端口了,上位机往这个 IP 和端口发 UDP 包,W5500 会自动收进 RX 缓冲区。

4.2 发送一条 UDP 报文:写目标地址、查缓冲、触发 SEND

UDP 发送比 TCP 简单,不需要建立连接,但 W5500 的做法是在发送前把目标 IP 和端口写到 Socket 的目标地址寄存器里,然后数据写入发送缓冲区,最后触发 SEND 命令。这一套动作的顺序不能乱,先写目标地址再写数据,否则 W5500 会把上一次的目标地址做默认目标。

uint16_t w5500_udp_send(uint8_t sock, uint8_t *data, uint16_t len, uint8_t *dst_ip, uint16_t dst_port) { uint16_t free_size, offset; /* 1. 写目标 IP 和端口 */ w5500_write_buf(sock_base(sock) + 0x0C, dst_ip, 4); w5500_write_buf(sock_base(sock) + 0x10, &dst_port, 2); /* 2. 查询发送缓冲剩余空间 */ free_size = w5500_read_reg16(sock_base(sock) + 0x20); if (free_size < len) return 0; /* 3. 取当前写指针 */ offset = w5500_read_reg16(sock_base(sock) + 0x24) & 0x1FFF; /* 4. 写数据到发送缓冲区,基地址固定 0x4000 */ w5500_write_buf(0x4000 + offset, data, len); /* 5. 写指针前移,触发 SEND */ w5500_write_reg16(sock_base(sock) + 0x24, offset + len); w5500_write_reg(sock_base(sock) + 0x01, 0x20); return len; }

几个寄存器含义要说明白:Sn_TX_FSR 在偏移 0x20,表示发送缓冲区剩余字节数;Sn_TX_WR 在偏移 0x24,是写指针,W5500 从硬件读指针到写指针之间的数据视为待发送。写指针更新后立即发 SEND 命令,W5500 硬件会自动完成以太网帧封装、ARP 解析和发送,MCU 不需要关心这些。

需要注意 W5500 的缓冲区地址是 0x4000 开始的 8KB 空间,偏移用 0x1FFF 掩码取余,超过缓冲区尾部会回绕。上面代码没有处理回绕的情况,如果 offset + len 超过 8192 就会写穿。在实际项目中把写数据改成两段式回绕写入,第一段从 offset 写到缓冲末尾,第二段从 0x4000 开始写剩余部分。

4.3 接收一条 UDP 报文:先读 8 字节头再读数据

W5500 收包之后把数据存在 RX 缓冲区,数据前面自动带了 8 字节的包信息头,依次是 4 字节发送端 IP、2 字节发送端端口、2 字节数据长度。接收处理的顺序也很固定:先查 Sn_RX_RSR 看到包数不为 0,再读 Sn_RX_RD 得到读指针,从 RX 缓冲区读 8 字节头,再根据长度字段读数据,最后更新读指针并回写 RECV 命令释放缓冲。

uint16_t w5500_udp_recv(uint8_t sock, uint8_t *buf, uint16_t max_len, uint8_t *src_ip, uint16_t *src_port) { uint16_t rx_size, offset, data_len; uint8_t header[8]; rx_size = w5500_read_reg16(sock_base(sock) + 0x26); if (rx_size == 0) return 0; offset = w5500_read_reg16(sock_base(sock) + 0x28) & 0x1FFF; /* 读 8 字节包头:IP(4) + Port(2) + Len(2) */ w5500_read_buf(0x6000 + offset, header, 8); memcpy(src_ip, header, 4); memcpy(src_port, &header[4], 2); data_len = (header[6] << 8) | header[7]; if (data_len > max_len) data_len = max_len; /* 读数据,注意回绕,这里简化处理 */ w5500_read_buf(0x6000 + offset + 8, buf, data_len); /* 读指针移动到包末尾,回写 RECV 命令 */ w5500_write_reg16(sock_base(sock) + 0x28, offset + 8 + data_len); w5500_write_reg(sock_base(sock) + 0x01, 0x40); return data_len; }

这里最关键的易错点是最后一个 RECV 命令。W5500 只有收到 RECV 命令后才会释放这段缓冲,如果应用层读完数据忘了回写,缓冲区会被持续占用,收到几包后 Sn_RX_RSR 变成满值,新包直接丢弃。数据长度字段是网络字节序,按大端转换,转换错会读出几百字节的垃圾。

4.4 主循环轮询:用标志位驱动收包、解析、上报

收发函数准备好之后,主循环里不需要复杂的状态机,按照“收包 → 处理 → 回包”的顺序轮询即可。实际项目中我习惯把收包做成模块函数,通过一个全局标志通知业务层有新数据,避免在中断里做耗时的 UDP 处理。

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x11, 0x22, 0x33}; uint8_t ip[4] = {192, 168, 1, 100}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t mask[4]= {255, 255, 255, 0}; w5500_hard_reset(); w5500_network_init(mac, ip, gw, mask); w5500_buffer_init(); w5500_udp_init(0, 5000); /* Socket0 监听本地 5000 端口 */ uint8_t rx_buf[512]; uint8_t src_ip[4]; uint16_t src_port, len; while (1) { len = w5500_udp_recv(0, rx_buf, sizeof(rx_buf), src_ip, &src_port); if (len > 0) { /* 收到上位机命令,解析后采集传感器数据并回传 */ } HAL_Delay(5); } }

轮询间隔选 5ms 是我在多个项目里验证过的折中值,既能保证 200ms 周期的上报任务不丢数据,又不会让 SPI 总线被无效查询占满。对接收实时性要求更高的场景,可以用 W5500 的 INT 引脚配合外部中断,数据到达时拉低 INT,MCU 进中断后只标记标志位、不做处理,主循环里消费数据。

5. 实战避坑:UDP 通不了、丢包和电气玄学的五个排查记录

5.1 读 VERSIONR 永远是 0xFF:SPI 模式不对、线序接反或者 CS 没拉低

现象是初始化时版本检查永远失败,读 0x0039 返回 0xFF 或者 0x00。

原因是多方面的,按概率排:SPI 时钟极性配置反了,CPOL/CPHA 和 W5500 的要求不一致;MOSI/MISO 接反,W5500 根本没收到有效命令;CS 引脚被 SPI 外设的硬件 NSS 占用,帧头被打断。

解决方法是先用示波器抓 SCLK 空闲电平,如果空闲是高电平说明 CPOL 配错;然后用 GPIO 翻转方式手动测试 MOSI 通路,把 MOSI 短接 MISO 自发自收验证 SPI 外设本身通不通;最后把 CS 的 GPIO 初始化单独拎出来看是否配置成了推挽输出且默认高。

5.2 ping 通设备但 UDP 上位机收不到:ARP 缓存和防火墙双重影响

现象是电脑 ping 设备 IP 能通,但设备发 UDP 包到电脑,网络调试助手完全没反应。

ping 通说明物理链路、IP 配置、ARP 解析都正常,问题大概率出在发送方向和目标端口上。第一步查 W5500 的发送目标地址,检查代码里 Sn_DIPR 和 Sn_DPORT 写入时字节序是否正确;第二步看电脑防火墙是否拦截了目标端口,UDP 入站默认需要单独放行,尤其 5000 以上端口在 Windows 上经常被静默丢弃。

我的排查顺序是:先在同一台电脑上用 UDP 调试助手回环测试查电脑本机网络,再用 Wireshark 抓包看 W5500 是否发出了 ARP 请求和 UDP 报文。如果 Wireshark 只能看到 ARP 看不到 UDP,说明发送流程没走完;如果 UDP 报文已经在链路上但调试助手没显示,基本可以断定是防火墙拦截。

5.3 收包越收越慢最后断流:Sn_RX_RD 没回写 RECV

现象是设备刚启动时能正常收几包数据,之后上位机怎么发都收不到,重启设备后又能恢复收包,然后再次断流。

原因是接收缓冲区被占满而不释放。W5500 的接收缓冲是硬件管理的环形队列,应用层读完数据必须把 Sn_RX_RD 更新到包尾,然后写 RECV 命令让硬件回收这段空间。遗漏更新的常见触发点有两个:一是 data_len 超过接收缓冲区剩余空间时没有做截断和回绕处理;二是在包头解析异常时提前 return,跳过了收尾回写。

解决的根治办法是把接收函数写成单一出口,包头解析和长度校验只做标记,最后统一走到更新 Sn_RX_RD 和 RECV 命令的代码路径。我在代码里加了 err_ 开头的跳转标签,任何异常分支都会走收尾逻辑,解决后连续 24 小时打流没有再出现断流。

5.4 发出去的包数据错位、长度变成 0xFFFF:长度字段字节序错

现象是设备发给上位机的 UDP 包能到达,但数据内容整体错位,长度字段解析出来是 65535 或乱码。

原因基本锁定在字节序处理上。W5500 的 Sn_PORT、Sn_TX_WR、Sn_RX_RD 以及包头里的长度字段都是大端存储,STM32 默认是小端。如果用 memcpy 直接拷贝 uint16_t 到寄存器或者把接收到的长度字节直接按小端拼接,高低字节就会反。

解决方法是统一封装 ntohs 和 htons 这类字节序转换宏,在所有涉及 16 位寄存器和包头长度字段的地方都经过转换。我第一次写 W5500 驱动时就是在 Sn_TX_WR 写指针上少转了一次,导致数据定位到错误偏移,发出去的包尾部全是 0x00。

5.5 设备断电重启后偶发不通:复位脉冲宽度不够

现象是设备冷启动时大约十分之一的概率以太网不通,按下复位键后又恢复,用万用表量 RJ45 座子的链路指示灯发现没亮。

原因是 MCU 的复位引脚给 W5500 的脉冲宽度太窄。W5500 数据手册要求复位低电平持续时间至少 500us,很多代码里复位拉低后用 HAL_Delay 延时不够,或者用了延时函数在优化等级较高时被编译器裁掉,导致复位不彻底。

解决方法是把复位拉低时间做到 50ms 以上,给足裕量,同时检查 RST 引脚是否被复用成了 JTAG/SWD 功能。STM32F103 的 PB0 没有复用冲突,但如果是 PA13/PA14 这类引脚就要注意默认 JTAG 占用。

6. 进阶验证:用 iperf3 UDP 打流和 Wireshark 抓包校准 W5500 的真实吞吐

代码跑通只是第一步,要验证这套方案值不值得投入,必须用工具量化吞吐和稳定性。常见做法是把 STM32 上的 UDP 接收函数改造成计数回传模式,设备收到多少字节就回传多少字节,然后在电脑上用 iperf3 以 UDP 模式打流,观察实际接收带宽和丢包率。

iperf3 的命令是服务端和客户端两端配合,但因为 W5500 是嵌入式设备,没法直接跑 iperf3 客户端,所以要让 W5500 扮演回显服务器,iperf3 只跑在电脑上,用 -u 指定 UDP、-b 指定目标带宽:

iperf3 -c 192.168.1.100 -u -b 2M -l 512 -t 60

-w 和 -l 参数分别控制窗口和包长,-b 从 1M 逐步加到 10M,观察丢包率在哪一档开始飙升。我实测下来 W5500 在 4.5MHz SPI 下的 UDP 回显吞吐稳定在 1.5Mbps 左右,SPI 得分频到 18MHz 后能到 6Mbps,再往上丢包率就会明显抬升。

Wireshark 抓包验证的重点不是能不能看到包,而是看 ARP 请求频率和是否有 IP 分片。正常稳定传输时 ARP 只在第一个包发出前出现一次,如果抓到的 ARP 请求特别频繁,说明目标设备 MAC 在老化或冲突。另外 UDP 数据超过 1472 字节就会触发 IP 分片,W5500 的硬协议栈虽然支持分片重组,但分片包到达乱序时丢包率显著上升。所以应用层发 UDP 时数据长度我建议卡在 1400 字节以内,留出余量给 IP 头和以太网帧头。

这套基于 STM32 + W5500 的 UDP 通讯方案,我在产线设备上跑了两年,期间只因为路由器 ARP 表老化出现过偶发断流,加心跳保活后彻底解决。如果你也准备用这个方案做上位机通讯,建议把心跳机制和缓冲区尺寸在立项时就定下来,别等联调阶段再回头改硬件,希望这些实现细节和踩坑记录能帮你少走一段弯路。

本文还有配套的精品资源,点击获取