Why Does a W5500 Board Get Its Address Back After a Cable Pull but Not Its DNS?
A wired Zephyr weather node on a board Zephyr has no definition for, and the four DHCP settings it took to survive a cable pull.
0
Project description
small-weather-station โ The Address Comes Back 3 ms After the Cable Does; DNS Does Not
#W5500 #Zephyr #MACRAW #ESP32S3 #DHCPv4 #DNS #OutOfTreeBoard #Prometheus #BME280
๐ Context: DenysMedvid/small-weather-station โ a wired weather node running Zephyr 4.4.1 on a Waveshare ESP32-S3-ETH.
01 โ What the repository builds
It reads temperature, humidity and pressure from a BME280, shows them on a 128x64 OLED, and pushes them to a Prometheus Pushgateway every 30 seconds. No wireless, no cloud account, no battery. Plug it into a switch and a USB-C charger and it looks after itself: DHCP for its address, SNTP for its clock, a reboot every eight hours, and a forced DHCP restart when the network goes quiet on it.
The repository carries a photograph of the assembled board and a Grafana screen beside it. That screen reads 27.4 ยฐC, 32.2 % and 996 hPa with sensor errors 0, push failures 0, DHCP restarts 0 and 1.84 hours of uptime.
| Part | Detail |
|---|---|
| Board | Waveshare ESP32-S3-ETH, ESP32-S3R8, 16 MB flash, 8 MB octal PSRAM |
| Ethernet | W5500 on SPI2 |
| Sensor | BME280 |
| Display | SSD1306 128x64 |
| RTOS | Zephyr 4.4.1 |
The wiring is four connections. Both modules share one I2C bus on GPIO16/17, and Ethernet and USB-C power join them.
Four threads, all statically allocated.
| Thread | Period | Stack | Priority |
|---|---|---|---|
| sensor | 5 s | 1536 | 7 |
| display | 1 s | 2048 | 7 |
| supervisor | 5 s | 1536 | 7 |
| push | 30 s | 16384 | 8 |
The first three share a priority because none of them is more urgent than the others and none holds the state mutex across a blocking call. Push sits one step lower: a TLS handshake is about 1.7 seconds of solid ECC on this CPU, and it must never be the reason the panel stops redrawing.
The push stack's 16 KB is explained too. The handshake runs on the calling thread's stack, carrying X.509 parsing, ECDSA and the mbedTLS record layer โ and at 4096 the board wedged mid-handshake with no message at all.
02 โ Zephyr has no board for this hardware
So the repository carries the board definition itself. Upstream boards/waveshare/ has no esp32s3_eth.
The dts header also sets down why a stock board will not do. esp32s3_devkitc is the wrong part: it selects SOC_ESP32S3_WROOM_N8, which means 8 MB of flash, no PSRAM and a partition table to match. A build for it links and boots, and maps flash wrong. Failing quietly is the worse kind.
So it composes the two in-tree pieces that do describe the part โ esp32s3_wroom_n16r8.dtsi and partitions_0x0_amp_16M.dtsi.
The W5500 node
| Item | Value |
|---|---|
| compatible | wiznet,w5500 |
| Bus | SPI2 |
| MOSI / MISO / SCLK / CS | 11 / 12 / 13 / 14 |
| RST / INT | 9 / 10 |
| SPI maximum | 20 MHz |
| Mode | MACRAW, with the full Zephyr IP stack above it |
The node comments explain the fixed MAC. The W5500 has no MAC EEPROM. The host has to supply one, and zephyr,random-mac-address would change it every boot, which churns the DHCP lease each time and makes any router-side reservation useless. So one locally-administered address (the 02: prefix) is pinned.
The radios are off as well. This is an Ethernet-only build, and leaving Wi-Fi and BT enabled would spend RAM on something never used. IPv6 goes for the same reason โ the Pushgateway is reached over IPv4.
03 โ When the cable comes back
This is the heart of the repository. The symptom: the link returns and the address returns, and the pushes keep failing.
The panel shows PUSH -118. net iface says bound with the right address and gateway, and the address was back 3 ms after link up. But net dns prints an empty server list beside it. That state holds through five failed pushes until the supervisor forces a re-acquire.
Chasing it to the end leaves four lines in prj.conf. Each one, fixed in turn, makes the symptom point somewhere other than the cause.
| Setting | What the default does | What you see instead |
|---|---|---|
| DNS_RESOLVER_MAX_SERVERS | 1, so the rest of what the router offers is dropped | a boot warning about not saving them, and no fallback for name resolution |
| ZVFS_POLL_MAX | 3, one short of the resolver sockets | not a DNS message but SNTP failing forever |
| DNS_RECONFIGURE_CLEANUP | off, so a pending query holds its slot through the outage | every later lookup answers -EAGAIN |
| NET_DHCPV4_INIT_REBOOT | on, so only a REQUEST goes out | the address arrives and the DNS servers never do |
First line. The resolver keeps one server and the router advertises several. The rest are dropped at boot and whichever came first is kept. Since the push is the only thing that resolves anything, that one server dying ends it. The failure arrives as tls_connect() returning -EHOSTUNREACH before a packet ever reaches the gateway. That is the -118 on the panel โ EHOSTUNREACH is 118 in Zephyr's errno.h and 113 on the host. Look the number up against the host's table and you chase a different error.
Second line. Raising the server count alone is not enough. Each resolver registers its own socket with the socket service, and ZVFS_POLL_MAX is 3 โ one short. The resolver then never runs at all, and what surfaces is not a DNS message but SNTP timing out forever: no clock, and therefore no push. The cause is printed by net_sock_svc at error level inside the first 200 ms of boot.
Third line. Pulling the cable makes Zephyr delete every DNS server the lease installed โ removed on NET_EVENT_IF_DOWN, sockets closed. The only thing that puts them back is option 6 on the next ACK. With the cleanup option off, a reconfiguration never closes the context and never cancels pending queries, so a query still in flight when the cable came out keeps the one concurrent-query slot and every later lookup answers -EAGAIN. This was the default up to Zephyr 4.1; 4.2 turned it off for multi-interface devices, and the Kconfig help says to re-enable it when there is only one.
The fourth line is the one. INIT-REBOOT skips DISCOVER and REQUESTs the previous address directly. But dhcpv4_add_req_options() is called only for DISCOVER. So the REQUEST carries no parameter request list, and a server that answers only what was asked for returns an ACK with the address and no option 6. The DNS servers deleted on IF_DOWN are never reinstalled, and no number of net_dhcpv4_restart() calls helps โ a restart re-enters INIT-REBOOT and sends another REQUEST.
Turned off, both the cable flap and the supervisor's escalation go INIT to DISCOVER, whose ACK carries the full option set. The cost is one extra round trip on an already-rare event, and the address itself comes back from the server's lease table unchanged.
04 โ So the device recovers on its own
The supervisor ticks every five seconds, reads the state and picks one of three things.
| Situation | What it does |
|---|---|
| the scheduled uptime has elapsed | reboot |
| link is up and no address follows it | restart DHCPv4 |
| lease and link both healthy, pushes failing in a row | restart DHCPv4, and reboot if that does not take |
The third is the failure from the previous section. The lease is held, the link is up and full-duplex, and every push fails โ a DHCP stall cannot see it. That watch disarms as soon as there is an address, and the scheduled reboot is up to eight hours away. So the escalation throws the whole network configuration away and asks again from scratch. Stopping the client drops the DNS servers it installed, and what follows is a DISCOVER from nothing rather than a REQUEST for the address it already has. The ACK carries option 6 again and the resolver is rebuilt with fresh sockets.
Two pieces of state carry to the next tick: when the stall began, and the failure count at which the last restart was issued. The reason for the second is written down โ without it, every tick past the threshold would fire another DISCOVER.
With either the link or the address missing, this path does not run at all. That is an ordinary outage, the first watch owns it, and a DISCOVER into a dead cable is noise.
05 โ The backoff was removed on purpose
The push retry interval is the same after a failure as after a success. That is deliberate, and the comment says the function exists to stop the alternative coming back.
It used to be an exponential backoff: 30 seconds doubling to a 300-second ceiling. It had two reset conditions โ a successful push, and the address being lost and regained. A broken name lookup on a healthy link triggers neither. The station settled at one attempt every five minutes and stayed there, which on a panel that redraws every second is indistinguishable from having stopped retrying.
Backoff is the right shape for a busy server that must not be stampeded. It is the wrong shape here: the gateway is this device's own, the failure is usually local, and a fixed cadence is what makes recovery immediate and the failure duration readable from the metrics.
A floor of five seconds sits under the interval, because the value is user-settable and a zero would turn the push thread into a busy loop of back-to-back 1.7-second TLS handshakes.
06 โ The decisions are lifted out and tested on the host
src/policy/ holds the judgement and nothing else โ functions over their arguments, with no clock, no socket and no mutex.
- The supervisor decision takes
nowandstalled_sinceas arguments, so a test can walk an eight-hour uptime or a ninety-second stall in a few microseconds. - Folding a push result into the counters is value-in, value-out. Success is 2xx and nothing else: a negative errno from the socket layer is a failure, and so is any 3xx, 4xx or 5xx from the gateway.
- A failure leaves the last-success timestamp alone. Moving it would make a permanently broken station look freshly healthy.
- Composing the credentials returns
-EINVALrather than truncating. A silently shortened password authenticates as nobody and reports 401, and that 401 is a long way from the real cause.
Host-side unit tests and CI sit on top of that.
07 โ Where it stands
- AI assistance is visible in the repository. A
CLAUDE.mdis committed alongside the code. - The push client is out of the default build. Enabling it brings mbedTLS, SNTP and the socket options it needs, and costs about 85 KB of flash plus full libc and the POSIX API. It works โ a 1.7 second handshake and a 200 from the gateway.
- Humidity reads exactly 100 for the first fifteen seconds or so after reset. That is the BME280's own power-on settling and it decays on its own. Nothing discards the early samples โ how many to drop would be a guess, as the note says.
- I2C runs at 100 kHz rather than 400. Against the OLED module's weak pull-ups even short command writes failed at 400, and nothing here needs the bandwidth.
- Logging is deferred, not immediate. The console *is* the USB-Serial-JTAG device, so immediate mode blocks whenever the host is not draining the port and the board goes silent from boot. The cost is that messages still queued when a thread faults are lost, so a crash can look like a hang โ the note points at UART0 on GPIO43/44 for when that matters.
- The figures are the author's own, from their own device, and were not reproduced here.
- GPL-3.0-or-later.
This is a repository that pairs each failure with its cause. For anyone running a W5500 under Zephyr who needs it to come back by itself when the network moves, what to suspect first is left behind in writing.
08 โ Similar Projects on WIZnet Makers
- How Does a Zephyr Driver Use W5500 Hardware TCP/IP When the In-Tree One Is MACRAW Only? is the exact opposite end. This station takes the in-tree MACRAW driver and puts the Zephyr native stack on top; that one drops the stack and writes a driver that uses the chip's hardware sockets instead.
- How to Drive a PIXPAPER-213-C Display on W6300-EVB-Pico2 with Zephyr? also pairs a WIZnet chip with a display under Zephyr. There the display binding is the problem; here it is the board definition and network recovery.
- ESPHome GPS/PPS Stratum-1 NTP Server on ESP32-S3 with a W5500 takes the same MCU and the same chip through ESPHome. Side by side, the two show what changing framework hands you and what it leaves you to write.
Q&A
Q. In what mode is the W5500 used?
MACRAW, through the in-tree wiznet,w5500 binding. The chip delivers raw Ethernet frames and Zephyr's native stack handles TCP/IP. It is on SPI2 at up to 20 MHz, with INT on GPIO10 and RST on GPIO9.
Q. Why write a board definition at all?
Zephyr 4.4.1 has no board for this hardware. Building for esp32s3_devkitc selects SOC_ESP32S3_WROOM_N8 โ 8 MB of flash and no PSRAM โ so the build links and boots but maps flash wrong. The definition composes the two in-tree dtsi files that describe the real part.
Q. Why is the MAC fixed in the devicetree?
The W5500 has no MAC EEPROM, so the host must supply one. A random MAC would take a new DHCP lease on every boot and make any router-side reservation useless, so one locally-administered address is pinned.
Q. What breaks when the cable is pulled and replaced?
The address comes back; the DNS servers do not. Zephyr deletes the servers the lease installed when the link goes down, and returning through INIT-REBOOT sends a REQUEST with no parameter request list, so the ACK carries no option 6. Setting CONFIG_NET_DHCPV4_INIT_REBOOT=n routes recovery through DISCOVER, whose ACK carries the full option set.
Q. Can it be reused?
It is GPL-3.0-or-later. The board definition sits on its own under boards/waveshare/esp32s3_eth/, which makes it easy to lift into another Zephyr project.
Original Link: https://github.com/DenysMedvid/small-weather-station
Board definition: boards/waveshare/esp32s3_eth/ ยท The reasoning behind the network settings: prj.conf ยท Lifted decisions and their tests: src/policy/, tests/unit/
