Introduction to Modbus RTU

Modbus RTU (Remote Terminal Unit) is a serial communication protocol used in industrial automation systems to enable communication between devices over RS-232, RS-422, or RS-485 networks. It's a compact, binary representation of the Modbus protocol.

What is Modbus RTU?

Modbus RTU is a serial communication variant of the Modbus protocol developed by Modicon in 1979. RTU stands for "Remote Terminal Unit" and uses a compact binary format for data transmission over serial links.

Key Features of Modbus RTU

  • Binary Format: Compact data representation for efficient transmission
  • Serial Communication: Uses RS-232, RS-422, or RS-485 physical layers
  • Master-Slave Architecture: Clear communication hierarchy
  • CRC Error Checking: 16-bit CRC for data integrity
  • Multi-drop Networks: Up to 247 devices on one network

How Modbus RTU Works

Modbus RTU follows a master-slave model where:

  • Master: Initiates all communications and polls slaves
  • Slave: Responds only when addressed by master
  • Address Field: 1-247 for device addressing, 0 for broadcast
  • Silent Interval: 3.5 character times between messages

Modbus Master

Initiates communication

Modbus Slave

Responds when polled

Practical Example: Flow Meter Reading

PLC Controller
(Modbus Master - Address: 1)
Flow Monitor
Current Flow: 125.7 L/min
Connected
1
Request: Read Flow Rate
Address: 05, Function: 03, Register: 0
2
Response: Flow Data
Value: 1257 (125.7 L/min)
Flow Meter Device
(Modbus Slave - Address: 5)
💧 125.7 L/min
Register 40001: 1257
(Flow Rate × 10)
How it Works:
  1. Flow Measurement: The flow meter continuously measures flow rate (125.7 L/min)
  2. Data Storage: Value is stored in Holding Register 40001 as integer 1257 (flow × 10)
  3. Master Request: PLC Controller polls the flow meter using slave address 5
  4. Slave Response: Flow Meter responds with raw value 1257
  5. Data Processing: PLC divides by 10 to get 125.7 L/min
  6. Control Logic: PLC uses flow data for process control decisions

Modbus RTU API Reference

Function Codes

Modbus RTU uses the same function codes as other Modbus variants:

01 - Read Coils

Read status of discrete outputs (coils)

Request: Slave Address + Function Code + Starting Address + Quantity + CRC

02 - Read Discrete Inputs

Read status of discrete inputs

Request: Slave Address + Function Code + Starting Address + Quantity + CRC

03 - Read Holding Registers

Read values from holding registers

Request: Slave Address + Function Code + Starting Address + Quantity + CRC

04 - Read Input Registers

Read values from input registers

Request: Slave Address + Function Code + Starting Address + Quantity + CRC

05 - Write Single Coil

Write a single coil (discrete output)

Request: Slave Address + Function Code + Address + Value + CRC

06 - Write Single Register

Write a single holding register

Request: Slave Address + Function Code + Address + Value + CRC

RTU Message Format

Modbus RTU Frame Structure

Silent Interval
3.5 char times
Slave Address
1 byte
Function Code
1 byte
Data
N bytes
CRC
2 bytes
Silent Interval
3.5 char times

RTU-Specific Features

🔍 CRC Error Detection

16-bit Cyclic Redundancy Check for data integrity

  • Polynomial: 0xA001 (reverse of 0x8005)
  • Initial value: 0xFFFF
  • Calculated over address, function, and data fields

⏱️ Silent Intervals

Message framing using timing gaps

  • 3.5 character times before message start
  • 3.5 character times after message end
  • No gaps allowed within message frame

📡 Serial Parameters

Common serial communication settings

  • Baud rates: 9600, 19200, 38400, 57600, 115200
  • Data bits: 8 (standard)
  • Parity: Even, Odd, or None
  • Stop bits: 1 or 2

Modbus RTU Terminology

Understanding Modbus RTU terminology is essential for effective implementation. Here are the key concepts specific to serial Modbus communication.

RTU (Remote Terminal Unit)

Definition: Binary encoding format for Modbus messages over serial communications.

Key Characteristics:

  • Compact binary format (vs. ASCII format)
  • Higher data throughput than ASCII
  • Uses 8-bit binary representation
  • Industry standard for serial Modbus

Comparison: More efficient than Modbus ASCII but less human-readable

Example:

RTU: 01 03 00 00 00 01 84 0A
ASCII: :010300000001FA

Slave Address

Definition: 8-bit identifier for devices on the serial network.

Address Ranges:

  • 1-247: Valid slave device addresses
  • 0: Broadcast address (no response expected)
  • 248-255: Reserved addresses

Network Topology: All devices share the same serial bus

Example:

Address 01: Main PLC
Address 05: Flow Meter
Address 10: Temperature Sensor

CRC (Cyclic Redundancy Check)

Definition: 16-bit error detection mechanism for RTU messages.

Implementation:

  • Calculated over entire message except CRC field
  • Appended as last 2 bytes (low byte first)
  • Receiver recalculates and compares
  • Polynomial: 0xA001

Example:

Message: 01 03 00 00 00 01
CRC: 84 0A
Complete: 01 03 00 00 00 01 84 0A

Silent Interval

Definition: Timing gap used to frame RTU messages on serial networks.

Requirements:

  • 3.5 character times minimum
  • Required before and after each message
  • No gaps allowed within message
  • Timing depends on baud rate

Example:

At 9600 baud: 3.5 × (11 bits/char ÷ 9600 bps) = 4.0ms
At 115200 baud: minimum 1.75ms

RS-485 Network

Definition: Differential serial communication standard commonly used with Modbus RTU.

Advantages:

  • Multi-drop capability (up to 32 devices)
  • Long distance transmission (up to 4000 feet)
  • High noise immunity
  • Half-duplex communication

Example:

Two-wire RS-485:
A+ (non-inverting)
B- (inverting)
+ 120Ω termination resistors

Baud Rate

Definition: Data transmission speed in bits per second.

Common Rates:

  • 9600 bps: Most common, good for long distances
  • 19200 bps: Good balance of speed and reliability
  • 38400 bps and higher: Shorter distances only
  • All devices must use same baud rate

Example:

Network Settings:
Baud: 19200
Data: 8 bits
Parity: Even
Stop: 1 bit

Address Formats & Configuration Challenges

Traditional 5-Digit Addressing

Same as TCP version but with serial considerations:

  • 0xxxx: Coils (00001-09999)
  • 1xxxx: Discrete Inputs (10001-19999)
  • 3xxxx: Input Registers (30001-39999)
  • 4xxxx: Holding Registers (40001-49999)

RTU Protocol Data Unit (PDU) Addressing

Direct register addressing in RTU messages:

  • 0-based indexing: Address 0 = first register
  • Function code defines type: No prefix needed
  • Example: Traditional 40001 = PDU address 0

⚠️ Serial-Specific Challenges

Additional RTU Considerations:

  • Timing sensitivity: Silent intervals must be precise
  • Baud rate consistency: All devices must match
  • Termination resistors: Required for RS-485 networks
  • Cable length limits: Distance affects maximum baud rate
RTU Impact: Serial communication adds timing and electrical considerations beyond TCP!

RTU Data Format & Timing Challenges

⏱️ Timing Requirements

Critical Timing Constraints:

Inter-Character Timeout

Maximum gap between characters in a message

1.5 character times

Exceeding this breaks the message frame

Inter-Frame Delay

Minimum gap between message frames

3.5 character times

Required to detect message boundaries

Timing Calculations by Baud Rate
9600 baud: 1 char = 1.146ms, Silent = 4.0ms
19200 baud: 1 char = 0.573ms, Silent = 2.0ms
115200 baud: 1 char = 0.096ms, Silent = 1.75ms (min)

🔗 Serial Network Issues

Common Serial Communication Problems:

Electrical Interference

EMI can corrupt data on long cable runs

Solutions:
  • Use shielded twisted pair cables
  • Proper grounding techniques
  • Avoid running parallel to power cables
Ground Loops

Potential differences between devices

Solutions:
  • Use isolation transformers
  • Optical isolators in interfaces
  • Single point grounding strategy
Termination Problems

Improper RS-485 termination causes reflections

Solutions:
  • 120Ω resistors at both ends only
  • No termination on intermediate devices
  • Use proper RS-485 topology

⚠️ RTU Configuration Challenges

1. Serial Parameter Mismatch

Symptom: No communication or garbled data

Causes:

  • Different baud rates between devices
  • Mismatched parity settings
  • Wrong stop bit configuration

Solution: Verify all devices use identical serial parameters

2. Timing and Framing Errors

Symptom: Intermittent communication failures

Causes:

  • Incorrect silent interval implementation
  • Character gaps within messages
  • Master polling too fast

Solution: Implement proper timing according to baud rate

3. Network Wiring Issues

Symptom: Communication works with some devices but not others

Causes:

  • Improper RS-485 wiring topology
  • Missing or incorrect termination
  • Cable length exceeding limits

Solution: Follow proper RS-485 installation practices

RTU Configuration Best Practices

📋 Serial Parameter Documentation
  • Document all device serial settings
  • Use consistent parameters network-wide
  • Test with simple devices first
🔧 Proper Wiring Practices
  • Use daisy-chain topology for RS-485
  • Install termination resistors correctly
  • Maintain proper cable impedance
⏱️ Timing Implementation
  • Calculate silent intervals accurately
  • Implement proper character timeout detection
  • Allow adequate slave response time
🛠️ Testing and Debugging
  • Use serial analyzers for troubleshooting
  • Monitor CRC error rates
  • Test at maximum network loading

Quick Reference Table

Data Type Size Access Traditional Range Read Function Write Function
Coils 1 bit Read/Write 00001-09999 01 05, 15
Discrete Inputs 1 bit Read Only 10001-19999 02 N/A
Input Registers 16 bit Read Only 30001-39999 04 N/A
Holding Registers 16 bit Read/Write 40001-49999 03 06, 16

Practical Examples

Python RTU Client Example

from pymodbus.client.sync import ModbusSerialClient

# Create RTU client connection
client = ModbusSerialClient(
    method='rtu',
    port='/dev/ttyUSB0',  # Linux
    # port='COM3',        # Windows
    baudrate=19200,
    timeout=1,
    parity='E',
    stopbits=1,
    bytesize=8
)

# Connect to serial port
connection = client.connect()
if connection:
    print("Connected to Modbus RTU")
    
    # Read holding registers from slave address 5
    result = client.read_holding_registers(0, 10, unit=5)
    if result.isError():
        print("Error reading registers")
    else:
        print(f"Register values: {result.registers}")
    
    # Write single register to slave address 5
    client.write_register(0, 1234, unit=5)
    
    # Close connection
    client.close()
else:
    print("Failed to connect to serial port")

Node.js RTU Client Example

const ModbusRTU = require("modbus-serial");
const client = new ModbusRTU();

async function modbusRTUExample() {
    try {
        // Connect to RTU via serial port
        await client.connectRTUBuffered("/dev/ttyUSB0", {
            baudRate: 19200,
            dataBits: 8,
            stopBits: 1,
            parity: 'even'
        });
        
        // Set slave ID
        client.setID(5);
        
        console.log("Connected to Modbus RTU");
        
        // Read holding registers
        const data = await client.readHoldingRegisters(0, 10);
        console.log("Register values:", data.data);
        
        // Write single register
        await client.writeRegister(0, 5678);
        console.log("Register written successfully");
        
        client.close();
    } catch (error) {
        console.error("Modbus RTU error:", error);
    }
}

modbusRTUExample();

C# RTU Client Example

using EasyModbus;

class Program
{
    static void Main()
    {
        ModbusClient modbusClient = new ModbusClient();
        
        try
        {
            // Configure serial port
            modbusClient.SerialPort = "COM3";
            modbusClient.Baudrate = 19200;
            modbusClient.Parity = System.IO.Ports.Parity.Even;
            modbusClient.StopBits = System.IO.Ports.StopBits.One;
            modbusClient.UnitIdentifier = 5; // Slave address
            
            modbusClient.Connect();
            Console.WriteLine("Connected to Modbus RTU");
            
            // Read holding registers
            int[] readData = modbusClient.ReadHoldingRegisters(0, 10);
            Console.WriteLine($"First register value: {readData[0]}");
            
            // Write single register
            modbusClient.WriteSingleRegister(0, 9999);
            Console.WriteLine("Register written successfully");
            
            modbusClient.Disconnect();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Arduino RTU Slave Example

#include 
#include 

// Create Modbus RTU slave with ID 5
Modbus slave(5, Serial, 0); // ID, Serial Port, Control Pin

// Data array for Modbus registers
uint16_t au16data[16] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1
};

void setup() {
  // Configure serial communication
  Serial.begin(19200, SERIAL_8E1); // 19200 baud, 8 data, even parity, 1 stop
  
  // Start Modbus slave
  slave.begin(19200);
  
  // Set up some initial values
  au16data[0] = 1234;  // Holding register 40001
  au16data[1] = 5678;  // Holding register 40002
}

void loop() {
  // Update register values (simulate sensor readings)
  au16data[0] = analogRead(A0);  // Read analog input
  au16data[1] = digitalRead(2) ? 1 : 0; // Read digital input
  
  // Poll for Modbus messages
  slave.poll(au16data, 16);
  
  delay(100);
}

RTU Timing Examples

Silent Interval Calculation

# Calculate silent interval for different baud rates
def calculate_silent_interval(baud_rate):
    # Character time = (start + 8 data + parity + stop) / baud_rate
    # = 11 bits / baud_rate
    char_time = 11.0 / baud_rate
    
    # Silent interval = 3.5 character times
    silent_interval = 3.5 * char_time
    
    # Minimum 1.75ms for high baud rates
    return max(silent_interval, 0.00175)

# Examples
print(f"9600 baud: {calculate_silent_interval(9600)*1000:.1f}ms")
print(f"19200 baud: {calculate_silent_interval(19200)*1000:.1f}ms")
print(f"115200 baud: {calculate_silent_interval(115200)*1000:.1f}ms")

CRC Calculation Example

def calculate_crc(data):
    """Calculate Modbus RTU CRC-16"""
    crc = 0xFFFF
    
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
                
    return crc

# Example: Calculate CRC for read holding registers
message = [0x01, 0x03, 0x00, 0x00, 0x00, 0x01]
crc = calculate_crc(message)
print(f"Message: {' '.join(f'{b:02X}' for b in message)}")
print(f"CRC: {crc & 0xFF:02X} {(crc >> 8) & 0xFF:02X}")
# Output: CRC: 84 0A

Modbus RTU Master Development

Master Responsibilities

  • Initiate all communications on the serial network
  • Poll slave devices in sequence
  • Manage timing and framing requirements
  • Handle error detection and recovery

Implementation Steps

1

Configure Serial Port

Set up baud rate, parity, data bits, and stop bits to match all network devices

2

Implement Timing Control

Ensure proper silent intervals and character timeout detection for message framing

3

Format RTU Messages

Build binary messages with slave address, function code, data, and CRC checksum

4

Handle Responses

Validate CRC, parse response data, and implement error recovery strategies

RTU-Specific Best Practices

Serial Communication

  • Use hardware flow control when available
  • Implement proper RS-485 driver enable/disable
  • Buffer incoming data to handle timing variations

Timing Management

  • Calculate silent intervals based on actual baud rate
  • Use high-resolution timers for accurate timing
  • Allow adequate response time for slower slaves

Error Handling

  • Validate CRC on all received messages
  • Implement timeout and retry mechanisms
  • Log communication errors for network diagnostics

Network Management

  • Poll slaves in round-robin fashion
  • Adjust polling rate based on network performance
  • Implement slave health monitoring

Modbus RTU Slave Development

Slave Responsibilities

  • Monitor serial line for incoming messages
  • Respond only when addressed by master
  • Maintain local data model (registers, coils)
  • Implement proper timing and CRC validation

Data Model (Same as TCP)

Coils (Discrete Outputs)

1-bit read/write values, typically representing digital outputs

Address Range: 00001 - 09999

Discrete Inputs

1-bit read-only values, typically representing digital inputs

Address Range: 10001 - 19999

Input Registers

16-bit read-only values, typically representing analog inputs

Address Range: 30001 - 39999

Holding Registers

16-bit read/write values, typically representing analog outputs or configuration

Address Range: 40001 - 49999

Simple RTU Slave Example (Python)

from pymodbus.server.sync import StartSerialServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext

# Create data blocks
store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [0]*100),  # Discrete Inputs
    co=ModbusSequentialDataBlock(0, [0]*100),  # Coils
    hr=ModbusSequentialDataBlock(0, [0]*100),  # Holding Registers
    ir=ModbusSequentialDataBlock(0, [0]*100)   # Input Registers
)

context = ModbusServerContext(slaves={1: store}, single=False)

# Server identification
identity = ModbusDeviceIdentification()
identity.VendorName = 'pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl = 'http://github.com/bashwork/pymodbus/'
identity.ProductName = 'pymodbus RTU Server'
identity.ModelName = 'pymodbus RTU Server'
identity.MajorMinorRevision = '1.0'

# Start RTU server
print("Starting Modbus RTU Server on /dev/ttyUSB0...")
StartSerialServer(
    context, 
    identity=identity,
    port='/dev/ttyUSB0',  # Serial port
    baudrate=19200,
    bytesize=8,
    parity='E',
    stopbits=1
)

RTU Slave Implementation Considerations

Message Framing

Implement precise timing to detect message boundaries using silent intervals

CRC Validation

Always validate incoming message CRC before processing and respond with correct CRC

Response Timing

Respond within specified time limits to prevent master timeouts

Address Filtering

Only respond to messages addressed to this slave or broadcast address (0)

Hardware Interface

Properly control RS-485 driver enable for half-duplex communication

Data Synchronization

Ensure data consistency when updating registers during communication

Modbus Gotchas

Common pitfalls and manufacturer-specific implementations that can cause confusion in RTU implementations.

🔢

Decimal Number Representations

Different manufacturers represent decimal numbers in various ways, especially when negative values are involved. Here are the most common approaches in RTU devices:

1. IEEE 754 32-bit Float

Registers Used: 2 (32-bit total) = 8 bytes transmitted

Range: ±3.4 × 10³⁸ (very large numbers)

How Sign is Determined: First bit of the 32-bit value (0 = positive, 1 = negative)

Scaling: Built into the floating-point format - no external scale factor needed

Used by: Modern RTU devices, energy meters, smart transmitters

Example Value Register 1 (Hex) Register 2 (Hex) RTU Bytes Sent Sign Bit What Device Shows
-123.45 0xC2F6 0xE666 C2 F6 E6 66 1 (negative) -123.45°C
1234.56 0x449A 0x51EC 44 9A 51 EC 0 (positive) 1234.56 kW
-0.001 0xBA83 0x126F BA 83 12 6F 1 (negative) -0.001 bar

RTU Notes: Bytes transmitted big-endian, but register order varies by manufacturer
Pros: Handles very large and very small numbers, standard format
Cons: Can have tiny rounding errors, requires 8 bytes over serial

2. Scaled Signed Integer

Registers Used: 1 (16-bit) = 2 bytes transmitted

Range: -32,768 to +32,767 (before scaling)

How Sign is Determined: If register value > 32,767 then it's negative (two's complement)

How Scaling Works: Device stores (actual value × scale factor). To get real value: register value ÷ scale factor

Used by: RTU temperature sensors, pressure transmitters, flow meters

Example Value Scale Factor Stored As
(value × scale)
Register Value (Hex) RTU Bytes Sent Sign Check What Device Shows
-123.45 × 100 -12,345 0xCFC7 CF C7 53,191 > 32,767 = Negative -123.45°C
25.6 × 10 256 0x0100 01 00 256 ≤ 32,767 = Positive 25.6°C
-50.25 × 100 -5,025 0xEC37 EC 37 60,471 > 32,767 = Negative -50.25 psi

RTU Notes: Scale factor usually documented in device manual
Pros: Exact decimal representation, efficient transmission, single register
Cons: Limited range, need to know scale factor from manual

3. Packed BCD (Binary Coded Decimal)

Registers Used: 2-4 (depends on precision needed) = 4-8 bytes transmitted

Range: Varies, typically 4-8 decimal digits

How Sign is Determined: Special nibble (4-bit) holds sign: 0-7 = positive, 8-F = negative

How Scaling Works: Each nibble represents one decimal digit exactly - no math needed

Used by: RTU flow meters, older industrial RTU devices

Example Value Register 1 (Hex) Register 2 (Hex) RTU Bytes Sent Sign Nibble Digits What Device Shows
-123.45 0x8123 0x4500 81 23 45 00 8 = Negative 1,2,3 . 4,5 -123.45 L/min
987.65 0x0987 0x6500 09 87 65 00 0 = Positive 9,8,7 . 6,5 987.65 gal
-0.12 0xF000 0x1200 F0 00 12 00 F = Negative 0,0,0 . 1,2 -0.12 mph

RTU Notes: Some devices use 0xF for negative instead of 0x8
Pros: No rounding errors, human-readable in hex, each digit stored exactly
Cons: Inefficient transmission, more complex to parse

4. Offset Binary (Excess-K)

Registers Used: 1 (16-bit) = 2 bytes transmitted

Range: Manufacturer specific (e.g., -200°C to +200°C)

How Sign is Determined: No negative values stored! Offset makes all values positive

How Scaling Works: (Register value ÷ scale factor) - offset = actual value

Used by: RTU temperature controllers, level sensors

Example Value Offset Used Scale Factor Calculation Register Value (Hex) RTU Bytes Sent What Device Shows
-123.45°C +200 × 100 (-123.45 + 200) × 100 = 7,655 0x1DE7 1D E7 -123.45°C
75.2°C +200 × 100 (75.2 + 200) × 100 = 27,520 0x6B74 6B 74 75.2°C
-50.0 psi +100 × 10 (-50 + 100) × 10 = 500 0x01F4 01 F4 -50.0 psi

RTU Notes: Offset values vary by manufacturer and measurement range
Pros: Avoids negative numbers completely, simple for device firmware
Cons: Need to know offset value from manual, not intuitive

5. ASCII over RTU (Rarely Used)

Registers Used: 3-6 (depends on number length) = 6-12 bytes transmitted

Range: Flexible, limited by register count

How Sign is Determined: ASCII minus character "-" (0x2D) as first character

How Scaling Works: No scaling - decimal point "." (0x2E) stored as character

Used by: Some display meters, legacy RTU devices

Example Value Register 1 Register 2 Register 3 RTU Bytes Sent Sign Character What Device Shows
-123.45 0x2D31 ("-1") 0x3233 ("23") 0x2E34 (".4") 2D 31 32 33 2E 34 0x2D = "-" -123.45
56.78 0x3536 ("56") 0x2E37 (".7") 0x3800 ("8\0") 35 36 2E 37 38 00 No sign = "+" 56.78
-9.1 0x2D39 ("-9") 0x2E31 (".1") 0x0000 2D 39 2E 31 00 00 0x2D = "-" -9.1

RTU Notes: Inefficient for RTU bandwidth, rarely used in practice
Pros: Human readable, flexible precision, exactly what you see
Cons: Very inefficient transmission, uses many bytes, slow over serial

💡 RTU-Specific Tips for Decimal Handling

  • Check device manuals: RTU devices often have unique scaling documented
  • Monitor raw bytes: Use RTU sniffers to see actual byte transmission
  • Test edge cases: Try zero, positive/negative extremes
  • Byte order matters: RTU uses big-endian, but register order varies
  • Validate with known readings: Use physical measurements to verify parsing
  • Consider update rates: Some RTU devices use different formats for fast vs. slow registers

Modbus RTU Quick Quiz

Question 1 of 15