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 Meter Device
(Modbus Slave - Address: 5)(Flow Rate × 10)
How it Works:
- Flow Measurement: The flow meter continuously measures flow rate (125.7 L/min)
- Data Storage: Value is stored in Holding Register 40001 as integer 1257 (flow × 10)
- Master Request: PLC Controller polls the flow meter using slave address 5
- Slave Response: Flow Meter responds with raw value 1257
- Data Processing: PLC divides by 10 to get 125.7 L/min
- 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)
02 - Read Discrete Inputs
Read status of discrete inputs
03 - Read Holding Registers
Read values from holding registers
04 - Read Input Registers
Read values from input registers
05 - Write Single Coil
Write a single coil (discrete output)
06 - Write Single Register
Write a single holding register
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 Data Format & Timing Challenges
⏱️ Timing Requirements
Critical Timing Constraints:
Inter-Character Timeout
Maximum gap between characters in a message
Exceeding this breaks the message frame
Inter-Frame Delay
Minimum gap between message frames
Required to detect message boundaries
Timing Calculations by Baud Rate
🔗 Serial Network Issues
Common Serial Communication Problems:
Electrical Interference
EMI can corrupt data on long cable runs
- Use shielded twisted pair cables
- Proper grounding techniques
- Avoid running parallel to power cables
Ground Loops
Potential differences between devices
- Use isolation transformers
- Optical isolators in interfaces
- Single point grounding strategy
Termination Problems
Improper RS-485 termination causes reflections
- 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
Configure Serial Port
Set up baud rate, parity, data bits, and stop bits to match all network devices
Implement Timing Control
Ensure proper silent intervals and character timeout detection for message framing
Format RTU Messages
Build binary messages with slave address, function code, data, and CRC checksum
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
Discrete Inputs
1-bit read-only values, typically representing digital inputs
Input Registers
16-bit read-only values, typically representing analog inputs
Holding Registers
16-bit read/write values, typically representing analog outputs or configuration
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