How to Simulate a Modbus Slave Without a PLC (2026)
Hardware procurement takes weeks. Site access requires permits. The PLC is sitting in a vendor's factory waiting for FAT. Meanwhile, your SCADA screens are built, your Modbus driver is configured, and you have nothing to test against.
This is the common situation that pushes engineers toward Modbus simulation. A software slave running on a Windows PC can stand in for any Modbus-capable device — PLC, energy meter, drive, remote I/O — and let you do real integration work before a single piece of hardware arrives. This guide walks through the complete setup, from a basic TCP slave to a realistic RTU device simulation with scaled register values and live data updates.
Why Simulate a Modbus Slave Without Hardware
The case for simulation is straightforward for anyone who has been on a commissioning schedule that slipped six weeks because the field devices weren't available.
Cost and Hardware Availability
Industrial PLCs and instruments are expensive. A mid-range Siemens or Allen-Bradley PLC with a Modbus TCP module costs anywhere from $800 to several thousand dollars. Prototype quantities of meters and sensors often have long lead times. A software Modbus slave costs nothing beyond the simulator license and runs on hardware you already have.
For training environments, test labs, and CI/CD pipelines for SCADA software, replacing physical hardware with software simulation eliminates both the capital cost and the logistics of setting up and maintaining physical test benches.
Early-Stage Integration Testing
The most expensive Modbus bugs are the ones discovered during commissioning. A register map error that would take 15 minutes to find in the office takes a full day at the site, with downtime, travel, and change management overhead. Running your Modbus driver against a simulator before the hardware arrives lets you validate:
- Register addresses and function codes are correct
- Byte order (big-endian vs. little-endian) is handled properly
- Scaling and engineering unit conversion logic is correct
- Polling rates don't cause timeout errors under load
- Error handling works correctly when a device goes offline or returns exception codes
Regression Testing and CI Integration
Once your system is in production, software updates to your SCADA or control software need to be validated against real Modbus behavior. A simulator with known register values provides a stable, repeatable test fixture. You can run the same test cases before and after every release without touching production hardware.
What You Need
For Modbus TCP simulation, the requirements are minimal:
- A Windows PC (Windows 10 or 11, 64-bit)
- ModbusSimulator installed and licensed (or running on the 30-day free trial)
- No additional hardware required — the simulator listens on the loopback interface or your Ethernet adapter
For Modbus RTU simulation over virtual serial ports:
- The same Windows PC with ModbusSimulator
- com0com — a free, open-source virtual COM port driver
- No physical RS-485 adapters or cables needed
If your workflow requires testing RTU communication over actual RS-485 hardware (for example, to validate a USB-to-RS485 adapter or a physical bus configuration), you will also need a USB-to-RS485 adapter and the corresponding cable.
Part 1: Setting Up a Modbus TCP Slave
This is the fastest path to a working Modbus simulation. From install to first register read takes under five minutes.
Step 1: Install ModbusSimulator
Download the installer from /download and run it. The installation is a standard Windows setup wizard with no special configuration required. After installation, launch ModbusSimulator from the Start menu or desktop shortcut.
Step 2: Select Slave Mode and TCP Transport
On the main toolbar, click the Mode selector and choose Slave. In the transport panel immediately below, select Modbus TCP. The configuration panel on the right side of the window will update to show TCP-specific settings.
In the TCP configuration panel, configure the following settings exactly:
- Port: 502 (the IANA-standard Modbus TCP port; change this only if your master software is configured for a different port)
- Bind Address: 0.0.0.0 (listen on all network interfaces, including loopback; use 127.0.0.1 if you want to restrict connections to the local machine only)
- Unit ID: 1 (this is the Modbus slave address; most masters default to Unit ID 1 for TCP, but set this to match whatever your master is configured to request)
- Max Connections: 4 (sufficient for most test scenarios; increase if you have multiple masters connecting simultaneously)
Step 3: Define the Register Table
Switch to the Register Table tab. ModbusSimulator maintains four independent register spaces matching the Modbus data model:
- Coils (0x) — read/write single-bit outputs, function codes 01/05/15
- Discrete Inputs (1x) — read-only single-bit inputs, function code 02
- Input Registers (3x) — read-only 16-bit registers, function code 04
- Holding Registers (4x) — read/write 16-bit registers, function codes 03/06/16
Select the Holding Registers tab. Click into the Address column and enter values for the registers your master will read. As a starting point, enter the following in addresses 0 through 4 to simulate a generic sensor device:
| Address (0-based) | Value | Description |
|---|---|---|
| 0 | 2300 | Voltage (scaled ×0.1, represents 230.0 V) |
| 1 | 500 | Frequency (scaled ×0.1, represents 50.0 Hz) |
| 2 | 3450 | Active Power (1 W per count, represents 3450 W) |
| 3 | 1000 | Current (scaled ×0.01, represents 10.00 A) |
| 4 | 850 | Power Factor (scaled ×0.001, represents 0.850) |
Values can be entered directly in the table cells. Use the Fill Range button to quickly populate a consecutive block of registers with a default value, which is useful when you need to define a large register map that your master expects to exist.
Step 4: Start the Slave
Click the green Start button in the toolbar. The status bar at the bottom of the window will show Listening on 0.0.0.0:502 and the connection count will read 0 / 4. The slave is now active and waiting for incoming connections.
If the status bar shows an error such as "Address already in use," another application is already listening on port 502. Run netstat -an | findstr :502 in a command prompt to identify the conflicting process. You can either stop that process or configure ModbusSimulator to use an alternate port such as 1502.
Step 5: Verify with a Quick Test
Before connecting your actual master software, verify the slave is responding correctly. Open a command prompt and run the following Python snippet (requires the pymodbus library; install with pip install pymodbus):
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient(host='127.0.0.1', port=502)
client.connect()
# Read 5 holding registers starting at address 0 from slave ID 1
result = client.read_holding_registers(address=0, count=5, slave=1)
if result.isError():
print(f"Error: {result}")
else:
regs = result.registers
print(f"Voltage: {regs[0] * 0.1:.1f} V")
print(f"Frequency: {regs[1] * 0.1:.1f} Hz")
print(f"Active Power: {regs[2]} W")
print(f"Current: {regs[3] * 0.01:.2f} A")
print(f"Power Factor: {regs[4] * 0.001:.3f}")
client.close()
Expected output with the register values from Step 3:
Voltage: 230.0 V
Frequency: 50.0 Hz
Active Power: 3450 W
Current: 10.00 A
Power Factor: 0.850
If you see this output, the TCP slave is working correctly. You can now point your actual master software — SCADA, HMI, PLC programming tool, or data logger — at 127.0.0.1:502 and it will receive the same data.
Part 2: Setting Up a Modbus RTU Slave via Virtual COM Port
If your master software or the device you are eventually connecting to uses Modbus RTU over serial, you need serial ports on both ends. com0com creates a linked pair of virtual COM ports that behave exactly like a physical null-modem serial connection.
Step 1: Install com0com
Download com0com from its SourceForge page. The installer requires administrator privileges because it installs a kernel-mode driver. During installation, accept the unsigned driver warning if prompted (this is a known issue with older com0com builds on Windows 10/11; the driver is safe and widely used).
After installation, the com0com Setup utility opens automatically. If it doesn't, find it in Start → com0com → Setup.
Step 2: Create a Virtual COM Port Pair
In the com0com Setup utility, you will see two default ports (CNCA0 and CNCB0) already paired. These appear in Windows Device Manager as COM3 and COM4 (or similar numbers; the exact numbers depend on what's already installed). For clarity in this guide, we will rename them to COM10 and COM11.
To assign specific port numbers:
- In the com0com Setup utility, click on CNCA0 and change the Port Name field to
COM10 - Click on CNCB0 and change the Port Name field to
COM11 - Click Apply to save the changes
- Open Device Manager → Ports (COM & LPT) and confirm both COM10 and COM11 appear
COM10 and COM11 are now a virtual null-modem pair. Any data written to COM10 is instantly readable on COM11, and vice versa. ModbusSimulator will use COM10 as the slave port; your master software will use COM11.
Step 3: Configure ModbusSimulator for RTU on COM10
In ModbusSimulator, with Slave mode selected, change the transport to Modbus RTU. The serial configuration panel will appear. Set the following parameters:
- COM Port: COM10
- Baud Rate: 9600 (match this to whatever your master will use; 9600 is a safe starting point)
- Data Bits: 8
- Parity: None
- Stop Bits: 1
- Slave ID: 1
The serial parameters on the master side must be identical. 9600, 8, None, 1 (commonly written as 9600 8N1) is the most widely used Modbus RTU default and a safe baseline for any device you haven't confirmed the settings for.
Step 4: Configure the Register Table and Start
Define your register values using the same Register Table interface described in the TCP setup. The register data model is identical between TCP and RTU — the transport changes, but the register addresses, function codes, and data format are the same.
Click Start. The status bar will show RTU Slave active on COM10 @ 9600 8N1. Your master software can now open COM11 with the same serial settings and send Modbus RTU requests to slave ID 1.
Step 5: Verify RTU Communication
Using the same Python verification approach, but with the Modbus RTU client:
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(
port='COM11',
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
client.connect()
result = client.read_holding_registers(address=0, count=5, slave=1)
if result.isError():
print(f"Error: {result}")
else:
regs = result.registers
print(f"Voltage: {regs[0] * 0.1:.1f} V")
print(f"Frequency: {regs[1] * 0.1:.1f} Hz")
print(f"Active Power: {regs[2]} W")
print(f"Current: {regs[3] * 0.01:.2f} A")
print(f"Power Factor: {regs[4] * 0.001:.3f}")
client.close()
If this returns the same values as the TCP test, your virtual COM port pair is working correctly and the RTU slave is responding as expected.
Part 3: Setting Realistic Register Values
A simulation that returns static zeros isn't useful for testing. Real-world Modbus devices use integer registers to represent floating-point physical values through a fixed scaling convention. Getting this right is what separates a simulation that catches real bugs from one that only confirms connectivity.
Understanding Integer Scaling in Modbus
Modbus holding registers are 16-bit unsigned integers (range 0 to 65535, or signed -32768 to 32767 in two's complement). Real-world physical measurements like voltage, current, temperature, and power are floating-point values with decimal places. Devices encode these as integers using a scale factor defined in their register map documentation.
The general formula is:
register_value = physical_value / scale_factor
physical_value = register_value * scale_factor
Energy Meter Example: Full Register Map
Consider a three-phase energy meter with the following register map (based on common DIN-rail meters such as the Eastron SDM630 or similar):
| Address | Parameter | Scale Factor | Unit | Simulated Value | Physical Value |
|---|---|---|---|---|---|
| 0 | Phase 1 Voltage | 0.1 | V | 2298 | 229.8 V |
| 1 | Phase 2 Voltage | 0.1 | V | 2301 | 230.1 V |
| 2 | Phase 3 Voltage | 0.1 | V | 2299 | 229.9 V |
| 3 | Phase 1 Current | 0.01 | A | 1523 | 15.23 A |
| 4 | Phase 2 Current | 0.01 | A | 1498 | 14.98 A |
| 5 | Phase 3 Current | 0.01 | A | 1511 | 15.11 A |
| 6 | Frequency | 0.1 | Hz | 500 | 50.0 Hz |
| 7 | Total Active Power | 1 | W | 10450 | 10450 W (10.45 kW) |
| 8 | Power Factor | 0.001 | — | 872 | 0.872 |
| 9 | Total Active Energy | 0.01 | kWh | 154320 | 1543.20 kWh |
Enter these values into the holding register table. When your SCADA system reads these registers and applies the correct scale factors in its tag configuration, it will display realistic engineering values that look like data from a real meter running at roughly 10 kW load.
Handling 32-Bit Values
Some devices store 32-bit values (long integers or single-precision floats) across two consecutive 16-bit Modbus registers. The byte order varies by manufacturer — some store the high word first (big-endian), others store the low word first (little-endian). The energy accumulator register (address 9 in the example above) is often a 32-bit value.
When simulating a device that uses 32-bit registers, confirm the byte order from the device's register map documentation and set both registers accordingly. For example, the value 154320 decimal as a 32-bit big-endian value splits into high word 0x0002 = 2 and low word 0x5AD0 = 23248. Set address 9 = 2 and address 10 = 23248.
For detailed guidance on register mapping and function codes, see the Modbus Slave Simulator Guide and ModbusSimulator product page.
Part 4: Using Auto-Update to Simulate Live Sensor Data
Static register values verify that your master can read the correct addresses with the correct function codes. But they don't test the part of your system that processes changing data — trend charts, alarm hysteresis logic, rate-of-change calculations, and data historian behavior.
ModbusSimulator's auto-update feature changes register values automatically on a configurable interval, simulating the natural variation of a live sensor or process variable.
Configuring Auto-Update
In the Register Table tab, right-click on any register row and select Configure Auto-Update. The auto-update dialog offers the following modes:
- Increment/Decrement: The register value increases or decreases by a fixed step on each update cycle. Useful for simulating running totals like energy accumulators or counters.
- Random within Range: The register value is set to a random integer between a configured minimum and maximum on each update cycle. Useful for simulating a noisy analog sensor.
- Sine Wave: The register value follows a sine function between a minimum and maximum value, completing one full cycle over a configurable period. Useful for simulating load cycles or oscillating process variables.
- Ramp: The register value increases linearly from a minimum to a maximum, then resets to the minimum and repeats. Useful for simulating fill tanks, startup sequences, or ramp-up processes.
Practical Auto-Update Configuration for the Energy Meter Example
For the energy meter simulation, apply the following auto-update settings to make it behave like a live load:
- Phase 1 Voltage (address 0): Random within range, min 2250 (225.0 V), max 2350 (235.0 V), update interval 5000 ms. This simulates normal grid voltage variation.
- Frequency (address 6): Random within range, min 498 (49.8 Hz), max 502 (50.2 Hz), update interval 10000 ms. Grid frequency variation around 50 Hz.
- Total Active Power (address 7): Sine wave, min 6000 (6 kW), max 15000 (15 kW), period 60000 ms (1 minute). Simulates a cyclic load.
- Total Active Energy (address 9): Increment by 3 every 1000 ms. At the 0.01 kWh scale factor, this adds 0.03 kWh per second — consistent with a roughly 10 kW average load (10 kW = 0.00278 kWh/s ≈ 0.03 kWh per 10-second block).
With these settings running, your SCADA historian will record realistic data, alarm thresholds will trigger and clear as values fluctuate, and trend displays will show meaningful waveforms instead of flat lines.
Part 5: Connecting SCADA and HMI Software to the Simulator
Ignition SCADA Configuration
Ignition by Inductive Automation is one of the most widely used SCADA platforms and includes a native Modbus TCP driver. To connect to a ModbusSimulator slave running on the same machine:
- Open the Ignition Gateway web interface (default:
http://localhost:8088) - Navigate to Config → OPC-UA Server → Device Connections
- Click Add Device and select Modbus TCP from the driver list
- Set Device Name to a descriptive name such as
EnergyMeter_Sim - Set Hostname to
127.0.0.1(or the IP address of the machine running ModbusSimulator if it's on a different PC) - Set Port to
502 - Set Unit ID to
1(must match the slave ID in ModbusSimulator) - Leave all other settings at defaults and click Save Changes
After saving, the device status in the Device Connections list should show a green indicator and the status Connected within a few seconds. If it shows Disconnected, verify that ModbusSimulator is running and the slave is started, and check that no firewall is blocking port 502 on the loopback interface.
To create tags in Ignition Designer, use the Modbus addressing syntax. For holding registers, the address format is [DeviceName]HR0 for address 0 (0-based) or [DeviceName]4:0 using the legacy 5-digit format. Refer to the Ignition Modbus driver documentation for the full addressing reference.
Generic Modbus TCP Configuration (Any SCADA/HMI)
Most Modbus TCP drivers require the same four parameters regardless of platform:
| Parameter | Value for Local Simulation | Notes |
|---|---|---|
| IP Address / Hostname | 127.0.0.1 | Use the PC's LAN IP to connect from another machine |
| Port | 502 | Standard Modbus TCP port; change if configured otherwise |
| Unit ID / Slave ID | 1 | Must match the Unit ID in ModbusSimulator |
| Timeout | 1000 ms | Local loopback responds in <1 ms; 1 s timeout gives headroom |
For a complete walkthrough of Modbus TCP driver configuration, including register addressing conventions and byte order settings, see the Modbus TCP/IP Setup Guide.
WinCC, Wonderware, and Other HMI Platforms
The same principle applies across all HMI platforms that support Modbus TCP. The specific menu paths differ, but the underlying configuration parameters are the same: IP, port, slave ID, and register addresses. If your HMI platform has a Modbus TCP driver and supports connecting to a user-specified IP address, it will connect to ModbusSimulator without any issues.
Part 6: Simulating Exception Responses to Test Error Handling
Well-written SCADA and control software handles Modbus errors gracefully — it distinguishes between a communications timeout, a device offline condition, and a Modbus exception response from a live device. Testing these error paths requires deliberately triggering them, which is difficult to do reliably with real hardware but trivial with a simulator.
Standard Modbus Exception Codes
The Modbus specification defines the following exception codes that a slave can return in response to a valid request:
| Exception Code | Name | Meaning |
|---|---|---|
| 01 | Illegal Function | The function code in the request is not supported by the slave |
| 02 | Illegal Data Address | The register address or address range in the request does not exist in the slave |
| 03 | Illegal Data Value | The value in the request is outside the acceptable range for the addressed register |
| 04 | Slave Device Failure | The slave encountered an unrecoverable error while processing the request |
| 05 | Acknowledge | The slave accepted the request but needs time to process it (long-duration commands) |
| 06 | Slave Device Busy | The slave is currently busy processing a previous long-duration command |
Triggering Exception Code 02 (Illegal Data Address)
The simplest way to trigger exception code 02 is to have your master request a register address that has not been defined in ModbusSimulator's register table. By default, any address that has not been explicitly populated will return exception code 02 when queried.
To test this: leave the register table empty above address 10, then configure your master to read starting at address 50. ModbusSimulator will return an exception response with code 02. Your master software should log this and display an appropriate error condition rather than crashing or silently returning a zero value.
Triggering Exception Codes on Demand
In ModbusSimulator's Exception Configuration panel (accessible from the Settings menu), you can define exception rules by address range and function code. For example:
- Set addresses 100–109 to return exception code 04 (Slave Device Failure) on any read — simulates a failed sensor block
- Set addresses 200–209 to return exception code 01 (Illegal Function) on any write — simulates a read-only register block with write protection
- Set a global exception for function code 16 (Write Multiple Registers) to return exception code 06 (Slave Device Busy) — simulates a device that is processing a previous write command
Simulating Device Offline Conditions
Beyond exception codes, you should also test how your master and SCADA system handle a complete loss of communication. Click the Stop button in ModbusSimulator while your master is actively polling. The master will begin receiving connection refused or timeout errors. Verify that your system:
- Raises a communications alarm within the expected time window
- Marks affected tags as stale or in fault state
- Does not trigger false process alarms on the stale values
- Reconnects automatically when the slave is started again
These failure modes are extremely common in field deployments — network outages, device restarts, and planned maintenance shutdowns all cause communication interruptions. Testing them in the lab against the simulator means fewer unpleasant surprises during commissioning and operation.
Complete Python Test Script
The following script provides a more complete test harness that exercises multiple register types, verifies exception handling, and reports results in a format suitable for inclusion in a commissioning acceptance test document:
"""
Modbus slave acceptance test script.
Target: ModbusSimulator running as TCP slave on 127.0.0.1:502, Slave ID 1.
Requires: pip install pymodbus
"""
from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException
HOST = '127.0.0.1'
PORT = 502
SLAVE_ID = 1
SCALE = {
0: ('Phase Voltage', 0.1, 'V'),
6: ('Frequency', 0.1, 'Hz'),
7: ('Active Power', 1, 'W'),
8: ('Power Factor', 0.001, ''),
}
def run_tests():
client = ModbusTcpClient(host=HOST, port=PORT, timeout=2)
if not client.connect():
print(f"FAIL: Could not connect to {HOST}:{PORT}")
return
print(f"PASS: TCP connection established to {HOST}:{PORT}")
# Test 1: Read holding registers (FC03)
result = client.read_holding_registers(address=0, count=9, slave=SLAVE_ID)
if result.isError():
print(f"FAIL: FC03 read failed: {result}")
else:
print("PASS: FC03 Read Holding Registers")
for addr, (name, scale, unit) in SCALE.items():
val = result.registers[addr] * scale
print(f" {name}: {val:.3g} {unit} (raw={result.registers[addr]})")
# Test 2: Write a single holding register (FC06)
write_result = client.write_register(address=0, value=2350, slave=SLAVE_ID)
if write_result.isError():
print(f"FAIL: FC06 write failed: {write_result}")
else:
# Verify the write took effect
read_back = client.read_holding_registers(address=0, count=1, slave=SLAVE_ID)
if not read_back.isError() and read_back.registers[0] == 2350:
print("PASS: FC06 Write Single Register (verified read-back: 2350 = 235.0 V)")
else:
print("FAIL: FC06 write did not persist on read-back")
# Test 3: Exception response on undefined address (FC03, high address)
exc_result = client.read_holding_registers(address=999, count=1, slave=SLAVE_ID)
if exc_result.isError():
print(f"PASS: FC03 on undefined address returned exception (expected): {exc_result}")
else:
print("WARN: FC03 on undefined address 999 returned a value — define exception handling")
client.close()
print("Test complete.")
if __name__ == '__main__':
run_tests()
Frequently Asked Questions
Can I simulate a Modbus slave without any hardware?
Yes. For Modbus TCP, a standard Windows PC running ModbusSimulator is all you need — no additional hardware whatsoever. The simulator listens on the loopback interface, so both master and slave run on the same machine. For Modbus RTU without physical serial hardware, install com0com to create a virtual COM port pair. The simulator occupies one port; your master occupies the other. The entire setup is software-only.
What is the difference between simulating a TCP slave and an RTU slave?
Modbus TCP uses IP networking; any master on the same network connects by IP address and port 502. Modbus RTU uses serial communication (RS-232 or RS-485). For software-only RTU simulation, com0com creates a linked virtual port pair (e.g., COM10 and COM11) that functions like a physical null-modem cable. The register data model, function codes, and addressing are identical between TCP and RTU — only the transport layer differs.
What is com0com and why is it needed for RTU simulation?
com0com is a free Windows kernel driver that creates pairs of virtual serial ports linked to each other. It is needed because Modbus RTU requires a serial port on both the slave and master sides. Without physical RS-232 or RS-485 hardware, com0com provides the software-equivalent of a null-modem connection between ModbusSimulator and your master software, enabling fully software-based RTU testing.
How do I set realistic register values for an energy meter?
Energy meters store physical values as integers with a documented scale factor. Voltage at scale 0.1 means register value 2300 represents 230.0 V. Frequency at scale 0.1 means register value 500 represents 50.0 Hz. Power at scale 1 means register value 10450 represents 10450 W. Always consult the specific device's register map for the correct scale factors — they vary by manufacturer and model. Enter the computed integer values directly into ModbusSimulator's register table.
How do I connect Ignition SCADA to the simulated slave?
In the Ignition Gateway, add a Modbus TCP device connection. Set Hostname to 127.0.0.1, Port to 502, and Unit ID to 1. Save the connection. The device status will show Connected within seconds. In Ignition Designer, create tags using the Modbus addressing syntax for your register type: [DeviceName]HR0 for holding register 0, [DeviceName]IR0 for input register 0, and so on. Any SCADA platform with a standard Modbus TCP driver follows the same four-parameter setup.
Can ModbusSimulator return exception responses to test error handling?
Yes. Any register address not defined in the register table automatically returns exception code 02 (Illegal Data Address). For explicit exception testing, use the Exception Configuration panel in ModbusSimulator to assign specific exception codes to address ranges or function codes. You can simulate exception code 04 (device failure), exception code 01 (unsupported function), and exception code 06 (device busy) on demand, and you can stop the slave entirely to test timeout and communications loss scenarios.
Start Simulating Your Modbus Slave Today
ModbusSimulator runs as a fully functional Modbus TCP or RTU slave on any Windows PC. Configure registers, set realistic values, enable auto-update, and connect your SCADA, HMI, or master script — no hardware required. Free 30-day trial, one-time lifetime license.
Download Free Trial