OpenPLC v3 + Modbus TCP: Open-Source PLC Architecture
The commercial PLC market—dominated by Siemens, Allen-Bradley, and Beckhoff—sets a high bar in determinism, safety certification, and ecosystem maturity. But for labs, educational robotics kits, low-volume custom controllers, and edge gateways where a £50k S7-1500 is pure overkill, OpenPLC v3 has matured into a credible alternative. Developed since 2014 by Thiago Alves, OpenPLC brings a full IEC 61131-3 runtime to Linux (Raspberry Pi, BeagleBone), microcontrollers (Arduino, ESP32), and Windows, with native Modbus TCP/RTU, DNP3, and EtherNet/IP support. The Open-Source PLC architecture—combining a compile-to-C editor, a deterministic-enough scan cycle, and a modular hardware abstraction layer—solves real control problems without license friction or vendor lock-in. This post dissects the runtime internals, Modbus TCP address mapping, hardware deployment patterns, and a production-ready stack (systemd + mTLS + OPC UA bridge) to show how and where OpenPLC fits in 2026 industrial automation.
What this post covers:
– OpenPLC v3 architecture, IEC 61131-3 language support, and what it isn’t
– Runtime scan cycle, editor pipeline, and Modbus TCP server mechanics
– Variable-to-address mapping for coils, discrete inputs, holding registers, and input registers
– Hardware deployment tiers from Raspberry Pi to industrial PCs
– A production stack with TLS, logging, OPC UA bridging, and Prometheus metrics
– Trade-offs and practical decision criteria
What OpenPLC v3 Is (And Isn’t)
OpenPLC v3 is an open-source IEC 61131-3 runtime and editor that compiles structured text (ST), ladder diagram (LD), functional block diagram (FBD), instruction list (IL), and sequential function chart (SFC) programs to executable binaries. It runs on Raspberry Pi, BeagleBone, Windows, macOS, Linux x86, Arduino, ESP32, and other embedded targets. The IDE (a browser-based GUI powered by Flask/Python) accepts drag-and-drop LD programs and ST text, routes them through the MATIEC compiler (an IEC 61131-3 translator), and outputs a C program. That C source is compiled by GCC to an ELF binary or shared library, then loaded at runtime by the OpenPLC executor—a lightweight process that orchestrates a 20–50 ms scan cycle, manages I/O via a hardware abstraction layer, and exposes Modbus TCP (port 502 by default) and optional DNP3/EtherNet/IP servers.
Key facts:
– Written in C/C++; core runtime is ~6000 lines
– Modbus TCP, RTU, DNP3, EtherNet/IP, OPC UA (via extensions)
– Web UI (Flask) + MariaDB for project storage and history
– GPL v3 licensed; active GitHub project (openplcproject.com)
– NOT SIL-certified—no functional safety, redundancy, or watchdog guarantees beyond basic systemd restarts
What OpenPLC is not:
– Not a safety-rated PLC (SIL 1/2/3)
– Not motion-control native (no axis blending, interpolation)
– Not hard real-time (scan determinism is best-effort, ~±10 ms jitter on RPi)
– Not a drop-in Siemens S7 replacement (different instruction sets, networking)
– Not a commercial product with vendor support or long-term stable APIs
OpenPLC v3 Runtime Internals
The OpenPLC v3 execution pipeline splits into two phases: compilation (offline, in the IDE) and runtime (online, on the target).
Compilation Phase
The IDE accepts a program in ST, LD, or other IEC 61131-3 format and passes it to MATIEC, a free IEC 61131-3 translator. MATIEC parses the source, builds an abstract syntax tree (AST), and emits C code that defines input/output variables (__LOCATED_VAR declarations), user functions, and the main plc_prog() function—the per-cycle logic executed by the runtime. That C source is compiled by GCC with -fPIC (for shared library builds) or -static (for statically-linked executables) and linked against libopenplc.so or libopenplc.a.
Runtime Scan Cycle
The OpenPLC executor (binary openplc or service openplc.service on systemd) initializes hardware I/O via the HAL, then enters an infinite loop:
while (running) {
// 1. Read inputs from HAL
read_inputs_from_hal();
// 2. Execute the compiled program logic
plc_prog(); // Calls user-defined IEC 61131-3 program
// 3. Write outputs to HAL
write_outputs_to_hal();
// 4. Sleep to maintain cycle time
usleep(cycle_time_us); // e.g., 50 ms = 50,000 us
}
Scan time (typical) = 20–50 ms on a Pi, 5–10 ms on an industrial PC. Jitter is ±10 ms on a busy Pi, <1 ms on a real-time patched kernel. Blocking operations (e.g., file I/O, Modbus RTU serial reads) can extend cycle time unpredictably—best practice is to move blocking calls into background threads or async handlers.
Hardware Abstraction Layer (HAL)
The HAL is a plugin architecture: each I/O backend (RPi GPIO, Modbus RTU master, Arduino serial, simulated) implements read_inputs() and write_outputs() functions. The runtime loads the selected HAL at startup. For example:
– RPi GPIO HAL: Reads /sys/class/gpio/... or uses libgpiod; writes to GPIO pins directly.
– Modbus RTU HAL: Opens a serial port, performs Modbus RTU master transactions, maps coils/registers to %IW/%QW.
– Arduino HAL: Communicates over USB/serial; relays digital/analog I/O from the Arduino MCU.
– Simulated HAL: Feeds constant or randomized test data, useful for offline development.
Modbus TCP Server
By default, OpenPLC runs a Modbus TCP server on port 502 (the official Modbus TCP port; requires root or CAP_NET_BIND_SERVICE on Linux). This server listens for Modbus function codes (FC):
- FC 01 (Read Coils) → read digital outputs (%QX0.0, %QX0.1, …)
- FC 02 (Read Discrete Inputs) → read digital inputs (%IX0.0, %IX0.1, …)
- FC 03 (Read Holding Registers) → read %QW, %MW (control variables, setpoints)
- FC 04 (Read Input Registers) → read %IW (sensor data, feedback)
- FC 05 (Write Single Coil) → write digital output (%QX0.0 = TRUE/FALSE)
- FC 06 (Write Single Register) → write single %QW
- FC 16 (Write Multiple Registers) → batch-write holding registers
The server runs in a background thread and does not block the scan cycle.
Web UI and Database
The OpenPLC IDE persists projects, compiled binaries, and runtime logs in a MariaDB database. The Flask-based web UI (port 8080 by default) allows users to:
– Upload or edit IEC 61131-3 programs
– Compile and download binaries
– Configure I/O HAL (GPIO pins, serial ports, Modbus RTU clients)
– Monitor runtime status (CPU, memory, scan cycle time)
– Retrieve logs and diagnostic data

Modbus TCP in OpenPLC: Server, Client, and Address Mapping
OpenPLC operates as a Modbus TCP server (field device) and can also function as a Modbus TCP client (master) via the Modbus RTU HAL or a dedicated Modbus client library. Understanding the address mapping is critical for integrating OpenPLC into a larger control system.
Modbus Data Model
Modbus defines four primary data types:
1. Coils (FC 01/05) — 1-bit writable outputs. OpenPLC maps these to %QX (digital output word variables, addressed by bit).
2. Discrete Inputs (FC 02) — 1-bit read-only inputs. OpenPLC maps these to %IX (digital input word variables).
3. Holding Registers (FC 03/06/16) — 16-bit writable values. OpenPLC maps %QW (output registers) and %MW (internal/memory registers).
4. Input Registers (FC 04) — 16-bit read-only values. OpenPLC maps these to %IW (input registers, sensor values).
Variable-to-Address Mapping
OpenPLC uses a 1-based Modbus addressing scheme (Modbus addresses 1, 2, 3, …) internally mapped to 0-based register indices in the IEC 61131-3 address space:
| IEC 61131-3 Variable | Type | Modbus Address (1-based) | Function Code |
|---|---|---|---|
| %IX0.0 (bit 0 of word 0) | Digital input | 1 | FC 02 |
| %IX0.1 (bit 1 of word 0) | Digital input | 2 | FC 02 |
| %IW0 | 16-bit input register | 0 | FC 04 |
| %QX0.0 (bit 0 of word 0) | Digital output | 1 | FC 01/05 |
| %QW0 | 16-bit output register | 0 | FC 03/06/16 |
| %MW0 | 16-bit memory register | 1024 | FC 03/06/16 |
| %MW1 | 16-bit memory register | 1025 | FC 03/06/16 |
Byte order: Modbus TCP uses big-endian (network byte order). A 32-bit floating-point value is stored across two 16-bit registers in big-endian word order: high word in register N, low word in register N+1.
Address collision avoidance: %MW (internal memory registers) start at address 1024 to avoid colliding with %QW (outputs, addresses 0–511). This convention is site-specific; always verify with your OpenPLC deployment.
Example: Reading a Pressure Sensor via Modbus TCP
// Compiled ST program running on OpenPLC
PROGRAM ReadPressure
VAR
pressure_raw: INT; // Mapped to %IW0 (Modbus input register 0)
pressure_Pa: REAL;
END_VAR
pressure_raw := 1500; // Simulated sensor reading (in raw ADC counts)
pressure_Pa := DINT_TO_REAL(pressure_raw) * 100.0; // Convert to Pa
END_PROGRAM
A Modbus TCP client (e.g., an SCADA system or Python script) reads this value via FC 04:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient(host='192.168.1.100', port=502)
result = client.read_input_registers(address=0, count=1)
raw_value = result.registers[0] # Returns 1500
pressure_Pa = raw_value * 100.0

Hardware Targets and Deployment Patterns
OpenPLC runs on a spectrum of targets, each with different cost, I/O capacity, and cycle-time constraints.
Tier 1: Raspberry Pi Lab/Educational
Hardware: Raspberry Pi 4 or 5 + Sequent Microsystems Mega-IO HAT (16x DI, 16x DO, 8x AI/0–10V, 4x AO 0–10V).
Cycle time: 50–100 ms (adequate for slow plant processes, heater/cooler control, pump sequencing).
Cost: £80–200 per unit.
Use case: Educational labs, maker spaces, proof-of-concept prototypes, home automation, greenhouse climate control.
Limitations: No industrial certification, susceptible to electrical noise (add shielding and 24 V supply isolation).
Tier 2: Industrial Raspberry Pi / Edge Gateway
Hardware: Raspberry Pi 5 + Pixtend V2 (24x DI, 20x DO, 8x AI/0–10V) in a DIN-rail enclosure with surge protection, 24 V PSU, and Ethernet isolator.
Cycle time: 20–50 ms.
Cost: £400–800 with industrial accessories.
Use case: Edge PLC gateway for remote sites (oil & gas, utilities, agriculture), Modbus RTU aggregator for legacy sensors, lightweight OPC UA bridge.
Advantages: Low power (5 W), fanless, long-term Linux support, wide temperature range HATs (−20 °C to +60 °C).
Tier 3: Industrial PC or Dedicated Single-Board Computer
Hardware: Beckhoff CX-series embedded PC, Siemens Simatic IOT2000, or HiCorereV industrial IPC (fanless, −20 °C to +60 °C) + EtherCAT coupler or Modbus gateway module.
Cycle time: 5–10 ms (soft real-time with stock Linux kernel).
Cost: £1500–5000.
Use case: Multi-axis coordinated control, high-speed scanning (e.g., 50 discrete analog input channels), motion-heavy applications, safety relay backup logic.
Advantages: Industrial-grade I/O reliability, extended temperature range, long product life (10+ years), some models support hard real-time (PREEMPT-RT kernel).
Tier 4: Microcontroller (Arduino, ESP32)
Hardware: Arduino Mega + Ethernet Shield or ESP32 with native WiFi; limited to ~50 digital I/O and 16 analog inputs.
Cycle time: 10–20 ms.
Cost: £20–60.
Use case: Tiny remote sensors, drone flight controllers, IoT-enabled thermostats, portable test equipment.
Limitations: No Modbus TCP (use Modbus RTU over serial), memory-constrained (no in-built database), no Web UI (must use external SCADA).

A Production-Ready Deployment with TLS, Logging, and OPC UA Bridge
Moving OpenPLC from a lab Pi to a production edge site requires defense-in-depth:
1. Process management (systemd service)
2. Network encryption (mTLS between client and Modbus TCP server)
3. Metrics and observability (Prometheus exporter, Grafana dashboards)
4. Protocol bridging (OPC UA server sidecar for enterprise SCADA integration)
5. Audit logging (journald + Loki)
Systemd Service Unit
Create /etc/systemd/system/openplc.service:
[Unit]
Description=OpenPLC v3 Runtime
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=openplc
Group=openplc
WorkingDirectory=/opt/openplc
ExecStart=/opt/openplc/bin/openplc -c /etc/openplc/openplc.conf
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=openplc
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable openplc.service
sudo systemctl start openplc.service
sudo journalctl -u openplc -f # Monitor logs in real-time
mTLS via Stunnel Front-End
Modbus TCP (RFC 1002) has no encryption. Wrap it with stunnel, which accepts TLS connections and forwards decrypted traffic to the local Modbus TCP server (port 502 in loopback).
Create /etc/stunnel/openplc-modbus.conf:
[openplc-modbus-tls]
accept = 0.0.0.0:20502
connect = 127.0.0.1:502
key = /etc/stunnel/openplc-key.pem
cert = /etc/stunnel/openplc-cert.pem
verify = 2
CAfile = /etc/stunnel/ca-cert.pem
Clients connect to port 20502 with mTLS. Stunnel decrypts and forwards plaintext Modbus TCP to port 502 (internal only).
OPC UA Server Sidecar (Python/asyncua)
Many enterprise SCADA systems prefer OPC UA over raw Modbus. A lightweight sidecar translates Modbus TCP variables to an OPC UA address space.
Stub implementation (Python + asyncua):
import asyncio
from asyncua import Server, ua
from pymodbus.client import ModbusTcpClient
class OpenPLCOpcUaBridge:
def __init__(self, modbus_host, modbus_port, opc_endpoint):
self.modbus_client = ModbusTcpClient(host=modbus_host, port=modbus_port)
self.server = Server()
self.endpoint = opc_endpoint
async def start(self):
await self.server.init()
self.server.set_endpoint(self.endpoint)
# Create OPC UA namespace and nodes
ns_idx = await self.server.register_namespace('urn:openplc:ua')
objects = self.server.get_objects_node()
# Example: expose %IW0 as an OPC UA variable
self.var_pressure = await objects.add_variable(
ns_idx, 'Pressure_Raw', 0, ua.VariantType.Int16
)
await self.var_pressure.set_writable()
async with self.server:
while True:
# Poll Modbus every 1 second
result = self.modbus_client.read_input_registers(address=0, count=1)
if result.isError():
continue
pressure_raw = result.registers[0]
await self.var_pressure.set_value(pressure_raw)
await asyncio.sleep(1)
if __name__ == '__main__':
bridge = OpenPLCOpcUaBridge(
modbus_host='127.0.0.1',
modbus_port=502,
opc_endpoint='opc.tcp://0.0.0.0:4840'
)
asyncio.run(bridge.start())
Run the bridge in a second systemd service:
[Unit]
Description=OpenPLC OPC UA Bridge
After=openplc.service
[Service]
Type=simple
User=openplc
ExecStart=/usr/bin/python3 /opt/openplc-opc-bridge/main.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
Prometheus Metrics Exporter
Export OpenPLC runtime metrics (scan cycle time, I/O reads/writes, error counts) via a custom Prometheus exporter. A openplc-metrics.py sidecar polls OpenPLC’s internal statistics and exposes them on port 9100:
from prometheus_client import start_http_server, Gauge, Counter
import subprocess
import json
import time
cycle_time_ms = Gauge('openplc_cycle_time_ms', 'Scan cycle time in milliseconds')
io_reads_total = Counter('openplc_io_reads_total', 'Total I/O read operations')
io_writes_total = Counter('openplc_io_writes_total', 'Total I/O write operations')
def collect_metrics():
# Query OpenPLC runtime stats (e.g., via REST API or shared memory)
result = subprocess.run(
['curl', '-s', 'http://127.0.0.1:8080/api/runtime/stats'],
capture_output=True, text=True
)
stats = json.loads(result.stdout)
cycle_time_ms.set(stats['cycle_time_ms'])
io_reads_total._value.set(stats['io_reads'])
io_writes_total._value.set(stats['io_writes'])
if __name__ == '__main__':
start_http_server(9100)
while True:
collect_metrics()
time.sleep(10)
Connect Prometheus to scrape localhost:9100 every 30 seconds; visualize in Grafana with alerts on cycle-time skew or I/O error spikes.

Trade-offs and Where OpenPLC Falls Short
No SIL Certification
Safety-critical applications (e.g., interlocks, emergency stop) require SIL 1/2/3 certification under IEC 61508 or IEC 62061. OpenPLC has no formal safety analysis, no redundancy architecture, and no certified watchdog. Use certified devices (Siemens S7-1500F, Beckhoff TwinCAT 3 with safety extensions, Phoenix Contact QUINT 4 redundant PSU) for safety loops.
Scan-Cycle Jitter
Commercial PLCs (Siemens) guarantee cycle times to ±5 % jitter via real-time kernels and interrupt prioritization. OpenPLC on a stock Linux kernel achieves ±10–20 % jitter; with PREEMPT-RT, this can improve to ±2–5 %. For motion control or synchronized multi-axis drive systems, this jitter is unacceptable. Use Beckhoff TwinCAT or B&R Automation for hard real-time requirements.
No Integrated Motion Control
OpenPLC does not natively support:
– Axis interpolation (G-code, Cartesian→joint transformation)
– PID servo loops (closed-loop control with encoder feedback)
– Multi-axis synchronization (3-axis milling machine blending)
For motion, add a dedicated motion control library (e.g., MOTOMAN for Yaskawa, Beckhoff’s TwinCAT Motion) or implement custom PID loops in ST code.
Smaller Ecosystem
Unlike Siemens (S7-1200, S7-1500) or Beckhoff (embedded PCs, I/O terminals), OpenPLC has a thin vendor market. Industrial HAT manufacturers (Sequent, Pixtend) support OpenPLC, but the selection is much smaller. No OEM discount programs or local distributor networks.
Weak Vendor Support
OpenPLC is a community-maintained open-source project. There is no 24/7 hotline, no SLA, and no professional services. Bug fixes depend on GitHub volunteers; critical security issues may see delays. Suitable for non-critical applications; unsuitable for mission-critical plants.
Practical Recommendations
When to Use OpenPLC
- Proof-of-Concept / Rapid Prototyping: Iterate quickly without commercial PLC licensing.
- Educational / Research: Teach IEC 61131-3 and control systems; low cost of entry (£50 Pi).
- Edge Gateways: Modbus RTU aggregator, IIoT bridge; runs on Pi in a remote substation.
- Low-Volume / Bespoke Products: Embedded controller for niche OEM applications (e.g., lab incubator, agricultural sensor controller).
- Home / Farm Automation: Non-safety-critical sequential logic (irrigation, heating, lighting).
When to Use Commercial PLCs
- Safety-Critical Loops: Interlocks, emergency stop, machine guarding → Siemens S7-1500F, TÜV-certified.
- Motion / CNC: Multi-axis milling, robot arm, conveyor synchronization → Beckhoff TwinCAT, Siemens 840D, FANUC.
- Sub-Millisecond Determinism: High-speed data acquisition, real-time feedback control → Beckhoff, B&R, ABB.
- Safety + Redundancy: Hot-standby, voting, SIL 3 architecture → Siemens, Beckhoff, ABB.
- Production Plant / Factory Floor: Long-term vendor support, spare parts, training → Siemens, AB, Schneider, Phoenix Contact.
Hybrid Approach
Run OpenPLC on a Pi for non-critical logic (pump scheduling, fan control, heater sequencing), and connect it via Modbus TCP to a dedicated safety relay module (Siemens 3SK3, Phoenix Contact QUINT 4) for emergency stop. The safety module remains certified; OpenPLC handles routine control.

FAQ
Q: Can I use OpenPLC for a safety-critical application?
A: No. OpenPLC has no SIL certification and no redundancy safeguards. Use certified devices (Siemens S7-1500F) for safety loops, and use OpenPLC only for non-safety logic (e.g., optimization, sequencing).
Q: What is the maximum scan cycle frequency?
A: On a Raspberry Pi, typically 20–50 ms (20–50 Hz). On an industrial PC with PREEMPT-RT kernel, 2–5 ms is achievable. Hard real-time requirements (<1 ms) demand Beckhoff or B&R.
Q: Does OpenPLC support EtherCAT?
A: OpenPLC core does not. However, you can run an EtherCAT master daemon (SOEM library) on the Pi and expose I/O via the Modbus RTU or GPIO HAL. This is custom integration, not native.
Q: Can I use OpenPLC on an Arduino or ESP32?
A: Yes, limited builds exist. Arduino Mega runs a subset of IEC 61131-3 (no floating-point, limited memory). ESP32 has more RAM (520 KB) but still cramped for larger programs. No Modbus TCP (serial Modbus RTU only). Suitable for tiny remote sensors, not complex control.
Q: What is the license?
A: GPL v3. You can distribute modified OpenPLC binaries only if you publish the source. Commercial use is allowed; redistribution without source disclosure is not.
Q: Is there a cloud-hosted OpenPLC IDE?
A: The upstream project does not provide cloud hosting. Some vendors (e.g., Sequent Microsystems) offer managed OpenPLC services on their hardware. Self-host the IDE on a Pi or VPS for full control.
Further Reading
- DDS: Data Distribution Service Protocol Complete Guide
- Sparkplug B 3.0 Protocol & Unified Namespace Guide
- EMQX MQTT Cluster on Kubernetes: Production Tutorial
- OpenPLC Project Official Documentation
- Modbus Organization TCP Specification (RFC 1002)
Author: Riju
Published: 2026-04-27
Last Updated: 2026-04-27
