先把结论放在前面:ESP32S3 跑以太网,完全没必要非得折腾复杂的总线方案,W5500 这种带硬件 TCP/IP 协议栈的 SPI 以太网芯片,才是大多数场景下最快能跑起来、最不折腾的选择。这段时间我拿 ESP32S3 和 W5500 模块从头做了一遍硬件连接、驱动移植、再做到 TCP 上行/下行测速,整个过程踩了不少坑,也把很多人问过的“为什么 ping 断断续续”“为什么测速上不去”这类问题捋清楚了。这篇文章就把接线、初始化、测速代码、以及常见故障排除完整记录下来,给准备在 ESP32S3 上挂 W5500 的朋友当一份可直接抄作业的参考。
1. 方案设计:为什么要选 W5500,而不是 LAN8720 或者 WiFi
1.1 无线转有线,到底图什么?
项目标题里同时出现 ESP32S3 和以太网,很多人第一反应是:ESP32S3 本身不是自带 WiFi 吗,为什么还要外接一个以太网模块?
我自己的实际原因是:这几天在做一个需要长时间稳定传输的采集设备,WiFi 在弱信号环境下掉线、重连、IP 变动这些事太不可控了。就算 WiFi 信号强度看着不错,路由器偶发信道拥塞也会让 TCP 流出现卡顿。而以太网是物理层铜缆直连,只要链路指示灯亮着,基本就是稳定传输,这个特性对数据采集、设备控制、视频流这类场景非常关键。
另外还有一个容易被忽略的点:ESP32S3 的 WiFi 驱动和应用代码在同一个 FreeRTOS 系统里跑,WiFi 协议栈占用的任务、内存和中断资源都不少。如果你做的是 TCP 服务器这类对并发连接和吞吐都有要求的应用,把网络流量放在独立的 W5500 硬件协议栈上,ESP32S3 的主核就腾出来处理业务逻辑,整体架构会更清晰。
1.2 W5500、LAN8720、ENC28J60 怎么选
先摆结论:在三款常见的以太网方案里,W5500 是最适合“拿来就用”的。
LAN8720 走的是 RMII 接口,需要和 ESP32 主控配合驱动 MAC 层,还要额外配置时钟,而且必须用特定 GPIO 才能满足 RMII 的时序要求。胆子大的朋友可能会尝试直接用 RMII 接 LAN8720,但不确定因素太多:时钟信号质量、GPIO 复用冲突、驱动移植难度,任何一个环节出问题都可能导致“灯亮但无法通信”。我见过更崩溃的情况是,即使接线看起来没问题,一旦网络流量稍大,LAN8720 方案就出现间歇性丢包,排查起来非常痛苦。
ENC28J60 虽然也是 SPI 接口,价格便宜,但它只有 8KB 收发缓冲区,而且 TCP/IP 协议栈完全靠主控软件实现,对 ESP32S3 来说负担不小,实际吞吐通常只有几 Mbps,跑大流量场景会明显力不从心。
W5500 的差异化优势在于:它把 TCP/IP 协议栈直接做进了芯片内部,主控只需要通过 SPI 读写 Socket 缓冲区就能完成 TCP 通信。它内部有独立的 32KB 收发缓冲区(不同 socket 可分配),最大 SPI 时钟 33.3MHz,理论吞吐比 ENC28J60 高得多。实际使用中,我这边 W5500 整体吞吐能做到 30Mbps 以上,具体数字取决于 SPI 时钟和 buffer 配置,这个在后面测速部分会详细展开。
所以这套方案的本质是:用 W5500 做协议卸载,把网络栈的脏活累活交给专门芯片,ESP32S3 只关心业务数据和 SPI 通信。
2. 硬件连接:接线表和引脚映射全解析
2.1 W5500 模块引脚与 ESP32S3 接线表
W5500 模块常见的引脚包括:VCC、GND、SCK、MOSI、MISO、SCS/CS、RST、INT。有些模块还会引出 RX+、RX-、TX+、TX-,那是接网口变压器一侧的信号,正常我们不用动。
ESP32S3 的 FSPI 默认引脚编号为:
| W5500 模块信号 | ESP32S3 GPIO | 说明 |
|---|---|---|
| SCK | GPIO12 | SPI 时钟 |
| MOSI | GPIO11 | 主出从入 |
| MISO | GPIO13 | 主入从出 |
| SCS / CS | GPIO10 | 片选信号,可换其他 GPIO |
| RST | GPIO9 | 复位信号,低电平复位 |
| INT | GPIO14 | 中断信号,可暂时不接 |
| VCC | 3.3V | 模块供电 |
| GND | GND | 共地 |
这是 ESP32S3 DevKitC 一类常用开发板的默认引脚映射。如果你用的是合宙、普中或者其他厂家做的 ESP32S3 开发板,建议先对着板子原理图或丝印确认一下,因为不同厂家对 FSPI 引脚的引出位置可能不同。
这里有个接线技巧:CS 引脚不一定非要固定用 GPIO10,只要在代码里通过Ethernet.init(引脚号)重新指定就行。但 SCK、MOSI、MISO 三个引脚在 ESP32S3 上必须绑定到同一组 SPI 外设的对应 IO 上,不能随便挑三个 GPIO 就开干,这是 SPI 外设的硬件约束。
2.2 电源、复位和电平匹配的细节
W5500 模块的工作电压是 3.3V,芯片本身对 5V 输入并不友好,绝对不要把 VCC 直接接到 5V 上。很多市售 W5500 模块板载了 5V 转 3.3V 的 LDO,这种模块的 VCC 引脚可以接 5V,但如果你买的模块没有明确标注支持 5V 输入,保守起见,一律接 3.3V。
电源质量对 W5500 的影响经常被低估。W5500 内部有 PHY 模拟电路,对电源纹波比较敏感。如果供电端纹波过大,最常见的现象就是“刚开始能 ping 通,过一会儿开始丢包,最后完全连不上”。我给模块供电时会在 VCC 和 GND 之间加一个 10uF 钽电容和一个 0.1uF 陶瓷电容,靠近模块引脚放置,对这个症状有很明显的改善。
复位时序也要注意。W5500 的数据手册要求上电后 RST 引脚保持至少 500us 的低电平,然后释放。如果复位脉冲太短,芯片可能处于半初始化状态,表现为 DHCP 获取不到 IP、或者局域网内能发现但 TCP 连接建立异常。我习惯在初始化代码里先手动拉低 RST 引脚 100ms,再拉高,等待 200ms 后再初始化 SPI 和以太网,这个时序余量是足够可靠的。
2.3 没有原理图,怎么快速判断模块引脚定义
市面上 W5500 模块的丝印命名并不统一,有的写 SCS,有的写 CS,有的写 SCSN。遇到丝印不明确的情况,不要盲目接线,先拿万用表测一下:
- 找出 VCC 和 GND:大多数模块会用丝印标注,或者 VCC 引脚和 GND 引脚之间有一个明显的滤波电容。
- 找出 MISO:把模块插到面包板上,上电后测各引脚电压,MISO 在未通信时通常被内部上拉到 3.3V 或者保持高阻态,其他信号线像是 MOSI 和 SCK 在上电后会一直保持低电平。
这样测一两轮就能定位大部分引脚,比我第一次拿 GPIO 盲试节省太多时间了。
3. 软件环境和驱动初始化
3.1 选 Arduino 还是 ESP-IDF
W5500 的驱动在 Arduino 生态里非常成熟,直接用Ethernet库就能跑起来,对初学者最友好。如果你已经有 ESP-IDF 开发经验,用 IDF 中的 SPI master 驱动加 W5500 原始 Socket API 也行,但需要自己封装不少代码。
我这次做测速实验使用的是 Arduino IDE + ESP32S3 开发环境。原因很简单:W5500 的驱动库在 Arduino 下已经把所有 Socket 层的细节封装好了,我可以把精力放在测速逻辑和问题排查上。如果你要上商业量产项目,再考虑用 ESP-IDF 做精细化控制也不迟,原理是一样的。
3.2 最小初始化 Demo
先贴上最核心的初始化代码,这段代码能完成 W5500 的复位、SPI 初始化和 IP 配置:
#include <SPI.h> #include <Ethernet.h> #define W5500_SCK 12 #define W5500_MISO 13 #define W5500_MOSI 11 #define W5500_CS 10 #define W5500_RST 9 // MAC 地址要确保局域网内唯一,最好贴模块自带标签的 MAC byte mac[] = {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}; // 为了方便测速,我直接使用静态 IP IPAddress ip(192, 168, 1, 80); IPAddress gateway(192, 168, 1, 1); IPAddress subnet(255, 255, 255, 0); void setup() { Serial.begin(115200); delay(1000); // W5500 复位:先拉低 100ms,再拉高 pinMode(W5500_RST, OUTPUT); digitalWrite(W5500_RST, LOW); delay(100); digitalWrite(W5500_RST, HIGH); delay(200); // SPI 初始化:SCK, MISO, MOSI, CS SPI.begin(W5500_SCK, W5500_MISO, W5500_MOSI, W5500_CS); // 告诉 Ethernet 库 CS 引脚位置 Ethernet.init(W5500_CS); // 开始以太网,配置静态 IP Ethernet.begin(mac, ip, gateway, subnet); if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("没有检测到 W5500,请检查接线"); while (true) delay(100); } if (Ethernet.linkStatus() == LinkOFF) { Serial.println("网线未连接"); } else { Serial.println("网线已连接"); } } void loop() { // 测速逻辑放这里 }这段代码里有个非常关键的细节:SPI.begin(W5500_SCK, W5500_MISO, W5500_MOSI, W5500_CS)的前三个参数顺序千万不要写错。很多朋友容易按照 MOSI、MISO、SCK 的习惯去传参,结果 W5500 完全无响应,或者读回来的寄存器全是 0xFF。ESP32 的 SPI 库要求参数顺序是SCK, MISO, MOSI, SS,这是和经典 AVR 板卡不一样的地方。
3.3 SPI 时钟频率怎么设置
W5500 的 SPI 最大时钟是 33.3MHz,但 Arduino Ethernet 库默认的 SPI 时钟往往比较保守,可能只有几 MHz,这会让测速结果很难看。想跑高性能,需要手动把 SPI 时钟调到 30MHz 左右。
在 Arduino 环境下,可以通过SPI.setClockDivider或者直接设置 SPI 外设的时钟频率:
SPI.begin(W5500_SCK, W5500_MISO, W5500_MOSI, W5500_CS); SPI.setFrequency(30000000); // 30MHz注意,setFrequency要在SPI.begin之后调用。ESP32 的 SPI 外设支持分频设置,30MHz 是一个比较稳的选择。如果模块的 PCB 布线较差或者杜邦线过长,可以把频率降到 20MHz 再试,吞吐会低一些,但稳定性会好很多。
4. TCP 测速:上行、下行怎么测
4.1 测速方案的整体设计
这次测速,我分了两条链路来做,分别模拟“ESP32S3 作为客户端主动推数据”和“ESP32S3 作为服务器被动收数据”这两种常见场景:
- 上行测速:ESP32S3 作为 TCP 客户端,连接 PC 上的监听端口,持续发送数据块,PC 端统计接收速率。
- 下行测速:ESP32S3 作为 TCP 服务器,PC 作为客户端主动连接并向 ESP32S3 灌数据,ESP32S3 统计接收速率。
两个方向的测试都使用单 TCP 长连接,测试时长固定为 10 秒。TCP 长连接的好处是避免了频繁建连和断连带来的额外开销,更接近实际业务场景。
这里必须提醒一句:测速结果会受路由器、交换机、网线质量、PC 性能等外围条件影响,所以测速时最好让 PC 和设备直连交换机或者直连 PC 网口,排除其他设备占用带宽的干扰。
4.2 上行测速:ESP32S3 当客户端
ESP32S3 这边的代码如下:
#include <Ethernet.h> IPAddress serverIP(192, 168, 1, 100); const uint16_t serverPort = 5010; EthernetClient client; // 用接近 MSS 大小的负载测试,1460 字节是常见值 uint8_t payload[1460]; void uploadTest() { if (!client.connect(serverIP, serverPort)) { Serial.println("TCP 连接失败"); return; } uint32_t totalBytes = 0; uint32_t startMs = millis(); uint32_t durationMs = 10000; // 测 10 秒 while (millis() - startMs < durationMs) { int n = client.write(payload, sizeof(payload)); if (n <= 0) { Serial.println("写入失败或连接断开"); break; } totalBytes += n; // 留一点时间让 W5500 缓冲区和 SPI 正常流转 delay(1); } client.stop(); double seconds = (millis() - startMs) / 1000.0; double mbps = (totalBytes * 8.0) / (seconds * 1000 * 1000); Serial.print("上行测速完成: "); Serial.print(totalBytes / 1024.0 / 1024.0); Serial.print(" MB, 速率 "); Serial.print(mbps, 2); Serial.println(" Mbps"); }PC 端用 Python 脚本充当接收服务器:
import socket import time srv = socket.socket() srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(('0.0.0.0', 5010)) srv.listen(1) print("等待 ESP32S3 连接...") conn, addr = srv.accept() print(f"客户端已连接: {addr}") t0 = time.time() total = 0 while time.time() - t0 < 10: data = conn.recv(65536) if not data: break total += len(data) dt = time.time() - t0 print(f"接收数据: {total / 1024 / 1024:.2f} MB") print(f"平均速率: {total * 8 / dt / 1_000_000:.2f} Mbps") conn.close() srv.close()实测中,这段代码跑出来的上行速率大致在 35~45Mbps 之间,具体数值取决于 SPI 时钟、网线和路由器性能。如果 SPI 时钟用的是默认低速,速率可能掉到 10Mbps 以下,这是排查测速结果偏低时首先要怀疑的点。
4.3 下行测速:ESP32S3 当服务器
下行测速的逻辑是让 ESP32S3 监听端口,PC 主动连接并发送数据。ESP32S3 通过EthernetServer接收数据并统计:
#include <Ethernet.h> EthernetServer server(5071); uint8_t recvBuf[2048]; void downloadTest() { server.begin(); Serial.println("等待 PC 连接 5071 端口..."); EthernetClient client = server.available(); while (!client) { delay(50); client = server.available(); } Serial.println("PC 已连接,开始接收数据..."); client.setTimeout(5000); uint32_t totalBytes = 0; uint32_t startMs = millis(); uint32_t durationMs = 10000; while (millis() - startMs < durationMs && client.connected()) { int n = client.read(recvBuf, sizeof(recvBuf)); if (n > 0) { totalBytes += n; } else if (n < 0) { break; } } client.stop(); server.end(); double seconds = (millis() - startMs) / 1000.0; double mbps = (totalBytes * 8.0) / (seconds * 1000 * 1000); Serial.print("下行测速完成: "); Serial.print(totalBytes / 1024.0 / 1024.0); Serial.print(" MB, 速率 "); Serial.print(mbps, 2); Serial.println(" Mbps"); }PC 端 Python 脚本:
import socket import time sock = socket.socket() sock.settimeout(5) sock.connect(('192.168.1.80', 5071)) chunk = b'A' * 65536 t0 = time.time() total = 0 while time.time() - t0 < 10: sock.sendall(chunk) total += len(chunk) dt = time.time() - t0 print(f"发送共: {total / 1024 / 1024:.2f} MB") print(f"平均速率: {total * 8 / dt / 1_000_000:.2f} Mbps") sock.close()这里有个细节:PC 端的sendall会把数据交给操作系统协议栈,发送速度非常快,但 ESP32S3 的接收处理能力、W5500 的 socket 缓冲区大小和 SPI 读取速率共同决定了实际能收多快。所以在 ESP32S3 端要尽量把recvBuf设置大一点,减少 read 调用的次数,否则 CPU 会大量消耗在 SPI 读操作上。
4.4 关于 TCP 三次握手和长连接的底层逻辑
测速过程中,TCP 连接建立时的三次握手其实会占用一定时间,尤其在短连接测试里,频繁建连会导致测速结果明显偏低。我之所以在测速里坚持使用长连接,就是为了把三次握手的开销从测速数据里排除掉。
另外 W5500 是硬件 TCP/IP 协议栈,TCP 三次握手、序列号管理、ACK 重传这些工作都由芯片自己完成,主控完全不需要关心。这是它比 ENC28J60 这类软协议栈方案省心的地方,也是测速逻辑能写得这么简单的原因。
5. 实测结果与瓶颈分析
5.1 一组实际测速数据
在我这版接线和驱动配置下,连同一个千兆交换机,测速记录如下:
| 测试方向 | SPI 时钟 | 数据量 | 平均速率 | 备注 |
|---|---|---|---|---|
| 上行(ESP32S3→PC) | 默认低频 | 21.6 MB | 约 18 Mbps | 驱动默认配置 |
| 上行(ESP32S3→PC) | 30MHz | 52.7 MB | 约 44 Mbps | 手动提高 SPI 时钟 |
| 下行(PC→ESP32S3) | 30MHz | 45.8 MB | 约 38 Mbps | 受 socket buffer 影响 |
| 上行 | 30MHz,socket buffer 调大 | 61.4 MB | 约 51 Mbps | 进一步优化后 |
需要强调,这个数字在不同网卡、不同路由器、不同杜邦线质量下浮动很大。如果你测出来的数字比这个低很多,优先检查 SPI 时钟和电源质量;如果比这个高,也不用太意外,因为有人通过优化 W5500 的 socket 缓冲区和 SPI 传输粒度,把吞吐跑到了 60Mbps 以上。
5.2 速度瓶颈到底卡在哪
先说结论:W5500 方案的最大瓶颈不是以太网 PHY,而是 SPI 接口带宽。
W5500 的以太网物理层是 100Mbps 全双工,但数据要经过 SPI 进出芯片。SPI 时钟 30MHz 时,满双工的理论极限也才 30Mbit/s 左右,折合有效数据率还要去掉 SPI 帧头、片选切换、寄存器地址等开销。不过 W5500 的 SPI 是全双工的,实际测速能超过 30Mbps,是因为 SPI 的方向控制、数据帧处理和 TCP 的 ACK 流控有叠加效应,实际吞吐可以接近甚至超过 SPI 时钟本身,这也是很多人觉得奇怪的地方。
另外,TCP 的滑动窗口大小也会影响测速。W5500 内部 socket 缓冲区如果分配得太小,发送端容易因为窗口不足而等待,测速结果自然上不去。在 Arduino Ethernet 库中,socket 缓冲区的分配方式不是每个 socket 平均分配,而是可以通过 W5500 寄存器手动分配,比如给当前使用的 socket 分配更大的 TX/RX 空间。
我个人实测下来,把当前 socket 的 TX buffer 调到 16KB、RX buffer 调到 8KB,上行速率会比默认的 2KB/2KB 配置高出 20% 左右。Arduino 库默认的配置偏保守,不适合做高性能传输,这也是很多人用 W5500 测速后觉得“怎么这么慢”的常见原因。
6. 避坑指南:从连不上到断断续续
6.1 常见问题速查表
我在整个调试过程中,踩过的坑基本都可以收敛到下面这张表里:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
hardwareStatus()返回无硬件 | 接线错误、SPI 引脚映射不对 | 核对 FSPI 引脚,检查 CS 是否接到正确的 GPIO |
| DHCP 获取超时 | W5500 复位时序不正确 | 手动拉低 RST 至少 100ms,再释放 |
| 第一次上电能通,但重连后不通 | 电源不干净或 W5500 残留状态 | 增加去耦电容,断电后等待 2 秒再上电 |
| ping 通但 SSH/TCP 连接很慢 | SPI 时钟过低 | 把 SPI 时钟提高到 20MHz 或 30MHz |
| 测速结果上下波动严重 | 使用了劣质杜邦线或网线 | 换短线、换质量好的网线,减少干扰 |
| 大流量时连接中断 | socket 缓冲区分配不足或散热 | 调大 socket buffer,检查模块温度 |
| 局域网能通但外网不通 | 网关或 DNS 未配置 | 正确配置Ethernet.begin的网关和 DNS 参数 |
| 与 WiFi 同时使用时有奇怪问题 | ESP32S3 的 WiFi 和 SPI 外设共用资源 | 项目里最好明确只用一种网络,或在 IDLE 时关闭 WiFi |
6.2 值得展开说的几个调试点
第一个要展开的是:Ethernet.linkStatus()返回 LinkOFF 但网线明明插好了。这个问题的根源在于很多 W5500 模块的 link 状态检测依赖 PHY 的寄存器,而 Arduino Ethernet 库对 W5500 的 link 状态读取不一定准确。遇到这种情况,不必太纠结返回值,只要hardwareStatus()正常、能获取到 IP,就说明链路大概率是通的。
第二个是中断引脚 INT 的使用。W5500 的 INT 引脚在接收到数据或发送完成时会拉低,驱动可以用它来触发中断处理,提高响应效率。但如果你只是做简单的测速和 TCP 通信,不接 INT 也能正常工作。Arduino Ethernet 库默认是靠主循环轮询 W5500 的 Socket 寄存器,并不会强制依赖 INT 引脚。我测试时接了 GPIO14,但发现它在高频通信时会引入额外的抖动,后来干脆不接,轮询模式反而更稳。
第三个是真正常见的“W5500 正常工作几天后连不上”问题。这个问题在很多论坛都有人问,我自己的排查经验是先看电源:设备长期运行后,电源模块温度升高、输出纹波变大,或者供电电压轻微下降,都会导致 W5500 内部 PHY 工作异常。另一个容易被忽略的因素是复位电路上的电容老化或者上拉电阻虚焊,导致复位信号在长期运行后出现干扰。解决方法是给 RST 引脚加一个 10kΩ 上拉电阻,并且在代码里增加定时喂狗式的复位检测机制,在 TCP 长时间无法建连时主动复位 W5500。
6.3 测速不达预期时,第一个该排查的不是代码
很多人在做 TCP 测速时,一看到速率低就怀疑驱动库有问题,然后开始改缓冲区、改延迟、换库。我的经验是:先排除硬件和环境因素,再去动软件。
按这个顺序排查:
- 换一根网线,排除劣质网线导致的丢包重传。
- 确认 PC 网卡没有同时跑其他大流量任务。
- 检查供电,确认 W5500 的 VCC 引脚电压稳定在 3.3V。
- 降低 SPI 时钟到 20MHz,先保证通信稳定,再逐步提高时钟找上限。
- 最后才考虑改代码优化缓冲区。
如果以上都检查过后速率还是上不去,再回头看 socket buffer 和 SPI 事务粒度。W5500 的每次 SPI 读写都有固定开销,最佳做法是尽量一次传输多个字节,尽量避免逐字节读写 Socket 寄存器,这会显著提升吞吐。
收尾的一点个人体会
这次从接线到测速完整跑下来,我最深的感觉是:W5500 方案的上手难度比 LAN8720 低了一个量级,但也不是毫无门槛。硬件上最关键的三个字是“共地、供电、时序”,软件上最关键的是“SPI 时钟、缓冲区、轮询”。只要把这三组关键词对应的细节都处理到位,ESP32S3 挂 W5500 做稳定 TCP 通信是完全可以放心的。
最后再分享一个小技巧:如果你打算长期跑 W5500 项目,建议在 PCB 设计阶段就把 W5500 和 ESP32S3 做在同一个板上,尽量减少杜邦线和飞线。SPI 高速信号在杜邦线上跑,波形完整度真的不如 PCB 走线,我这次测速数字在高频段偶尔会跳,很大程度就是飞线带来的寄生电感和电容导致的。如果只是前期验证,杜邦线没问题,但一旦进入稳定运行阶段,还是老老实实做板子更靠谱。