Wiznet makers

Grace_Koo

Published September 03, 2026 ©

114 UCC

25 WCC

11 VAR

0 Contests

0 Followers

0 Following

Original Link

How Does a Zephyr Driver Use W5500 Hardware TCP/IP When the In-Tree One Is MACRAW Only?

Zephyr's in-tree W5500 driver is MACRAW only. This repo adds an out-of-tree socket-offload driver on the chip's hardware TCP/IP.

COMPONENTS Hardware components

WIZnet - W5500

x 1


PROJECT DESCRIPTION

iot-zephyr-app — A W5500 TOE Driver Built Outside Zephyr, Because Zephyr Has None

#Zephyr #W5500 #TOE #SocketOffload #STM32F407 #STM32F103 #MACRAW #devicetree #MCUboot

📚 Context: kabirz/iot-zephyr-app — a west manifest workspace holding the Zephyr firmware for three pieces of custom hardware, on Zephyr v4.4.0, with MCUboot-signed OTA. Created 2026-08-06, last pushed 2026-09-02. The driver files carry Apache-2.0 headers. ✅ Verification status: Read from main: drivers/w5500_toe/w5500_toe.c (2,089 lines), its Kconfig, dts/bindings/net/wiznet,w5500-toe.yaml, examples/w5500-toe-echo/ (README, overlay, prj.conf, main.c), applications/n2e-gw/ (overlay, prj.conf, README), and boards/io_edge_f407vet6/. The figures are the repository's own, quoted from its README and not reproduced here.


01 — What the repository builds

One developer (kabirz) keeping the Zephyr firmware for three devices he built in a single workspace.

  • angle-handler — a handheld laser-rangefinder controller, sending its data out over nRF24L01+ or CAN.
  • n2e-gw — the relay gateway between that controller and a host PC, forwarding what arrives over nRF24 out through W5500 Ethernet as UDP.
  • io-edge-hub — an industrial IO acquisition node: 16 DI / 8 DO / 4 AI, Modbus TCP/RTU.

What to look at is under drivers/. He implemented a W5500 TOE driver, which Zephyr does not have.


02 — It builds a path Zephyr does not have

Zephyr's in-tree W5500 driver, drivers/ethernet/eth_w5500.c, is MACRAW only. It treats the chip as a plain MAC and feeds raw Ethernet frames into the native IP stack; the TCP/IP engine the W5500 carries in silicon goes unused. Choosing Zephyr has meant giving up TOE.

drivers/w5500_toe/ implements the other side. One 2,089-line driver exposes the chip's eight hardware sockets as application sockets through Zephyr's socket-offload framework.

It adds a devicetree binding of its own, layered on include: [spi-device.yaml]:

compatible = "wiznet,w5500-toe";
 

The binding draws the boundary itself:

Mutually exclusive with the in-tree MACRAW driver ("wiznet,w5500" in zephyr/drivers/ethernet/eth_w5500.c): a board picks exactly one compatible for its W5500 node, and Kconfig (W5500_TOE depends on !ETH_W5500) keeps the two drivers from being enabled together.

Kconfig enforces it:

config W5500_TOE
depends on !ETH_W5500
depends on NET_SOCKETS
select NET_SOCKETS_OFFLOAD
select NET_SOCKETS_OFFLOAD_DISPATCHER
 

Which is why the example's prj.conf reads:

CONFIG_NETWORKING=y
CONFIG_NET_NATIVE=n
CONFIG_W5500_TOE=y
 

CONFIG_NET_NATIVE=n. Zephyr's IP stack is compiled out entirely, and the socket(), bind(), listen(), accept(), send() and recv() an application calls all land on chip registers across SPI. w5500_toe_iface_init() registers the interface with net_if_socket_offload_set(iface, w5500_toe_socket_create) and writes the address, netmask and gateway straight into the chip's SIPR, SUBR and GAR. The static address comes from devicetree properties (local-ip, netmask, gateway).


03 — Both models live in the same repository

This is where the repository earns its value. The same author drives the same chip two ways, and writes the reasoning into both sets of config files. All three product applications are still on the in-tree MACRAW path; what uses the TOE driver is an example.

 n2e-gww5500-toe-echo
BoardSTM32F103RCT6 (48 KB RAM)STM32F407VET6
compatiblewiznet,w5500 (in-tree MACRAW)wiznet,w5500-toe
IP stackZephyr nativeNone (NET_NATIVE=n); the chip's
SPISPI3 @ 18 MHzSPI2 @ 21 MHz

The wiring:

  • n2e-gw — SCK PB3 / MISO PB4 / MOSI PB5 / CS PA15, INT PD2, RST PC12
  • w5500-toe-echo — SCK PB13 / MISO PB14 / MOSI PB15 / CS PB12, INT PD1, RST PD0

The MACRAW side's prj.conf records what running the native stack in 48 KB of RAM cost. The default Ethernet buffer counts (14/14/36/36) are too much for the application, and when the RX TC thread cannot release RX net_pkt the RX pool runs dry → w5500_rx blocks for 100 ms → w5500_thread freezes without processing SENDOK → everything stalls, ping times out, configuration stops responding. The fix was 8/8/16/16 with a 128 B data size.

A related note sits in the other example (eth-echo, on a GD32 internal MAC): the default 128-byte data chunks leave only about three TCP segments in flight, stalling any transfer past ~4 KB, so NET_BUF_DATA_SIZE goes to 1536 with RX/TX counts at 192.

And right next to that is a driver that removes the native stack altogether. The motivation for offload is not in a document; it is in the neighbouring config comments.

What TOE bought

  • The MCU stops being a network node. With the IP stack out of the build there are no net_pkt or net_buf pools, so the pool-exhaustion-to-thread-freeze sequence above cannot occur at all. Packet memory is the chip's 32 KB (16 TX + 16 RX).
  • The host never touches a frame. ARP, ICMP and TCP retransmission finish in silicon — the example README notes that ping is answered inside the W5500. The MCU only moves payload.
  • Periodic polling disappears. With INTn wired, readiness arrives as an interrupt and no recurring work runs.
  • It holds up under connect storms. 3000/3000 cycles at ~190 conn/s, zero refusals.

None of that is a claim about speed. Measured throughput is ~0.5 MB/s and the ceiling is the SPI link, which MACRAW crosses as well, so the ordering does not flip. What changes is how much MCU it costs to reach that speed. The repository publishes no before-and-after build sizes, so the saving cannot be put in numbers here.


04 — Inside the driver

Most of the length goes into making W5500 TOE behave like POSIX sockets.

🔷 Two listeners are kept armed

W5500_LISTEN_KEEP = 2, with the reason directly above the definition. The W5500 processes OPEN → SOCK_INIT → LISTEN in sequence, so a SYN arriving while a replacement listener is being armed meets SOCK_INIT and gets an RST — ECONNREFUSED on the host. A second, already-LISTENing socket covers that window.

A completed handshake is detached into the pending FIFO from the INTn worker, not inside accept(), and the pool is refilled immediately (listener_promote()listener_fill()), so the port is never without a listener while the application is still asleep.

🔷 DISCON is never used to close

static void hw_close(const struct device *dev, int8_t hw)
{
w5500_wr8(dev, sock_reg(hw, W5500_Sn_IR), 0xFF);
(void)w5500_cmd(dev, hw, Sn_CR_CLOSE);
}
 

As the file header explains: as the active closer, the W5500 parks the socket in FIN_WAIT/TIME_WAIT, and a connect/disconnect storm exhausts all eight hardware sockets. Closing is always Sn_CR=CLOSE — RST semantics, instant SOCK_CLOSED.

For the same reason hw_alloc() only hands out sockets the chip confirms as SR_CLOSED. A socket just closed keeps its old state internally for a while, and re-opening it then is silently ignored, leaving a "listener" that never listens. One that keeps ignoring OPEN/LISTEN is excluded through an hw_broken bitmask — but only after three independent failures, since a socket still sending its RST ignores OPEN transiently and recovers on its own.

This is a problem only TOE has, cycling all eight hardware sockets. MACRAW uses socket 0 and nothing else.

🔷 Blocking operations sleep without the mutex

wait_slice() sleeps in bounded slices while releasing data->lock, so the interrupt worker can always run and wake them. In-flight operations survive a racing close() through a per-socket reference count (users, zombie): the object stays alive until the last operation leaves.

The TX-space wait in sendto() is two-stage — 30 busy-polls of 100 µs (3 ms) before falling back to sliced sleeping — because, as the comment says, a 10 ms quantum on every send would cap echo throughput outright.

🔷 Only the data phase bypasses the SPI framework

This is the performance core, and the most debatable choice in the file.

#include <stm32_ll_spi.h>
#define W5500_SPI ((SPI_TypeDef *)DT_REG_ADDR(DT_INST_BUS(0)))
 

The stated reasoning: Zephyr's generic polled SPI path costs about 1.4 µs per byte through the spi_context layering, capping a 2 KiB transfer at ~3.7 ms. So the command frame still goes through the framework, which owns CS framing, while the data phase is streamed with a tight full-duplex register loop — leaving wire time (~0.4 µs/byte at 21 MHz) as the only limit. The fast path engages only at 32 bytes or more (W5500_FAST_MIN).

CS is held asserted across both phases, because the W5500 delimits frames by CS edges rather than clock continuity.


05 — How far it has been measured

The TOE figures are two lines in examples/w5500-toe-echo/README.md.

  • 3000/3000 connect/close cycles at ~190 conn/s with zero refusals, with periodic echo checks along the way.
  • ~0.5 MB/s sustained echo, full duplex, 2 KiB chunks. The generic per-byte SPI path costs ~1.4 µs/byte and caps the raw link at ~0.7 MB/s.

Client-side in-flight data has to stay at or below the 2 KiB socket buffer, because the W5500 answers a segment that does not fit the receive buffer with an RST rather than dropping it. To make that 2 KiB the effective figure, the driver rewrites the MEM_SIZE registers at init to the chip's own defaults — with a comment that this board's part does not come up at the documented reset values.

Neither iperf nor zperf is used. The repository is not short of measurement assets: tools/udp_bench.py, UDP RTT and Modbus benchmarks under tests/performance/, UDP flood and long-duration load under tests/stress/, and a TCP bandwidth endpoint on port 9900 in src/bwtest.c. But all of it belongs to io-edge-hub, and that application uses the in-tree MACRAW driver via wiznet,w5500 — its prj.conf says so.

So there is no side-by-side measurement of MACRAW against TOE on the same hardware yet. The scripts exist; the results are not committed.


06 — Limits, and what is still open

Kconfig states the constraints itself: IPv4 only, no TLS, eight sockets, and a static address from devicetree or Kconfig with no DHCP. These come from the nature of TOE rather than from the driver. Taking the in-tree MACRAW path instead keeps Zephyr's IPv6, DHCP and TLS but spends MCU RAM on the stack — and this repository shows both sides of that trade in two applications.

One thing clearly blocks upstreaming as written. The fast SPI path includes stm32_ll_spi.h directly and derives the SPI register address from devicetree, which makes it STM32-specific. Transfers under 32 bytes still use the framework path, so the split is already isolated in one function.

One detail caught the eye while reading. The interrupt-mask loop in init reads:

for (int i = 0; i < W5500_NUM_SOCKETS; i++) {
w5500_wr8(dev, sock_reg(i, W5500_Sn_IR), 0x1F);
w5500_wr8(dev, W5500_Sn_IMR, 0x1F);
}
 

The second line is missing sock_reg(i, ...), so the address resolves to common-block 0x0016 (SIR). The clone behaviour the comment describes — Sn_IMR reset to 0 — would therefore not actually be covered. On genuine parts Sn_IMR defaults to 0xFF so nothing shows, and the common SIMR is enabled separately as 0xFF.


07 — Similar Projects on WIZnet Makers

  • W6300 Zephyr Driver PR #102727: Under Review is the opposite direction: getting WIZnet support into Zephyr's tree, where iot-zephyr-app builds out of tree a path the tree does not offer. Put side by side, they raise the obvious question of whether this TOE driver is an upstream candidate.
  • Zephyr W5500-EVB-Pico2 and W5500-EVB-Pico with Zephyr RTOS(Nordic) are the standard route — the in-tree wiznet,w5500 binding with MACRAW and the native stack. That is exactly what n2e-gw does in this repository, with the TOE example sitting beside it as the counterpart.
  • zephyr_echo_test_suite shares the idea of treating an echo server on Zephyr as a measurement target. The difference is that this repository runs the same echo on both the native stack and the offload path.

Q&A

Q. Can the in-tree Zephyr driver use W5500 TOE? No. drivers/ethernet/eth_w5500.c is MACRAW only: the chip acts as a MAC and Zephyr's native stack does TCP/IP. This repository opens the other path with a separate wiznet,w5500-toe compatible and a socket-offload driver, and Kconfig prevents both from being enabled at once.

Q. What is left of Zephyr networking under offload? With CONFIG_NET_NATIVE=n the IP stack is out of the build. What remains is the socket API and the network interface object; ARP, ICMP, TCP and UDP run inside the chip. The cost is IPv4 only, no TLS, eight sockets and a static address.

Q. Why keep two listeners open? Because the W5500 processes OPEN → SOCK_INIT → LISTEN in sequence. A SYN that arrives while a listener is being re-armed meets SOCK_INIT and is answered with RST, which the host sees as ECONNREFUSED. A second socket already in LISTEN covers that window.

Q. Why drive the SPI peripheral directly? Zephyr's generic polled path spends about 1.4 µs per byte in spi_context layering, which pins a 2 KiB transfer at ~3.7 ms. Data phases of 32 bytes or more are streamed through an LL register loop instead, bringing it down to wire time at 21 MHz (~0.4 µs/byte). The cost is that this path is STM32-only.

Q. Could the driver go upstream as it stands? Not in this form — the fast path depends on stm32_ll_spi.h and is vendor-specific. The branch is already isolated, though, since anything under 32 bytes uses the framework path.


Original Link: https://github.com/kabirz/iot-zephyr-app

Driver: drivers/w5500_toe/w5500_toe.c · Binding: dts/bindings/net/wiznet,w5500-toe.yaml · Example: examples/w5500-toe-echo/

Documents
Comments Write