Modbus RTU
Use PySerial with pymodbus to read registers, write coils, and communicate with Modbus RTU devices over RS-485.
Read and write Modbus RTU registers over serial using pymodbus, the standard Python library for Modbus communication.
Read Holding Registers with pymodbus
Install pymodbus (pip install pymodbus), then read registers from a Modbus slave:
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(
port='/dev/ttyUSB0',
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1,
)
client.connect()
# Read 3 holding registers starting at address 0 from slave 1
result = client.read_holding_registers(address=0, count=3, slave=1)
if not result.isError():
print(f"Registers: {result.registers}")
else:
print(f"Error: {result}")
client.close()Log Modbus test results automatically
TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
Write Registers and Coils
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600, timeout=1)
client.connect()
# Write value 1234 to holding register 0 on slave 1
result = client.write_register(address=0, value=1234, slave=1)
if not result.isError():
print("Register written")
else:
print(f"Error: {result}")
client.close()from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600, timeout=1)
client.connect()
# Turn on coil 0 on slave 1
client.write_coil(address=0, value=True, slave=1)
# Read coils back
result = client.read_coils(address=0, count=8, slave=1)
if not result.isError():
print(f"Coils: {result.bits[:8]}")
client.close()from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600, timeout=1)
client.connect()
# Write 3 registers starting at address 0 on slave 1
values = [100, 200, 300]
result = client.write_registers(address=0, values=values, slave=1)
if not result.isError():
print(f"Wrote {len(values)} registers")
client.close()Read All Register Types
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600, timeout=1)
client.connect()
slave = 1
# Coils (read/write discrete outputs)
coils = client.read_coils(address=0, count=8, slave=slave)
if not coils.isError():
print(f"Coils: {coils.bits[:8]}")
# Discrete inputs (read-only)
inputs = client.read_discrete_inputs(address=0, count=8, slave=slave)
if not inputs.isError():
print(f"Discrete inputs: {inputs.bits[:8]}")
# Input registers (read-only, 16-bit)
input_regs = client.read_input_registers(address=0, count=3, slave=slave)
if not input_regs.isError():
print(f"Input registers: {input_regs.registers}")
# Holding registers (read/write, 16-bit)
holding = client.read_holding_registers(address=0, count=3, slave=slave)
if not holding.isError():
print(f"Holding registers: {holding.registers}")
client.close()Modbus RTU Frame Format
Every Modbus RTU frame follows this structure:
| Field | Size | Description |
|---|---|---|
| Slave ID | 1 byte | Device address (1-247) |
| Function Code | 1 byte | Operation to perform |
| Data | 0-252 bytes | Request/response payload |
| CRC-16 | 2 bytes | Error check (little-endian) |
Function codes:
| Code | Name | Access | Description |
|---|---|---|---|
| 0x01 | Read Coils | R | 1-2000 discrete outputs |
| 0x02 | Read Discrete Inputs | R | 1-2000 discrete inputs |
| 0x03 | Read Holding Registers | R | 1-125 registers (16-bit) |
| 0x04 | Read Input Registers | R | 1-125 registers (16-bit) |
| 0x05 | Write Single Coil | W | ON (0xFF00) or OFF (0x0000) |
| 0x06 | Write Single Register | W | One 16-bit register |
| 0x0F | Write Multiple Coils | W | 1-1968 coils |
| 0x10 | Write Multiple Registers | W | 1-123 registers |
Modbus data model:
| Object Type | Access | Address Range | Function Codes |
|---|---|---|---|
| Coils | Read/Write | 00001-09999 | 01, 05, 15 |
| Discrete Inputs | Read Only | 10001-19999 | 02 |
| Input Registers | Read Only | 30001-39999 | 04 |
| Holding Registers | Read/Write | 40001-49999 | 03, 06, 16 |
Protocol addresses are 0-based (subtract 1 from the Modbus address in device documentation).
CRC-16 Reference
If you need to compute or verify CRC manually (debugging, protocol analysis):
def modbus_crc16(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc
# Verify: read 3 holding registers from slave 1, starting at address 1
frame = bytes([0x01, 0x03, 0x00, 0x01, 0x00, 0x03])
crc = modbus_crc16(frame)
print(f"CRC: 0x{crc:04X}")
print(f"Frame: {(frame + crc.to_bytes(2, 'little')).hex().upper()}")RS-485 Serial Settings
Most Modbus RTU devices use these defaults:
| Parameter | Default | Notes |
|---|---|---|
| Baud rate | 9600 or 19200 | Must match all devices on the bus |
| Data bits | 8 | Always 8 for Modbus RTU |
| Parity | None or Even | Check device docs |
| Stop bits | 1 (None parity) or 1 (Even parity) | 2 stop bits if no parity |
| Timeout | 1s | Increase for slow devices |
| Inter-frame gap | 3.5 char times | Handled by pymodbus |
RS-485 adapters with automatic direction control (RTS) work out of the box with most USB-to-RS485 converters. If you're using a manual-direction adapter, you'll need to toggle RTS/DTR around writes. Check your adapter's datasheet.
Exception Codes
When a Modbus slave rejects a request, it returns an exception response (function code + 0x80):
| Code | Name | Common Cause |
|---|---|---|
| 0x01 | Illegal Function | Function code not supported by device |
| 0x02 | Illegal Data Address | Register address out of range |
| 0x03 | Illegal Data Value | Value outside allowed range |
| 0x04 | Slave Device Failure | Internal device error |
| 0x06 | Slave Device Busy | Device processing another request |