PyModbus
PyModbusDocs

Modbus Protocol Basics

Learn Modbus protocol fundamentals - function codes, data models, addressing, master/slave architecture.

Modbus Protocol Basics

Modbus is a serial communication protocol from 1979. Still widely used because it's simple and reliable.

Master/Slave Architecture

[Master/Client] ---request---> [Slave/Server]
                <--response---
  • Master (Client): Initiates requests
  • Slave (Server): Responds to requests
  • One master can talk to 247 slaves
  • Slaves have unique IDs (1-247)

Data Model

Modbus has 4 data types:

TypeAccessSizeAddress RangeCommon Use
CoilsRead/Write1 bit00001-09999Outputs, relays
Discrete InputsRead only1 bit10001-19999Switches, sensors
Input RegistersRead only16 bit30001-39999Measurements
Holding RegistersRead/Write16 bit40001-49999Configuration

PyModbus uses 0-based addressing!

  • Coil 00001 = address 0
  • Register 40001 = address 0
  • Register 40100 = address 99

Function Codes

Main function codes you'll use:

# Reading
0x01 - Read Coils
0x02 - Read Discrete Inputs  
0x03 - Read Holding Registers
0x04 - Read Input Registers

# Writing
0x05 - Write Single Coil
0x06 - Write Single Register
0x0F - Write Multiple Coils
0x10 - Write Multiple Registers

PyModbus Implementation

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.1.100')

# Function code 0x03 - Read Holding Registers
result = client.read_holding_registers(address=0, count=10, slave=1)

# Function code 0x01 - Read Coils
result = client.read_coils(address=0, count=8, slave=1)

# Function code 0x06 - Write Single Register
client.write_register(address=0, value=123, slave=1)

# Function code 0x05 - Write Single Coil
client.write_coil(address=0, value=True, slave=1)

Frame Formats

Modbus TCP Frame

[Transaction ID][Protocol ID][Length][Unit ID][Function Code][Data]
     2 bytes       2 bytes    2 bytes  1 byte     1 byte      n bytes

Modbus RTU Frame

[Slave Address][Function Code][Data][CRC]
    1 byte         1 byte     n bytes 2 bytes

Modbus ASCII Frame

[:][Slave Address][Function Code][Data][LRC][CR][LF]
 1      2 chars       2 chars    n chars 2   1   1

Exception Codes

When something goes wrong:

result = client.read_holding_registers(9999, 10)
if result.isError():
    print(f"Exception code: {result.exception_code}")
CodeMeaningFix
01Illegal FunctionDevice doesn't support this function
02Illegal Data AddressAddress doesn't exist
03Illegal Data ValueValue out of range
04Slave Device FailureDevice error
05AcknowledgeRequest accepted, processing
06Slave Device BusyTry again later

Address Mapping Example

Device manual says "Temperature at register 40101"?

# WRONG - This won't work
result = client.read_holding_registers(40101, 1)

# RIGHT - Subtract 40001
result = client.read_holding_registers(100, 1)  # 40101 - 40001 = 100

Timing and Limits

Modbus TCP

  • Default timeout: 3 seconds
  • Max registers per read: 125
  • Max coils per read: 2000

Modbus RTU

  • Inter-frame delay: 3.5 character times
  • Response timeout: depends on baud rate
  • Max frame size: 256 bytes
# Set custom timeout
client = ModbusTcpClient('192.168.1.100', timeout=10)

# RTU timing depends on baudrate
client = ModbusSerialClient(
    '/dev/ttyUSB0',
    baudrate=9600,  # Slower = longer delays
    timeout=5
)

Endianness (Byte Order)

Different devices use different byte orders:

from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

# Read 2 registers for a 32-bit float
result = client.read_holding_registers(0, 2)

# Try different combinations if value looks wrong
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,    # or Endian.LITTLE
    wordorder=Endian.BIG     # or Endian.LITTLE
)
value = decoder.decode_32bit_float()

Practical Tips

  1. Always check connections first

    if not client.connect():
        print("Can't connect")
        exit()
  2. Always check for errors

    result = client.read_holding_registers(0, 10)
    if result.isError():
        print(f"Error: {result}")
        exit()
  3. Close connections

    try:
        # Your code
        pass
    finally:
        client.close()
  4. Use context managers

    with ModbusTcpClient('192.168.1.100') as client:
        result = client.read_holding_registers(0, 10)

Next Steps

How is this guide?