---
title: "How Do You Prove a Rust Rewrite Matches the C Firmware It Replaces?"
url: "https://maker.wiznet.io/Grace_Koo/projects/io-edge-hub-rust/"
markdown_url: "https://maker.wiznet.io/Grace_Koo/projects/io-edge-hub-rust/md"
type: "UCC: User Created Content"
author: "kabirz"
author_url: "https://github.com/kabirz/io-edge-hub-rust"
editor: "WIZnet"
editor_url: "https://maker.wiznet.io/"
original_author: "kabirz"
original_url: "https://github.com/kabirz/io-edge-hub-rust"
published: "2026-09-03"
language: "en"
likes: 0
views: 100
comments: 0
source: "WIZnet Makers (https://maker.wiznet.io/)"
---

# How Do You Prove a Rust Rewrite Matches the C Firmware It Replaces?

> One industrial gateway written in C and again in Rust on the same STM32F407 and W5500, reconciled by the same 93-test e2e suite.

Original author: kabirz (source: https://github.com/kabirz/io-edge-hub-rust)

## Article

## io-edge-hub-rust — The Same Device Built Twice, Reconciled by 93 Tests

`#Rust` `#embassy` `#W5500` `#MACRAW` `#STM32F407` `#MCUboot` `#ModbusTCP` `#littlefs`

> 📚 **Context**: [kabirz/io-edge-hub-rust](https://github.com/kabirz/io-edge-hub-rust) — a Rust/embassy rewrite of an industrial IO gateway firmware. Hardware is an STM32F407VET6 with a W5500 on SPI2 and a W25Q128 NOR on SPI1. Last pushed 2026-09-01, MIT declared in Cargo. ✅ **Verification**: Read from the repository at `main`. The figures are its own, measured 2026-08-23, and were not reproduced here.

---

### 01 — This is the third implementation of one device

The repository is not a new product. It is **an existing device written again**. io-edge-hub is an industrial acquisition node reading and driving 16 digital inputs, 8 digital outputs and 4 analogue inputs.

What the device exposes:

| Service | Port | Notes |
| --- | --- | --- |
| UDP config | 8600 | config commands, v2 bulk upgrade channel, broadcast on 8601 |
| Modbus TCP | 502 | FC01-08/15/16, up to 2 masters |
| Modbus RTU | USART2 | DE on PA1, 9600 by default |
| HTTP | 80 | gzip SPA, JSON API, 2 connections |
| WebSocket | /ws | 1 s IO and registers, 10 s info push |
| FTP | 21 | RFC 959, 3 sessions |
| Shell | USART1 | 115200 |

History is written as `data_MMDD_HHMMSS.raw` in a 1 MB by 10 rotation, config and filesystem live on the W25Q128 NOR, and a 30 s IWDG watchdog runs. Firmware upgrades arrive over three channels: UDP, WebSocket and CAN.

The README names its references directly.

| Implementation | Role |
| --- | --- |
| C / FreeRTOS | reference firmware |
| Zephyr | protocol authority |
| Rust / embassy | this repository |

And **all three drive the W5500 the same way.** The first line of the feature table:

> Network — W5500 MACRAW → embassy-net (smoltcp), static IP (default 192.168.12.101), MAC derived from the UID

`crates/firmware/src/net.rs` opens with `//! W5500 MACRAW + embassy-net bring-up`.

- Driver — `chip::W5500` from `embassy-net-wiznet = "0.3.0"`

- Bus — SPI2 at 21 MHz on a DMA shared bus

- Addressing — static IPv4, MAC from XOR-folding the STM32's 96-bit UID onto the WIZnet OUI

---

### 02 — A rewrite you can swap in and out

Most rewrites stop at "rewritten". This one set out to keep the boundaries identical.

- **Same MCUboot bootloader, same RSA-2048 signing key.** `boot.bin` is untouched.

- **Same NOR partition layout** on the W25Q128: IOCF A/B config (32K × 2) plus littlefs from 0xF0000.

- **It mounts what the C build wrote and keeps going.** The comparison document records the Rust firmware continuing the same history file the C firmware had been writing.

- **The upgrade channels work in both directions.** The Rust firmware can be reflashed through its own UDP v2, WebSocket and CAN channels — **and can be flashed back to C.**

So either build can go onto a field device with the disk and bootloader intact. That is what makes this a replaceable alternative rather than an experiment.

---

### 03 — Reconciled against 93 tests

The verification method is the centre of this repository. **The C repository's 93-item e2e suite was reused as-is**: same hardware, same Windows/Python host, same list.

| Suite | Items | C | Rust |
| --- | --- | --- | --- |
| basic | 9 | pass | pass |
| udp (8600) | 8 | pass | pass |
| modbus_tcp | 14 | pass | pass |
| modbus_rtu | 3 | pass | pass |
| web (:80) | 17 | pass | pass |
| websocket | 3 | pass | pass |
| history (littlefs) | 5 | pass | pass |
| uart shell | 6 | pass | pass |
| ftp (:21, 3 concurrent) | 14 | pass | pass |
| stress | 10 | pass | pass |
| reboot | 3 | pass | pass |
| fw_upgrade | 3 | pass | pass |
| Total | 93 | 93 | 93 |

All 93 passed in a single pytest run of 5 minutes 18 seconds. The stress suite includes three concurrent FTP sessions, 1 MB transfers and a 30-second mixed load; fw_upgrade includes two real MCUboot swaps.

---

### 04 — Measured side by side

From `docs/comparison-c-vs-rust.md`, dated 2026-08-23:

| Metric | C | Rust |
| --- | --- | --- |
| Signed image size | 329,732 B | 205,708 B |
| FTP STOR 1 MiB | 42 KB/s | 14 KB/s |
| FTP RETR 1 MiB | 318 KB/s | 365 KB/s |
| 3 clients storing 128 KiB each | about 11 s | about 11 s |
| UDP v2 firmware push | 56 KB/s | 42 KB/s |
| MCUboot swap and reboot offline window | 24 s | 21 s |

The image is **38% smaller**, which the author attributes to dropping mbedtls and RSA.

The losses are not hidden either. **FTP upload runs at a third of C's rate**, and the cause is written down: `w25q::wait_not_busy` polls on a fixed ~1 ms interval after each page program, inflating a 4 KiB page write to about 18 ms per page, where C polls more tightly. The note says a short spin before falling back to a delay should close the gap. Download is slightly faster in Rust, and three concurrent uploads tie because NOR writes are the bottleneck.

---

### 05 — Four things the port turned up

The reliability section is the most useful material here. All four were **hit and fixed during this port**, each with its cause recorded.

1. **Do not sync littlefs on every record.** It rewrites the whole inline file tag roughly every eight records and triggers directory compaction, producing a NOR erase storm that starves the storage task. The symptom was every FTP and storage operation timing out with the IWDG resetting in a loop. Restored to C's behaviour: buffered writes with an event-level flush.

2. **Do not leave the session-limit rejecter listening.** It should only listen while the limit is reached and stand down after 50 ms of idle; a permanent listener steals the next legitimate connection.

3. **Clean up FTP slot file handles on the next open.** After an abnormal transfer, both read and write handles are closed before the slot is reopened, preventing a re-entrant self-loop in littlefs's mlist.

4. **Never run NOR I/O inside a critical section.** Erase and program take 10–100 ms, and masking interrupts that long **loses UART, W5500 and upgrade-window data.** That applies directly to any design with an SPI-attached Ethernet controller.

---

### 06 — What is still open

- **The three implementations have not been cross-verified.** The comparison document states that three-way interop with the Zephyr build was not done, as the repository has no Zephyr build artefacts. Only C and Rust are confirmed against each other.

- **One CAN direction does not work.** Device-to-PCAN produces no waveform on either firmware. The author treats it as a physical-layer issue and leaves the transceiver, termination and wiring to be checked. Both builds behave the same, so it is not a port regression.

- **The README's reference paths are the author's local Windows paths**, which nobody outside can follow.

---

### 07 — Why it is worth reading

- Rewrite stories are common; **matching the bootloader and the on-disk format so the two builds can flash each other is not.**

- Publishing **both the wins and the losses as numbers** is rarer still.

- And all three implementations drive the W5500 in **MACRAW**. Zephyr's in-tree driver defaults to it and so does `embassy-net-wiznet`. The same author wrote a separate Zephyr driver using the chip's hardware stack — yet all three product firmwares still stand on MACRAW.

---

### 08 — Similar Projects on WIZnet Makers

- [**How Does a Zephyr Driver Use W5500 Hardware TCP/IP When the In-Tree One Is MACRAW Only?**](https://maker.wiznet.io/Grace_Koo/projects/iot-zephyr-app/) is the Zephyr build of the same device by the same author — the one this repository cites as its protocol authority. Read together, they show one io-edge-hub implemented three times, in C, Zephyr and Rust.

- [**How Much Does W5500 Hardware Offload Save Over MACRAW on the Same Board?**](https://maker.wiznet.io/Grace_Koo/projects/nucleo-h723zg-udp-echo/) answers in numbers the question this one leaves open. All three builds here are MACRAW; that post measures, on one board, what changes when the same chip runs its hardware stack instead.

- [**How Does an ESP32-S3 Modbus Gateway Serve Field Points as MCP Tools over W5500 Ethernet?**](https://maker.wiznet.io/Benjamin/projects/how-does-an-esp32-s3-modbus-gateway-serve-field-points-as-mcp-tools-over-w5500-ethernet/) is the closest in purpose — field points published across W5500 Ethernet by a gateway. The difference is that this repository builds one device twice on different stacks and reconciles them.

---

### Q&A

**Q. How is the W5500 used here?** `embassy-net-wiznet` 0.3.0's `chip::W5500` opened in MACRAW, with embassy-net (smoltcp) above it. SPI2 runs at 21 MHz on a DMA shared bus, the static address defaults to 192.168.12.101, and the MAC is derived from the STM32's 96-bit UID.

**Q. How closely does it match the C build?** Same MCUboot and same RSA-2048 signing key, and the NOR partition layout is identical, so it mounts the littlefs the C build wrote and continues the same history file. Its upgrade channels can also flash C back.

**Q. Is the Rust build better?** It depends on the metric. The signed image is 38% smaller, and FTP download and the MCUboot swap window are slightly faster. FTP upload is well behind at 14 KB/s against 42 KB/s, and the author points to the NOR page-program polling interval as the cause.

**Q. Are the three implementations interchangeable?** C and Rust are confirmed. Three-way verification including the Zephyr build has not been done, as the comparison document states.

---

**Original Link**: <https://github.com/kabirz/io-edge-hub-rust>

**Comparison report**: `docs/comparison-c-vs-rust.md` · **Per-module docs**: `docs/embassy.md` · **Zephyr build**: [kabirz/iot-zephyr-app](https://github.com/kabirz/iot-zephyr-app)

---

Source: https://maker.wiznet.io/Grace_Koo/projects/io-edge-hub-rust/
