PySerial
PySerialDocs

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:

FieldSizeDescription
Slave ID1 byteDevice address (1-247)
Function Code1 byteOperation to perform
Data0-252 bytesRequest/response payload
CRC-162 bytesError check (little-endian)

Function codes:

CodeNameAccessDescription
0x01Read CoilsR1-2000 discrete outputs
0x02Read Discrete InputsR1-2000 discrete inputs
0x03Read Holding RegistersR1-125 registers (16-bit)
0x04Read Input RegistersR1-125 registers (16-bit)
0x05Write Single CoilWON (0xFF00) or OFF (0x0000)
0x06Write Single RegisterWOne 16-bit register
0x0FWrite Multiple CoilsW1-1968 coils
0x10Write Multiple RegistersW1-123 registers

Modbus data model:

Object TypeAccessAddress RangeFunction Codes
CoilsRead/Write00001-0999901, 05, 15
Discrete InputsRead Only10001-1999902
Input RegistersRead Only30001-3999904
Holding RegistersRead/Write40001-4999903, 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:

ParameterDefaultNotes
Baud rate9600 or 19200Must match all devices on the bus
Data bits8Always 8 for Modbus RTU
ParityNone or EvenCheck device docs
Stop bits1 (None parity) or 1 (Even parity)2 stop bits if no parity
Timeout1sIncrease for slow devices
Inter-frame gap3.5 char timesHandled 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):

CodeNameCommon Cause
0x01Illegal FunctionFunction code not supported by device
0x02Illegal Data AddressRegister address out of range
0x03Illegal Data ValueValue outside allowed range
0x04Slave Device FailureInternal device error
0x06Slave Device BusyDevice processing another request