
简介面向STM32嵌入式网络开发场景这份rar压缩包提供HAL库W5500 SPI版本完整工程结合FreeRTOS实现W5500以太网控制器驱动与TCP/IP网络通信适合需要快速上手W5500的开发者参考。压缩包共366个文件以85个h头文件和71个c源文件为核心同时包含uvprojx工程文件、hex与axf编译产物、readme及txt说明、map/lst调试文件等整体大小18.82MB。内容涵盖W5500寄存器配置、SPI参数初始化、HAL库SPI收发流程以及FreeRTOS任务创建、延时与队列通信等关键实现实验1演示了从初始化、网络参数配置到建立连接与收发数据的完整过程。工程内readme与txt说明对引脚连接、NSS控制方式和任务划分均有交代便于二次开发该版本适配STM32F1系列HAL库适用于物联网节点、工控设备等需要低成本以太网接入的场景。已有1719人学习浏览适合有STM32基础、希望落地W5500与FreeRTOS组合的开发者参考。1. 为什么大家都在找 HAL 库的 W5500 SPI 版本驱动做嵌入式网络设备的人十有八九都经历过这种场景主控芯片的 MAC 外设不够用或者不想碰 RMII 那堆时钟和 PHY 的配置于是挑了一颗 W5500 硬协议栈芯片来兜底。这颗芯片最省心的地方在于 TCP/IP 协议栈是硬件实现的MCU 只需要通过 SPI 接口读写寄存器就能完成建连、收发数据这些操作。但真到了动手写驱动那天你会发现网上流传的代码大多是标准库版本或者干脆是寄存器直操作版本拿到 HAL 库工程里要改半天才能编译过。这篇笔记就是围绕「HAL 库 SPI 版本」这个组合把驱动怎么组织、寄存器怎么操作、缓冲区怎么管理、以及那些容易翻车的细节一次讲透。适合读这篇的人是手里已经有一块 W5500 模块主控是 STM32打算用 HAL 库在裸机或者 RTOS 下把网络功能跑起来。不需要你精通 TCP/IP但至少要知道 SPI 是怎么读写寄存器的以及什么是回调函数。我默认你用的是 CubeMX 生成的工程代码基于 HAL 库的 SPI 驱动来做这样你拿到的是一套能直接编译、能按步骤验证的思路而不是一个让你到处找补丁的碎片代码。2. HAL 库 SPI 驱动 W5500帧格式、文件划分与 CubeMX 配置2.1 先理解 W5500 的 SPI 帧格式地址、控制字节、数据三段的约定W5500 的 SPI 从机协议并不复杂但有一个前提性的设计要知道它内部寄存器是按 16 位地址来编址的分为通用寄存器和 Socket 寄存器区。MCU 每次发起传输要严格按照「16 位地址 8 位控制字节 N 字节数据」的顺序来发送。控制字节里包含了 BNKbank 选择、RWB读写位和 FAI自动递增标志。很多初次接触的人把 W5500 当成普通 SPI 从机只发地址就发数据结果读回来的全是 0xFF其实就是忽略了控制字节这一段的拼接。我一般会在驱动里把这些 SPI 帧的拼装逻辑封成一个底层接口建议不要在每个功能函数里直接拼地址否则后期维护地址分配表时会很痛苦。下面是基于 HAL 库的最基本读写接口全部走阻塞式 SPI保证代码可读性优先。#define W5500_SPI hspi1 #define W5500_CS_LOW() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET) #define W5500_CS_HIGH() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET) uint8_t W5500_ReadByte(uint16_t addr, uint8_t bnk) { uint8_t tx[3], rx[3]; tx[0] addr 8; tx[1] addr 0xFF; tx[2] (bnk 5) | (0x00 2) | 0x00; // 读操作RWB0 W5500_CS_LOW(); HAL_SPI_TransmitReceive(W5500_SPI, tx, rx, 3, 100); W5500_CS_HIGH(); return rx[2]; }这段代码里有一个细节HAL_SPI_TransmitReceive第三个参数是rx而 W5500 在读操作时第三个字节发什么数据是无关紧要的但为了统一收发仍然把tx[2]拼出来。控制字节的构成是BNK[2:0] 5 | RWB 2 | FAI 0读操作 RWB 置 0写操作置 1。如果同时需要自动地址递增就把 FAI 位置 1比如后面要连续读 Socket 的接收缓冲区时就会用到。参数层面的建议是SPI 时钟别上来就拉满。W5500 手册标称 SPI 时钟可以到 80MHz 左右但那是理想走线下的结论。我的习惯是先用 18MHz 或 36MHz 验证功能确认整条链路稳定后再尝试往上提。尤其是模块通过杜邦线连接的时候线长超过 10cm高频下波形畸变很严重容易导致「功能时好时坏、换一块板子就崩」的玄学问题。2.2 驱动文件的组织方式把硬件层与协议层分开拿到别人基于标准库写的 W5500 驱动第一件事就是看它有没有把底层 SPI 读写和上层 Socket 逻辑解耦。如果整个文件里到处是SPI_I2S_SendData这种标准库函数那移植到 HAL 库的工作量就会落在所有函数里而不是只改一个底层接口。所以我自己组织代码时通常会分成三个文件w5500_spi.c负责 SPI 底层收发与 CS 控制w5500_reg.c负责寄存器读写封装w5500_socket.c负责 Socket 的建连、收发、断开等逻辑。这样划分的好处是换主控或者换 SPI 外设时只需要改w5500_spi.c里的HAL_SPI_TransmitReceive和 CS 引脚宏协议层的代码完全不用动。对新手来说这也是一个能直接抄作业的结构。如果你手里的工程是标准库的那就先花半小时把 SPI 底层全部替换成 HAL 库风格不要试图在原有代码上打补丁否则之后查问题的时候你会分不清到底是逻辑错还是底层接口错。2.3 CubeMX 里的 SPI 参数设置模式、分频、极性相位CubeMX 配置 SPI 外设时有四个参数直接决定 W5500 能不能正常通信工作模式、时钟分频、CPOL、CPHA。W5500 要求 SPI 工作在 Mode 0 或 Mode 3而我推荐 Mode 0即 CPOL0、CPHA0这样和大多数 W5500 模块的默认时序一致。Parameter Settings - SPI Mode: Transmit Receive Parameter Settings - Clock Prescaler: 根据主频选择目标是 SPI 时钟不超过 36MHz Parameter Settings - CPOL: Low Parameter Settings - CPHA: 1 Edge Parameter Settings - Data Size: 8 bit Parameter Settings - First Bit: MSB First这里有个容易被忽略的坑HAL 库的 SPI 初始化结构体里SPI_InitTypeDef的CLKPolarity和CLKPhase分别对应 CPOL 和 CPHA但如果你在 CubeMX 里改了时钟极性生成的代码不会自动重设外设需要重新生成工程或者手动调用HAL_SPI_Init才会生效。另一个参数是NSS建议设置为 Software 模式由 GPIO 手动控制 CS不要依赖 SPI 硬件的 NSS因为 HAL 库在HAL_SPI_TransmitReceive调用过程中不会自动管理 NSS 引脚手动控制反而更可控。// 初始化 SPI1 后默认 HAL_SPI_TransmitReceive 是阻塞模式 // 注意HAL 库在每次传输时会自动拉低 NSS不会除非你启用了硬件 NSS // 所以 CS 控制必须由用户代码负责这也是上面用宏控制 CS 的原因2.4 硬件复位顺序上电后为什么必须等待再访问寄存器W5500 的复位有两种一种是硬件复位引脚拉低再拉高另一种是写软件复位寄存器。硬件复位引脚建议接到 MCU 的 GPIO 上而不是直接接一个 RC 复位电路因为 RC 时间常数受温度影响大会导致上电后访问寄存器时机不确定。上电流程里我固定是先拉低复位引脚至少 500us再拉高然后延时 100ms 左右再去读写寄存器给芯片内部 PLL 锁定的时间。如果跳过这一步很容易出现第一次读寄存器全为 0xFF、再读又正常的情况。void W5500_HardReset(void) { HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(100); // 等待 PLL 锁定与内部 PHY 就绪 }3. 从复位到能 ping 通网络参数初始化与 Socket 配置3.1 通用寄存器初始化MAC、IP、网关、子网掩码的写入顺序W5500 上电后第一步不是立刻建 Socket而是把网络参数写入通用寄存器区。通用寄存器地址从 0x0000 开始分为 Basic Mode、PHY、网络参数、Socket 空间分配等区域。MAC 地址寄存器是SHAR[5:0]IP 地址是SIPR[3:0]网关是GAR[3:0]子网掩码是SUBR[3:0]。写入顺序有一个细节先写 MAC再写 IP、网关、子网掩码最后配置 Socket 的 TX/RX 缓冲区大小。W5500 内部会根据这些寄存器生成 ARP 应答和 IP 报文如果 MAC 还没写就配置了网络参数某些固件版本会出现网关 ping 不通的现象。下面是一段初始化代码注意每个寄存器都通过封装好的W5500_WriteReg写入。void W5500_NetworkInit(uint8_t *mac, uint8_t *ip, uint8_t *gw, uint8_t *mask) { W5500_WriteReg(0x0009, mac[0]); // SHAR0 W5500_WriteReg(0x000A, mac[1]); // SHAR1 W5500_WriteReg(0x000B, mac[2]); // SHAR2 W5500_WriteReg(0x000C, mac[3]); // SHAR3 W5500_WriteReg(0x000D, mac[4]); // SHAR4 W5500_WriteReg(0x000E, mac[5]); // SHAR5 W5500_WriteReg(0x0010, gw[0]); // GAR0 W5500_WriteReg(0x0011, gw[1]); W5500_WriteReg(0x0012, gw[2]); W5500_WriteReg(0x0013, gw[3]); W5500_WriteReg(0x0014, mask[0]); // SUBR0 W5500_WriteReg(0x0015, mask[1]); W5500_WriteReg(0x0016, mask[2]); W5500_WriteReg(0x0017, mask[3]); W5500_WriteReg(0x0018, ip[0]); // SIPR0 W5500_WriteReg(0x0019, ip[1]); W5500_WriteReg(0x001A, ip[2]); W5500_WriteReg(0x001B, ip[3]); }这段代码里没有启用自动递增标志因为每个地址都是分散的分开写更直观。实际工程里如果追求效率可以用连续写的方式但那是优化阶段的事。初始化完成后建议做一次回读验证把SIPR寄存器读出来和写入值对比这是排查 SPI 通信是否正常的快速手段。3.2 Socket 的打开流程从 Sn_MR 到 Sn_CR 的完整时序W5500 提供 8 个 Socket编号 0 到 7。每个 Socket 的寄存器区占据一个 bank控制逻辑几乎一样。打开一个 Socket 的步骤是先写 Socket 模式寄存器Sn_MR再写端口号寄存器Sn_PORT然后写命令寄存器Sn_CR发出 OPEN 命令最后等待Sn_SR的状态变为 SOCK_INIT0x13。这中间有个容易出错的点Sn_CR是一次性命令寄存器写入 OPEN 命令后芯片执行完会自动清零所以不能通过反复写同一个值来重试每次新操作都要重新写命令。我的习惯是在 Socket 打开前先关闭它防止上一次异常退出导致的残留状态影响新连接。下面是一个打开 TCP Server Socket 的示例。void W5500_SocketOpenTcpServer(uint8_t sock, uint16_t port, uint8_t *ip) { W5500_WriteSnReg(sock, 0x0000, 0x01); // 先关闭Sn_MR 写 TCP 模式前先复位 W5500_SocketClose(sock); W5500_WriteSnReg(sock, 0x0000, 0x21); // Sn_MR: TCP 模式 MAC 过滤可选项 W5500_WriteSnReg(sock, 0x0004, port 8); // Sn_PORT 高字节 W5500_WriteSnReg(sock, 0x0005, port 0xFF); // Sn_PORT 低字节 // Sn_CR 写 OPEN 命令 W5500_WriteSnReg(sock, 0x0001, 0x01); // 轮询 Sn_SR 直到 0x13 uint32_t timeout 10000; while (timeout--) { if (W5500_ReadSnReg(sock, 0x0003) 0x13) break; HAL_Delay(10); } }Sn_MR写入的 0x21 表示选择 TCP 模式同时启用了 MAC 地址过滤功能。如果不需要这个过滤可以只写 0x01。关于 Socket 缓冲区W5500 默认每个 Socket 的 TX/RX 缓冲都是 2KB如果你需要更大的吞吐可以通过通用寄存器区的TMSR和RMSR来调整但这会影响其他 Socket 的可用缓冲要根据实际并发连接数来权衡。3.3 一个可交作业的初始化函数把上面的步骤收拢成一个入口如果直接把上面的代码散落在main函数里会显得很乱。我通常会封装一个W5500_Init函数它把复位、网络参数写入、Socket 打开全部串起来然后返回一个表示初始化是否成功的状态码。这样在main里只需要一行调用业务代码和驱动代码完全分开。uint8_t W5500_Init(uint8_t *mac, uint8_t *ip, uint8_t *gw, uint8_t *mask, uint16_t port) { W5500_HardReset(); W5500_NetworkInit(mac, ip, gw, mask); // 设置 Socket0 的 RX/TX 缓冲区大小为 2KB // TMSR 地址 0x001CRMSR 地址 0x001Dbit 位对应每个 Socket W5500_WriteReg(0x001C, 0x55); // 8 个 Socket 各 2KB TX W5500_WriteReg(0x001D, 0x55); // 8 个 Socket 各 2KB RX W5500_SocketOpenTcpServer(0, port, ip); return W5500_CheckLinkStatus(); }W5500_CheckLinkStatus是读取 PHY 状态寄存器判断网线是否插上、对端是否协商成功。这一步不是必须的但强烈建议保留因为在没有网线的时候Socket 打开再多次也不会收到任何数据如果你不知道这一点会以为是驱动写错了其实只是物理链路没通。4. 数据收发Socket 缓冲区偏移、回绕与中断标志的正确玩法4.1 写入发送缓冲区从 Sn_TX_WR 到 Sn_CR 的 SEND 命令W5500 的发送缓冲区是一个环形队列MCU 写数据时要先拿到当前写指针Sn_TX_WR然后通过 SPI 把数据写入对应的偏移地址写完后再更新Sn_TX_WR最后向Sn_CR写入 SEND 命令。这里的关键是写入缓冲区时地址要在实际寄存器地址的基础上加上一个偏移量这个偏移量由Sn_TXBUF寄存器提供回绕逻辑也要自己处理。void W5500_SocketSend(uint8_t sock, uint8_t *data, uint16_t len) { uint16_t tx_write W5500_ReadSnReg16(sock, 0x0024); // Sn_TX_WR uint16_t tx_base W5500_ReadSnReg16(sock, 0x0022); // Sn_TXBUF // 计算实际写入地址注意加上偏移 uint16_t offset tx_write - tx_base; // 如果数据跨越缓冲区末尾需要分两段写 if (offset len 2048) { uint16_t first_len 2048 - offset; W5500_WriteSnBuf(sock, tx_write, data, first_len); W5500_WriteSnBuf(sock, tx_base, data first_len, len - first_len); } else { W5500_WriteSnBuf(sock, tx_write, data, len); } // 更新 Sn_TX_WR tx_write len; W5500_WriteSnReg16(sock, 0x0024, tx_write); // 发送命令 W5500_WriteSnReg(sock, 0x0001, 0x20); // SEND 命令 }判断缓冲区是否满、是否可写用的是一个隐性条件发送缓冲区里的已用空间可以通过Sn_TX_FSR获取它是只读寄存器单位为字节。发送前建议先检查Sn_TX_FSR是否大于等于要发送的长度避免覆盖还未发出的数据。Sn_TXBUF的地址是每个 Socket 独立编址的同一个 Socket 在 bank 内是固定的所以读取它时不需要额外传 bank 参数。4.2 从接收缓冲区读数据Sn_RX_RD 与 Sn_RX_BUF 的配合接收流程和发送是对称的但有一个不同的坑接收数据时Sn_RX_RD的更新是在读取完数据之后、并且是在向Sn_CR写入 RECV 命令之前完成的。很多人第一次写接收逻辑时读完了数据却不更新Sn_RX_RD结果下一包数据直接被新数据覆盖表现为接收内容错乱、丢包。uint16_t W5500_SocketReceive(uint8_t sock, uint8_t *buf, uint16_t max_len) { uint16_t rx_write W5500_ReadSnReg16(sock, 0x0026); // Sn_RX_RD uint16_t rx_base W5500_ReadSnReg16(sock, 0x0022); // Sn_RX_BUF // 检查接收数据大小 uint16_t rx_size W5500_ReadSnReg16(sock, 0x0020); // Sn_RX_RSR if (rx_size 0) return 0; if (rx_size max_len) rx_size max_len; uint16_t offset rx_write - rx_base; if (offset rx_size 2048) { uint16_t first_len 2048 - offset; W5500_ReadSnBuf(sock, rx_write, buf, first_len); W5500_ReadSnBuf(sock, rx_base, buf first_len, rx_size - first_len); } else { W5500_ReadSnBuf(sock, rx_write, buf, rx_size); } // 更新 Sn_RX_RD rx_write rx_size; W5500_WriteSnReg16(sock, 0x0026, rx_write); // 发送 RECV 命令释放缓冲区 W5500_WriteSnReg(sock, 0x0001, 0x40); // RECV 命令 return rx_size; }这段代码里隐藏了一个重要概念Sn_RX_RSR表示的是「当前可读字节数」它是一个动态变化的值在 RECV 命令执行后才能确认这些数据已被 MCU 取走。所以读取Sn_RX_RSR后接着读Sn_RX_RD时必须保证中间没有做任何耗时操作否则可能因为中断或网络数据夹插导致数值错位。我在工程里会用一个临界区保护这段逻辑最简单的做法是临时关闭全局中断。4.3 连接事件与中断标志怎么知道对端连上来了在裸机循环里最常见的做法是在主循环里轮询Sn_SR和Sn_IR。Sn_IR是中断标志寄存器里面有 CON连接建立、RECV数据到达、SEND_OK发送完成、TIMEOUT超时、DISCON断开这几位。每次处理完中断标志后要手动写 1 清除对应的标志位。void W5500_SocketPoll(uint8_t sock) { uint8_t ir W5500_ReadSnReg(sock, 0x0002); // Sn_IR if (ir 0x01) // CON { W5500_WriteSnReg(sock, 0x0002, 0x01); printf(socket connected\r\n); } if (ir 0x04) // RECV { W5500_WriteSnReg(sock, 0x0002, 0x04); // 读取数据 } if (ir 0x08) // DISCON { W5500_WriteSnReg(sock, 0x0002, 0x08); W5500_SocketClose(sock); } }裸机轮询的写法适合大多数应用场景但如果你对实时性要求高可以把中断使能寄存器Sn_IMR打开把 MCU 的 EXTI 引脚接到 W5500 的 INT 引脚上由硬件中断触发处理。这样 CPU 不用频繁轮询中断里只置标志位真正的数据处理放到主循环里做。注意不要在中断回调里直接执行耗时的 SPI 收发否则会影响其他中断的响应。5. W5500 SPI 驱动避坑指南从寄存器读到 0xFF 到丢包死机5.1 现象上电后第一次读寄存器全是 0xFF再次读取又正常这个坑几乎每个人都踩过。原因有两类一类是复位等待时间不够芯片内部 PLL 还没稳定SPI 从机时序不对另一类是 CS 引脚在复位期间被拉高或拉低的不确定电平干扰了芯片状态。解决办法是严格按照上电时序来先拉低复位引脚等待至少 10ms再拉高然后再延时 100ms。如果你用的模块自带 RC 复位电路并且复位引脚悬空建议还是把复位引脚接到 MCU 上用 GPIO 控制更保险。5.2 现象SPI 回读数据全是 0x00但写入似乎不报错写入操作从来不知道芯片有没有收到所以回读全 0 一般意味着 SPI 时钟极性和相位和芯片要求不匹配。W5500 要求 Mode 0 或 Mode 3很多开发板默认的 HAL 配置是 Mode 0但你如果复制了别人的 CubeMX 工程它的时钟配置可能被改过。直接查看hspi1.Init.CLKPolarity和CLKPhase确保是SPI_POLARITY_LOW和SPI_PHASE_1EDGE。另外有些模块板载了电平转换芯片会把 SPI 信号翻转或加上额外延时这时候回读全 0 就要检查模块原理图确认是否和 MCU 电平兼容。5.3 现象能 ping 通网关但 ping 不通远端或者动不动超时这套现象通常是 MTU 或 ARP 缓存问题。W5500 的 ARP 请求是自己处理的但网关的 ARP 缓存如果刷新不及时可能一直在找旧 IP。常见做法是检查Sn_SR是否保持在 SOCK_ESTABLISHED 状态以及Sn_IR的 TIMEOUT 标志是否频繁置位。如果 TIMEOUT 频繁置位多半是网络参数配置里的网关或子网掩码写错了先确认SUBR是不是 255.255.255.0GAR是不是网关地址。5.4 现象长时间跑数小时后死机SPI 访问无响应只能断电恢复这种问题大多是看门狗没喂或者某个 Socket 的缓冲区溢出后芯片进入异常状态。W5500 内部有硬件看门狗但默认是关闭的如果你长时间没有任何网络数据芯片不会自动复位。更常见的原因是发送缓冲区写满了还继续写覆盖了未发出的数据导致芯片内部状态错乱。解决方法是每次发送前都查Sn_TX_FSR不够就等待每次接收后都及时更新Sn_RX_RD并发 RECV 命令。5.5 现象使用 DMA 发送 SPI 数据时偶发数据错乱DMA 和 SPI 的配合在 HAL 库里是经典翻车点。如果HAL_SPI_Transmit_DMA尚未完成你又调用了阻塞版本的HAL_SPI_TransmitReceive两个操作会同时抢占 SPI 外设。DMA 版本建议只在需要高吞吐的场景用并且要保证所有操作都走 DMA 或都走阻塞式不要混用。混用的后果是偶发错帧排查起来极其浪费时间。// 错误用法先 DMA 发送紧接着又阻塞读 HAL_SPI_Transmit_DMA(hspi1, txbuf, len); HAL_SPI_TransmitReceive(hspi1, tx, rx, 3, 100); // 这里会打断 DMA 传输// 正确做法保证同一时刻只有一个 SPI 操作在进行 HAL_SPI_Transmit_DMA(hspi1, txbuf, len); while (HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY); // 等待 DMA 完成 HAL_SPI_TransmitReceive(hspi1, tx, rx, 3, 100);6. 进阶技巧把 HAL 库 SPI 版本的 W5500 驱动改为中断驱动并塞进 RTOS驱动跑通以后接下来值得投入的方向有两个一是把轮询改成中断驱动来降低 CPU 占用二是把驱动嵌进 RTOS 的任务里让网络收发不阻塞其他逻辑。先说中断驱动。W5500 的 INT 引脚是低电平有效的中断输出只要有任何 Socket 发生事件它就会拉低。把这个引脚接到 MCU 的 EXTI 引脚上配置下降沿触发中断回调里只置一个标志位然后唤醒网络处理任务。// 中断回调里只做置标志这件事 extern volatile uint8_t w5500_int_flag; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin W5500_INT_Pin) { w5500_int_flag 1; } } // 网络任务里查标志位 void NetworkTask(void *arg) { while (1) { if (w5500_int_flag) { w5500_int_flag 0; W5500_SocketPoll(0); // 真正的 SPI 操作放在任务上下文 } HAL_Delay(10); } }这里有个细节SPI 操作耗时按微秒级算但在 RTOS 环境下如果优先级分配不合理网络任务可能会饿死其他任务。我一般把网络任务优先级设置为中等并且用信号量代替标志位这样任务可以在没有事件时挂起进一步降低 CPU 占用。类似的做法在 FreeRTOS 里就是xSemaphoreGiveFromISR配合xSemaphoreTake注意中断回调里不要直接调用 HAL 库的阻塞 API。关于 SPI 时钟上限我实测过的结论是在四层板、走线短、模块焊接在板上的情况下SPI 时钟可以稳定跑到 60MHz 左右但在杜邦线连接的面包板场景30MHz 以上就开始偶发错帧。「能通」和「能长时间稳定跑」是两回事我自己的习惯是长期运行的设备锁定在 36MHz既保证吞吐够用又留足了容错空间。最后说一个我一直保留的习惯每次写完驱动我会在初始化里加一个寄存器回读的断言读回VER寄存器W5500 的版本号是 0x04如果不是这个值直接进错误处理。这一步成本极低却能在硬件接错线、SPI 配置不对时第一时间把问题暴露出来而不是等网络超时了才开始怀疑人生。希望这篇笔记能帮你省下几个晚上的排查时间祝你的 W5500 项目一次点亮。本文还有配套的精品资源点击获取