资讯动态

STM32+W5500 UDP以太网通信:从硬件原理到裸机代码实战

发布时间:2026/10/31 6:29:20 来源:尧图企业网站定制
简介围绕STM32F103与W5500芯片的物联网UDP通信例程面向嵌入式开发与物联网项目实战人群解决单片机经SPI接口驱动W5500实现以太网RJ45 UDP报文的收发问题。代码基于Keil开发演示DHCP动态获取IP、创建UDP、等待客户端连接、关闭连接等关键流程当前在STM32F103C8T6上运行也可通过修改芯片型号与Flash容量兼容同系列其他型号。压缩包共182个文件以44个h头文件、42个c源文件为主体附带Keil工程配置uvprojx/uvoptx、编译链接产物axf/hex/map及调试中间文件包体仅5.88MB目录结构便于对照学习。已有1442人学习下载压缩包内含完整Keil工程从系统初始化、SPI驱动到W5500寄存器配置均有清晰模块划分便于二次开发与协议扩展适合STM32平台快速搭建以太网UDP通信也可作为实战项目的基础模板与排错参照。1. STM32 W5500 UDP一套能抄的以太网通讯方案做物联网设备最烦的不是传感器采集而是数据怎么出去。W5500这块芯片把MAC和TCP/IP协议栈全吞了MCU只用SPI读写它的缓存RJ45接口连上变压器就能直接送数据到公网服务器。UDP无连接、不握手一条飞线就能把STM32的状态上报到上位机或云端。我拆过的这个工程跑的是W5500驱动加UDP收发代码按寄存器操作写不依赖复杂操作系统裸机也能跑。适合正在做低成本以太网网关、或者毕业设计想用UDP传数据的读者。这里头有初始化配置、Socket收发还有我实际踩过的三个坑。2. W5500硬件与驱动原理把TCP/IP协议栈从MCU上摘掉2.1 为什么选W5500硬件协议栈的好处与边界W5500内置了全硬件的TCP/IP协议栈支持TCP、UDP、IPv4、ICMP、ARP等MCU不需要再跑LWIP或者uIP这些软件协议栈。这意味着ST官方推荐的W5500方案让STM32只用SPI就能完成网络收发CPU占用率极低。我用STM32F103C8T6跑主频72MHzSPI时钟拉到18MHzUDP吞吐可以稳定到几Mbps完全够传感器上报。选W5500而不选ENC28J60关键点在缓存和Socket数。W5500有8个独立Socket硬件通道每个Socket的收发缓存可灵活配置为2KB到16KB而ENC28J60只有8KB缓存且只能跑半双工。另一个差异是W5500支持SPI读写的同时还能配置中断引脚把Socket接收事件直接拉给MCU的外部中断。硬件协议栈的边界在于它不支持WiFi也不支持DHCP服务器端但做客户端上报绰绰有余。2.2 硬件连接与引脚规划RJ45到SPI的一根线都不能错W5500标准接线是SPI四线加两个控制脚。我一般用SPI1CS接PB12RST接PB13中断脚接PB10这样和W5500的EVB参考电路一致。注意W5500的SPI时钟极性CPOL1、相位CPHA1也就是SPI模式3这个不匹配会导致寄存器读写全是0xFF。W5500的VCC是3.3VRJ45的变压器在模块里如果自己画板网口座子的中心抽头要接对差分线要差分对走线否则信号完整性会出问题。引脚连接固定后代码层面最核心的是SPI底层读写。W5500的SPI帧格式是地址字节高8位 控制字节第8位用于读写、第7位用于自动递增 数据。控制字节的低5位是区段选择比如读版本寄存器是0x00写Socket0的发送缓存是0x10。2.3 寄存器映射与Socket号驱动前的概念铺垫W5500的寄存器分几个区通用寄存器如版本寄存器VERSIONR、网关、IP地址、Socket寄存器区每个Socket 0x00开头的一段空间、发送缓存区、接收缓存区。操作Socket的核心就是一组名称以Sn_开头的寄存器比如Sn_MR模式、Sn_CR命令、Sn_SR状态、Sn_PORT端口号、Sn_TX_FIFO发送缓存入口。初学时最容易绕晕的是缓存区偏移。W5500把8个Socket的收发缓存映射到统一的地址空间写发送缓存时先往Sn_TX_WR写入目标地址再把数据写到Sn_TX_FIFO对应的偏移处最后发命令Sn_CR0x20发送。这块建议直接抄官方驱动库的调用方式先跑通再自己按寄存器重写。3. SPI初始化与寄存器配置上电到能收发数据的必经之路3.1 SPI底层读写函数先让数据能在总线上跑W5500的所有访问都通过读写函数完成。下面的代码基于STM32 HAL库实现了基本的读和写。先定义CS和RST的控制宏再实现SPI收发。#define W5500_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define W5500_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET) #define W5500_RST_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET) #define W5500_RST_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET) void W5500_SPI_CS_ENABLE(void) { W5500_CS_LOW(); } void W5500_SPI_CS_DISABLE(void) { W5500_CS_HIGH(); } uint8_t W5500_SPI_ReadByte(void) { uint8_t rx_data 0; HAL_SPI_Receive(hspi1, rx_data, 1, HAL_MAX_DELAY); return rx_data; } void W5500_SPI_WriteByte(uint8_t byte) { HAL_SPI_Transmit(hspi1, byte, 1, HAL_MAX_DELAY); }CS拉低后整个SPI帧都必须在CS有效期间完成。HAL_SPI_Receive在接收数据的同时主机会持续输出SCKW5500就会把当前Socket缓存区地址上的数据发出来。如果只用Transmit不发Receive读操作会拿到空数据。3.2 W5500初始化流程模式设置、网关、IP、掩码、MAC初始化分三步硬件复位、SPI配置、写入网络参数并打开Socket。硬件复位要拉低RST至少500微秒然后拉高再等100毫秒确保芯片内部LDO稳定。SPI配置参考下面的代码void W5500_Init(void) { W5500_RST_LOW(); HAL_Delay(10); W5500_RST_HIGH(); HAL_Delay(100); W5500_Write_Reg(0x0000, 0x00); // 写入MR间接访问模式0表示直接访问 W5500_Write_Reg(0x0029, 0x07); // SHAR寄存器写入MAC地址的前三个字节 W5500_Write_Reg(0x0015, 0xC0); // GAR网关192.168.1.1 W5500_Write_Reg(0x0016, 0xA8); W5500_Write_Reg(0x0017, 0x01); W5500_Write_Reg(0x0018, 0x01); W5500_Write_Reg(0x0019, 0xC0); // SUBR掩码255.255.255.0 W5500_Write_Reg(0x001A, 0xA8); W5500_Write_Reg(0x001B, 0x01); W5500_Write_Reg(0x001C, 0x00); W5500_Write_Reg(0x001F, 0x00); // SHAR MAC地址00:08:DC:01:02:03 W5500_Write_Reg(0x0020, 0x08); W5500_Write_Reg(0x0021, 0xDC); W5500_Write_Reg(0x0022, 0x01); W5500_Write_Reg(0x0023, 0x02); W5500_Write_Reg(0x0024, 0x03); }注意MAC地址不能和局域网内其他设备冲突网关、掩码、IP必须和你的路由器网段一致。W5500_Write_Reg是封装的写寄存器函数第一个参数是16位地址第二个是8位数。3.3 常见初始化失败的现象与排查初始化后必须读VERSIONR地址0x0039正确值应该是0x03。如果读出来不是0x03先看SPI模式是不是0再看CS引脚是不是被其他外设占用。还有一个易错点W5500的寄存器地址是16位但控制字节只占一个字节地址的高8位和低8位是在同一个SPI帧里先后发的写驱动时不要把地址顺序搞反。我在实践中发现初始化顺序也很关键先写通用寄存器设置网络参数再打开Socket。如果先打开Socket再写IPSocket状态可能会异常导致后续发送命令得不到响应。4. UDP数据收发与Socket管理裸机上把数据包传出去4.1 UDP Socket打开与绑定端口UDP的使用流程比TCP简单很多不用三次握手。W5500的Socket0要配置为UDP模式绑定本地端口然后就可以直接发送。下面是打开Socket的代码#define SOCKET_UDP_PORT 6000 void W5500_Socket_Open(void) { uint8_t sock 0; W5500_Write_Reg(0x000F, 0x00); // Sn_MR(sock0) 0x00清理配置 W5500_Write_Reg(0x0010, 0x02); // Sn_MR 0x02UDP模式 W5500_Write_Reg(0x0011, 0x00); // Sn_CR 0x00 W5500_Write_Reg(0x0012, 0x02); // 打开命令 SOCKET_OPEN W5500_Write_Reg(0x001C, 0x17); // 设置源端口 0x1770 6000 W5500_Write_Reg(0x001D, 0x70); W5500_Write_Reg(0x001E, 0x00); // Sn_CR 0x00 W5500_Write_Reg(0x001F, 0x00); // Sn_SR清零等待 }Socket打开后要读Sn_SR如果等于SOCK_UDP0x22说明成功。如果一直是SOCK_CLOSED0x00大概率是Sn_MR没写成UDP模式可能是SPI写操作被中断打断导致的。4.2 发送数据从用户缓冲到W5500发送缓存UDP发送需要把数据复制到W5500的发送缓存然后更新Sn_TX_WR指针最后写命令0x20触发发送。下面这段代码是我常用的发送函数uint8_t W5500_UDP_Send(const uint8_t *buf, uint16_t len, uint16_t dest_port, const uint8_t *dest_ip) { uint8_t sock 0; uint16_t ptr W5500_Read_Reg16(0x0020); // Sn_TX_WR W5500_Write_Reg16(0x0000, ptr); // 设置发送缓存地址 W5500_Write_Buffer(0x0002, buf, len); // 写数据到Sn_TX_FIFO (0x0002) W5500_Write_Reg16(0x0020, ptr len); // 更新Sn_TX_WR // 写目标IP和端口到Sn_DIPR和Sn_DPORT for (uint8_t i 0; i 4; i) { W5500_Write_Reg(0x0004 i, dest_ip[i]); // Sn_DIPR } W5500_Write_Reg(0x0009, (dest_port 8) 0xFF); // Sn_DPORT W5500_Write_Reg(0x000A, dest_port 0xFF); W5500_Write_Reg(0x0001, 0x20); // Sn_CR SEND while (W5500_Read_Reg8(0x0003) 0x20); // 等待发送完成 return 1; }这段代码里Sn_TX_WR是发送缓存写指针每次发送完要回滚到初始位置。如果忘记更新指针第二次发送会把数据覆盖到错误位置上位机收到的是乱码。目标IP和端口必须写在Sn_DIPR和Sn_DPORT寄存器里发送命令0x20会打包这些信息成UDP报文。4.3 接收数据中断与轮询两种方式UDP接收有三个关键点Sn_RX_RSR是否非零、Sn_RX_RD指针、Sn_RX_FIFO读取。最简单的轮询方式代码如下uint16_t W5500_UDP_Receive(uint8_t *buf, uint16_t max_len) { uint16_t len 0; uint8_t sock 0; uint16_t rx_size W5500_Read_Reg16(0x0026); // Sn_RX_RSR if (rx_size 0) return 0; uint16_t ptr W5500_Read_Reg16(0x0027); // Sn_RX_RD uint16_t read_len rx_size max_len ? max_len : rx_size; W5500_Write_Reg16(0x0000, ptr); // 设置接收缓存地址 W5500_Read_Buffer(0x0002, buf, read_len); // 读数据 W5500_Write_Reg16(0x0027, ptr read_len); // 更新Sn_RX_RD W5500_Write_Reg(0x0001, 0x40); // Sn_CR RECV return read_len; }接收函数的第一步必须读Sn_RX_RSR这个寄存器保存着当前RSR中未读字节数。如果数据没读完Sn_RX_RSR不会减到零。每次读完必须写Sn_CR0x40命令释放缓存否则后续数据会累积顶掉老数据。中断方式和轮询的区别是W5500的INT引脚在收到数据时会拉低MCU外部中断里调用一样的接收代码。5. 避坑与常见问题W5500三连问都是实测翻车记录5.1 现象1W5500初始化卡死VERSIONR读出来是0xFF原因SPI模式错误。W5500只支持SPI模式0和模式3而我第一次用CubeMX默认配置的是模式0的极性相位不对导致所有寄存器读出来全是0xFF。解决在CubeMX里把SPI Clock Polarity设为HighClock Phase设为2Edge也就是模式3。或者用寄存器手动配先测一个已知寄存器的读操作能读到0x03再往下走。5.2 现象2UDP发送成功上位机收到乱码原因发送时Sn_TX_WR指针没有回滚。W5500的发送缓存是循环的需要把Sn_TX_WR重置为Socket缓存区起始地址否则每次发送会把数据写到递增的地址上。解决在发送结束后把Sn_TX_WR写回Socket base address。比如Socket0发送缓存基址是0x4000每次发送就把Sn_TX_WR设为0x4000。5.3 现象3长时间运行后通讯断开PING不通原因W5500的UDP Socket在大流量下如果没在接收后及时释放缓存Sn_RX_RSR会溢出Socket自动关闭。解决接收代码必须保证每次都读尽并写Sn_CR0x40。另外如果MCU主循环太慢以太网流量又大可以在中断里读缓存或者增大Socket的接收缓存配置比如把Socket0的内存区配成16KB接收16KB发送。5.4 现象4复位后寄存器值读出来正常但发送后芯片无反应原因RST引脚的复位时序不满足芯片要求。W5500要求复位低电平持续时间至少500微秒我在初始化代码里用了10毫秒拉低看起来没问题但之后马上拉高没等内部LDO稳定就写寄存器。解决拉高RST后至少等待200毫秒让芯片完成上电自检再初始化SPI。这三个坑是我实际调到半夜才查出来的。尤其第二个乱码问题用逻辑分析仪抓了SPI波形才明白是缓存指针没回滚。从那以后我每次做W5500工程都强制先写一个自检函数读VERSIONR、写状态寄存器再读回等于给每个驱动函数上一道保险。6. 进阶验证用网络调试助手和打流工具压测UDP链路当你把UDP收发跑通后别急着上板子先用PC端工具做一轮压力测试验证链路稳定性。我一般用两种方法最简单的是网络调试助手输入开发板的IP和端口手动发字符串看回显更严格的是用iperf3打UDP流量看带宽和丢包率。先写一个简单的回环测试程序把收到的UDP数据原样发回给发送方方便验证W5500的硬件收发是否正常。void UDP_Test_Loopback(void) { uint8_t buf[512]; uint16_t len 0; uint8_t src_ip[4]; uint16_t src_port 0; len W5500_UDP_Receive(buf, sizeof(buf)); if (len 0) { // 从接收头8字节中提取源IP和端口 for (uint8_t i 0; i 4; i) src_ip[i] buf[i 4]; src_port (buf[2] 8) | buf[3]; W5500_UDP_Send(buf, len, src_port, src_ip); } }这个代码必须在接收后立刻解析UDP头的源端口和源IP因为W5500的接收缓存里前8字节是帧信息后面才是数据载荷。用网络调试助手把数据发到板子的6000端口板子原样返回如果发送和接收都正常你会在调试助手里看到一模一样的字符串。再进一步用iperf3打流。PC端先启动iperf3作为UDP服务器板子端无法直接跑iperf3但你可以让板子连续往PC发定长数据包然后看PC端的接收带宽和丢包率。协议栈是硬件跑的底层吞吐差异不大重点观察长时间高负载下Socket是否复位。建议用1000个包、每个包512字节间隔10毫秒发送跑30分钟看有没有丢包或Socket关闭。如果中途出现通讯断开大概率是接收缓存没及时释放排查接收代码。最后一个小技巧把W5500的Sn_CR命令封装成带超时等待的函数避免死循环卡死主程序。比如发送命令后循环等待Sn_CR清零但超过10毫秒就强制清Sn_CR并重新初始化Socket这样网络异常时还能自救。做完这套验证我把工程里的固定IP改成动态获取DHCP又发现DHCP客户端的处理逻辑比UDP收发复杂三倍但那是另一个话题了。希望这篇笔记能帮你少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑