Status: ✅ Complete
Focus: W5500 Ethernet, Modbus TCP protocol, OPC-UA gateway, real-time OLED display
Key Achievement: Production-ready industrial automation system with dual STM32 Modbus TCP slaves
- Week 1: RTIC LoRa Basics | Blog Post
- Week 2: Sensor Fusion | Blog Post
- Week 3: Binary Protocols
- Week 5: Gateway Firmware
- Week 6: Async Gateway
- Week 7+8: MQTT + InfluxDB + Grafana
- Week 9: Modbus TCP + OPC-UA (You are here) | Blog Post
- Overview
- Week 9 Focus: Industrial Protocols
- Architecture
- Hardware Configuration
- Quick Start
- Modbus Register Map
- OPC-UA Integration
- Current Status
Week 9 moves from IoT protocols (MQTT, InfluxDB) to industrial automation protocols (Modbus TCP, OPC-UA). This isn't just "adding Ethernet" - it's implementing industry-standard SCADA integration with real embedded Modbus TCP slaves.
What Changed from Week 7+8:
- ✅ W5500 Ethernet (SPI-based, static IP configuration)
- ✅ Modbus TCP slaves (custom implementation, FC03 Read Holding Registers)
- ✅ OPC-UA gateway (desktop server polling both devices)
- ✅ Real-time OLED display (SSD1306 showing live status)
- ✅ I2C bus sharing (SHT3x sensor + OLED on same bus)
- ✅ Socket state machine (robust TCP connection handling)
- ✅ IEEE 754 encoding (float32 temperature/humidity)
Why This Matters: Modbus TCP and OPC-UA are the backbone of industrial automation. PLC integration, SCADA systems, factory monitoring - this is what runs industrial plants.
| Aspect | IoT (Weeks 7-8) | Industrial (Week 9) |
|---|---|---|
| Protocol | MQTT, HTTP | Modbus TCP, OPC-UA |
| Device Role | Publisher | Slave/Server |
| Network | Cloud-oriented | LAN-focused |
| Data Format | JSON, flexible | Binary, standardized |
| Client | Any subscriber | SCADA, PLC, HMI |
| Standards Body | OASIS (MQTT) | Modbus Organization, OPC Foundation |
Modbus TCP is the universal language of industrial automation:
- ✅ Legacy compatibility: Works with devices from the 1970s onward
- ✅ Simplicity: Easy to implement, easy to debug
- ✅ PLC integration: Every major PLC supports Modbus
- ✅ SCADA standard: Used in factories, utilities, buildings
- ✅ Well-documented: Specification is free and clear
Real-world use cases:
- Factory floor monitoring (conveyor speeds, temperatures, pressures)
- Building automation (HVAC, lighting, energy meters)
- Utility SCADA (water pumps, electrical substations)
- Process control (chemical plants, oil refineries)
OPC-UA is the modern industrial interoperability standard:
- ✅ Platform-independent: Works on Windows, Linux, embedded
- ✅ Secure: Built-in authentication and encryption
- ✅ Rich data model: Not just values, but types and relationships
- ✅ Subscription support: Clients notified of changes
- ✅ Vendor-neutral: Open standard, no licensing fees
Gateway architecture:
STM32 Modbus Slaves → Python OPC-UA Server → UaExpert/SCADA clients
┌─────────────────────────────────────────────────────────────┐
│ Desktop PC (OPC-UA Server) │
│ 10.10.10.1:4840 │
│ Python Gateway (opcua_modbus_gateway.py) │
│ • Polls Modbus devices every 2s │
│ • Exposes OPC-UA variables │
│ • Connection monitoring │
└────────────┬─────────────────────────┬──────────────────────┘
│ Modbus TCP (FC03) │ Modbus TCP (FC03)
│ Poll every 2s │ Poll every 2s
┌─────────▼──────────┐ ┌────────▼──────────┐
│ MODBUS_1 (F446) │ │ MODBUS_2 (F446) │
│ 10.10.10.100:502 │ │ 10.10.10.200:502 │
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ W5500 SPI │ │ │ │ W5500 SPI │ │
│ │ Ethernet │ │ │ │ Ethernet │ │
│ │ Static IP │ │ │ │ Static IP │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ SHT3x I2C │ │ │ │ SHT3x I2C │ │
│ │ Temp/Humidity│ │ │ │ Temp/Humidity│ │
│ └──────────────┘ │ │ └──────────────┘ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ SSD1306 OLED │ │ │ │ SSD1306 OLED │ │
│ │ 128x64 I2C │ │ │ │ 128x64 I2C │ │
│ │ Real-time │ │ │ │ Real-time │ │
│ │ Status │ │ │ │ Status │ │
│ └──────────────┘ │ │ └──────────────┘ │
└────────────────────┘ └───────────────────┘
1. SHT3x sensor reading (every 2s)
↓
2. Stored in Modbus registers (in RAM)
↓
3. OPC-UA Gateway polls via Modbus TCP
↓
4. Data decoded (IEEE 754 floats)
↓
5. Exposed as OPC-UA variables
↓
6. Clients (UaExpert, SCADA) subscribe/read
Latency: ~50ms sensor reading + ~20ms Modbus TCP round-trip = ~70ms total
| Component | Specification | Interface | Notes |
|---|---|---|---|
| MCU | STM32F446RE Nucleo-64 | - | Cortex-M4F @ 84 MHz |
| Ethernet | W5500 module | SPI1 | Static IP: 10.10.10.100 |
| Sensor | SHT3x | I2C1 (PB8/PB9) | Temperature + Humidity |
| Display | SSD1306 128x64 OLED | I2C1 (shared) | Real-time status |
| MAC | 02:00:00:00:00:10 | - | Locally administered |
| Component | Specification | Interface | Notes |
|---|---|---|---|
| MCU | STM32F446RE Nucleo-64 | - | Cortex-M4F @ 84 MHz |
| Ethernet | W5500 module | SPI1 | Static IP: 10.10.10.200 |
| Sensor | SHT3x | I2C1 (PB8/PB9) | Temperature + Humidity |
| Display | SSD1306 128x64 OLED | I2C1 (shared) | Real-time status |
| MAC | 02:00:00:00:00:20 | - | Locally administered |
| W5500 Pin | F446 Pin | Function | Notes |
|---|---|---|---|
| MOSI | PA7 | SPI1_MOSI | Morpho connector |
| MISO | PA6 | SPI1_MISO | Morpho connector |
| SCK | PA5 | SPI1_SCK | Morpho connector |
| CS | PB6 | GPIO | Active low |
| RST | PC7 | GPIO | Active low reset |
| GND | GND | Ground | - |
| VCC | 3V3 | Power | 3.3V only |
Critical: Use Morpho connector pins (PA5/PA6/PA7), NOT Arduino connector (PB3/PB4/PB5)
| SHT3x Pin | F446 Pin | Function |
|---|---|---|
| SDA | PB9 | I2C1_SDA |
| SCL | PB8 | I2C1_SCL |
| GND | GND | Ground |
| VDD | 3V3 | Power |
I2C Address: 0x44 (default, Address::Low)
| OLED Pin | F446 Pin | Function |
|---|---|---|
| SDA | PB9 | I2C1_SDA (shared with SHT3x) |
| SCL | PB8 | I2C1_SCL (shared with SHT3x) |
| GND | GND | Ground |
| VCC | 3V3 | Power |
I2C Address: 0x3C (standard for SSD1306)
| Parameter | Value | Notes |
|---|---|---|
| Subnet | 10.10.10.0/24 | Private network |
| Gateway | 10.10.10.1 | Desktop PC |
| Board 1 IP | 10.10.10.100 | MODBUS_1 |
| Board 2 IP | 10.10.10.200 | MODBUS_2 |
| Modbus Port | 502 | Standard Modbus TCP port |
| OPC-UA Port | 4840 | Standard OPC-UA port |
# Rust embedded toolchain
rustup target add thumbv7em-none-eabihf
# probe-rs for flashing
cargo install probe-rs-tools --locked
# Python dependencies for OPC-UA gateway
pip3 install asyncua pymodbusBoard 1 (MODBUS_1):
# Build
cargo build --release --bin modbus_1
# Flash (Terminal 1)
probe-rs run --probe 0483:374b:0671FF3833554B3043164817 \
--chip STM32F446RETx \
target/thumbv7em-none-eabihf/release/modbus_1Board 2 (MODBUS_2):
# Build
cargo build --release --bin modbus_2
# Flash (Terminal 2)
probe-rs run --probe 0483:374b:066DFF3833584B3043115433 \
--chip STM32F446RETx \
target/thumbv7em-none-eabihf/release/modbus_2Ping test:
ping 10.10.10.100 # Board 1
ping 10.10.10.200 # Board 2Modbus query (using mbpoll):
# Read all registers from Board 1
mbpoll -a 1 -r 1 -c 7 -t 4 -1 10.10.10.100
# Expected output:
# [1]: 16502 16650 (Temperature: 30.3°C as IEEE 754)
# [3]: 16824 -26214 (Humidity: 56.7% as IEEE 754)
# [5]: 0 (Status: OK)
# [6]: 0 56 (Uptime: 56 seconds)Terminal 3:
python3 opcua_modbus_gateway.pyOutput:
INFO:__main__:Starting OPC-UA to Modbus TCP Gateway
INFO:__main__:Creating OPC-UA namespace for MODBUS_1
INFO:__main__:Creating OPC-UA namespace for MODBUS_2
INFO:__main__:OPC-UA server starting on opc.tcp://0.0.0.0:4840/freeopcua/server/
INFO:__main__:OPC-UA server is running
INFO:__main__:[MODBUS_1] T=30.3°C, H=56.7%, Status=0, Uptime=56s
INFO:__main__:[MODBUS_2] T=31.0°C, H=52.6%, Status=0, Uptime=292s
Terminal 4:
python3 test_both_boards.pyExpected output:
============================================================
MODBUS_1 (10.10.10.100:502) - CONNECTED
Temperature: 30.3°C
Humidity: 56.7%
Device Status: 0
Uptime: 56s (0min 56s)
MODBUS_2 (10.10.10.200:502) - CONNECTED
Temperature: 31.0°C
Humidity: 52.6%
Device Status: 0
Uptime: 292s (4min 52s)
============================================================
| Modbus Address | Protocol Addr | Type | Size | Description | Format |
|---|---|---|---|---|---|
| 40001-40002 | 0-1 | f32 | 2 regs | Temperature (°C) | IEEE 754 BE |
| 40003-40004 | 2-3 | f32 | 2 regs | Humidity (%RH) | IEEE 754 BE |
| 40005 | 4 | u16 | 1 reg | Device Status | Big-endian |
| 40006-40007 | 5-6 | u32 | 2 regs | Uptime (seconds) | Big-endian |
| 40008-40010 | 7-9 | u16 | 3 regs | Reserved | 0x0000 |
Note: Modbus uses 1-based addressing (40001+), but protocol uses 0-based (address 0).
Temperature/Humidity (IEEE 754 float32):
30.3°C = 0x41F26666
Registers: [0x41F2, 0x6666] (big-endian)
Decoding in Python:
import struct
bytes_data = struct.pack('>HH', 0x41F2, 0x6666)
temperature = struct.unpack('>f', bytes_data)[0] # 30.299999...
Status (u16):
0 = OK
1 = Error
Uptime (u32):
56 seconds = 0x00000038
Registers: [0x0000, 0x0038] (big-endian)
Architecture:
# Polls Modbus devices
for device in MODBUS_DEVICES:
result = client.read_holding_registers(address=0, count=7)
temperature = decode_float32(result.registers[0:2])
humidity = decode_float32(result.registers[2:4])
# Update OPC-UA nodes
await temp_node.write_value(temperature)
await hum_node.write_value(humidity)Polling interval: 2 seconds
opc.tcp://10.10.10.1:4840/freeopcua/server/
└── ModbusDevices/
├── MODBUS_1/
│ ├── Temperature (Float)
│ ├── Humidity (Float)
│ ├── DeviceStatus (UInt16)
│ ├── Uptime (UInt32)
│ └── ConnectionStatus (String)
└── MODBUS_2/
├── Temperature (Float)
├── Humidity (Float)
├── DeviceStatus (UInt16)
├── Uptime (UInt32)
└── ConnectionStatus (String)
- Download UaExpert
- Add Server:
opc.tcp://10.10.10.1:4840/freeopcua/server/ - Browse to
ModbusDevices→MODBUS_1 - Drag variables to Data Access View
- Monitor live updates every 2 seconds
Startup screen:
MODBUS_1
IP: 10.10.10.100
Initializing...
Running screen:
MODBUS_1
10.10.10.100:502
T: 30.2C
H: 59.0%
LISTENING
Connected screen (when Modbus client connected):
MODBUS_1
10.10.10.100:502
T: 30.3C
H: 56.7%
CONNECTED
Status indicators:
LISTENING: Waiting for Modbus connectionCONNECTED: Active Modbus TCP session
- 2x STM32F446RE boards with W5500 Ethernet modules
- SHT3x sensors reading temperature and humidity
- SSD1306 OLED displays showing real-time status
- All wiring verified and documented
- Custom W5500 SPI driver (register-level access)
- Static IP configuration (no DHCP)
- TCP socket state machine (LISTEN, ESTABLISHED, CLOSE_WAIT)
- Modbus TCP protocol implementation (FC03)
- MBAP header parsing
- Register map with IEEE 754 encoding
- I2C bus sharing (async DMA sensor + blocking OLED)
- Real-time OLED updates
- Graceful error handling (works without OLED)
- Python OPC-UA gateway server
- Modbus TCP client polling
- Data type conversion (IEEE 754, big-endian u32)
- OPC-UA namespace configuration
- Connection status monitoring
- Test clients (both individual and dual-board)
| Metric | Value | Notes |
|---|---|---|
| Sensor reading | Every 2s | SHT3x on I2C1 |
| Modbus polling | Every 2s | OPC-UA gateway |
| OLED update | Every 2s | Real-time status |
| Round-trip latency | ~70ms | Sensor → Modbus → OPC-UA |
| Uptime tested | 6+ hours | Zero crashes |
| Packet loss | 0% | TCP guaranteed delivery |
Live test (January 2026):
| Device | Temperature | Humidity | Status | Uptime |
|---|---|---|---|---|
| MODBUS_1 | 30.3°C | 56.7% | 0 (OK) | 3329s |
| MODBUS_2 | 31.0°C | 52.6% | 0 (OK) | 1456s |
Network connectivity: ✅ Both boards pingable, Modbus queries successful
OPC-UA gateway: ✅ Polling both devices, no errors
OLED displays: ✅ Real-time updates, connection status accurate
| Technology | Version | Purpose |
|---|---|---|
| Embassy | 0.6 | Async runtime (executor, timers) |
| embassy-stm32 | 0.1 | STM32 HAL with async support |
| w5500 | 0.4 | Ethernet chip library |
| shtcx | 0.11 | SHT3x sensor driver |
| ssd1306 | 0.8 | OLED display driver |
| embedded-graphics | 0.8 | Graphics primitives for OLED |
| rmodbus | 0.9 | Modbus protocol parser (not used yet) |
| defmt/defmt-rtt | 0.3/0.4 | Efficient embedded logging |
| Technology | Version | Purpose |
|---|---|---|
| asyncua | Latest | OPC-UA server library |
| pymodbus | Latest | Modbus TCP client library |
| Python | 3.8+ | Gateway runtime |
wk9-opcua-modbus/
├── Cargo.toml # Multi-binary project
├── .cargo/config.toml # Embedded target config
├── memory.x # STM32F446RE linker script
├── build.rs # Build script
├── src/
│ ├── common.rs # Shared code (W5500, Modbus, sensors, OLED)
│ ├── modbus_1.rs # Board 1 entry point (10.10.10.100)
│ └── modbus_2.rs # Board 2 entry point (10.10.10.200)
├── opcua_modbus_gateway.py # Desktop OPC-UA server
├── test_opcua_client.py # Basic OPC-UA test
├── test_both_boards.py # Dual-board formatted output
├── README.md # This file
├── USERGUIDE.md # Complete operational guide
├── STATUS.md # Project status summary
├── SUMMARY.md # Quick reference
├── NOTES.md # Development session log
├── TODO.md # Task tracking
└── TROUBLESHOOTING.md # Common issues and solutions
# Ping test
ping 10.10.10.100
ping 10.10.10.200
# TCP connection test
nc 10.10.10.100 502# Read all registers
mbpoll -a 1 -r 1 -c 7 -t 4 -1 10.10.10.100
# Read temperature only
mbpoll -a 1 -r 1 -c 2 -t 4 -1 10.10.10.100
# Read humidity only
mbpoll -a 1 -r 3 -c 2 -t 4 -1 10.10.10.100# Start gateway
python3 opcua_modbus_gateway.py
# Test client
python3 test_both_boards.pyChallenge: No existing Embassy driver for W5500
Solution: Direct SPI register access
async fn w5500_write_register(spi, cs, address, data) {
let control = (BSB_COMMON_REG << 3) | CONTROL_PHASE_WRITE;
let tx_buf = [
(address >> 8) as u8, // Address high
(address & 0xFF) as u8, // Address low
control, // Control byte
// ... data bytes
];
// SPI transaction with CS control
}Benefits:
- ✅ Complete control over W5500
- ✅ Static IP only (no DHCP complexity)
- ✅ Async-friendly
W5500 socket states handled:
0x00 (CLOSED) → Reopen and LISTEN
0x13 (INIT) → Send LISTEN command
0x14 (LISTEN) → Wait for connection
0x17 (ESTABLISHED) → Process Modbus requests
0x1C (CLOSE_WAIT) → Close and reopen
Robustness: Automatic recovery from any state
Challenge: SHT3x uses async I2C with DMA, OLED uses blocking I2C
Solution: Peripheral "stealing"
// Initialize async I2C for sensor
let mut i2c_async = I2c::new(p.I2C1, ..., p.DMA1_CH6, p.DMA1_CH0);
let sht3x = SHT3x::new(i2c_async, ...);
// Later, steal peripheral for OLED (blocking, no DMA)
let i2c_blocking = I2c::new(p.I2C1, ..., NoDma, NoDma);
let oled = Ssd1306::new(i2c_blocking, ...);Why it works: Devices accessed sequentially in main loop, never simultaneously
MBAP Header parsing:
pub struct MbapHeader {
pub transaction_id: u16, // For matching requests/responses
pub protocol_id: u16, // Always 0x0000 for Modbus
pub length: u16, // Bytes following this field
pub unit_id: u8, // Slave address
}FC03 (Read Holding Registers) response:
[MBAP Header (7 bytes)]
[Function Code (1 byte)]
[Byte Count (1 byte)]
[Register Data (N*2 bytes)]
Temperature/Humidity as float32:
fn f32_to_registers(value: f32) -> [u16; 2] {
let bytes = value.to_be_bytes();
[
u16::from_be_bytes([bytes[0], bytes[1]]),
u16::from_be_bytes([bytes[2], bytes[3]]),
]
}Why big-endian? Modbus standard uses big-endian byte order
- FC06 (Write Single Register)
- Writable configuration registers
- FC16 (Write Multiple Registers)
- Modbus exception responses
- Multiple SHT3x sensors per board
- BME680 gas sensor integration
- Pressure sensors
- Flow meters
- SNMP support for monitoring
- Web server for diagnostics
- Historical data logging
- Alert/threshold monitoring
- Connect to real SCADA systems
- PLC communication
- HMI dashboard integration
- Production deployment
Week 9 demonstrates production-ready industrial automation knowledge:
- Modbus TCP Protocol: Industry-standard for PLC/SCADA communication
- OPC-UA Integration: Modern interoperability standard
- Ethernet Networking: W5500 SPI-based TCP/IP stack
- Real-time Display: OLED showing live system status
- Robust Error Handling: Graceful degradation, auto-recovery
This is what runs factories, utilities, and building automation systems.
| Component | Implementation | Industry Use |
|---|---|---|
| Modbus TCP | FC03 Read Holding Registers | PLCs, sensors, meters worldwide |
| OPC-UA | Python asyncua gateway | SCADA systems, MES, historians |
| W5500 | SPI Ethernet module | Industrial embedded devices |
| IEEE 754 | Float32 encoding | Universal data exchange |
| Static IP | No DHCP dependency | Deterministic industrial networks |
These aren't "hobby implementations" - they're production patterns.
After Week 9, you can confidently claim:
- ✅ Implemented Modbus TCP slave devices on embedded hardware
- ✅ Developed custom W5500 Ethernet driver with async support
- ✅ Created OPC-UA gateway for SCADA integration
- ✅ Worked with industrial protocols (Modbus, OPC-UA)
- ✅ Implemented IEEE 754 float encoding for data exchange
- ✅ Designed robust socket state machines for TCP communication
- ✅ Integrated real-time displays with shared I2C buses
- ✅ Debugged SPI, I2C, and TCP protocols at register level
This is industrial automation engineering experience.
- USERGUIDE.md - Complete operational guide
- STATUS.md - Current system status
- SUMMARY.md - Quick reference
- NOTES.md - Development session logs
- TROUBLESHOOTING.md - Common issues
Author: Antony (Tony) Mapfumo
Part of: 4-Month Embedded Rust Learning Roadmap
Week: 9 of 16
Status: ✅ Production Ready - Industrial Automation
