简介:本资源是一套面向物联网嵌入式开发者的STM32以太网UDP通信实战工程,适用于具备C语言与单片机基础的中级开发者,解决STM32F103系列芯片通过W5500模块快速接入局域网并实现UDP数据收发的核心问题。压缩包共182个文件,含44个头文件(.h)定义硬件驱动与协议接口、42个源文件(.c)涵盖SPI底层驱动、W5500寄存器配置、DHCP动态IP获取、UDP Socket创建与数据收发等完整逻辑,以及编译中间文件(.o/.d/.crf)和KEIL工程配置(.uvprojx/.uvoptx/.axf/.hex),总大小5.88MB,结构规范,便于移植与调试。已有1436人学习下载,工程已在STM32F103C8T6平台实测通过,兼容同系列其他型号;提供从硬件连接(SPI+W5500)、软件初始化、网络参数自动获取到UDP通信全流程代码,附带清晰注释与模块化分层设计,可直接用于智能终端联网、远程数据上报等物联网项目原型开发。
1. 项目缘起:为什么选择STM32+W5500做UDP通讯?
最近在做一个工业数据采集的小项目,需要把现场几个分散的传感器数据汇总到一个中控屏上。现场环境布线复杂,走串口或者CAN总线成本高、扩展性差,而Wi-Fi信号又不太稳定。琢磨了一圈,最后还是决定用最传统也最可靠的以太网,RJ45网线一插,物理连接上就让人安心。主控芯片选了STM32F407,性能足够,生态也成熟。网络芯片这块,没有用内置MAC需要外接PHY的方案,而是直接用了W5500这颗硬核TCP/IP协议栈芯片。原因很简单:项目对实时性要求高,但逻辑并不复杂,用W5500可以让我从繁琐的协议栈移植和调试中解放出来,把精力集中在应用逻辑上。通讯协议则用了UDP,因为我的数据是周期性的、小批量的状态上报,丢一两个包问题不大,但速度必须快,UDP的简单高效正合适。
这个组合——STM32做大脑,W5500处理网络,UDP负责传输——在很多嵌入式网络化场景里其实是个“黄金搭档”。无论是智能家居的控制器、小型工业网关,还是实验室的数据采集设备,你都能看到它的身影。网上资料多,坑也基本被前辈们踩平了。但即便如此,从零开始把代码调通,让STM32通过W5500用UDP发出第一个数据包,中间还是有不少细节要注意。这篇文章,我就结合自己的实战代码,把这套方案的硬件连接、软件驱动、UDP通讯的核心流程,以及调试过程中遇到的几个典型问题,从头到尾捋一遍。目标是让你看完后,能快速复现一个稳定可用的基础通讯框架。
2. 硬件基石:STM32与W5500的电路连接要点
硬件是软件跑起来的前提,连接不可马虎。W5500通过SPI接口与STM32通信,这是一个高速全双工的同步串行接口,也是整个通讯的物理基础。
2.1 核心引脚连接解析
首先看必须连接的几组引脚:
SPI引脚(通信主干道):
- SCK (Serial Clock):时钟线,由STM32主机产生,用于同步数据。接STM32的SPIx_SCK引脚。
- MOSI (Master Out Slave In):主机输出,从机输入。STM32通过这根线发送数据给W5500。接STM32的SPIx_MOSI引脚。
- MISO (Master In Slave Out):主机输入,从机输出。W5500通过这根线返回数据给STM32。接STM32的SPIx_MISO引脚。
- /CS (Chip Select):片选信号,低电平有效。这是SPI总线区分多个从设备的关键。当STM32需要和W5500说话时,必须将这个引脚拉低。接STM32的一个任意GPIO(如PA4),配置为推挽输出。
这里有个关键点:STM32的SPI接口通常有多个,比如SPI1、SPI2、SPI3。选择哪一个?原则是避免引脚冲突,并考虑性能。SPI1通常挂在APB2高速总线上,时钟频率可以更高。我的项目里用了SPI1,对应的引脚是PA5(SCK), PA7(MOSI), PA6(MISO)。片选/CS我用了PA4。
复位引脚(/RST):低电平复位。上电后,STM32需要用一个GPIO(如PC0)控制这个引脚,先拉低至少500us,再拉高,完成对W5500的硬件复位。这是确保芯片从确定状态开始工作的必要步骤。
中断引脚(/INT):中断输出。当W5500收到数据、发送完成或发生错误时,可以通过此引脚主动通知STM32,避免STM32不断轮询查询状态,提高效率。接STM32的一个具有外部中断功能的GPIO(如PB0),配置为上拉输入,并在STM32中使能对应的外部中断线。这是一个优化项,初期调试为了简化,可以先不接,采用轮询方式。
电源与时钟:
- W5500需要3.3V供电,注意与STM32的电压域匹配。
- XI (晶振输入)和XO (晶振输出):需要连接一个25MHz的无源晶振,并配上两个20pF左右的负载电容到地。这是W5500内部PHY和逻辑电路的时钟心脏,必须稳定。
网络接口:
- TPOUT+/TPOUT- (发送差分对)和TPIN+/TPIN- (接收差分对):这四根线直接连接到RJ45接口的隔离变压器(也叫网络变压器,如HX1188NL)上。变压器的作用至关重要:一是信号耦合与阻抗匹配,二是电气隔离,防止外部网络浪涌损坏核心电路。变压器的另一端则连接到RJ45插座。
2.2 原理图设计与PCB布局避坑指南
画原理图和PCB时,下面这些经验能帮你省去很多调试的麻烦:
- 电源去耦:在W5500的每个电源引脚(VDD)附近,务必放置一个0.1uF的陶瓷电容到地(GND)。最好再有一个10uF的钽电容或电解电容作为电源入口的储能电容。高频数字芯片开关瞬间电流大,好的去耦是稳定工作的第一道保险。
- 晶振布局:25MHz晶振及其负载电容必须尽可能靠近W5500的XI和XO引脚,走线短而粗,下方铺地屏蔽。避免将晶振线路靠近高频信号线或电源线,防止干扰。
- 差分走线:TPOUT+/TPOUT-和TPIN+/TPIN-是高速差分信号线。在PCB上,应遵循差分走线规则:等长、等宽、等间距,并尽量在相邻层保持完整的地平面作为参考。从W5500到变压器,再从变压器到RJ45接口的走线都要注意。
- 网络变压器选择:常用的是1:1的10/100M自适应变压器。注意变压器中心抽头的接法:有的需要接3.3V(用于产生共模电压),有的需要通过电容接地(用于滤波),具体必须参照变压器和W5500数据手册的推荐电路。
- 接地:模拟地(AGND)和数字地(DGND)的处理。W5500内部有PHY(模拟部分)和数字逻辑。通常建议在芯片下方或附近,用一个0欧电阻或磁珠将模拟地和数字地单点连接,并在电源入口处将大地(PE)通过高压电容与系统地连接,以增强抗干扰能力。
注意:第一次焊接调试,如果条件有限,强烈建议使用成熟的W5500模块(比如带隔离变压器的官方模块)。这能极大降低硬件不确定性,让你快速聚焦在软件调试上。等通讯调通后,再深入研究自己的PCB设计。
3. 软件驱动构建:从SPI底层到W5500控制层
硬件准备就绪后,我们进入软件世界。整个驱动可以自底向上分为三层:SPI底层读写、W5500寄存器操作、Socket抽象层。
3.1 SPI驱动实现与速度优化
STM32的HAL库提供了SPI的初始化函数,但直接使用HAL的阻塞式读写函数(HAL_SPI_Transmit/HAL_SPI_Receive)在频繁访问W5500寄存器时效率偏低。我们需要对其进行封装和优化。
首先,初始化SPI。以SPI1为例,配置为主机全双工模式,数据宽度8位,时钟极性低电平,相位第一个边沿,MSB先行。关键点是设置时钟预分频器,决定SCK速度。W5500的SPI时钟最高支持80MHz,但为了稳定,通常设置在20-40MHz之间。STM32F407的APB2时钟是84MHz,分频系数设为4,即可得到21MHz的SCK频率,这是一个兼顾速度和稳定性的值。
// spi.c void SPI1_Init(void) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; // PA5-SCK, PA6-MISO, PA7-MOSI 复用推挽输出,高速 GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // PA4 as /CS GPIO GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); W5500_CS_HIGH(); // 初始化为高(不选中) 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; // 软件控制NSS(即我们的/CS) hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 21 MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; HAL_SPI_Init(&hspi1); }接下来,封装核心的读写函数。W5500的SPI时序要求,在/CS拉低后,先发送一个8位的命令帧(包含读写位、块选择、地址偏移信息),然后是16位的地址,最后才是连续的数据。为了提高效率,我们可以把地址和数据打包,用HAL_SPI_TransmitReceive一次性完成。
// w5500_spi.c uint8_t W5500_ReadByte(uint16_t addr) { uint8_t cmd = W5500_READ_CTRL; // 例如 0x0F uint8_t addr_high = (uint8_t)(addr >> 8); uint8_t addr_low = (uint8_t)(addr); uint8_t rx_data = 0; uint8_t tx_buf[4] = {cmd, addr_high, addr_low, 0x00}; uint8_t rx_buf[4] = {0}; W5500_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 4, HAL_MAX_DELAY); W5500_CS_HIGH(); return rx_buf[3]; // 第四个字节是读回的数据 } void W5500_WriteByte(uint16_t addr, uint8_t data) { uint8_t cmd = W5500_WRITE_CTRL; // 例如 0xF0 uint8_t addr_high = (uint8_t)(addr >> 8); uint8_t addr_low = (uint8_t)(addr); uint8_t tx_buf[4] = {cmd, addr_high, addr_low, data}; W5500_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx_buf, 4, HAL_MAX_DELAY); W5500_CS_HIGH(); }对于连续读写多个字节(比如读写Socket的发送/接收缓冲区),W5500支持地址自增模式。在发送命令帧时,将地址自增位置1,之后每读写一个字节,内部地址自动加1。我们可以利用这个特性,用DMA进行大数据块传输,极大减轻CPU负担。这是性能优化的关键一步。
void W5500_ReadBuff(uint16_t addr, uint8_t* pBuf, uint16_t len) { uint8_t cmd = W5500_READ_CTRL | 0x08; // 设置地址自增位 uint8_t addr_high = (uint8_t)(addr >> 8); uint8_t addr_low = (uint8_t)(addr); uint8_t tx_buf[3] = {cmd, addr_high, addr_low}; W5500_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx_buf, 3, HAL_MAX_DELAY); // 发送命令和地址 HAL_SPI_Receive(&hspi1, pBuf, len, HAL_MAX_DELAY); // 连续读取len个字节 W5500_CS_HIGH(); }3.2 W5500寄存器初始化与网络配置
有了可靠的SPI读写函数,我们就可以像操作STM32外设寄存器一样操作W5500了。W5500内部寄存器分为通用寄存器和Socket寄存器两大类。初始化流程如下:
- 硬件复位:通过/RST引脚产生一个低电平脉冲(>500us)。
- 软件复位与模式设置:向**模式寄存器(MR)写入0x80,触发软件复位。等待一小段时间(通常检查版本寄存器(VERSIONR)**是否为0x04),确认复位完成。然后根据需求配置MR,比如使能PPPoE、使能ping响应等。对于基础UDP,通常保持默认(0x00)即可。
- 配置网络信息:这是让W5500接入网络的关键。
- 源硬件地址(SHAR):即MAC地址。需要写入6个字节。务必确保在你的局域网内唯一。可以烧录一个固定的,或者用STM32的唯一ID生成一个。
- 源IP地址(SIPR):给W5500分配的本地IP地址,如192.168.1.100。
- 子网掩码(SUBR):如255.255.255.0。
- 默认网关地址(GAR):路由器地址,如192.168.1.1。
- 配置中断掩码(IMR):决定哪些事件能产生中断。如果使用了/INT引脚,这里需要使能对应Socket的中断。初期轮询调试,可以先全部关闭(0x00)。
- 配置重试时间(RTR)和重试次数(RCR):这两个寄存器主要针对TCP,UDP可以忽略。
// w5500_conf.c void W5500_Init(void) { // 1. 硬件复位 W5500_RST_LOW(); HAL_Delay(1); // 保持低电平1ms W5500_RST_HIGH(); HAL_Delay(100); // 等待稳定 // 2. 软件复位并等待就绪 W5500_WriteByte(W5500_MR, 0x80); HAL_Delay(10); while(W5500_ReadByte(W5500_VERSIONR) != 0x04); // 等待版本号正确 // 3. 配置MAC地址 (示例) uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x01, 0x02, 0x03}; W5500_WriteBuff(W5500_SHAR, mac, 6); // 4. 配置IP地址、子网掩码、网关 uint8_t ip[4] = {192, 168, 1, 100}; uint8_t subnet[4] = {255, 255, 255, 0}; uint8_t gateway[4] = {192, 168, 1, 1}; W5500_WriteBuff(W5500_SIPR, ip, 4); W5500_WriteBuff(W5500_SUBR, subnet, 4); W5500_WriteBuff(W5500_GAR, gateway, 4); // 5. 设置中断掩码(本例禁用中断,使用轮询) W5500_WriteByte(W5500_IMR, 0x00); }配置完成后,你可以通过读取这些寄存器来验证配置是否成功写入。也可以尝试用电脑ping一下你设置的IP地址(192.168.1.100),如果W5500的MR寄存器中Ping响应位被使能,并且物理连接正常,你应该能收到回复。这是验证硬件和底层驱动是否正常的第一步。
4. UDP Socket的创建、发送与接收流程
W5500最多支持8个独立的硬件Socket,每个Socket都可以被配置为TCP、UDP、IPRAW或MACRAW模式。对于UDP通讯,我们需要操作Socket的寄存器组。每个Socket都有自己的一套寄存器,地址偏移量是固定的。例如,Socket 0的寄存器基址是0x0400。
4.1 Socket初始化与端口绑定
使用一个Socket进行UDP通讯,需要以下步骤:
- 选择Socket并设置模式:向Socket的**模式寄存器(Sn_MR)**写入0x02,表示使用UDP协议。你还可以在这里设置多播、广播等选项。
- 绑定本地端口:向Socket的**源端口寄存器(Sn_PORT)**写入一个16位的端口号。这个端口号是本地监听端口,远程设备需要向这个端口发送数据。例如,绑定端口号为8080。
- 打开Socket:向Socket的**命令寄存器(Sn_CR)**写入OPEN命令(0x01)。W5500硬件会执行打开操作。
- 等待打开完成:轮询Socket的状态寄存器(Sn_SR),直到其值变为SOCK_UDP(0x22),表示Socket已成功打开并处于UDP模式。
#define SOCKET_0 0 #define LOCAL_PORT 8080 uint8_t W5500_UDP_Socket_Init(uint8_t sn) { // 1. 设置Socket为UDP模式 W5500_WriteByte(W5500_Sn_MR(sn), Sn_MR_UDP); // 2. 绑定本地端口 W5500_WriteByte(W5500_Sn_PORT0(sn), (uint8_t)(LOCAL_PORT >> 8)); W5500_WriteByte(W5500_Sn_PORT1(sn), (uint8_t)LOCAL_PORT); // 3. 执行OPEN命令 W5500_WriteByte(W5500_Sn_CR(sn), Sn_CR_OPEN); // 4. 等待命令完成 while(W5500_ReadByte(W5500_Sn_CR(sn)) != 0); // 5. 检查Socket状态 if(W5500_ReadByte(W5500_Sn_SR(sn)) == SOCK_UDP) { return 1; // 成功 } else { return 0; // 失败 } }4.2 数据发送:填充缓冲区与执行SEND命令
UDP发送数据的过程,是把应用层的数据包,加上目标IP和端口信息,交给W5500发送出去。
- 填写目标地址和端口:在发送前,必须先告诉W5500这包数据要发给谁。
- 目标IP地址寄存器(Sn_DIPR):写入4字节的目标设备IP地址,如192.168.1.50。
- 目标端口寄存器(Sn_DPORT):写入2字节的目标端口号。
- 写入发送缓冲区:W5500为每个Socket分配了独立的发送缓冲区(大小可配置)。我们需要将待发送的数据,通过SPI写入到该Socket的发送缓冲区。首先需要获取当前缓冲区的写指针偏移地址,这个地址由**发送写指针寄存器(Sn_TX_WR)**指示。然后使用
W5500_WriteBuff函数,从该地址开始写入数据。 - 更新发送写指针:数据写入后,必须更新Sn_TX_WR寄存器,将其值增加已写入数据的长度。这样W5500才知道有多少新数据待发送。
- 执行SEND命令:向Sn_CR寄存器写入SEND命令(0x20)。
- 等待发送完成:轮询Sn_CR寄存器直到命令完成(变为0),或者检查**中断寄存器(Sn_IR)**的SEND_OK位是否置1。发送完成后,Sn_TX_WR的值会被硬件更新。
uint16_t W5500_UDP_Send(uint8_t sn, uint8_t *buf, uint16_t len, uint8_t *dest_ip, uint16_t dest_port) { uint16_t ptr; uint8_t status; // 1. 检查Socket是否处于UDP打开状态 if(W5500_ReadByte(W5500_Sn_SR(sn)) != SOCK_UDP) { return 0; } // 2. 设置目标IP和端口 W5500_WriteBuff(W5500_Sn_DIPR(sn), dest_ip, 4); W5500_WriteByte(W5500_Sn_DPORT0(sn), (uint8_t)(dest_port >> 8)); W5500_WriteByte(W5500_Sn_DPORT1(sn), (uint8_t)dest_port); // 3. 获取当前发送缓冲区写指针 ptr = W5500_ReadByte(W5500_Sn_TX_WR0(sn)); ptr = (ptr << 8) + W5500_ReadByte(W5500_Sn_TX_WR1(sn)); // 4. 复制数据到发送缓冲区 W5500_WriteBuff(ptr + W5500_TX_BUF_START(sn), buf, len); // 5. 更新发送写指针 ptr += len; W5500_WriteByte(W5500_Sn_TX_WR0(sn), (uint8_t)(ptr >> 8)); W5500_WriteByte(W5500_Sn_TX_WR1(sn), (uint8_t)ptr); // 6. 发送命令 W5500_WriteByte(W5500_Sn_CR(sn), Sn_CR_SEND); // 7. 等待命令完成 while(W5500_ReadByte(W5500_Sn_CR(sn))); // 可选:检查发送状态,更严谨的做法是等待Sn_IR的SEND_OK中断 // while((W5500_ReadByte(W5500_Sn_IR(sn)) & Sn_IR_SEND_OK) == 0); // W5500_WriteByte(W5500_Sn_IR(sn), Sn_IR_SEND_OK); // 清除中断标志 return len; }4.3 数据接收:轮询接收大小与读取缓冲区
UDP接收是被动的,我们需要定期检查是否有数据到来。
- 检查接收数据大小:读取Socket的接收数据大小寄存器(Sn_RX_RSR)。这是一个16位寄存器,告诉你当前接收缓冲区里有多少字节的数据待读取。如果大于0,说明有数据包到达。
- 读取数据包:UDP数据包在缓冲区中是带有8字节首部信息的。首部结构是:2字节源端口 + 2字节数据包长度 + 4字节源IP地址。因此,实际读取过程是: a. 获取当前接收缓冲区读指针(Sn_RX_RD)。 b. 读取8字节的首部信息,从中解析出源IP、源端口和数据包长度(len_in_header)。注意,这个长度包含了UDP首部(8字节)和数据长度。所以实际用户数据长度 = len_in_header - 8。 c. 根据读指针和首部信息,计算出用户数据的起始地址,然后读取相应长度的用户数据。 d. 更新接收读指针(Sn_RX_RD),将其增加 len_in_header,告诉W5500这批数据已经处理完毕,可以释放缓冲区空间。
- 执行RECV命令:更新读指针后,需要向Sn_CR写入RECV命令(0x40),让硬件更新内部状态。
uint16_t W5500_UDP_Recv(uint8_t sn, uint8_t *buf, uint16_t buf_len, uint8_t *src_ip, uint16_t *src_port) { uint16_t ptr; uint16_t rx_size; uint8_t head[8]; uint16_t data_len; // 1. 检查接收数据大小 rx_size = W5500_ReadByte(W5500_Sn_RX_RSR0(sn)); rx_size = (rx_size << 8) + W5500_ReadByte(W5500_Sn_RX_RSR1(sn)); if(rx_size < 8) { // 至少需要一个完整UDP首部 return 0; } // 2. 获取当前接收读指针 ptr = W5500_ReadByte(W5500_Sn_RX_RD0(sn)); ptr = (ptr << 8) + W5500_ReadByte(W5500_Sn_RX_RD1(sn)); // 3. 读取8字节UDP首部 W5500_ReadBuff(ptr + W5500_RX_BUF_START(sn), head, 8); // 4. 解析首部 *src_port = (head[0] << 8) | head[1]; data_len = (head[2] << 8) | head[3]; // 总长度(UDP头+数据) src_ip[0] = head[4]; src_ip[1] = head[5]; src_ip[2] = head[6]; src_ip[3] = head[7]; // 5. 计算用户数据长度和起始地址 data_len -= 8; // 减去UDP头长度 if(data_len > buf_len) { data_len = buf_len; // 防止缓冲区溢出 } ptr += 8; // 指针移动到用户数据开始处 // 6. 读取用户数据 if(data_len > 0) { W5500_ReadBuff(ptr + W5500_RX_BUF_START(sn), buf, data_len); } // 7. 更新接收读指针 ptr += data_len; // 此时ptr指向下一个数据包开始处 W5500_WriteByte(W5500_Sn_RX_RD0(sn), (uint8_t)(ptr >> 8)); W5500_WriteByte(W5500_Sn_RX_RD1(sn), (uint8_t)ptr); // 8. 执行RECV命令 W5500_WriteByte(W5500_Sn_CR(sn), Sn_CR_RECV); while(W5500_ReadByte(W5500_Sn_CR(sn))); return data_len; }5. 实战调试与排坑:从灯不亮到数据不通
理论流程走通了,但实际调试中总会遇到各种问题。下面是我在项目中遇到的几个典型问题及其排查思路。
5.1 硬件连接检查:SPI通讯是否建立?
现象:程序运行后,读取W5500的版本寄存器(VERSIONR,地址0x0039)返回值不是0x04,或者一直是0xFF/0x00。
排查步骤:
- 测量电源:用万用表测量W5500的VCC引脚是否为稳定的3.3V。
- 检查复位:用示波器或逻辑分析仪抓取/RST引脚波形,确保上电后有一个明显的低电平脉冲(>500us)。
- 检查晶振:用示波器测量XI引脚,看是否有25MHz的正弦波(注意示波器探头电容可能影响起振,最好用高阻无源探头)。这是最容易被忽略的一点,晶振不起振,芯片根本不会工作。
- 检查SPI信号:用逻辑分析仪连接SCK、MOSI、MISO、/CS四根线。运行一次读版本寄存器的操作。你应该能看到:
- /CS拉低。
- SCK上出现时钟脉冲。
- MOSI上出现你发送的命令帧(0x0F)、地址高字节(0x00)、地址低字节(0x39)。
- MISO上在发送地址的同时,会返回数据(对于读操作,数据在第四个字节返回,应该是0x04)。 如果MOSI上没有数据,检查STM32的SPI配置和GPIO复用是否正确。如果MISO上没有数据或全是高/低,检查W5500的电源、复位、晶振,或者SPI模式(CPOL/CPHA)是否匹配。W5500的SPI模式固定为模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1),必须与STM32配置一致。我最初就栽在这里,STM32默认配置可能不是模式0。
5.2 网络链路检查:物理层与IP配置
现象:SPI读写正常,版本号能读到,但电脑ping不通W5500的IP地址。
排查步骤:
- 网线与指示灯:连接网线后,W5500的PHY应该能自动协商速率(10/100M)。观察RJ45接口的LED指示灯是否亮起(通常一个常亮表示链路接通,一个闪烁表示有数据活动)。如果灯不亮,检查网络变压器连接、差分线走线,或者尝试更换网线、交换机端口。
- IP地址冲突:确认你为W5500设置的IP地址(如192.168.1.100)在局域网内是唯一的,没有被其他设备占用。
- 防火墙与软件设置:关闭电脑的防火墙,或者添加规则允许ICMP(ping)请求。确保电脑的IP地址(如192.168.1.50)与W5500在同一网段(192.168.1.x)。
- W5500 PHY状态寄存器:W5500提供了PHY配置寄存器(PHYCFGR),可以读取链路状态(Link)、速度(Speed)、双工模式(Duplex)。通过SPI读取该寄存器(地址0x002E),检查bit0(LINK)是否为1。如果不是,说明物理链路未接通,回头检查硬件连接。
5.3 数据收发调试:抓包工具是终极裁判
当Ping通之后,UDP数据收发不通,就需要借助网络抓包工具了。我强烈推荐使用Wireshark。
- 在电脑端抓包:打开Wireshark,选择你电脑正在使用的网卡(如“以太网”),开始抓包。然后运行你的STM32程序,让它发送一个UDP广播包(目标IP设为255.255.255.255)到某个端口(如12345)。
- 分析发送的数据:在Wireshark的过滤栏输入
udp.port == 12345。你应该能看到从W5500的IP(192.168.1.100)发来的UDP数据包。点开查看详情:- 以太网帧:源MAC地址是否正确(是你设置的MAC)?
- IP层:源IP、目标IP是否正确?协议是否为UDP(17)?
- UDP层:源端口、目标端口是否正确?长度字段是否匹配(数据长度+8)?校验和是否正确(W5500硬件计算,通常没问题)?
- 数据:应用层数据内容是否正确? 如果看不到任何包,说明STM32端的发送代码可能根本没执行,或者Socket没有正确打开。如果看到了包但内容不对,检查你的数据填充和指针更新逻辑。
- 模拟接收:在电脑上,你可以用网络调试助手(如NetAssist)创建一个UDP客户端,绑定本地端口(如8080,对应W5500的发送目标端口),然后向W5500的IP和端口(192.168.1.100:8080)发送数据。同时在Wireshark中过滤
ip.src == 你的电脑IP,查看发送出去的UDP包。在STM32端,不断轮询调用W5500_UDP_Recv函数,并打印接收到的数据和源地址,看是否能正确解析。
5.4 稳定性与性能优化实战心得
当基础通讯跑通后,就要考虑稳定性和性能了。
- 缓冲区管理:W5500每个Socket的发送和接收缓冲区大小是共享的,需要在初始化时通过**Socket内存分配寄存器(Sn_TXBUF_SIZE, Sn_RXBUF_SIZE)**进行分配。对于UDP,接收缓冲区建议设置大一些,以防大数据包或突发数据导致溢出。发送缓冲区可以根据单次最大发送数据量来设置。
- 超时与重发机制:UDP是不可靠的。在应用层,对于重要的数据,需要设计简单的应答重传机制。例如,发送方发送一个带序列号的数据包,等待接收方的ACK包。如果超时未收到ACK,则重发。序列号也可以用于处理乱序到达的数据包。
- 中断 vs 轮询:轮询简单,但占用CPU。对于低数据量应用足够。如果系统繁忙或需要快速响应,务必使用中断。配置好Sn_IMR(Socket中断掩码)和Sn_IR(Socket中断寄存器),在STM32端编写外部中断服务函数,在中断里快速读取Sn_IR判断事件类型(SEND_OK, RECV, TIMEOUT等),然后置位标志位,在主循环中处理。切记在中断服务函数中不要进行复杂的SPI读写操作,应只做标记。
- 多Socket使用:W5500支持8个Socket,你可以用不同的Socket绑定不同端口,实现多服务。例如,Socket0用于与主服务器通讯,Socket1用于响应发现广播。注意合理分配内存和优先级。
- SPI DMA优化:这是提升吞吐量的关键。将
W5500_ReadBuff和W5500_WriteBuff函数改用DMA传输。配置SPI的Tx/Rx DMA流,在非阻塞模式下工作。这能极大释放CPU,特别是在高速、连续传输数据时。
本文还有配套的精品资源,点击获取