Waiting for hardware to arrive? Need to test your SCADA or HMI before the PLC shows up? Here are all five ways to simulate a Modbus slave without real hardware — with honest tradeoffs for each.
Every automation engineer hits this problem eventually. You're building a SCADA system, an HMI, or a data acquisition application that reads from a Modbus device — energy meter, VFD, PLC, temperature controller. But the actual device won't arrive for weeks, or you need to test edge cases (exception responses, register overflows, communication timeouts) that are painful or impossible to trigger on real hardware.
Simulating the Modbus slave solves this in multiple scenarios:
The core requirement: your simulation needs to respond to Modbus function codes (FC01-FC06, FC15-FC16, FC23) with correct data formats, register sizes, and error codes. Anything that does this correctly is a valid Modbus slave simulation.
| Method | Setup Time | Cost | TCP | RTU | GUI | Multi-slave | Data Logging | Best For |
|---|---|---|---|---|---|---|---|---|
| ModbusSimulator | 5 min | Free trial / ~$40 | ✓ | ✓ | ✓ | ✓ (247) | ✓ | Fast, professional, all use cases |
| Python pymodbus | 30-60 min | Free | ✓ | ✓ | ✗ | ✓ | ✓ (custom) | Custom logic, CI/CD testing |
| Arduino | 1-2 hours | $5-25 (hardware) | ✗ | ✓ | ✗ | ✗ | ✗ | RTU serial testing with real cable |
| Raspberry Pi | 2-4 hours | $35-75 (hardware) | ✓ | ✓ | ✗ | ✓ | ✓ | Persistent slave on network |
| Virtual PLC (CODESYS) | 2-8 hours | Free / $300+ | ✓ | ✓ | ✓ | ✓ | ✓ | Full PLC logic simulation |
ModbusSimulator.com — Windows GUI Application
Recommended for Most Use CasesA dedicated Modbus slave simulator application for Windows. You install it, configure your slave device(s) in a GUI, and it starts responding to Modbus requests. No code, no configuration files, no hardware.
Python + pymodbus Library
Code Required Freepymodbus is the most mature Python Modbus library. Writing a Modbus slave (server) in Python gives you full flexibility — custom logic, dynamic register values, integration with databases, test automation frameworks, and CI/CD pipelines.
pip install pymodbus
import asyncio
from pymodbus.server import StartAsyncTcpServer
from pymodbus.datastore import (
ModbusSequentialDataBlock,
ModbusSlaveContext,
ModbusServerContext,
)
def create_slave_context():
# Initialize registers: coils, discrete inputs, holding regs, input regs
# Arguments: address, count, value
store = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0] * 100), # Discrete Inputs
co=ModbusSequentialDataBlock(0, [0] * 100), # Coils
hr=ModbusSequentialDataBlock(0, [0] * 200), # Holding Registers
ir=ModbusSequentialDataBlock(0, [0] * 200), # Input Registers
)
# Set some initial values (holding register 0-4)
store.setValues(3, 0, [1234, 5678, 9012, 3456, 7890])
return store
async def run_server():
context = ModbusServerContext(
slaves={1: create_slave_context()}, # unit ID 1
single=False
)
print("Starting Modbus TCP slave on 0.0.0.0:502")
await StartAsyncTcpServer(
context=context,
address=("0.0.0.0", 502)
)
if __name__ == "__main__":
asyncio.run(run_server())
import asyncio
from pymodbus.server import StartAsyncSerialServer
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
async def run_rtu_server():
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [100, 200, 300, 400, 500] + [0]*195)
)
context = ModbusServerContext(slaves={1: store}, single=False)
print("Starting Modbus RTU slave on COM3, 9600 baud")
await StartAsyncSerialServer(
context=context,
port="COM3", # Windows: COM3, COM4... Linux: /dev/ttyUSB0
baudrate=9600,
bytesize=8,
parity="N",
stopbits=1,
framer="rtu"
)
asyncio.run(run_rtu_server())
One advantage of Python is dynamic register updates — simulate a running process:
import asyncio
import random
from pymodbus.server import StartAsyncTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
async def update_registers(context):
"""Update registers every second to simulate a running process."""
counter = 0
while True:
await asyncio.sleep(1.0)
counter += 1
slave = context[1] # unit ID 1
# Simulate: temperature (reg 0), current (reg 1), power (reg 2)
values = [
int(220 + random.uniform(-5, 5)), # voltage ~ 220V
int(10 + random.uniform(-1, 1)), # current ~ 10A
counter % 65535, # energy accumulator
int(random.uniform(20, 80)), # temperature
]
slave.setValues(3, 0, values) # FC03 holding registers
async def run_server():
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [0] * 100)
)
context = ModbusServerContext(slaves={1: store}, single=False)
await asyncio.gather(
StartAsyncTcpServer(context=context, address=("0.0.0.0", 502)),
update_registers(context)
)
asyncio.run(run_server())
Arduino + ModbusSlave or Modbus-Arduino Library
Hardware RequiredAn Arduino (Uno, Mega, Nano) with a Modbus library acts as a real serial Modbus RTU slave device. This is useful when you need a physical RS485 connection — to test your RS485 wiring, terminators, and physical layer before the real PLC arrives.
#include <ModbusSlave.h>
// Modbus slave instance: Serial, unit ID=1, RS485 DE/RE pin=-1 (RS232 mode)
Modbus slave(Serial, 1, -1);
// Holding registers storage
uint16_t holdingRegisters[10] = {0};
// Callback to handle FC03/FC06/FC16 (read/write holding registers)
uint8_t readHoldingRegisters(uint8_t fc, uint16_t address, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
slave.writeRegisterToBuffer(i, holdingRegisters[address + i]);
}
return STATUS_OK;
}
uint8_t writeHoldingRegisters(uint8_t fc, uint16_t address, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
holdingRegisters[address + i] = slave.readRegisterFromBuffer(i);
}
return STATUS_OK;
}
void setup() {
Serial.begin(9600, SERIAL_8N1);
// Set initial register values
holdingRegisters[0] = 1234; // Temperature * 10 = 123.4°C
holdingRegisters[1] = 2200; // Voltage * 10 = 220.0V
holdingRegisters[2] = 500; // Current * 100 = 5.00A
slave.cbVector[CB_READ_HOLDING_REGISTERS] = readHoldingRegisters;
slave.cbVector[CB_WRITE_HOLDING_REGISTERS] = writeHoldingRegisters;
}
void loop() {
slave.poll();
// Simulate changing values
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 1000) {
holdingRegisters[0] = 1200 + random(100); // Temperature fluctuation
lastUpdate = millis();
}
}
Raspberry Pi 4/5 — Linux-based persistent slave
Hardware RequiredA Raspberry Pi running pymodbus is a solid option when you need a persistent Modbus slave on your network that doesn't tie up your workstation. The Pi can run 24/7, serve multiple test setups, and simulate both TCP and RTU (with USB-to-serial adapter).
# Install Python and pymodbus sudo apt update && sudo apt install python3 python3-pip -y pip3 install pymodbus # For RTU via USB-serial adapter pip3 install pyserial # Create the server script (same code as Method 2) # Run as a systemd service to start on boot: sudo nano /etc/systemd/system/modbus-slave.service
[Unit] Description=Modbus TCP Slave Simulator After=network.target [Service] Type=simple User=pi ExecStart=/usr/bin/python3 /home/pi/modbus_slave.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
CODESYS Runtime or OpenPLC — Full PLC Logic Simulation
CODESYS free runtime / OpenPLC freeIf you need to simulate not just Modbus registers but actual PLC ladder logic — rungs, function blocks, timers, counters — a virtual PLC is the right approach. CODESYS offers a free Windows runtime. OpenPLC is a fully open-source IEC 61131-3 compliant PLC implementation.
Note: CODESYS Virtual Control SL (full Modbus support) requires a paid licence after trial. The free runtime has limitations. OpenPLC is free but has more limited Modbus TCP server capabilities. For pure Modbus testing (register-level), a dedicated simulator is faster and cheaper.
| Your Situation | Recommended Method | Why |
|---|---|---|
| SCADA/HMI development before hardware arrives | ModbusSimulator (Method 1) | Fast setup, GUI register map, auto-update simulation |
| Automated unit testing in CI/CD pipeline | Python pymodbus (Method 2) | Code integration with pytest, Docker-compatible |
| Test physical RS485 wiring and cable | Arduino (Method 3) | Real serial hardware, real RS485 port |
| Persistent slave on team network for shared testing | Raspberry Pi (Method 4) | 24/7 availability, network-accessible |
| Customer FAT with real PLC code execution | CODESYS Virtual PLC (Method 5) | Actual IEC 61131-3 program running |
| Training PLC/SCADA engineers | ModbusSimulator (Method 1) | Zero setup, visual interface, good for demo |
| Debugging communication issues between master and slave | ModbusSimulator (Method 1) | Exception response control, detailed logging |
| IoT gateway testing (Modbus to MQTT/cloud) | Python pymodbus (Method 2) | Can integrate with IoT test frameworks |
For RTU simulation without physical serial hardware, use virtual COM port pairs. On Windows, install com0com (free, open-source). This creates linked virtual COM port pairs — anything written to COM10 appears at COM11 and vice versa.
# After installing com0com: # 1. Open com0com setup utility # 2. Create a port pair, e.g., COM10 <--> COM11 # 3. Run your Modbus RTU slave simulator on COM10 # 4. Connect your Modbus master (Modbus Poll, SCADA, etc.) to COM11 # Same baud rate, parity, stop bits on both sides
On Linux, use socat:
# Install socat sudo apt install socat # Create linked virtual serial ports: /tmp/ttyV0 and /tmp/ttyV1 socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1 & # Run pymodbus RTU slave on /tmp/ttyV0 # Connect your Modbus master to /tmp/ttyV1
Key point: When using virtual COM ports, both the slave and master must use identical serial settings — same baud rate (9600, 19200, 115200, etc.), same parity (N, E, O), same stop bits (1 or 2). A mismatch will cause CRC errors or no response.
One of the most valuable things about simulation is testing exception responses — scenarios that are hard to trigger on real hardware. Key exception codes you should test your master against:
| Exception Code | Name | What to Test |
|---|---|---|
| 01 (0x01) | Illegal Function | Master sends unsupported function code |
| 02 (0x02) | Illegal Data Address | Master reads register address beyond slave's range |
| 03 (0x03) | Illegal Data Value | Master writes value outside valid range |
| 04 (0x04) | Slave Device Failure | Slave internal error — test your master's retry logic |
| No response | Timeout | Test master timeout and retry behavior |
ModbusSimulator lets you configure a slave to return any exception code for any request — useful for verifying that your SCADA raises the correct alarm and that your Modbus master handles errors gracefully without crashing or hanging.
Download ModbusSimulator and have a Modbus TCP or RTU slave running before your coffee is ready. Free 30-day trial — no credit card required.
Download Free TrialUse dedicated simulator software (ModbusSimulator — fastest, GUI-based), Python pymodbus (flexible, code-based, free), Arduino (real serial, cheap hardware), Raspberry Pi (persistent network slave), or a virtual PLC (CODESYS/OpenPLC for full logic simulation). For most engineers testing SCADA or HMI, a dedicated Windows simulator is the fastest option.
ModbusSimulator.com offers a free 30-day trial with all features. Python pymodbus is permanently free but requires coding. For permanently free GUI-based simulation, options are limited — most commercial simulators require a licence.
Yes. Modbus TCP runs entirely over standard TCP/IP. Run the slave simulator on your PC and connect your master to localhost (127.0.0.1) port 502. No hardware needed — works on a single machine or over a LAN.
Use virtual COM port software: com0com on Windows (free) or socat on Linux. This creates linked virtual serial ports — slave listens on COM10, master connects to COM11. No physical hardware required.
pymodbus is the most widely used and actively maintained library. Use StartAsyncTcpServer() for TCP and StartAsyncSerialServer() for RTU. For simpler needs, the umodbus library is lighter but less feature-rich.
ModbusSimulator supports up to 247 slaves simultaneously (the full Modbus unit ID range, 1-247). Python pymodbus can handle multiple unit IDs on a single server instance. Arduino handles one slave per device. Raspberry Pi can run multiple Python server instances.