Build a Wired ESP32 IoT Sensor Node with W5500 Ethernet for Reliable Uptime
This site's ESP32 connectivity coverage has focused almost entirely on wireless links: the WiFi getting-started guide, the ESP-NOW mesh project, the LoRaWAN walkthrough, and the notes on Matter and Thread. Wired Ethernet hasn't come up, which is a gap, because there's a whole category of ESP32 project where wireless is the wrong tool. A security sensor bolted to a shed wall, an irrigation controller buried behind a fence, or an always-on dashboard in a server closet all share one requirement: they cannot silently drop off the network because a router rebooted, a neighbor's microwave keyed up on 2.4GHz, or the WiFi signal degraded through one too many stud walls. A wired link doesn't suffer from any of that. This project builds a standalone ESP32 sensor node driven over 100Mbps Ethernet through a WIZnet W5500 SPI module, with a BME280 environmental sensor and a PIR or reed-switch input as the sensing payload, publishing over MQTT to a local broker. The result is a node that comes back online the instant power and cable are restored, with no association, handshake, or roaming logic to fail.
Why Wired Ethernet for an ESP32 Node
WiFi on the ESP32 is convenient because it needs no extra hardware, but it has real failure modes that matter for unattended infrastructure. The radio has to re-associate after every access point reboot or firmware update, DHCP leases can be slow to renew on a congested network, and RSSI at the edge of a garage or basement can be marginal enough that the node drops packets under load without ever fully disconnecting. Wired Ethernet sidesteps all three: link state is binary and instantaneous, there's no contention with other 2.4GHz devices (baby monitors, Bluetooth speakers, and other WiFi APs all share that band), and a Cat5e or Cat6 run can go through a wall, down a conduit, or across a yard without worrying about attenuation. The tradeoff is that you need a cable, a switch port, and slightly more hardware and wiring effort than screwing a WiFi module onto a board. For a security sensor at a gate or an irrigation manifold at the far end of a property where you're already running a cable for power, that tradeoff is worth it.
Choosing the Hardware
The ESP32 doesn't have a native Ethernet MAC and PHY combination that works without extra silicon (some ESP32 variants have an internal EMAC, but it needs an external PHY chip wired over RMII, as seen on boards like the Olimex ESP32-POE or the WT32-ETH01). For a DIY breadboard-to-perfboard build, the simpler and more widely available route is an SPI Ethernet controller, and the WIZnet W5500 is the standard choice: it's a full TCP/IP offload chip (not just a MAC/PHY), it's cheap, it's available as a small breakout board from half a dozen vendors, and it's directly supported in both the Arduino-ESP32 core and ESP-IDF's esp_eth component.
For the ESP32 board itself, a standard ESP32-WROOM-32 DevKitC (or DevKitC-32E) is the easiest starting point because it exposes the VSPI/SPI3 pins cleanly on the outer header rows and has enough free GPIOs left over for the I2C sensor and a digital input. An ESP32-S3 DevKitC works equally well and is a reasonable substitute if you want native USB and more GPIOs for a second sensor, but the pin assignments below assume the classic WROOM-32 layout.
OptionWhat it isWhen to use it ESP32 DevKitC + W5500 SPI breakoutSeparate boards wired over SPIMost flexible, cheapest, easiest to source; this project Olimex ESP32-POE / ESP32-POE-ISOESP32 with built-in LAN8720 PHY and PoEProduction-grade single-board deployments, no wiring needed WT32-ETH01ESP32 module with integrated LAN8720 RMII PHYCompact all-in-one board, but fewer exposed GPIOs for sensorsThis build uses the DevKitC plus W5500 breakout route since it keeps the parts generic, keeps cost low, and leaves the most GPIO headroom for sensors.
Wiring the W5500 to the ESP32
The W5500 talks to the ESP32 over standard SPI, so four data lines plus power, ground, reset, and an optional interrupt line are all that's needed. This project uses the ESP32's default VSPI (SPI3_HOST) pins, the same bus this site's guide to SPI wiring for OLED displays uses, just with a different chip select pin so it doesn't collide with anything else on the bus.
W5500 pinESP32 GPIONotes VCC3V3W5500 is 3.3V logic; do not feed it 5V GNDGNDCommon ground with the ESP32 SCKGPIO18VSPI clock MISOGPIO19VSPI master-in MOSIGPIO23VSPI master-out CS / SCSGPIO5Chip select, active low RSTGPIO33Active-low reset; hold high in normal operation INTGPIO4Optional; only needed if you use interrupt-driven link detectionFor the sensor side, the BME280 goes on the I2C bus at SDA GPIO21 / SCL GPIO22, the same default pins used throughout this site's I2C wiring guide, and a PIR module or magnetic reed switch (for a door/gate security variant) goes on a free digital input such as GPIO27, pulled up or down depending on the sensor's output type. Keep the SPI wiring runs short (under 10cm on a breadboard) since the W5500's SPI clock can run up to 80MHz and long unshielded jumper wires will introduce noise at higher clock settings; if you see intermittent link resets later, dropping the SPI clock in software to 20MHz is usually the fix before you start reworking wiring.
Software Setup
Install the ESP32 board package in Arduino IDE (Boards Manager, "esp32 by Espressif Systems") at version 2.0.3 or newer, since that's when native SPI Ethernet support for the W5500, W5100S, DM9051, and KSZ8851SNL landed in the Arduino-ESP32 core's ETH.h library. This matters because it means you don't need a separate Ethernet.h/EthernetClient library port the way older ESP32 W5500 tutorials required; ETH.h registers the W5500 as a standard network interface alongside WiFi, so existing code using WiFiClient, HTTPClient, or PubSubClient works unchanged once the Ethernet link comes up.
A minimal bring-up sketch looks like this:
#include <ETH.h> #define ETH_PHY_TYPE ETH_PHY_W5500 #define ETH_PHY_ADDR 1 #define ETH_PHY_CS 5 #define ETH_PHY_IRQ 4 #define ETH_PHY_RST 33 #define ETH_SPI_SCK 18 #define ETH_SPI_MISO 19 #define ETH_SPI_MOSI 23 void onEvent(arduino_event_id_t event) { if (event == ARDUINO_EVENT_ETH_GOT_IP) { Serial.print("ETH IP: "); Serial.println(ETH.localIP()); } } void setup() { Serial.begin(115200); Network.onEvent(onEvent); ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, SPI3_HOST, ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI); } void loop() {}If you're stuck on an older core (pre-2.0.3) or building against ESP-IDF directly instead of Arduino, ESP-IDF's esp_eth component has native support for SPI Ethernet modules through its spi_eth_module driver; the idf.py example under examples/ethernet/basic covers W5500 configuration explicitly and is the reference to follow if you'd rather stay outside the Arduino ecosystem. Either way, exact pin macro names shift slightly between core versions, so check the specific W5500 example shipped with whichever Arduino-ESP32 version you installed rather than copying pin numbers blind.
Assembly
- Solder pin headers onto the W5500 breakout and BME280 module if they didn't ship pre-soldered.
- Prototype the full circuit on a breadboard first: ESP32, W5500, BME280, and the PIR/reed switch input, wired per the tables above.
- Flash the bring-up sketch and confirm the ETH IP address prints over serial and the W5500's link LED lights up when a patch cable is connected to a switch.
- Once verified, transfer the circuit to perfboard: mount the ESP32 DevKitC and W5500 breakout with a short standoff gap between them, run the SPI wiring on the underside, and keep the I2C wiring for the BME280 physically separated from the SPI lines to avoid crosstalk.
- Mount the assembled perfboard, ESP32, and W5500 inside a project enclosure with a cutout or cable gland for the RJ45 jack and, if the node will sit outdoors or in a garage, a second gland for the sensor wiring or a weatherproof vent for the BME280 (it needs airflow to read ambient conditions accurately, so don't fully seal it inside the box).
- Terminate a Cat5e or Cat6 patch cable to the W5500's RJ45 jack and run it back to a switch or PoE injector, applying power to the ESP32 either via USB, a barrel-jack 5V supply, or a PoE splitter feeding the 5V/VIN pin (see the power section below).
Calibrating and Testing the Link
Once assembled, confirm the node behaves like any other wired device on the network before layering sensor logic on top. Start with DHCP (the default in the sketch above) and verify the assigned address shows up in your router's client list with a sensible hostname; then ping it from another machine on the LAN and check for 0% packet loss over a few hundred pings, which flushes out any marginal SPI wiring immediately since a flaky connection to the W5500 shows up as intermittent RX/TX resets rather than a clean network dropout. If you plan to run the node long-term, switch from DHCP to a static IP (or a DHCP reservation at the router) so the sensor node's address doesn't shift after a router reboot, which matters more here than on a WiFi node since one of the points of this build is to remove reboot-related flakiness. Compare round-trip latency against an equivalent WiFi-connected ESP32 running the same firmware; a wired link typically shows sub-2ms local pings versus 5-15ms and higher jitter over WiFi, which is a useful sanity check that the Ethernet path is actually being used and not silently falling back.
Adding the Sensor Payload
With the network layer solid, wire in the BME280 over I2C using the Adafruit_BME280 or Bosch BME280 library, and read the PIR or reed switch on a simple digitalRead() with a debounce delay. Publish readings over MQTT using the PubSubClient library; because ETH.h registers the Ethernet interface as the active network client, you pass a plain WiFiClient object (not a special EthernetClient) into PubSubClient's constructor and it transparently rides over the wired link. Publish temperature, humidity, pressure, and the digital sensor state on a topic tree like sensors/nodename/temperature, sensors/nodename/motion, and so on, at an interval appropriate to the use case (every 30-60 seconds for environmental monitoring, immediate publish-on-change for a security trigger). If the node needs to run on battery backup during a power cut rather than mains, note that Ethernet PHYs draw meaningfully more current than an idle WiFi radio in light-sleep, so this site's ESP32 deep sleep guide's low-power techniques apply less cleanly here since the W5500 has to stay powered and linked to remain reachable; a wired node is best treated as an always-on device backed by a small UPS or battery pack rather than a deep-sleep node.
Power and PoE Options
The simplest power path is a 5V wall adapter into the ESP32's VIN pin or USB port, run alongside the Ethernet cable. For a cleaner single-cable install, add a passive PoE splitter rated for the W5500's low power draw: it taps 48V from spare pairs on the Cat5e/Cat6 cable (or from a PoE switch/injector upstream) and steps it down to 5V for the ESP32, so only one cable needs to reach the node. The W5500 breakout itself has no PoE circuitry, so the splitter does the DC conversion before power ever reaches the board; make sure the splitter's output is a regulated 5V feed, not raw 12V, since that will damage the ESP32's onboard regulator.
Troubleshooting
SymptomLikely causeFix ETH.begin() never fires ARDUINO_EVENT_ETH_GOT_IPWrong CS/RST pin macros for your core version, or DHCP server unreachableCheck the W5500 example bundled with your installed Arduino-ESP32 core version; try a static IP to isolate DHCP issues Link LED on W5500 stays offBad Ethernet cable, wrong pinout on a DIY cable, or dead switch portSwap the cable and switch port; test the same cable on a laptop Intermittent disconnects under loadSPI clock too high for wiring quality, long/unshielded jumper leadsShorten SPI leads, move to perfboard, or drop SPI clock to 20MHz in the ETH.begin() config BME280 readings are stuck or return NaNI2C address conflict with another device, or address mismatch (0x76 vs 0x77)Run an I2C scanner sketch, confirm the address matches the module's actual solder-jumper setting MQTT connects then immediately dropsBroker keepalive timeout shorter than publish interval, or duplicate client ID on the brokerIncrease MQTT keepalive, give the node a unique client ID string Node works on the bench but not after enclosure installRJ45 cable pinch or strain at the gland, or BME280 sealed in still airRecheck cable termination, add a vent near the BME280 for accurate ambient readingsSafety Notes
This is a low-voltage build (3.3V/5V logic, PoE at most 48V isolated through a splitter), so the main risks are ESD damage to the W5500 and ESP32 during handling and simple wiring mistakes rather than fire or shock hazard. Ground yourself before handling the bare boards, double-check polarity before applying power the first time, and confirm any PoE injector or switch you use is a genuine 802.3af/at-compliant source rather than an unregulated passive injector of unknown voltage, since feeding the wrong voltage into a passive splitter is the most common way to kill the ESP32 on a build like this.
Once running, this node behaves like any other piece of wired network infrastructure: it comes up the moment power and link are present, survives router reboots and WiFi interference without a blip, and reports its state over a predictable, low-latency path. That reliability is the entire point of choosing Ethernet over WiFi for this class of project, and the same W5500-plus-ESP32 pattern extends cleanly to other always-on nodes on this site, from irrigation valve controllers to gate sensors, anywhere a dropped wireless link is not an acceptable failure mode.
Related Guides
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- Arduino vs ESP32: Which Should You Use? A Practical Comparison
- Getting Started with ESP32: GPIO, WiFi, and Your First Project
- MQTT and Node-RED on Raspberry Pi: Visual Automation for ESP32 Sensor Networks
- Shift Registers and I/O Expanders for Arduino and ESP32: 74HC595, MCP23017, and PCF8574 Explained
- Building a Standalone ESP32 Web Control Panel: AsyncWebServer, LittleFS, and Captive Portal Setup
- Driving E-Paper Displays with ESP32 and Arduino: SPI Wiring, GxEPD2, and Partial Refresh
- Build a DIY ESP32 DMX512 Lighting Controller