# [ESP32: Adding Ethernet with the W5500 module](https://blog.hirnschall.net/esp32-w5500-ethernet/)

author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/)

meta description: Add wired Ethernet to an ESP32 using a W5500 module and ETH.h. Covers SPI setup, DHCP, WebServer, and mDNS. Full working example included.

meta title: ESP32 Ethernet with W5500 — ETH.h Arduino Code Example

date published: 11.04.2026 (DD.MM.YYYY format)
date last modified: 11.04.2026 (DD.MM.YYYY format)

---

Introduction
------------

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](https://amzn.to/4mlfMUW), [amazon.de1](https://amzn.to/4mlfGwy)) and an ESP32-S3 DevKitC-1 ([amazon.com1](https://amzn.to/4tEXG2G), [amazon.de1](https://amzn.to/4c26zgR)). The same approach works for any W5500 breakout board.

This post is part of [a complete ESP32 reference you can find here.](https://blog.hirnschall.net/esp32/)

The Two TCP Stacks Problem
--------------------------

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.

Requirements
------------

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.

![ESP32 Arduino core v3.x is required for W5500 support](https://blog.hirnschall.net/esp32-w5500-ethernet/resources/img/esp32-core.jpg)


Figure 1: ESP32 Arduino core v3.x is required for W5500 support

Hardware
--------

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
```

SPI Setup
---------

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()
-----------

`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.

Event Handling
--------------

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
----

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:

* **Using the wrong library:** If the code compiles but DHCP silently fails, the most likely cause is that some third-party Ethernet library is interfering with the ESP32 core's network stack. Remove any installed libraries that include their own `ETH.h` or W5500 driver and just use the ESP32 core v3.x built-in version on its own.
* **Wrong IRQ pin:** The W5500 uses the IRQ pin to signal incoming data including DHCP responses. If this pin is configured incorrectly or the connection is bad, the driver can miss responses. Double-check the wiring and the pin definition.
* **No cable or no DHCP server:** Verify this with `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.

Disabling WiFi and Bluetooth
----------------------------

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.

Web Server over Ethernet
------------------------

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](https://blog.hirnschall.net/esp32-webserver/).

mDNS
----

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.

Full Example
------------

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);
}
```

Conclusion
----------

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.