Build a multi-switch useless box with a single moving arm, custom PCB, and 3D-printed mounting — full Arduino code and STL files included.
For projects where a reliable wired network connection matters, industrial sensors, local dashboards, devices that need to stay connected without worrying about WiFi interference, the W5500 is my go to choice for adding Ethernet to an ESP32. It connects over SPI, is well supported, and as of ESP32 Arduino core v3.x requires no additional libraries. Given its onboard TCP/IP stack it is also quite fast.
This post covers the complete setup using the USR-ES1 W5500 module (amazon.com1, amazon.de1) and an ESP32-S3 DevKitC-1 (amazon.com1, amazon.de1). The same approach works for any W5500 breakout board.
This post is part of a complete ESP32 reference you can find here.
The W5500 has a hardwired TCP/IP stack built into the chip itself. The ESP32 also has its own TCP/IP stack (lwIP). When we connect a W5500 to an ESP32 we therefore have two independent TCP stacks in the same system, and which one handles our network traffic matters.
The traditional Arduino Ethernet.h library targets the W5500's onboard stack. On the ESP32 this means the W5500 and the ESP32's network layer end up isolated from each other. WebServer.h, HTTPClient.h, mDNS, and every other ESP32 networking library go through lwIP. They have no visibility into the W5500's stack. This does not produce a compiler error. The code compiles, the module initializes, and then things silently fail: the device resets in a loop, DHCP never completes, or requests are simply never received. Unfortunately this is quite hard to debug.
A good, working, approach for the ESP32 is to use ETH.h, which is part of the ESP32 Arduino core. With ETH.h, lwIP handles all TCP/IP processing and the W5500's onboard stack is not used. The result is that every ESP32 networking library works exactly as it does over WiFi, just through the W5500 instead.
For the same reason we prefer the W5500 over alternatives like the ENC28J60. The W5500 is well supported by the ESP32 core, widely available, and works reliably with this setup. The ENC28J60 is a different architecture and requires different drivers. For an ESP32 project I am unaware of any good reason to use it over the W5500.
W5500 support via ETH.h requires ESP32 Arduino core v3.x or later. No additional libraries are needed. Everything is included in the core. Earlier versions of the core do not support the ETH_PHY_W5500 driver or the ETH.begin() signature used here.
To check or install the correct core version, open the Arduino IDE, go to Tools → Board → Boards Manager, search for esp32 by Espressif, and install or update to v3.x. as shown in fig. 1.
The USR-ES1 is a compact W5500 breakout module that exposes the SPI interface and the CS, INT, and RST pins on a standard 2.54mm header. It runs on 3.3V and is directly compatible with the ESP32-S3 DevKitC-1 without level shifting.
The wiring used in this example is shown below. The SPI pins are chosen to avoid conflicts with other peripherals on the board. The RST pin is not connected as the W5500 handles its own reset on power-up and the driver accepts -1 to skip it.
// W5500 SPI pins
#define ETH_SPI_SCK 12
#define ETH_SPI_MISO 13
#define ETH_SPI_MOSI 11
#define ETH_PHY_CS 10
#define ETH_PHY_IRQ 14
#define ETH_PHY_RST -1 // not connected
Rather than using the default shared SPI instance, we create a dedicated SPIClass for the W5500. This is good practice whenever multiple SPI peripherals are present. It avoids bus contention and makes the pin assignment explicit. If we want to use additional SPI peripherals like sensors or external ADCs we can connect them to SPI3.
On the ESP32-S3, SPI pins are remappable via the GPIO matrix so the HSPI/VSPI distinction of the original ESP32 is less rigid. Creating a named instance and passing it to ETH.begin() is the approach we'll go with regardless.
SPIClass ethSPI(HSPI);
// in setup():
ethSPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI, ETH_PHY_CS);
ETH.begin() initializes the W5500 driver and starts the lwIP network interface. The parameters are:
ETH.begin(ETH_PHY_W5500, 0, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ethSPI);
Going through each argument:
ETH_PHY_W5500: Selects the W5500 driver.0: PHY address, not used by the W5500. Both 0 and 1 work.ETH_PHY_CS: Chip select pin.ETH_PHY_IRQ: Interrupt pin for incoming data. The W5500 uses this to signal incoming data.ETH_PHY_RST: Reset pin, set to -1 if not connected.ethSPI: The SPI instance to use for communication.
ETH.begin() must be called after ethSPI.begin() and after registering the event handler.
The ESP32 network stack is event-driven. Rather than polling for connection status, we register a callback with Network.onEvent() that is called when the network state changes. We use a global eth_connected flag to track whether the link is up and an IP address has been assigned.
static bool eth_connected = false;
void onEvent(arduino_event_id_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_ETH_START:
Serial.println("ETH Started");
ETH.setHostname("esp32-eth0");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.println("ETH Connected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.print("ETH Got IP: ");
Serial.println(ETH.localIP());
eth_connected = true;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("ETH Disconnected");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("ETH Stopped");
eth_connected = false;
break;
default:
break;
}
}
// in setup(), before ETH.begin():
Network.onEvent(onEvent);
The hostname must be set inside the ARDUINO_EVENT_ETH_START handler. This is the only point at which the network interface exists but has not yet negotiated a connection, making it the correct place to configure it.
eth_connected becomes true only after ARDUINO_EVENT_ETH_GOT_IP fires. At this point, DHCP has completed and the device has a usable IP address. Any code that uses the network (e.g., web server, HTTP client, mDNS) should be guarded by this flag. To do so, we will use the eth_connected variable inside loop().
DHCP is handled automatically by lwIP once ETH.begin() is called and the physical link is established. There is nothing to configure. When the cable is connected and a DHCP server is available on the network, ARDUINO_EVENT_ETH_GOT_IP will fire and ETH.localIP() returns the assigned address.
If DHCP never completes, ETH Got IP never appears on the serial output. The most common causes for this are:
ETH.h or W5500 driver and just use the ESP32 core v3.x built-in version on its own.ARDUINO_EVENT_ETH_CONNECTED. If this event never fires, the physical link is not established. If it fires but GOT_IP does not, the DHCP server is not responding.If the project uses only Ethernet and does not need WiFi or Bluetooth, we can disable both to reduce power consumption and free up resources.
WiFi.mode(WIFI_OFF);
btStop();
These calls go at the start of setup(), before ETH.begin(). For a project that uses ethernet anyway, this is good practice.
Since ETH.h integrates with lwIP, WebServer.h works identically over Ethernet as it does over WiFi. We do not have to configure anything Ethernet-specific. The only difference is that server.handleClient() should be called only when eth_connected is true as shown below.
void loop() {
if (eth_connected) {
server.handleClient();
}
}
For a full explanation of route registration, handler functions, JSON endpoints, and GET arguments see the ESP32 web server post.
mDNS also works identically to the WiFi case. We include ESPmDNS.h and call MDNS.begin() once the network is up. Since we have already set the hostname in the event handler, we use the same name here for consistency.
#include <ESPmDNS.h>
// in setup(), after ETH.begin():
if (MDNS.begin("esp32-eth0")) {
Serial.println("mDNS responder started");
}
The device is now reachable at http://esp32-eth0.local from any device on the same network.
The following is a complete minimal example combining everything above. It initializes the W5500 over HSPI, handles DHCP using events, disables WiFi and Bluetooth, and runs a simple web server once the connection is established.
#include <ETH.h>
#include <WebServer.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <SPI.h>
// SPI instance for W5500
SPIClass ethSPI(HSPI);
// W5500 pins
#define ETH_SPI_SCK 12
#define ETH_SPI_MISO 13
#define ETH_SPI_MOSI 11
#define ETH_PHY_CS 10
#define ETH_PHY_IRQ 14
#define ETH_PHY_RST -1 // not connected
WebServer server(80);
static bool eth_connected = false;
void onEvent(arduino_event_id_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_ETH_START:
Serial.println("ETH Started");
ETH.setHostname("esp32-eth0");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.println("ETH Connected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.print("ETH Got IP: ");
Serial.println(ETH.localIP());
eth_connected = true;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("ETH Disconnected");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("ETH Stopped");
eth_connected = false;
break;
default:
break;
}
}
void handleRoot() {
server.send(200, "text/plain", "hello from esp32 over ethernet!");
}
void handleNotFound() {
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
}
void setup() {
WiFi.mode(WIFI_OFF);
btStop();
Serial.begin(115200);
delay(500);
ethSPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI, ETH_PHY_CS);
Network.onEvent(onEvent);
ETH.begin(ETH_PHY_W5500, 0, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ethSPI);
if (MDNS.begin("esp32-eth0")) {
Serial.println("mDNS responder started");
}
server.on("/", handleRoot);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("HTTP server started");
}
void loop() {
if (eth_connected) {
server.handleClient();
}
delay(10);
}
All in all, adding a wired Ethernet connection to an ESP32 project has gotten quite easy as long as we know which library to use. With this sorted, we can simply use a W5500 module and use the esp32 as normal, just that it has a reliable wired ethernet connection.
This project (with exceptions) is published under the CC Attribution-ShareAlike 4.0 International License.
1: As an Amazon Associate I earn from qualifying purchases.