ESP32 玩腻了 Wi-Fi,想给它添一条更稳的有线网线?我这几年的结论是:W5500 是性价比最高、最不容易出幺蛾子的选择,而把它接到 ESP32 上,核心就是 SPI 通信。很多朋友一听到“SPI 例程”就头皮发麻,觉得寄存器多、时序复杂,其实把一条条代码拆开看,逻辑相当直白。
这篇文章我用一个能直接跑的 W5500 例程,从底层 SPI 读写函数开始,逐行讲到 TCP 数据收发。你不用再去零散查资料,照着抄就能在一个下午把有线以太网跑通。如果你正卡在“例程下载下来看不懂”“不知道寄存器为什么要这么写”这类问题上,这篇就是为你准备的。
1. 项目概述与方案选型
1.1 为什么选 W5500 而不是其他以太网方案
做物联网项目,最常见的联网方式还是 Wi-Fi,但 Wi-Fi 在复杂电磁环境里不稳定,断线重连、IP 冲突、信道干扰都很烦人。给设备加有线以太网,其实就是加一个 TCP/IP 协议栈芯片,让它自己把 TCP、UDP、ICMP 这些协议处理掉,MCU 只要通过 SPI 读写寄存器、收发数据即可。
市面上常见的有三类方案:
一是 MCU 内置 MAC + 外部 PHY,比如 STM32F407 + LAN8720,优点是用 RMII 接口,速率能上 100Mbps,但 MAC 层驱动要自己写,底层 buffer、描述符、中断处理一堆,复杂度明显偏高。
二是用老牌 ENC28J60,便宜,但只有 10Mbps,而且内置缓冲区只有 8KB,收发并发吞吐一般,高负载下容易丢包。
三是用 W5500,内置全硬件 TCP/IP 协议栈,支持 TCP、UDP、IPv4、ICMP、ARP 等,MCU 完全不参与协议处理,通过 SPI 接口操作即可。传输速率理论可达 10/100Mbps,实际项目跑到几 Mbps 很轻松。这就像把整套网络协议栈做成了一个外设,把你从协议实现里解放出来。
所以如果你的目标是快速、稳定地给 ESP32 加一个有线上网通道,W5500 是投入产出比最高的路线。
1.2 为什么用 SPI 连接而不是 SDIO 或并行总线
W5500 对外只提供 SPI 接口,这是设计上的一大优势。ESP32 自带两个 SPI 控制器(HSPI 和 VSPI),可以同时挂多个 SPI 设备,接线只需要 SCLK、MOSI、MISO、SCS 四根线,MCU 的资源占用很少。
相比并行总线要占十几个 GPIO,SPI 让硬件连接和 PCB 布线都简单很多。而且 W5500 最高支持 80MHz SPI 时钟,对大多数设备来说带宽足够。ESP32 的 SPI 主模式跑到 40MHz 很稳定,我平时甚至保守一点,用 20MHz 做长线连接,可靠性优先。
SPI 虽然看起来只是四根线,但它有主从模式、时钟极性和相位、片选控制这些讲究。如果这些搞不明白,后面 W5500 可能会“时好时坏”。这篇文章后面会结合例程把这些问题都串起来讲。
2. W5500 核心原理:搞懂寄存器就成功一半
2.1 寄存器是怎么组织的
W5500 内部有很多寄存器,按功能分成两个区域:通用寄存器和 Socket 寄存器。
通用寄存器负责全局配置,包括重复超时时间、MAC 地址、本机 IP、网关、子网掩码、中断屏蔽等。Socket 寄存器则管理每个独立的套接字,W5500 有 4 个 Socket,每个 Socket 都有一组相对独立的寄存器,用来配置协议类型、源端口、目标 IP、目标端口、缓冲区指针和命令状态等。
在 SPI 通信时,我们需要通过寄存器地址和块选择来定位要操作的寄存器。这里有一个关键概念:W5500 的 16 位寄存器地址,在各种手册里都用偏移量表示,而在实际 SPI 帧里,还需要通过控制字节里的块选择字段(BSB[4:0])来告诉芯片“我要访问的是通用寄存器,还是 Socket0 的寄存器,还是 Socket0 的发送缓冲区”。
举个例子:通用寄存器的地址偏移里有一个版本寄存器 VER,偏移是 0x0039,它只能通过 BSB=5’b00000 访问。Socket0 的 Sn_MR 寄存器,地址偏移是 0x0000,但它的 BSB 是 5’b00001。如果访问地址相同但块选择不同,对应的寄存器完全不同。
这个设计容易让新手懵,所以读代码时一定要把“寄存器地址”和“块选择”看成一对组合,而不是只看一个数字。
2.2 数据收发缓冲区在哪
W5500 内部有 32KB 缓冲区,分成发送和接收两半,每半 16KB,可以由用户分配到 4 个 Socket 上。默认情况下每个 Socket 分到 2KB 发送缓冲和 2KB 接收缓冲。
当我用 SPI 向 Socket0 发送数据时,实际上是把数据写入“Socket0 TX Buffer”这个块,然后通过写 Sn_CR 寄存器发送 SEND 命令,W5500 硬件就开始自动组包、发送并等待 ACK。接收数据时,硬件会把收到的数据放进“Socket0 RX Buffer”,MCU 只需要查询 Sn_RX_RSR 寄存器,看有多少可读字节,然后把它读出来。
缓冲区块也是通过 BSB 来映射的,例如 Socket0 的 TX Buffer 对应 BSB=5’b00101,RX Buffer 对应 BSB=5b00110。读例程时看到地址是 0x0000,但控制字节写的是 0x25,那就说明这是在往 Socket0 的 TX Buffer 写数据。
2.3 SPI 帧格式:三次传输才能完成一次访问
W5500 的 SPI 访问不是发一个字节就能完成的,而是固定三个阶段:
第一个阶段:发送 16 位寄存器地址。第二个阶段:发送 8 位控制字节。控制字节的最高位 RWB 用来表示读还是写,1 表示读,0 表示写;接着是地址偏移模式 OM[1:0],00 表示固定地址、01 表示自动递增;低位是 BSB 块选择。第三阶段:根据读或写,发送或接收数据字节。
用代码来表达会更直观。下面是最基本的读写函数,也是整个例程的地基:
#include <SPI.h> #define W5500_CS 5 #define W5500_RST 4 void w5500_write8(uint16_t addr, uint8_t bsb, uint8_t data) { SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); digitalWrite(W5500_CS, LOW); SPI.transfer16(addr); // 阶段1:地址 SPI.transfer((0 << 7) | (0 << 5) | (bsb & 0x1F)); // 阶段2:控制字节,写操作 SPI.transfer(data); // 阶段3:写数据 digitalWrite(W5500_CS, HIGH); SPI.endTransaction(); } uint8_t w5500_read8(uint16_t addr, uint8_t bsb) { SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); digitalWrite(W5500_CS, LOW); SPI.transfer16(addr); SPI.transfer((1 << 7) | (0 << 5) | (bsb & 0x1F)); // 控制字节:读操作 uint8_t data = SPI.transfer(0x00); // 阶段3:读数据 digitalWrite(W5500_CS, HIGH); SPI.endTransaction(); return data; }读操作时,第三阶段主机需要发出一个时钟才能把 MISO 上的数据读回来,所以SPI.transfer(0x00)里的参数是什么其实无所谓,主要是产生时钟。
3. 硬件接线与实操准备
3.1 引脚连接表
我使用的是 ESP32 DevKitC 开发板和一块常见的 W5500 以太网模块,它们之间用杜邦线连接。下面这个接线表可以直接照抄:
| W5500 引脚 | ESP32 GPIO | 说明 |
|---|---|---|
| SCLK | GPIO18 | SPI 时钟,对应默认 VSPI CLK |
| MOSI | GPIO23 | SPI 主机输出,接 W5500 数据输入 |
| MISO | GPIO19 | SPI 主机输入,接 W5500 数据输出 |
| SCS | GPIO5 | 片选信号,低电平有效 |
| RST | GPIO4 | 复位引脚,低电平复位 |
| INT | GPIO34 | 中断输出,可选,轮询模式下可不接 |
| VCC | 3.3V | W5500 供电,必须 3.3V |
| GND | GND | 共地 |
需要注意的是,ESP32 的 Arduino 环境里默认 SPI 是 VSPI,默认引脚就是 SCK=18、MISO=19、MOSI=23,SS 引脚默认是 5。用 GPIO5 做 W5500 片选,刚好和默认配置一致,省去额外指定引脚的麻烦。
GPIO34 是输入专用引脚,只能做中断输入,不能输出。如果你以后想用 W5500 的中断功能,这个引脚可以直接读 INT 状态。不用中断的话,轮询查询寄存器状态也完全可行。
3.2 供电和复位电路容易踩的坑
W5500 模块虽然标称 3.3V 供电,但启动时电流峰值不低,尤其在进行 PHY 自协商时需要比较大电流。很多朋友用 ESP32 开发板的 3.3V 输出直接给 W5500 供电,结果出现“能识别芯片版本号但 TCP 连接不稳定”的问题。解决方法很土但有效:用外部独立 3.3V 稳压电源给 W5500 供电,同时在 VCC 和 GND 之间靠近模块位置加一个 10uF 电解电容和一个 0.1uF 陶瓷电容。
复位电路也很关键。W5500 的 RST 引脚是低电平有效,复位完成后要保证有足够的延迟时间。我在例程里用 GPIO4 控制复位:
pinMode(W5500_RST, OUTPUT); digitalWrite(W5500_RST, LOW); // 拉低复位 delay(10); digitalWrite(W5500_RST, HIGH); // 释放复位 delay(200); // 等待芯片完成初始化这里第 10ms 的拉低时间要保证在 W5500 数据手册要求的复位最小脉宽之上。释放复位之后,芯片还要做 PHY 初始化和启动,我习惯等 200ms 以上再开始 SPI 通信,实测更稳。如果复位后立刻读寄存器,有时候会读到 0xFF,那就是芯片还没准备好。
3.3 SPI 引脚软件配置
在 Arduino 环境下,初始化 SPI 的代码只有几行:
SPI.begin(); // 使用默认 VSPI 引脚如果要把 CS 引脚设成输出并默认拉高,建议在setup()里做:
pinMode(W5500_CS, OUTPUT); digitalWrite(W5500_CS, HIGH); // 片选默认高电平,防止误选中片选信号低电平有效,所以平时要拉高,只有访问 W5500 时才拉低。实际操作中最常见的低级错误,就是片选忘了初始化,导致芯片一直处于选中状态,SPI 总线冲突,读出来的数据全是乱码。
4. 例程逐行讲透:从复位到 TCP 收发
4.1 读版本号验证 SPI 通路
先把 SPI 通信这条路打通,再谈网卡逻辑。W5500 有一个固定的版本寄存器 VER,地址偏移 0x0039,属于通用寄存器块,读出来的值固定是 0x04。如果读到的不是 0x04,说明 SPI 线路、片选、供电或者复位某个环节有问题。
验证代码如下:
void setup() { Serial.begin(115200); SPI.begin(); pinMode(W5500_CS, OUTPUT); digitalWrite(W5500_CS, HIGH); pinMode(W5500_RST, OUTPUT); digitalWrite(W5500_RST, LOW); delay(10); digitalWrite(W5500_RST, HIGH); delay(200); uint8_t ver = w5500_read8(0x0039, 0x00); Serial.printf("W5500 Version: 0x%02X\n", ver); if (ver != 0x04) { Serial.println("SPI 通路异常,请检查接线和供电"); while (1); } // 继续初始化网络... }如果读到 0x04,恭喜,SPI 链路通了,后面的网络配置才可能有效。这一步是整套程序的第一道安全门,我不管做什么板子都会保留这个检查。
4.2 软件复位与网络参数写入
SPI 通之后,先对芯片做一次软件复位,把内部状态恢复到已知的干净状态。W5500 的模式寄存器 MR 地址偏移是 0x0000,通用寄存器块,把 bit7 写成 1 就会触发软件复位。复位后硬件会自动把位清 0,所以不需要手动清。
w5500_write8(0x0000, 0x00, 0x80); // MR[7] = 1,软件复位 delay(50);接下来写入 MAC 地址、网关、子网掩码和本机 IP。这些参数分布在通用寄存器里,按字节顺序写入:
- GAR 网关地址,偏移 0x0001
- SUBR 子网掩码,偏移 0x0005
- SHAR MAC 地址,偏移 0x0009
- SIPR 本机 IP,偏移 0x000F
我封装了一个批量写函数:
void w5500_write_bytes(uint16_t addr, uint8_t bsb, const uint8_t* data, uint16_t len) { SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); digitalWrite(W5500_CS, LOW); SPI.transfer16(addr); SPI.transfer((0 << 7) | (1 << 5) | (bsb & 0x1F)); // 写 + 地址自动递增 for (uint16_t i = 0; i < len; i++) { SPI.transfer(data[i]); } digitalWrite(W5500_CS, HIGH); SPI.endTransaction(); }注意这里控制字节的 OM[1:0] 使用的是 01,也就是地址自动递增模式。连续写入 MAC、IP 这类多字节参数时非常方便,不用每个字节都重新发一次地址。
在setup()里这样配置:
uint8_t mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; uint8_t ip[4] = {192, 168, 1, 100}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t mask[4] = {255, 255, 255, 0}; w5500_write_bytes(0x0001, 0x00, gw, 4); // 网关 w5500_write_bytes(0x0005, 0x00, mask, 4); // 子网掩码 w5500_write_bytes(0x0009, 0x00, mac, 6); // MAC 地址 w5500_write_bytes(0x000F, 0x00, ip, 4); // 本机 IP很多现成例程会直接调用开源库的wizchip_setnetinfo()函数完成这些写入,本质上就是这一串寄存器操作。搞懂了底层,再去看库源码就不觉得玄乎了。
4.3 Socket 配置与 TCP 连接建立
W5500 有 4 个 Socket,编号从 0 到 3。我用 Socket0,所以所有 Socket 寄存器的 BSB 都是 0x01。TCP 连接的建立分为四步。
第一步,设置协议模式。Sn_MR 偏移 0x0000,写入 0x01 表示 TCP 模式:
w5500_write8(0x0000, 0x01, 0x01); // Socket0 Sn_MR = TCP第二步,打开 Socket。向 Sn_CR 偏移 0x0001 写入 OPEN 命令 0x01,然后轮询 Sn_SR 偏移 0x0003,直到状态变成 0x13(SOCK_INIT),表示 Socket 打开成功:
w5500_write8(0x0001, 0x01, 0x01); // Sn_CR = OPEN uint8_t status; while ((status = w5500_read8(0x0003, 0x01)) != 0x13) { delay(1); } Serial.printf("Socket0 status: 0x%02X\n", status);这里有个容易犯的错:Sn_CR 是命令寄存器,写完命令后芯片会立刻执行并自动清零,不需要手工清。但你必须等待操作完成,尤其是 OPEN、CONNECT、SEND、RECV 这些命令,不能连续猛写。轮询状态寄存器是更稳妥的方式。
第三步,配置目标 IP 和目标端口。目标 IP 写在 Sn_DIPR 偏移 0x000C,目标端口写在 Sn_DPORT 偏移 0x0010,Sn_DPORT 是 16 位,先写高字节再写低字节:
uint8_t remote_ip[4] = {192, 168, 1, 10}; uint16_t remote_port = 8080; w5500_write_bytes(0x000C, 0x01, remote_ip, 4); w5500_write8(0x0010, 0x01, (uint8_t)(remote_port >> 8)); // 端口高字节 w5500_write8(0x0011, 0x01, (uint8_t)(remote_port & 0xFF)); // 端口低字节TCP 是有连接协议,本地源端口如果没指定,芯片会在打开 Socket 时自动分配一个临时端口。如果要指定本地端口,可以在 OPEN 前写 Sn_PORT 偏移 0x0004,同样是 16 位。
第四步,发出 CONNECT 命令并等待连接建立。Sn_CR 写 0x04,然后轮询 Sn_SR 直到变成 0x17(SOCK_ESTABLISHED):
w5500_write8(0x0001, 0x01, 0x04); // Sn_CR = CONNECT uint32_t timeout = millis() + 5000; while ((status = w5500_read8(0x0003, 0x01)) != 0x17) { if (millis() > timeout) { Serial.println("TCP 连接超时"); break; } delay(1); }这个超时很有必要。如果远端 IP 不通、端口没监听,CONNECT 命令不会立刻失败,而是一直在重发 SYN 包,如果不加超时,程序会卡死在 while 循环里。我见过不少人调试时以为死机了,其实是在等 TCP 超时。
4.4 发送数据:先写缓冲区再发命令
TCP 连接建立后,发送数据的过程比写寄存器要多一个步骤,那就是先把数据填到 Socket0 的 TX Buffer 里,然后发 SEND 命令。
发送缓冲区对应的 BSB 是 0x05。因为要写入一大段连续数据,控制字节使用自动递增模式。代码是这样的:
void w5500_send(const char* data, uint16_t len) { uint16_t free_size; free_size = (w5500_read8(0x0020, 0x01) << 8) | w5500_read8(0x0021, 0x01); // Sn_TX_FSR if (free_size < len) { Serial.printf("发送缓冲区不足,剩余 %d,需要 %d\n", free_size, len); return; } SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); digitalWrite(W5500_CS, LOW); SPI.transfer16(0x0000); SPI.transfer((0 << 7) | (1 << 5) | 0x05); // 写 Socket0 TX Buffer,地址自动递增 for (uint16_t i = 0; i < len; i++) { SPI.transfer(data[i]); } digitalWrite(W5500_CS, HIGH); SPI.endTransaction(); w5500_write8(0x0001, 0x01, 0x20); // Sn_CR = SEND delay(5); // 等待发送完成,也可以轮询 Sn_IR 的 SEND_OK 标志 }注意 Sn_TX_FSR 是 16 位寄存器,偏移 0x0020 和 0x0021,先读高字节,再读低字节。如果你的程序在发送大量数据时发现free_size长时间小于 0x0800,说明上次 SEND 没有完成,或者有数据残留。调试的时候把这段打印打开会很有帮助。
发送命令为什么要等一下?因为 SEND 命令触发后,W5500 需要时间从 TX Buffer 里取数据、封装 TCP 段、发送并等待 ACK。SEND_OK 中断标志会告诉你这次发送已经完成了,但是我实际测试下来,简单应用里直接delay(5)也能跑,只是吞吐量上不去。如果你想提高效率,可以用中断标志代替固定延时。
4.5 接收数据:查询可读字节再读取
接收端逻辑和发送端是对称的。W5500 硬件会把收到的 TCP 数据放在 Socket0 RX Buffer 里,BSB 是 0x06。我们只需要查看 Sn_RX_RSR 偏移 0x0026 这个 16 位寄存器,就知道当前有多少字节可以读:
uint16_t w5500_rx_size() { uint16_t size; size = (w5500_read8(0x0026, 0x01) << 8); size |= w5500_read8(0x0027, 0x01); return size; } uint16_t w5500_recv(uint8_t* buf, uint16_t max_len) { uint16_t size = w5500_rx_size(); if (size == 0) return 0; if (size > max_len) size = max_len; SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); digitalWrite(W5500_CS, LOW); SPI.transfer16(0x0000); SPI.transfer((1 << 7) | (1 << 5) | 0x06); // 读 Socket0 RX Buffer,地址自动递增 for (uint16_t i = 0; i < size; i++) { buf[i] = SPI.transfer(0x00); } digitalWrite(W5500_CS, HIGH); SPI.endTransaction(); w5500_write8(0x0001, 0x01, 0x40); // Sn_CR = RECV return size; }这里有一个新手特别容易漏掉的细节:读取完 RX Buffer 之后,必须向 Sn_CR 写 RECV 命令 0x40,表示这帧数据已经被主机取走了,芯片才能释放相应缓冲区接收新数据。如果忘了写 RECV,第二次可能还能读到数据,但很快就会感觉接收越来越慢,最后完全卡住,因为 RX Buffer 被占满了。
主循环里可以这样调用:
uint8_t buf[512]; uint16_t len = w5500_recv(buf, sizeof(buf)); if (len > 0) { for (uint16_t i = 0; i < len; i++) { Serial.print((char)buf[i]); } Serial.println(); }5. 常见问题与排查技巧实录
5.1 版本寄存器一直读不到 0x04
这个问题十有八九出在硬件层面。按可能性从高到低排序:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 一直读到 0xFF | CS 没拉低或电平不对 | 确认 CS 引脚初始化为高电平,访问时拉低 |
| 读到 0x00 或随机值 | MOSI/MISO 接反 | 对照原理图确认接线 |
| 刚上电第一次读错,复位后再读正常 | 复位时间不够 | RST 释放后 delay 至少 200ms |
| 读到的值不稳定 | 供电不足 | 用独立 3.3V 电源并加电容 |
| 完全无响应 | SCLK 极性/相位不匹配 | 确认使用 SPI_MODE0 或 SPI_MODE3 |
我调试过一块自制板子,怎么读都是 0xFF,最后发现是片选引脚被默认初始化成了输出低电平,W5500 一直被选中但数据总线被其他 SPI 外设干扰。把初始电平改成高电平,问题立刻解决。
5.2 能读到版本号,但 Ping 不通
版本号能读出来,说明 SPI 通路没问题,问题大概率在网络参数或硬件 PHY 上。
先看 RJ45 的网口指示灯,正常连接时 LINK 灯应该亮。如果不亮,检查网线、对端交换机端口,以及 W5500 模块的变压器是否有问题。
再看寄存器里的本机 IP 和网关是否写对,尤其是字节顺序。W5500 网络参数都是从低字节开始按顺序存储,如果你习惯了大端方式写 IP,很容易把 192.168.1.100 写成 100.1.168.192。
还有一个经典故障:写网络参数前没有对芯片做软件复位,导致部分参数没有生效。我的建议是每次配置网络前都先软件复位,然后再写参数。
5.3 TCP 连不上,但 ARP 能通
如果对端设备能 Ping 通,说明二层和三层的链路都通了,TCP 连不上基本是端口或者防火墙的问题。先用 PC 上的网络调试工具开一个 TCP 服务器,确认端口号没有被占用,再检查目标 IP 和端口有没有写反字节序。
端口是 16 位值,先高后低。如果你写反了,比如把端口 8080 的 0x1F90 写成 0x901F,对端自然没有程序监听那个端口。
另外 W5500 的 Sn_MR 里还有其他配置位,比如 No Delayed ACK、IGMP 版本等,普通 TCP 连接用不到,但如果你改了这些位,连接建立后可能出现响应慢或者数据吞吐异常的情况。没有特殊需求就保持 Sn_MR=0x01。
5.4 能收发小包,大包发送失败
这个最常见的原因是发送缓冲区大小不够。默认每个 Socket 的 TX Buffer 是 2KB,一次发送超过 2KB 数据,free_size不够,就会直接打印缓冲区不足。
解决办法有两个:一是应用层把数据分片,一次发 1KB 左右;二是通过 Socket 寄存器的 Sn_TXBUF_SIZE(偏移 0x001F)和 Sn_RXBUF_SIZE(偏移 0x001E)增大缓冲区。比如把 Socket0 的发送缓冲设为 4KB,写入 0x04 即可。需要注意的是,缓冲区大小配置必须在 Socket OPEN 之前设置,否则不会生效。
我在实际项目中用的是一组更大的发送缓冲区,同时配合轮询 Sn_IR 寄存器里的 SEND_OK 标志来确认上一包发完,才发下一包。这样做的好处是可以用足芯片的缓冲区,又不会溢出。
5.5 ESP32 与 W5500 的共存问题
ESP32 本身有 Wi-Fi,如果你同时开了 Wi-Fi STA 和 W5500 以太网,会遇到“默认路由被抢占”的问题。Arduino 环境下,以太网库和 Wi-Fi 库可能会抢默认网关,导致某些请求走了 Wi-Fi 而不是有线网络。
解决办法很直接:在实际项目中明确指定默认网卡,比如在 Wi-Fi 连接后调用eth = WiFi.eth()接口强制以太网为默认路由,或者在初始化顺序上,先初始化 W5500 再初始化 Wi-Fi。
另外,ESP32 的 SPI 和 Wi-Fi 共用部分硬件资源,在 Wi-Fi 收发频繁时 SPI 中断可能会被延迟。如果你的项目对以太网实时性要求高,建议让 W5500 使用独立的中断引脚,并在中断服务函数里只做标志位处理,实际的数据读写放在主循环里完成。
6. 手写底层之后再回头用库
如果你完整跑通了上面这套手写寄存器例程,再去用现成的开源库会非常有底气。常用的几个库我都试过。
Arduino 环境下,最省心的是Ethernet3库,它专门针对 W5500 做了优化,底层通过 SPI 访问 W5500,使用方法类似标准Ethernet库。初始化代码就这么简单:
Ethernet.begin(mac, ip);然后就可以用EthernetClient来收发数据了。但用了库不意味着可以不懂底层。实际调试中,库能帮你把大部分细节藏起来,可一旦出问题,你还是要回到寄存器层面去排查。
所以我建议的学习路径是:先花一个下午把底层寄存器操作调通,理解每一个 SPI 事务做了什么,然后再切换到库,把这个手写的例程作为一个“调试探针”保留备用。以后遇到再怪的以太网问题,都有底气定位到是 SPI 层、寄存器配置层还是网络协议层的问题。
7. 扩展思路:有线以太网还能怎么玩
W5500 配上 ESP32,能做的事远不止建立一个 TCP 连接。我简单列几个我自己做过的方向。
一是做本地 Web 服务器。ESP32 和 W5500 配合,可以跑一个轻量级的 HTTP 服务器,在浏览器里直接监控传感器数据、控制继电器。由于是有线网络,响应比 Wi-Fi 方案更稳定,不会出现“页面转圈打不开”的情况。
二是做远程 OTA 升级。ESP32 支持通过 HTTP 下载固件升级包,在有线网络环境下下载速度和稳定性都比 Wi-Fi 好很多。你可以把固件放在局域网服务器里,批量给设备升级,设备数量多了以后尤其省事。
三是做多设备组网。W5500 有 4 个 Socket,可以同时建立多个 TCP 连接或 UDP 会话,比如一个 Socket 做 MQTT 客户端,另一个 Socket 做本地 Modbus TCP 服务器,再加上一个用于远程日志上传。这在工业数据采集项目里很常见。
四是配合 ESP32 的低功耗特性做休眠唤醒。W5500 在无数据时可以通过 INT 引脚唤醒 ESP32,设备平时休眠,有网络事件时再醒来处理,非常省电。
不管你最终要用在哪个场景,先把 SPI 底层搞扎实,后面都是水到渠成的事。
我自己调试 W5500 的经历里,最有价值的经验就是“拆开库,从寄存器看问题”。很多项目卡了好几天,最后都是靠一步一步检查寄存器状态解决的。这篇文章给你的这套代码,其实就是一个最小可用的底层调试工具,保留好它,以后你会感谢现在的自己。