How to Build Modbus TCP Slaves with W5500 on STM32 (Rust) and Bridge Them to OPC-UA?
Antony Mapfumo’s Week 9 project builds two STM32F446RE Modbus TCP slave nodes using a WIZnet W5500 Ethernet controller
Summary
Antony Mapfumo’s Week 9 project builds two STM32F446RE Modbus TCP slave nodes using a WIZnet W5500 Ethernet controller, then bridges their register data into an OPC-UA server written in Python. It shows static-IP setup, socket state machines, IEEE-754 register encoding, and a SCADA-friendly data model.
Author introduction
Antony “Tony” Mapfumo is a network engineer currently focused on Industrial IoT (IIoT) and embedded systems. On his blog he documents hands-on experiments that connect communications engineering (networks/protocols) with embedded development on STM32, using tools like Embedded Rust/C++, LoRa/LoRaWAN, and industrial protocols—aiming for reliable, practical, “real-world engineering” explanations rather than abstract tutorials.
About this blog series (and why Rust keeps showing up)
This GitHub repository is labeled “Week 9” in a structured series that progresses from early embedded foundations into full “device → gateway → industrial integration.” The repo itself links the journey: RTIC/LoRa basics, gateway firmware, async gateway work, MQTT + InfluxDB + Grafana (Weeks 7–8), and then Modbus TCP + OPC-UA for Week 9.
Rust appears here because Tony is explicitly building “reliable, efficient” embedded systems and repeatedly chooses Rust-based embedded tooling (e.g., Embassy async on STM32 in this project). More generally, embedded Rust is popular for memory safety without GC and strong tooling—useful when you’re implementing low-level networking and protocol parsing on constrained targets.
What the Week 9 project actually does (main points)
This repository implements a small but very “industrial-shaped” automation stack:
- Two embedded devices (STM32F446RE) act as Modbus TCP slaves/servers on port 502, each with a static IP (10.10.10.100 and 10.10.10.200)
- Each device reads temperature/humidity from an SHT3x sensor and exposes values through holding registers (FC03 / 0x03) using IEEE-754 float32 encoding.
- A desktop Python OPC-UA gateway polls both Modbus devices every 2 seconds, decodes registers, and publishes the values as OPC-UA variables for clients (e.g., UaExpert / SCADA-style consumers)
- Each device also drives an SSD1306 128×64 OLED over I²C to display IP, sensor values, and connection state (“LISTENING/CONNECTED”).
- The author reports practical engineering metrics: ~70 ms end-to-end (sensor → Modbus → OPC-UA) and 6+ hours uptime testing with 0% packet loss in the test scenario.
Note on code review: GitHub’s HTML “file view” didn’t fully render source files in my browsing tool session, so I’m basing implementation details on the repository README’s documented structure, snippets, and behavior (e.g.,
common.rs,modbus_1.rs,modbus_2.rs, and the included Python gateway script).
Why this blog/project is useful to other developers
If you’re building real systems (not demos), this repo is valuable because it’s packed with the “sharp edges” people usually skip:
- Industrial protocol thinking: you must commit to strict register maps, endian rules, and deterministic polling—not “just publish JSON.” The README shows concrete register layouts, float encoding examples, and Modbus addressing gotchas.
- Gateway pattern you can reuse: the Modbus→OPC-UA bridge turns “legacy-ish device registers” into a richer, interoperable data model (namespaces + typed variables) that tools understand.
- Real embedded networking concerns: TCP socket state handling matters. The project explicitly documents a socket state machine and recovery approach—exactly what prevents field devices from “locking up” after imperfect connections.
- Practical bus-sharing lessons: sharing I²C between an async DMA sensor driver and a blocking OLED driver is the sort of integration problem that appears constantly in embedded work
MQTT → Modbus, Grafana → SCADA (what that shift really means)
Tony summarizes Week 9 as a move from “IoT protocols (MQTT, InfluxDB)” toward “industrial automation protocols (Modbus TCP, OPC-UA)” and SCADA integration. Here’s the practical meaning:
MQTT vs Modbus
- MQTT is a lightweight publish/subscribe messaging protocol: devices publish telemetry to a broker, subscribers receive updates. It’s excellent for IoT pipelines and constrained networks.
- Modbus TCP is a request/response, register-based protocol: a client reads/writes fixed addresses (registers/coils) on a server/slave. That rigidity is a feature in industrial environments—PLCs, HMIs, and SCADA systems expect stable “addresses.” The Modbus spec ecosystem formalizes this, including MBAP headers over TCP.
In short: MQTT is “stream messages to whoever’s listening,” Modbus is “read these registers at these addresses on this device.”
Grafana vs SCADA
- Grafana is a general dashboarding/visualization platform for metrics and time-series data (great for “observability”-style monitoring).
- SCADA (Supervisory Control and Data Acquisition) is an industrial monitoring and control architecture: it gathers process data and can support operational control, alarm concepts, multi-site supervision, and integration with PLCs/industrial protocols.
In short: Grafana is “visualize your data,” SCADA is “operate the plant.”
Why OPC-UA shows up here
OPC-UA is widely used as a vendor-neutral interoperability layer for industrial systems (richer data modeling and security than “raw registers”). In this repo, OPC-UA is the “northbound” interface: SCADA/HMI/test clients connect to the OPC-UA server instead of speaking Modbus directly.
Where W5500 fits, and why it’s a sensible choice
This project uses WIZnet W5500 Ethernet on both STM32 boards and even documents a custom SPI driver approach due to the lack of an existing Embassy driver at the time.
Why W5500 fits Modbus TCP devices especially well:
- W5500 is a hardwired TCP/IP Ethernet controller with integrated stack + MAC/PHY, letting MCUs talk TCP/UDP/IP via a socket-like interface over SPI (up to 80 MHz supported).
- It includes 32 KB internal buffer memory and supports 8 hardware sockets, which is a good match for small industrial endpoints that need stable TCP behavior without a heavy software network stack.
- The repo’s “static IP only” decision aligns with common industrial LAN deployments, where fixed addressing is often preferred for PLC/SCADA mapping.
(That last point is an engineering inference, but it’s consistent with the repo’s documented static IP configuration and the typical way Modbus devices are deployed.)
Architecture and implementation notes (from the repo’s documented design)
The README provides a clear end-to-end diagram and timing:
- Polling interval: every 2 seconds for sensor read, Modbus poll, and OLED refresh.
- Data path: Sensor → registers in RAM → Modbus TCP read → float decode → OPC-UA variables → clients subscribe/read.
- Latency estimate: ~50 ms sensor read + ~20 ms Modbus TCP RTT ≈ ~70 ms total.
- The README also highlights the “hard parts” explicitly:
- Custom W5500 register access (SPI control byte + address bytes) to achieve async-friendly behavior.
- Socket state machine handling CLOSED/INIT/LISTEN/ESTABLISHED/CLOSE_WAIT with auto-recovery.
- Modbus correctness details: MBAP header fields, FC03 response shape, and a concrete register map including float32 big-endian packing.
FAQ
1) Why use W5500 for Modbus TCP devices instead of “just running a software TCP/IP stack”?
W5500 is built around a hardwired TCP/IP stack and exposes networking through a socket-like model over SPI, which reduces the integration burden on the MCU compared to maintaining a full software network stack. It also provides 32 KB internal buffers and 8 hardware sockets, which helps when you need stable TCP behavior for industrial polling patterns like Modbus TCP.
2) How do you connect W5500 to an STM32 platform in a project like this?
In this repo, the STM32F446RE connects to a W5500 module over SPI and runs a static-IP configuration, then implements Modbus TCP server behavior on port 502. The author documents a custom, register-level SPI approach (because an Embassy-ready driver wasn’t available), and highlights socket-state handling as a core reliability technique.
3) What role does W5500 play in the OPC-UA + Modbus architecture?
W5500 is the embedded Ethernet “front end” that makes each STM32 board appear as a Modbus TCP slave on the LAN (10.10.10.100:502 and 10.10.10.200:502). The Python gateway then polls those endpoints and republishes the values as OPC-UA variables for SCADA-style clients. Without W5500-class Ethernet, you don’t get a standard Modbus TCP device on the wire.
4) Can beginners follow this project, or is it “industrial-only”?
Beginners can follow the system concept easily because the README includes diagrams, testing commands (ping, mbpoll), and a fully explained register map with float decoding examples. The advanced part is embedded networking: SPI-register Ethernet control, TCP socket states, and strict endian/encoding rules. If you’re new, treat it as a guided path into “real protocol work,” not a quick weekend sketch.
5) How does W5500 compare to Wi-Fi modules or ENC28J60 for a similar gateway?
W5500 integrates MAC/PHY and a hardwired TCP/IP stack, and communicates via SPI (up to 80 MHz supported), giving a straightforward wired-LAN endpoint that fits industrial networks well. Wi-Fi can be excellent but often adds RF variability, provisioning, and security surface area. ENC28J60 typically requires a software TCP/IP stack on the MCU, which increases firmware complexity and resource pressure

