ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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

简介:围绕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跑,主频72MHz,SPI时钟拉到18MHz,UDP吞吐可以稳定到几Mbps,完全够传感器上报。

选W5500而不选ENC28J60,关键点在缓存和Socket数。W5500有8个独立Socket(硬件通道),每个Socket的收发缓存可灵活配置为2KB到16KB,而ENC28J60只有8KB缓存,且只能跑半双工。另一个差异是W5500支持SPI读写的同时,还能配置中断引脚,把Socket接收事件直接拉给MCU的外部中断。硬件协议栈的边界在于:它不支持WiFi,也不支持DHCP服务器端,但做客户端上报绰绰有余。

2.2 硬件连接与引脚规划:RJ45到SPI的一根线都不能错

W5500标准接线是SPI四线加两个控制脚。我一般用SPI1,CS接PB12,RST接PB13,中断脚接PB10,这样和W5500的EVB参考电路一致。注意W5500的SPI时钟极性CPOL=1、相位CPHA=1,也就是SPI模式3,这个不匹配会导致寄存器读写全是0xFF。W5500的VCC是3.3V,RJ45的变压器在模块里,如果自己画板,网口座子的中心抽头要接对,差分线要差分对走线,否则信号完整性会出问题。

引脚连接固定后,代码层面最核心的是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_CR=0x20发送。这块建议直接抄官方驱动库的调用方式,先跑通,再自己按寄存器重写。

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在接收数据的同时,主机会持续输出SCK,W5500就会把当前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再写IP,Socket状态可能会异常,导致后续发送命令得不到响应。

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 = 0x02,UDP模式 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_UDP(0x22),说明成功。如果一直是SOCK_CLOSED(0x00),大概率是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_CR=0x40命令释放缓存,否则后续数据会累积顶掉老数据。中断方式和轮询的区别是,W5500的INT引脚在收到数据时会拉低,MCU外部中断里调用一样的接收代码。

5. 避坑与常见问题:W5500三连问,都是实测翻车记录

5.1 现象1:W5500初始化卡死,VERSIONR读出来是0xFF

原因:SPI模式错误。W5500只支持SPI模式0和模式3,而我第一次用CubeMX默认配置的是模式0的极性相位不对,导致所有寄存器读出来全是0xFF。解决:在CubeMX里把SPI Clock Polarity设为High,Clock Phase设为2Edge,也就是模式3。或者用寄存器手动配,先测一个已知寄存器的读操作,能读到0x03再往下走。

5.2 现象2:UDP发送成功,上位机收到乱码

原因:发送时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_CR=0x40。另外,如果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收发复杂三倍,但那是另一个话题了。希望这篇笔记能帮你少走弯路。

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

返回列表