ESP32与W5500有线以太网实战:从SPI底层到TCP通信
2026/9/16 8:14:33 网站建设 项目流程

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说明
SCLKGPIO18SPI 时钟,对应默认 VSPI CLK
MOSIGPIO23SPI 主机输出,接 W5500 数据输入
MISOGPIO19SPI 主机输入,接 W5500 数据输出
SCSGPIO5片选信号,低电平有效
RSTGPIO4复位引脚,低电平复位
INTGPIO34中断输出,可选,轮询模式下可不接
VCC3.3VW5500 供电,必须 3.3V
GNDGND共地

需要注意的是,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

这个问题十有八九出在硬件层面。按可能性从高到低排序:

现象可能原因排查方法
一直读到 0xFFCS 没拉低或电平不对确认 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 的经历里,最有价值的经验就是“拆开库,从寄存器看问题”。很多项目卡了好几天,最后都是靠一步一步检查寄存器状态解决的。这篇文章给你的这套代码,其实就是一个最小可用的底层调试工具,保留好它,以后你会感谢现在的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询