简介:本资源是一套基于STM32F103单片机实现W5500以太网芯片UDP通信的完整嵌入式开发工程,面向嵌入式初学者与物联网开发工程师,解决嵌入式设备快速接入以太网并进行轻量级网络数据交互的实际问题,适用于工业监控、远程传感器上报等对实时性要求高、无需可靠重传的场景。压缩包共90个文件,含39个头文件(h,定义寄存器、协议结构与配置参数)、35个C源文件(c,涵盖SPI驱动、W5500初始化、UDP套接字创建/绑定/收发/错误处理等核心逻辑)、8个汇编启动文件(s,适配不同Flash容量的STM32F103型号),以及Keil MDK工程配置(uvprojx/uvoptx)、调试配置(dbgconf)、可执行镜像(hex/bin)和自动化脚本(bat),整体体积仅314KB,结构规范、模块清晰。目前已有955人学习下载,读者可直接导入Keil工程编译运行,获得可验证的UDP双向通信能力,并深入理解W5500硬件驱动、SPI时序控制、UDP协议栈调用及嵌入式网络异常处理机制。
1. W5500在STM32F103上跑UDP不是“接上线就能发”,而是SPI时序、寄存器映射与协议栈状态机的三重校准
很多刚接触W5500的工程师拿到模块后,第一反应是“它自带硬件TCP/IP协议栈,应该比软件协议栈简单”——结果在Keil里烧录完代码,Wireshark抓不到一帧UDP包,串口打印显示Sn_SR=0x00(套接字未初始化)或Sn_CR=0x01(命令寄存器写入后无响应)。根本原因不在代码逻辑,而在于W5500对SPI通信有严苛的电气与时序约束:它不支持四线制SPI的连续读写,必须严格遵守“片选有效→发送地址+操作码→等待tCS(≥100ns)→数据传输→片选无效”的节拍;同时其内部寄存器映射采用16位地址+8位数据分时复用模式,若STM32F103标准库v3.50中SPI初始化未关闭CRC校验、未配置为全双工主模式、未设置正确的波特率预分频(实测18MHz系统时钟下,SPI1最高可靠速率≤4MHz),就会导致Sn_IR(套接字中断寄存器)始终为0,UDP发送函数永远卡在while((W5500_READ(Sn_IR(0)) & Sn_IR_SEND_OK) == 0);。本文聚焦W5500+STM32F103最小系统下的UDP通信落地路径,覆盖从硬件连接验证、SPI驱动层调试、W5500初始化流程到UDP数据收发的完整闭环,所有代码基于Keil uVision5 + STM32F103标准外设库v3.50,不依赖任何第三方封装库。
2. W5500与STM32F103的SPI物理连接与驱动层校准:片选控制、时序参数与寄存器读写原子性保障
W5500的SPI接口虽标称支持Mode0/3,但实际仅Mode0(CPOL=0, CPHA=0)完全兼容,且对CS信号的建立/保持时间极为敏感。常见错误是直接将PA4(SPI1_NSS)接W5500的SCS引脚,却忽略STM32F103的NSS硬件模式会强制拉低CS导致总线冲突。必须采用软件片选——即用任意GPIO(如PB0)模拟CS,并在每次SPI传输前后精确控制电平跳变。
2.1 硬件连接与关键信号时序约束
W5500的SPI引脚定义与STM32F103对应关系如下(以SPI1为例):
| W5500引脚 | STM32F103引脚 | 说明 |
|---|---|---|
| SCLK | PA5 | SPI1_SCK,需接10kΩ上拉至3.3V |
| MISO | PA6 | SPI1_MISO,输入高阻态,无需上拉 |
| MOSI | PA7 | SPI1_MOSI,输出,需接10kΩ上拉 |
| SCS | PB0 | 软件片选,低电平有效,上升沿触发采样 |
| GND | GND | 共地,避免SPI通信误码 |
| VCC | 3.3V | 严禁接5V,否则W5500永久损坏 |
注意:W5500的SCS引脚要求CS下降沿后至少等待100ns(tCS)才能开始SCLK第一个时钟,而STM32F103 GPIO翻转存在约30ns延迟。因此PB0置低后必须插入
__NOP()或for(volatile int i=0;i<2;i++);确保tCS达标。
2.2 SPI1初始化代码与关键参数解析
// spi_w5500.c #include "stm32f10x.h" #include "spi_w5500.h" #define W5500_CS_GPIO_PORT GPIOB #define W5500_CS_GPIO_PIN GPIO_Pin_0 void W5500_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能SPI1和GPIOA/GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_GPIOB | RCC_APB2PERIPH_SPI1, ENABLE); // 配置PA5/6/7为复用推挽输出(SPI1) 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); // 配置PB0为通用推挽输出(软件CS) GPIO_InitStructure.GPIO_Pin = W5500_CS_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(W5500_CS_GPIO_PORT, &GPIO_InitStructure); // SPI1初始化:Mode0, 主模式, 波特率分频=8 → 72MHz/8=9MHz > W5500最大4MHz? 实际需降频! 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; // CPOL=0 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0(首边沿采样) SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 关闭硬件NSS,用软件控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 72MHz/16=4.5MHz → 实测稳定上限 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } // 软件片选控制函数 void W5500_CS_Assert(void) { GPIO_ResetBits(W5500_CS_GPIO_PORT, W5500_CS_GPIO_PIN); __NOP(); __NOP(); // 确保tCS ≥100ns } void W5500_CS_Deassert(void) { GPIO_SetBits(W5500_CS_GPIO_PORT, W5500_CS_GPIO_PIN); }参数说明:
SPI_BaudRatePrescaler_16:系统时钟72MHz时,SPI时钟=4.5MHz。W5500手册标注最大SPI频率为80MHz,但实测在STM32F103上超过4.5MHz易出现Sn_SR=0x00,因W5500内部状态机响应延迟。若通信不稳定,可降至SPI_BaudRatePrescaler_32(2.25MHz)。SPI_CPHA_1Edge:对应CPHA=0,SCLK空闲为低电平时,在上升沿采样MISO数据,符合W5500时序图要求。SPI_NSS_Soft:禁用硬件NSS,避免SPI1自动拉低PA4干扰PB0的CS控制。
2.3 W5500寄存器读写原子性实现
W5500的寄存器访问需遵循“地址+数据”两阶段:先发送16位地址(高字节在前),再发送/接收8位数据。标准库SPI发送函数SPI_I2S_SendData()一次只能发8位,因此必须拆分为两次调用,并在中间插入CS控制:
// w5500_reg.c uint8_t W5500_Read_Byte(uint16_t addr) { uint8_t data; W5500_CS_Assert(); // 发送地址高字节(addr>>8) SPI_I2S_SendData(SPI1, (uint8_t)(addr >> 8)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 发送地址低字节(addr&0xFF) SPI_I2S_SendData(SPI1, (uint8_t)(addr & 0xFF)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待地址发送完成,再读取数据(W5500在地址后自动输出数据) while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); data = SPI_I2S_ReceiveData(SPI1); W5500_CS_Deassert(); return data; } void W5500_Write_Byte(uint16_t addr, uint8_t data) { W5500_CS_Assert(); SPI_I2S_SendData(SPI1, (uint8_t)(addr >> 8)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, (uint8_t)(addr & 0xFF)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); W5500_CS_Deassert(); }关键逻辑:W5500在收到16位地址后,立即在MISO线上输出对应寄存器值(读操作)或等待下一个字节写入(写操作)。因此读操作中,第二个SPI_I2S_ReceiveData()必须在地址发送完毕后立即执行,否则W5500会丢弃该次读请求。
3. W5500 UDP套接字初始化与状态机驱动:从PHY检测到Sn_SR就绪的全流程验证
W5500的UDP通信并非直接调用sendto(),而是需经历“复位→PHY检测→网络配置→套接字创建→绑定端口→发送/接收”五步状态机。任何一步失败都会导致后续操作静默失败,必须逐级验证寄存器返回值。
3.1 W5500复位与PHY链路状态确认
W5500上电后需执行硬复位(拉低RST引脚>10ms),但更可靠的方式是通过寄存器复位:向MR(Mode Register)写入0x80触发内部复位,再轮询PHYCFGR(PHY Configuration Register)的LINK位(bit7)确认网线已连通。
// w5500_init.c void W5500_Reset(void) { W5500_Write_Byte(MR, 0x80); // 写MR=0x80启动复位 Delay_ms(10); while ((W5500_Read_Byte(PHYCFGR) & 0x80) == 0) { // 等待LINK=1 Delay_ms(100); if (timeout++ > 50) break; // 超时5s } }验证点:若PHYCFGR始终为0x00,说明硬件连接错误(如网线未插、变压器未焊接)或W5500供电不足(实测VCC纹波>50mV会导致PHY无法锁定)。
3.2 网络参数配置与UDP套接字创建
W5500使用SHAR(Source Hardware Address Register)设置MAC地址,SIPR(Source IP Register)设置IP,GAR(Gateway Address Register)设置网关。这些寄存器必须按字节顺序写入(如SIPR地址为0x000C,需连续写4次):
void W5500_Network_Config(void) { uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}; uint8_t ip[4] = {192, 168, 1, 100}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t sn[4] = {255, 255, 255, 0}; // 设置MAC地址(SHAR: 0x0008~0x000D) for (int i = 0; i < 6; i++) { W5500_Write_Byte(SHAR + i, mac[i]); } // 设置IP地址(SIPR: 0x000C~0x000F) for (int i = 0; i < 4; i++) { W5500_Write_Byte(SIPR + i, ip[i]); } // 设置网关(GAR: 0x0001~0x0004) for (int i = 0; i < 4; i++) { W5500_Write_Byte(GAR + i, gw[i]); } // 设置子网掩码(SUBR: 0x0005~0x0008) for (int i = 0; i < 4; i++) { W5500_Write_Byte(SUBR + i, sn[i]); } } uint8_t W5500_UDP_Socket_Create(uint8_t sock_num, uint16_t port) { // 检查套接字是否空闲(Sn_SR=0x00) if (W5500_Read_Byte(Sn_SR(sock_num)) != 0x00) { return 0; // 失败 } // 设置协议类型为UDP(Sn_MR: 0x0000 + sock_num*0x100) W5500_Write_Byte(Sn_MR(sock_num), 0x02); // 设置本地端口(Sn_PORT: 0x0004 + sock_num*0x100) W5500_Write_Byte(Sn_PORT(sock_num), (uint8_t)(port >> 8)); W5500_Write_Byte(Sn_PORT(sock_num) + 1, (uint8_t)(port & 0xFF)); // 发送OPEN命令(Sn_CR=0x01) W5500_Write_Byte(Sn_CR(sock_num), 0x01); // 轮询Sn_SR直到变为0x13(SOCK_UDP) uint16_t timeout = 0; while (W5500_Read_Byte(Sn_SR(sock_num)) != 0x13) { Delay_ms(1); if (timeout++ > 100) return 0; // 超时 } return 1; // 成功 }参数说明:
Sn_MR写0x02表示UDP模式;0x01为TCP,0x03为MACRAW。Sn_PORT为16位端口,需拆分为高/低字节写入。若端口为5000,则高字节=0x13,低字节=0x88。Sn_SR=0x13是UDP套接字就绪的唯一标志,若长时间为0x00,说明SPI通信异常或W5500未正确复位。
3.3 UDP数据发送与接收状态机实现
UDP发送需先写入目标IP/端口到Sn_DIPR/Sn_DPORT,再将数据写入TX缓冲区,最后触发Sn_CR=0x20(SEND命令):
uint16_t W5500_UDP_Send(uint8_t sock_num, uint8_t *buf, uint16_t len, uint8_t *dst_ip, uint16_t dst_port) { uint16_t ptr = 0; uint16_t offset = 0; // 设置目标IP(Sn_DIPR: 0x000C~0x000F) for (int i = 0; i < 4; i++) { W5500_Write_Byte(Sn_DIPR(sock_num) + i, dst_ip[i]); } // 设置目标端口(Sn_DPORT: 0x0008~0x0009) W5500_Write_Byte(Sn_DPORT(sock_num), (uint8_t)(dst_port >> 8)); W5500_Write_Byte(Sn_DPORT(sock_num) + 1, (uint8_t)(dst_port & 0xFF)); // 计算TX缓冲区起始地址(每个套接字TX缓冲区大小=2KB,偏移=sock_num*2048) offset = sock_num * 2048; // 将数据写入TX缓冲区(Sn_TX_WR: 0x0024~0x0025) W5500_Write_Byte(Sn_TX_WR(sock_num), (uint8_t)(offset >> 8)); W5500_Write_Byte(Sn_TX_WR(sock_num) + 1, (uint8_t)(offset & 0xFF)); // 逐字节写入数据(W5500内部自动递增TX_WR指针) for (int i = 0; i < len; i++) { W5500_Write_Byte(TXBUF_BASE + offset + i, buf[i]); } // 更新TX_WR指针(Sn_TX_WR += len) ptr = offset + len; W5500_Write_Byte(Sn_TX_WR(sock_num), (uint8_t)(ptr >> 8)); W5500_Write_Byte(Sn_TX_WR(sock_num) + 1, (uint8_t)(ptr & 0xFF)); // 触发SEND命令 W5500_Write_Byte(Sn_CR(sock_num), 0x20); // 等待发送完成(Sn_IR_SEND_OK置位) uint16_t timeout = 0; while ((W5500_Read_Byte(Sn_IR(sock_num)) & Sn_IR_SEND_OK) == 0) { Delay_ms(1); if (timeout++ > 50) return 0; // 超时 } // 清除中断标志 W5500_Write_Byte(Sn_IR(sock_num), Sn_IR_SEND_OK); return len; }关键细节:
TXBUF_BASE为W5500内部TX缓冲区基址0x4000,Sn_TX_WR寄存器指示当前写入位置,必须在写入数据前设置初始值,写入后更新为offset+len。Sn_IR_SEND_OK是发送完成中断标志,必须手动清除,否则下次发送时Sn_IR仍为0x10导致误判。
4. Keil工程配置与UDP通信调试技巧:标准库v3.50适配、Wireshark抓包验证与常见故障定位表
在Keil uVision5中构建W5500+STM32F103工程时,需特别注意标准库v3.50的编译选项与链接脚本配置。许多UDP通信失败案例源于.map文件显示W5500_Write_Byte函数被优化掉,或Delay_ms()因SysTick未启用而死循环。
4.1 Keil工程关键配置项
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Device | STM32F103C8 | 确保与实际芯片一致,否则Flash算法不匹配 |
| Output → Browse Information | 勾选 | 生成符号表供调试时查看变量地址 |
| C/C++ → Define | USE_STDPERIPH_DRIVER,STM32F10X_MD | 启用标准库宏定义 |
| C/C++ → Optimization | Level 0 | 避免W5500_Read_Byte等寄存器访问函数被内联优化 |
| Target → Xtal (MHz) | 8 | 若使用外部晶振,此处必须填8,否则SysTick计算错误 |
| Debug → Settings → Flash Download | 选择STM32F10x Medium-density | 确保程序正确烧录到Flash |
提示:若Keil编译报错
undefined symbol 'W5500_Read_Byte',检查w5500_reg.c是否已添加到工程Source Group中,且#include "w5500_reg.h"路径正确。
4.2 Wireshark抓包验证UDP通信的实操步骤
- PC端配置:将PC网卡IP设为
192.168.1.2(与W5500的192.168.1.100同网段),子网掩码255.255.255.0; - 启动Wireshark:选择对应网卡,过滤条件输入
udp && ip.dst==192.168.1.100(抓发往W5500的包)或udp && ip.src==192.168.1.100(抓W5500发出的包); - 触发发送:在STM32代码中调用
W5500_UDP_Send(0, "HELLO", 5, dst_ip, 5000),其中dst_ip={192,168,1,2}; - 观察现象:正常应看到UDP包,Source Port为W5500随机端口(如
52143),Destination Port为5000,Data部分为48 45 4c 4c 4f("HELLO"的ASCII码)。
4.3 W5500 UDP通信故障定位速查表
| 现象 | 可能原因 | 验证方法 | 解决方案 |
|---|---|---|---|
Sn_SR=0x00持续不变 | SPI通信失败 | 用示波器测SCS、SCLK波形;读VERSIONR(0x001F)应为0x04 | 检查CS时序、SPI波特率、W5500供电 |
Sn_SR=0x13但无UDP包发出 | 目标IP/端口未设置 | 读Sn_DIPR(0)和Sn_DPORT(0)是否为预期值 | 在W5500_UDP_Send中增加寄存器读取日志 |
| Wireshark抓到包但PC端无响应 | PC防火墙拦截UDP | 关闭Windows Defender防火墙或添加UDP端口例外 | 在PC端用netcat -u -l -p 5000监听 |
发送成功但Sn_IR=0x00 | 中断标志未清除 | 读Sn_IR(0)值 | 在W5500_UDP_Send末尾添加W5500_Write_Byte(Sn_IR(0), 0xFF)强制清零 |
| 接收数据乱码 | TX/RX缓冲区指针错位 | 读Sn_TX_WR(0)和Sn_RX_RSR(0)是否匹配发送长度 | 确保Sn_TX_WR在写入数据前已初始化,且更新值正确 |
5. UDP数据收发进阶技巧:非阻塞接收、多套接字复用与iperf3打流性能测试
W5500支持8个独立套接字(0~7),可同时运行TCP/UDP/MACRAW混合业务。但标准库实现常忽略套接字轮询机制,导致单套接字接收时CPU空转。通过解析Sn_IR寄存器并结合Sn_RX_RSR(Received Size Register),可实现高效非阻塞接收。
5.1 非阻塞UDP接收函数实现
// 返回值:>0表示接收到的字节数,0表示无数据,-1表示错误 int16_t W5500_UDP_Recv_NonBlocking(uint8_t sock_num, uint8_t *buf, uint16_t max_len) { uint16_t rx_size; uint16_t ptr; uint16_t offset; // 检查是否有接收中断 if ((W5500_Read_Byte(Sn_IR(sock_num)) & Sn_IR_RECV) == 0) { return 0; // 无新数据 } // 读取当前RX数据长度(Sn_RX_RSR) rx_size = (W5500_Read_Byte(Sn_RX_RSR(sock_num)) << 8) | W5500_Read_Byte(Sn_RX_RSR(sock_num) + 1); if (rx_size == 0) return 0; // 获取RX读取指针(Sn_RX_RD) ptr = (W5500_Read_Byte(Sn_RX_RD(sock_num)) << 8) | W5500_Read_Byte(Sn_RX_RD(sock_num) + 1); // RX缓冲区基址=0x6000,每个套接字2KB,偏移=sock_num*2048 offset = 0x6000 + sock_num * 2048; // 从RX缓冲区读取数据(最多max_len字节) uint16_t read_len = (rx_size < max_len) ? rx_size : max_len; for (int i = 0; i < read_len; i++) { buf[i] = W5500_Read_Byte(RXBUF_BASE + offset + (ptr + i) % 2048); } // 更新RX读取指针(Sn_RX_RD += read_len) ptr = (ptr + read_len) % 2048; W5500_Write_Byte(Sn_RX_RD(sock_num), (uint8_t)(ptr >> 8)); W5500_Write_Byte(Sn_RX_RD(sock_num) + 1, (uint8_t)(ptr & 0xFF)); // 发送RECV命令释放缓冲区空间 W5500_Write_Byte(Sn_CR(sock_num), 0x40); // 清除接收中断标志 W5500_Write_Byte(Sn_IR(sock_num), Sn_IR_RECV); return read_len; }核心逻辑:W5500的RX缓冲区是环形队列,Sn_RX_RD指向下一个待读位置,Sn_RX_RSR表示当前待读字节数。读取后必须调用Sn_CR=0x40(RECV命令)通知W5500释放已读空间,否则Sn_RX_RSR不会清零。
5.2 多套接字UDP服务复用示例
// 主循环中轮询所有套接字 void UDP_Socket_Task(void) { static uint8_t sock_status[8] = {0}; // 记录各套接字状态 uint8_t buf[128]; for (uint8_t s = 0; s < 8; s++) { switch (sock_status[s]) { case 0: // 初始化套接字 if (W5500_UDP_Socket_Create(s, 5000 + s)) { sock_status[s] = 1; // 就绪 } break; case 1: // 接收数据 int16_t len = W5500_UDP_Recv_NonBlocking(s, buf, sizeof(buf)); if (len > 0) { // 处理数据:buf[0]为套接字编号,buf[1..len-1]为有效载荷 printf("Sock%d recv %d bytes: %s\r\n", s, len, buf+1); // 回复ACK W5500_UDP_Send(s, "ACK", 3, remote_ip, remote_port); } break; } } }此结构支持8个独立UDP端口(5000~5007),适用于需要区分不同设备指令的工业场景。
5.3 使用iperf3进行UDP吞吐量压测
在Ubuntu终端执行以下命令,向W5500发起UDP流量压力测试:
# 安装iperf3(若未安装) sudo apt-get install iperf3 # 以10Mbps速率向W5500的5000端口发送UDP流,持续30秒 iperf3 -c 192.168.1.100 -u -b 10M -t 30 -i 1预期结果:W5500在STM32F103驱动下可持续处理约8.2Mbps UDP流量(受SPI带宽与中断响应延迟限制)。若实测吞吐量低于5Mbps,检查W5500_UDP_Recv_NonBlocking中Sn_RX_RD更新是否正确,或降低iperf3发送速率至-b 5M验证瓶颈位置。
本文还有配套的精品资源,点击获取