---
title: "ESP32-S3 + WIZnet W5500 — MicroPython Ethernet (TOE) Guide"
url: "https://maker.wiznet.io/josephsr/projects/esp32-s3-wiznet-w5500--micropython-ethernet-toe-guide/"
markdown_url: "https://maker.wiznet.io/josephsr/projects/esp32-s3-wiznet-w5500--micropython-ethernet-toe-guide/md"
type: "WCC: WIZnet Created Content"
author: "Joseph"
editor: "WIZnet"
editor_url: "https://maker.wiznet.io/"
original_author: "Joseph"
published: "2026-09-14"
language: "en"
hardware: ["WIZnet W5500"]
likes: 0
views: 96
comments: 0
source: "WIZnet Makers (https://maker.wiznet.io/)"
---

# ESP32-S3 + WIZnet W5500 — MicroPython Ethernet (TOE) Guide

> Use the W5500's Hardwired TCP/IP (TOE) from MicroPython on the ESP32-W5500-Dev-V1: flash the prebuilt firmware, bring the interface up, and verify all 16 exampl

Original author: Joseph

## Components

- **WIZnet W5500** x 1 ([docs](https://docs.wiznet.io/Product/Chip/Ethernet/W5500))

WIZnet parts: W5500 ([Datasheet](https://docs.wiznet.io/Product/Chip/Ethernet/W5500/datasheet?utm_source=maker&utm_medium=project&utm_campaign=w5500), [product hub](https://maker.wiznet.io/products/w5500/))

## Documents and links

- [esp32-wiznet-toe-backend](https://github.com/simryang/micropython/tree/esp32-wiznet-toe-backend) (code)
- [TOE examples](https://github.com/simryang/esp32-w5500-toe-examples) (code)

## Article

**ESP32-S3 + WIZnet MicroPython series**

1. W5500 MACRAW guide

2. **W5500 TOE guide** (this article)

3. W6300 MACRAW guide (coming soon)

4. W6300 TOE guide (coming soon)

This guide walks through using the W5500's Hardwired TCP/IP function (TOE mode) from MicroPython on the **ESP32-W5500-Dev-V1** board (ESP32-S3-WROOM-1 + W5500 SPI Ethernet SoM), starting from a blank board. In the MACRAW guide, TCP/IP was handled by MicroPython's built-in lwIP stack. Here the W5500 handles TCP/IP itself, and MicroPython uses the chip's hardware sockets through the `network.WIZNET_TOE` driver. The driver is not in upstream MicroPython yet, so there is one extra step: flashing a firmware build that includes it (section 0).

- Board: **ESP32-W5500-Dev-V1** (ESP32-S3-WROOM-1 + W5500 SPI Ethernet SoM) — the same board as the MACRAW guide, sold as the ESP32 W5500 Dev Kit

- Example repository: github.com/simryang/esp32-w5500-toe-examples

- Firmware: MicroPython fork `simryang/micropython`, branch `esp32-wiznet-toe-backend` (v1.30.0-preview lineage), board definition `WIZNET_ESP32_W5500_DEV_KIT`. A prebuilt `firmware.bin` is available on the example repository's Releases page (section 0).

- Mode: **TOE** — the W5500 runs the TCP/IP stack. Connection management, retransmission and window control happen inside the chip; the MCU exchanges commands and data over SPI.

Every command and log below was run on real hardware on 2026-09-10, firmware commit `3396e59d1` (Release v0.2), with the board reset before each example. Follow the steps in order and you should get the same results.

**Board specs**

| Item | Value |
| --- | --- |
| MCU | ESP32-S3-WROOM-1 |
| SPIRAM | Octal SPIRAM |
| Ethernet chip | WIZnet W5500 (SPI) — used in TOE mode in this guide |
| Ethernet SPI pins | see section 1 |

## What changes in TOE mode

The W5500 has 8 hardware sockets and 16 KB of transmit plus 16 KB of receive buffer memory (datasheet v1.1.0). In MACRAW mode that memory only queues Ethernet frames and lwIP does the TCP/IP work. In TOE mode three things change, and the rest of this guide follows from them.

**1) Socket buffers live on the chip, and their total is fixed.** The 16 KB of TX and 16 KB of RX memory is divided among the hardware sockets. A larger buffer per socket means fewer usable sockets. The default is 2 KB per socket, 8 sockets. This split determines throughput and is covered in section 3.

**2) **`**accept()**`** consumes a hardware socket per client.** The W5500 has no accept queue; a hardware socket in LISTEN state becomes the connection. The driver hides this: `accept()` returns a new file descriptor for the connection and the listener moves to another hardware socket, back in LISTEN state. Ordinary `select.poll()` server code therefore works unchanged. Each connected client holds one hardware socket, so when none is free the listener accepts no new connections until a client disconnects.

**3) Sockets use no host RAM.** Because the socket buffers are on the chip, opening a socket adds nothing to the ESP-IDF heap. Measurements against MACRAW on the same firmware are in the table in section 5.

## Environment setup

Prepare the following on the PC.

**1) Install the Python tools**

```plaintext
pip install esptool mpremote
```

- `esptool` — flashes firmware to the board.

- `mpremote` — connects to the REPL, copies files, and runs scripts.

**2) Get the firmware**

This is the one step that differs from the MACRAW guide. The `network.WIZNET_TOE` driver is not in upstream MicroPython, so the builds on micropython.org do not work here.

A prebuilt image is on the example repository's Releases page as `firmware.bin`. It is built for the board definition `WIZNET_ESP32_W5500_DEV_KIT`, so the SPI wiring is included and `network.WIZNET_TOE()` can be called without arguments. The release notes state the MicroPython commit it was built from. This guide was written with v0.2 (commit `3396e59d1`).

To build it yourself, check out branch `esp32-wiznet-toe-backend` of the fork `simryang/micropython` and run the following in an ESP-IDF v5.5.1 environment:

```plaintext
cd ports/esp32

make BOARD=WIZNET_ESP32_W5500_DEV_KIT submodules

make BOARD=WIZNET_ESP32_W5500_DEV_KIT
```

When building for the generic `ESP32_GENERIC_S3` board, enabling the `MICROPY_PY_NETWORK_WIZNET_TOE` option includes the driver. In that case the SPI wiring must be passed to the constructor as shown in section 1.

**3) Put the board in bootloader mode and find its port**

Connect the board over USB. This board does not enter the bootloader through esptool's automatic reset, so hold BOOT, press and release RESET, then release BOOT. The board enumerates as a new serial port (USB VID 303A, PID 1001). On Windows, check Device Manager → "Ports (COM & LPT)" for the COM number. Replace `COMx` below with that number.

**4) Flash the firmware**

```plaintext
python -m esptool --chip esp32s3 -p COMx -b 460800 --after watchdog_reset write_flash 0x0 firmware.bin
```

`firmware.bin` is a combined image (bootloader, partition table and application) written at offset 0; `erase_flash` is not needed. The `--after watchdog_reset` option reboots the board into the application when flashing finishes. Without it the board stays in the bootloader and you have to press RESET. After the reboot the REPL is on the board's native USB port (PID 4001), which has a different COM number from the bootloader port. The CH340 UART is a separate third port and carries the boot log.

**5) Verify the connection**

```plaintext
mpremote connect COMx
```

`COMx` is now the application port. You should see the MicroPython banner and the `>>>` prompt. Exit the REPL with `Ctrl+]` or `Ctrl+X`.

**6) Prepare the example files**

```plaintext
git clone https://github.com/simryang/esp32-w5500-toe-examples.git

cd esp32-w5500-toe-examples

mpremote connect COMx fs cp net_init.py :net_init.py
```

The examples are run one at a time from section 4.

**Driver structure.** The ESP32 port's `socket` module calls socket, connect, accept, poll, name resolution and the other operations through a backend table (`socket_backend_t`). The default backend is lwIP; the TOE driver is a second backend of the same shape. Each file descriptor records which backend created it, so calls on a TOE socket go to the W5500 and calls on a WiFi socket go to lwIP. This is what lets `13_wifi_coexist.py` use WiFi and W5500 sockets in one firmware, and what keeps the MACRAW driver (`network.LAN`) usable from the same firmware (see section 6). The backend table is compiled in, which is why a separate build is needed.

![](https://maker.wiznet.io/upload/ckeditor5/833831819%5F1789383989%2Epng)

*Figure 1. What changes in the upstream MicroPython ESP32 port. The socket module picks a backend per file descriptor; the new TOE backend and driver use the W5500's Hardwired TCP/IP. Against upstream: 24 new files, 10 modified.*

## Hardware wiring

Same board as the MACRAW guide, so the wiring is the same. The pin assignment below was confirmed from the board schematic (`ESP32-W5500-Dev-V1_SCH.pdf`). **If you use a different board, do not copy these pin numbers; check that board's schematic.**

| Signal | GPIO |
| --- | --- |
| SCK | 12 |
| MOSI | 11 |
| MISO | 13 |
| CS | 10 |
| INT | 14 |
| RESET | 9 |

The `network.WIZNET_TOE` constructor takes the wiring the same way `network.LAN` does. The SPI bus is passed as a `machine.SPI` object, and its `baudrate` is the SPI clock the W5500 is driven at. The `cs=` and `reset=` pins are given alongside it. There is no `int=` argument: in TOE mode the chip handles TCP/IP, so the driver does not read the INT pin. Firmware built for the board definition (the `firmware.bin` on Releases) includes this wiring, so `network.WIZNET_TOE()` with no arguments returns the same object. The examples work either way.

## Shared init module

All examples import `init_lan()` from this module. It initializes the SPI bus and the `network.WIZNET_TOE` interface, waits for the link, then waits up to `dhcp_timeout_s` seconds for a DHCP lease.

```python
# net_init.py
import time
import network
from machine import Pin, SPI


def wiznet_toe():
    # ESP32-W5500-Dev-V1 wiring. SPI(1) is the FSPI bus; its baudrate is the
    # clock the chip is driven at. INT (GPIO 14) is not used by the driver.
    spi = SPI(1, baudrate=20_000_000, sck=Pin(12), mosi=Pin(11), miso=Pin(13))
    return network.WIZNET_TOE(spi=spi, cs=Pin(10), reset=Pin(9))


def init_lan(dhcp_timeout_s=15):
    lan = wiznet_toe()
    lan.active(True)

    # A DHCP DISCOVER sent before the link is up gets no answer, so wait for
    # auto-negotiation first.
    for _ in range(dhcp_timeout_s * 10):
        if lan.status() != network.ETH_DISCONNECTED:
            break
        time.sleep(0.1)

    lan.ifconfig('dhcp')

    for _ in range(dhcp_timeout_s):
        if lan.isconnected():
            return lan
        time.sleep(1)

    raise RuntimeError("W5500 TOE: no link/DHCP within %ds (ifconfig=%s)"
                       % (dhcp_timeout_s, lan.ifconfig()))
```

`network.WIZNET_TOE` uses the same method names as `network.LAN`: `active()`, `status()`, `isconnected()`, `ifconfig()`, and the `ETH_*` status code values are compatible. The MAC address defaults to the Ethernet MAC stored in the ESP32's eFuse.

One difference from `network.LAN` is that DHCP is started explicitly with `ifconfig('dhcp')`, because the DHCP client runs on the W5500 side rather than in lwIP.

Socket usage is otherwise standard MicroPython. `socket`, `select.poll()`, `getaddrinfo()`, timeouts and non-blocking mode all work through the existing API, and none of the examples below calls a TOE-specific function.

## Tuning: socket buffer size and SPI clock

The chip's total buffer memory is fixed, so the buffer per socket and the number of sockets are adjusted with these settings:

```python
nic.config(sock_kb=4)            # buffer per socket, 1/2/4/8/16 KB, applied from the next active(True)

nic.config('usable_socks')       # how many sockets the current setting allows

nic.config(spi_hz=20_000_000)    # SPI clock, applied immediately
```

`11_throughput.py` measures receive throughput for these combinations. Actual output for a 512 KB TCP download with varying read sizes (one run per combination):

```plaintext
sock_kb=2  usable_socks=8  spi_hz=20000000  ip=192.168.7.106
    read   512 B ->    321 KB/s
    read  1024 B ->    547 KB/s
    read  2048 B ->    731 KB/s
    read  4096 B ->    702 KB/s
    read  8192 B ->    694 KB/s
sock_kb=4  usable_socks=4  spi_hz=20000000  ip=192.168.7.106
    read   512 B ->    286 KB/s
    read  1024 B ->    547 KB/s
    read  2048 B ->    850 KB/s
    read  4096 B ->   1147 KB/s
    read  8192 B ->   1095 KB/s
sock_kb=8  usable_socks=2  spi_hz=20000000  ip=192.168.7.106
    read   512 B ->    321 KB/s
    read  1024 B ->    548 KB/s
    read  2048 B ->    817 KB/s
    read  4096 B ->   1170 KB/s
    read  8192 B ->   1481 KB/s
restored sock_kb=2 (usable_socks=8)
```

Two things can be seen. First, throughput rises until the read size reaches the socket buffer size and does not rise beyond it, because `recv()` cannot return more than the socket buffer holds. Second, a larger buffer per socket (fewer sockets) gives higher throughput.

Repeating each combination five times and taking the median gives the following (the single run above is a few percent lower):

| `sock_kb` | usable sockets | actually openable | best read size | throughput |
| --- | --- | --- | --- | --- |
| 2 (default) | 8 | 7 | 2048 B | 788 KB/s |
| 4 | 4 | 3 | 4096 B | 1157 KB/s |
| 8 | 2 | 1 | 8192 B | 1489 KB/s |

The DHCP client uses one socket while the interface is active, so the number of sockets you can actually open is one less than `usable_socks`. At `sock_kb=8` only one socket can be opened, which suits a single stream but not a server. Decide how many sockets you need first, then choose the largest buffer that allows it.

**SPI clock.** The default is 20 MHz. The W5500 datasheet gives the theoretical design speed of the SPI clock as 80 MHz and the minimum guaranteed speed, measured with a stable waveform, as 33.3 MHz. The ESP32-S3 derives its SPI clock by integer division of 80 MHz, so the steps above 20 MHz are 26.67 MHz and 40 MHz. On this board, one hour at 40 MHz — 2,451 downloads of 512 KB (1,225 MB, every byte compared), 245,100 TCP and UDP echoes and 98 DHCP renewals — completed without errors. The default is kept at 20 MHz to allow for signal quality on other boards and wiring; on this board it can be raised with `nic.config(spi_hz=40_000_000)`.

If bulk transfers start failing at a higher clock, check the socket count before the clock. A driver that leaks a file descriptor on close fails at a fixed socket count regardless of clock, and when that happens during a high-clock measurement it is easily mistaken for a clock problem.

## Verify functionality with the examples (16 in total, PASS on hardware)

Run each example with `mpremote connect COMx run <file>`. The LAN cable must be connected and the router must be able to hand out a DHCP lease.

```plaintext
mpremote connect COMx run 01_link.py
```

All 16 were run this way on 2026-09-10, resetting the board before each one.

![](https://maker.wiznet.io/upload/ckeditor5/833831819%5F1789384090%2Epng)

*Figure 2. How the examples run. The PC flashes the firmware and runs the examples over USB; each example brings the interface up with net_init.init_lan() and then uses only the standard socket API. PC-side peers talk to the board over the LAN, not USB.*

| # | File | What it checks | Note |
| --- | --- | --- | --- |
| 01 | `01_link.py` | Link, `status()`, MAC, SPI and buffer settings | quickref\[1\] |
| 02 | `02_dhcp.py` | The DHCP step run directly, without `net_init` | quickref\[1\] |
| 03 | `03_dns.py` | Domain lookup with `socket.getaddrinfo` (W5500's built-in DNS client) | standard socket |
| 04 | `04_tcp_client.py` | TCP client — connect/send/recv/close | standard socket |
| 05 | `05_tcp_server.py` | TCP echo server (accept loop) | standard socket |
| 06 | `06_udp_client.py` | UDP client — sendto/recvfrom | standard socket |
| 07 | `07_udp_server.py` | UDP echo server | standard socket |
| 08 | `08_http_get.py` | Minimal HTTP GET | adapted from MicroPython's examples |
| 09 | `09_http_server.py` | Minimal HTTP server | adapted from MicroPython's examples |
| 10 | `10_reconnect_stress.py` | `active(False)`/`active(True)` 5 times, 120 s link and heap monitoring | written for this guide |
| 11 | `11_throughput.py` | Throughput per read size × `sock_kb` combination | written for this guide |
| 12 | `12_poll_server.py` | Multi-client echo server with `select.poll()` (one listener) | standard socket |
| 13 | `13_wifi_coexist.py` | WiFi and W5500 sockets at the same time | written for this guide |
| 14 | `14_static_ip.py` | Static IP configuration | quickref\[1\] |
| 15 | `15_loopback.py` | Loopback measurement in the manner of WIZnet's SPI Performance document | WIZnet docs\[2\] |
| 16 | `16_ntp.py` | SNTP request over UDP (packet built by hand) | written for this guide |

04 and 06 need an echo server on the PC; for example, run `ncat -l 5000 -k -e /bin/cat` on a PC in the same network. 11 and 15 have PC-side programs in the repository's `tools/` folder.

05 is a TCP echo server that accepts clients in a loop and returns whatever it receives. It is the same code as 05 in the MACRAW guide; the only difference is that `init_lan()` brings up the TOE driver.

```python
import socket
from net_init import init_lan

LISTEN_PORT = 5000
CHUNK = 256

lan = init_lan()
print("listening on", lan.ifconfig()[0], LISTEN_PORT)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", LISTEN_PORT))
s.listen(1)

while True:
    conn, addr = s.accept()
    print("client connected:", addr)
    try:
        while True:
            data = conn.recv(CHUNK)
            if not data:
                break
            print("recv:", data)
            conn.send(data)  # echo
    finally:
        conn.close()
        print("client disconnected:", addr)
```

### Run logs

Actual output on hardware (board IP `192.168.7.106`, PC IP `192.168.7.2`).

01_link.py

```plaintext
link up, active = True connected = True

status = GOT_IP

mac = 14:c1:9f:d9:39:63

spi_hz = 20000000 actual = 20000000

sock_kb = 2 usable_socks = 8
```

02_dhcp.py

```plaintext
link up, address before DHCP: 0.0.0.0

lease acquired in 682 ms

ip: 192.168.7.106

subnet: 255.255.255.0

gateway: 192.168.7.1

dns: 1.1.1.1
```

03_dns.py (the query is handled by the W5500's built-in DNS client)

```plaintext
www.wiznet.io -> 183.111.138.249
8.8.8.8 -> 8.8.8.8
```

05_tcp_server.py (a client on the PC connected and sent one line)

```plaintext
listening on 192.168.7.106 5000

client connected: ('192.168.7.2', 14866)

recv: b'hello from host\n'

client disconnected: ('192.168.7.2', 14866)
```

08_http_get.py (646-byte body received; only the beginning is shown)

```plaintext
b'HTTP/1.1 200 OK\r\nDate: Thu, 10 Sep 2026 06:13:59 GMT\r\nServer: Apache\r\n

Last-Modified: Wed, 05 Feb 2014 16:00:31 GMT\r\nContent-Length: 646\r\n

Connection: close\r\nContent-Type: text/html\r\n\r\n<html><head></head><body>

<header>\n<title>http://info.cern.ch</title>\n</header>\n\n<h1>http://info.cern.ch

 - home of the first website</h1>\n...'
```

10_reconnect_stress.py (interface restarted 5 times)

```plaintext
initial link OK: ('192.168.7.106', '255.255.255.0', '192.168.7.1', '1.1.1.1')

cycle 0 recovered in 2490 ms

  free heap: 8314320

cycle 1 recovered in 2236 ms

  free heap: 8314320

cycle 2 recovered in 13565 ms

  free heap: 8314320

cycle 3 recovered in 2245 ms

  free heap: 8314320

cycle 4 recovered in 2241 ms

  free heap: 8314320
```

Cycle 2 took 13.6 s to recover (the cause was not investigated); the other four took 2.2–2.5 s. Heap usage did not change in any of the five. During the 120 s of monitoring that follows, the link stayed up and the heap stayed constant.

12_poll_server.py (three clients from the PC, one after another)

```plaintext
listening on 192.168.7.106:5000 with poll

client connected: ('192.168.7.2', 29804)

recv: b'client-0 says hi\n'

client disconnected

client connected: ('192.168.7.2', 29805)

recv: b'client-1 says hi\n'

client disconnected

client connected: ('192.168.7.2', 29806)

recv: b'client-2 says hi\n'

client disconnected
```

The same server also passed a test with three clients connected at the same time (each client received its own data back).

13_wifi_coexist.py

```plaintext
ethernet : 192.168.7.106

before WiFi is up:

  W5500 socket                 -> HTTP/1.1 200 OK

wifi     : stack up, scanning...

  22 access points visible

with WiFi stack active:

  W5500 socket                 -> HTTP/1.1 200 OK
```

14_static_ip.py (no DHCP client, so all 8 sockets are available)

```plaintext
ip:       192.168.7.23

subnet:   255.255.255.0

gateway:  192.168.7.1

dns:      1.1.1.1

status:   5 (ETH_GOT_IP = 5)

usable_socks: 8 - none held by DHCP

dns lookup: 172.66.147.243
```

15_loopback.py (the PC-side program sends 512 KB and compares the echoed data byte by byte)

```plaintext
loopback server on 192.168.7.106:5000  (chunk 2048 B, spi 20000000 Hz, sock_kb 2)

client: ('192.168.7.2', 33673)

  524288 B in 1249 ms = 3.36 Mbps (420 KB/s)
```

16_ntp.py

```plaintext
interface: 192.168.7.106  dns: 1.1.1.1

pool.ntp.org     121.174.142.82 -> 2026-09-10 06:19:03 UTC  (rtt 9 ms)
```

**Note on 12 and 13.** `12_poll_server.py` has the usual structure — one listener, client sockets from `accept()` registered with `select.poll()` — and runs on the TOE driver without changes. Each connected client holds one hardware socket, so with the default 8 sockets the server can hold up to 6 clients at once (one socket for DHCP, one for the listener). When all sockets are in use the listener accepts no new connections until one client disconnects. `13_wifi_coexist.py` confirms that HTTP requests over the W5500 socket keep succeeding while the WiFi stack comes up and scans; this is the per-descriptor backend dispatch from section 0 in operation.

## Performance comparison

**Against WIZnet's published measurements.** WIZnet publishes W5500 SPI throughput measured with a TCP loopback test\[2\] (conditions: STM32F103C8 at 72 MHz, SPI 36 MHz, test tool AX2, "Loopback Test (Only TCPs/1CH)"). `15_loopback.py` runs the same kind of test and reports the same unit (Mbps).

|  | MCU / language | SPI clock | socket buffer | Mbps |
| --- | --- | --- | --- | --- |
| WIZnet published | Cortex-M3 72 MHz / C | 36 MHz | 4 KB | 3.50 |
| WIZnet published, best | Cortex-M3 72 MHz / C | 36 MHz | 16 KB (16 KB data buffer) | 3.63 |
| This guide | ESP32-S3 / MicroPython | 20 MHz | 4 KB | 4.92 |
| This guide | ESP32-S3 / MicroPython | 20 MHz | 8 KB | 6.77 |

The MCUs differ (240 MHz ESP32-S3 versus 72 MHz Cortex-M3), so this does not compare driver quality. It shows that MicroPython at SPI 20 MHz reaches throughput in the range of the published figures.

**Against MACRAW mode on the same board.** Measured on the same firmware, wiring and SPI clock with only the stack changed. The details are in a separate document; the summary is:

|  | MACRAW (lwIP) | TOE |
| --- | --- | --- |
| Download throughput, best | 818 KB/s | 1489 KB/s |
| Loopback | 3.30 Mbps | 6.77 Mbps |
| ESP-IDF heap at bring-up (constructor + `active(True)` + address) | 14,440 B | 776 B |
| Heap per idle connected socket | 454 B | 0 B |
| Heap per socket with 512 KB of unread receive data | 6.3 KB | 0 B |
| Concurrent sockets | as memory allows | up to 7 |

The heap figures were measured on 2026-09-10 by initializing both stacks in turn within one boot and reading the total free bytes of the ESP-IDF heap regions. The throughput figures are the medians of five runs from the comparison document. TOE's 1489 KB/s is at `sock_kb=8` (one usable socket); at the default setting with seven usable sockets it is 788 KB/s, about 3% below MACRAW. TOE mode is best understood as trading socket count for throughput.

## Notes

**Close sockets before **`**active(False)**`**.** Restarting the interface invalidates every file descriptor the driver manages. A socket object left without `close()` prints a bare `OSError:` line, with no error number, when its finalizer runs at the next garbage collection (one line per abandoned socket). It does not affect operation; the underlying error is `EBADF`. The number is missing because no exception argument can be allocated during garbage collection.

**Reset the board before measuring.** Interrupting a script with Ctrl-C leaves its socket objects on the heap. If the next initialization frees those descriptors and the numbers are reused before garbage collection runs, a descriptor belonging to a live connection can be closed. The symptom is a transfer that stops part-way without an error. Starting from a board reset avoids this.

**GPIO 9 is the W5500 RESET pin and also the ESP32-S3 port's default I2C SCL pin.** On a generic `ESP32_GENERIC_S3` build, creating `machine.I2C(0)` without pin arguments assigns SCL 9 and SDA 8, and the W5500 is reset every time the bus is configured. The board definition `WIZNET_ESP32_W5500_DEV_KIT` moves the default I2C pins to SCL 5 and SDA 4. On other builds, specify the I2C pins explicitly.

**Using TOE and **`**network.LAN**`** in the same boot.** From firmware `3396e59d1`, the two drivers can be used alternately within one boot. Initializing LAN after TOE, TOE after LAN, and TOE again after that were all confirmed to obtain an address. Re-activating `network.LAN` after the TOE driver has been used is not guaranteed: the two drivers drive the CS pin differently, and only the TOE driver reconfigures the pin on initialization. To return to MACRAW in the same session, reset the board.

**Data verification in throughput measurements.** A benchmark that only counts bytes does not detect transfer errors. The measurements in this guide compared the full received data against the original, separately from the timing. The problems found this way are covered in a separate document.

## Appendix. Porting to another board or chip

For anyone who wants to use this driver elsewhere, this is what to change and in what order to check it, case by case. File locations refer to the fork `simryang/micropython`, branch `esp32-wiznet-toe-backend`.

![](https://maker.wiznet.io/upload/ckeditor5/833831819%5F1789384383%2Epng)

*Figure 3. What to change when porting, and the order of checks. Same W5500: wiring only. Another WIZnet chip: four chip-specific points. Another ESP32-family chip: SPI host and pin settings. Then the common procedure: build, bring-up, sockets, data integrity, coexistence.*

**A. Same W5500, different wiring or board** — no C source changes. Pass the wiring to `network.WIZNET_TOE(spi=, cs=, reset=)`. For an argument-less constructor, add a board definition under `ports/esp32/boards/<BOARD>/` and define `MICROPY_HW_WIZNET_SPI_ID`, `MICROPY_HW_WIZNET_SPI_BAUDRATE`, `MICROPY_HW_WIZNET_SPI_SCK/MOSI/MISO`, `MICROPY_HW_WIZNET_PIN_CS` and `MICROPY_HW_WIZNET_PIN_RST` in `mpconfigboard.h` (the same names upstream's `network_wiznet5k.c` reads). Check that the port's default I2C and UART pins do not overlap the W5500 pins, and put `set(MICROPY_PY_NETWORK_WIZNET_TOE ON)` in `mpconfigboard.cmake`.

**B. Another WIZnet chip (W5100S, W6100, W6300, …)** — for a chip ioLibrary supports there are four points to change: the chip selection `_WIZCHIP_` in `esp32_common.cmake`, the expected VERSIONR value and the buffer budget `TOE_BUF_BUDGET_KB` in `toe_net_bringup.c`, and the SPI frame layout (address and control bytes) and clock range in `toe_spi_port.c`. The socket count follows ioLibrary's `_WIZCHIP_SOCK_NUM_`. The DHCP and DNS clients (ioLibrary application code) and the backend and VFS layers stay as they are. The only chip verified on hardware for this guide is the W5500.

**C. Another ESP32-family chip (ESP32-C3, C6, S2, …)** — the driver uses only ESP-IDF's `spi_master`, `esp_vfs`, `esp_netif` and `gpio` APIs, so it is expected to build without source changes. Set the `machine.SPI` host number and pins (whether they are IOMUX pins) and the `sdkconfig` for the board. PSRAM does not matter, because the socket buffers are on the chip. This was not verified on hardware.

**Common order of checks** — (1) build with `make BOARD=<BOARD> submodules` then `make BOARD=<BOARD>` and confirm there are no compiler warnings. (2) Check link and address with `01_link.py` and `02_dhcp.py`, and the buffer split with `config('usable_socks')`. (3) Check socket behavior and descriptor leaks with the clients and servers in `04`–`09`, repeated accept in `12`, and the five restarts in `10`. (4) Measure data integrity and throughput with `11` (every byte compared) and `15`, raising the SPI clock from a low value. (5) If needed, confirm WiFi coexistence with `13`.

**Proposing to upstream MicroPython** — the plan is two stages. Stage 1 is the backend table (`modsocket.c`, the new `modsocket.h`, the NULL check in `network_lan.c`), for which an A/B regression showed no change in lwIP behavior. Stage 2 is the driver (`ports/esp32/wiznet_toe/`), the `lib/wiznet5k` submodule, the `esp32_common.cmake` option and the board definition.

## References

**[1] MicroPython ESP32 port quickref** — `network.LAN` methods and the `ETH_*` status codes. `network.WIZNET_TOE` uses the same method names and status code values.

- docs.micropython.org/en/latest/esp32/quickref.html

**[2] WIZnet W5500 official documentation**

- Product page: docs.wiznet.io/Product/Chip/Ethernet/W5500

- Datasheet v1.1.0 (SPI clock specification in §6 Electrical Specifications): W5500_ds_v110e.pdf

- SPI Performance (loopback test conditions and results): docs.wiznet.io/Product/Chip/Ethernet/W5500/Application/spi-performance

**[3] MACRAW guide for the same board**: maker.wiznet.io/josephsr/projects/esp32-s3-wiznet-w5500--micropython-ethernet-macraw-guide

**[4] Example repository and firmware (Releases)**: github.com/simryang/esp32-w5500-toe-examples

**[5] MicroPython fork with the driver** (branch `esp32-wiznet-toe-backend`): github.com/simryang/micropython/tree/esp32-wiznet-toe-backend

---

Source: https://maker.wiznet.io/josephsr/projects/esp32-s3-wiznet-w5500--micropython-ethernet-toe-guide/
