Introduction to Modbus TCP
Modbus TCP is a communication protocol used in industrial automation systems to enable communication between devices over Ethernet networks. It's an adaptation of the traditional Modbus protocol for TCP/IP networks.
What is Modbus?
Modbus is a serial communication protocol developed by Modicon (now Schneider Electric) in 1979. It has become a de facto standard communication protocol in the industrial manufacturing environment.
Key Features of Modbus TCP
- Open Protocol: Free to use without licensing fees
- Simple: Easy to implement and understand
- Ethernet-based: Runs over standard TCP/IP networks
- Master-Slave Architecture: Clear communication hierarchy
- Port 502: Uses standard port 502 for communication
How Modbus TCP Works
Modbus TCP follows a client-server model where:
- Client (Master): Initiates requests for data
- Server (Slave): Responds to client requests
- Data Model: Uses four primary data types (coils, discrete inputs, holding registers, input registers)
Modbus Client
Sends requests
Modbus Server
Processes & responds
Practical Example: Temperature Monitoring
HMI Display Unit
(Modbus Client - IP: 192.168.1.50)Temperature Sensor Unit
(Modbus Server - IP: 192.168.1.100)(Temperature × 100)
How it Works:
- Temperature Measurement: The sensor continuously measures temperature (23.5°C)
- Data Storage: Value is stored in Input Register 30001 as integer 2350 (temp × 100)
- Client Request: HMI Display Unit sends Modbus request to read the temperature register
- Server Response: Temperature Sensor Unit responds with raw value 2350
- Data Processing: HMI divides by 100 to get 23.5°C
- Display Update: Temperature is shown on HMI screen for operator viewing
Modbus TCP API Reference
Function Codes
Modbus TCP uses function codes to define the type of operation being performed:
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
Modbus TCP Terminology
Understanding Modbus terminology is essential for effective implementation. Here are the key concepts and data types you'll encounter when working with Modbus TCP.
Coils
Definition: 1-bit read/write discrete values that represent binary outputs or flags.
Common Uses:
- Digital outputs (relay states, valve positions)
- Control flags (start/stop commands)
- Status indicators (alarm conditions)
- Boolean configuration settings
Address Range: 00001 - 09999 (traditional addressing)
Function Codes: 01 (Read), 05 (Write Single), 15 (Write Multiple)
Example:
Coil 00001 = 1 (Motor Running)
Coil 00002 = 0 (Pump Stopped)
Discrete Inputs
Definition: 1-bit read-only discrete values that represent binary inputs from sensors or switches.
Common Uses:
- Digital sensors (proximity switches, limit switches)
- Push button states
- Safety interlocks
- Equipment status feedback
Address Range: 10001 - 19999 (traditional addressing)
Function Codes: 02 (Read only)
Example:
Input 10001 = 1 (Door Open)
Input 10002 = 0 (Emergency Stop Not Pressed)
Holding Registers
Definition: 16-bit read/write values used for analog outputs, setpoints, and configuration data.
Common Uses:
- Analog outputs (speed setpoints, position commands)
- Configuration parameters
- Control setpoints and limits
- Writable process variables
Address Range: 40001 - 49999 (traditional addressing)
Function Codes: 03 (Read), 06 (Write Single), 16 (Write Multiple)
Value Range: 0 - 65535 (unsigned) or -32768 to 32767 (signed)
Example:
Register 40001 = 1500 (Motor Speed RPM)
Register 40002 = 250 (Temperature Setpoint °C)
Input Registers
Definition: 16-bit read-only values representing analog inputs and measured process variables.
Common Uses:
- Analog sensor readings (temperature, pressure, flow)
- Measured process variables
- System status values
- Calculated measurements
Address Range: 30001 - 39999 (traditional addressing)
Function Codes: 04 (Read only)
Value Range: 0 - 65535 (unsigned) or -32768 to 32767 (signed)
Example:
Register 30001 = 2350 (Current Temperature × 10)
Register 30002 = 1024 (Pressure in PSI)
Unit ID (Slave ID)
Definition: An 8-bit identifier that specifies which device should respond to a request in multi-device networks.
Details:
- Range: 1-247 for device addressing
- 0 = Broadcast (not typically used in TCP)
- 248-255 = Reserved
- Each device must have a unique Unit ID
Example:
Unit ID 1 = PLC Controller
Unit ID 2 = Temperature Monitor
Function Code
Definition: An 8-bit code that specifies the type of operation to be performed.
Common Function Codes:
- 01-04: Read operations
- 05-06: Write single operations
- 15-16: Write multiple operations
- 128+: Exception responses
Example:
Function Code 03 = Read Holding Registers
Function Code 131 = Exception Response (128+3)
Transaction ID
Definition: A 16-bit identifier used to match requests with responses in Modbus TCP.
Purpose:
- Associates responses with their corresponding requests
- Enables concurrent request handling
- Typically incremented for each new request
- Echo'd back unchanged in the response
Example:
Request: Transaction ID = 0x0001
Response: Transaction ID = 0x0001
Exception Codes
Definition: Error codes returned when a Modbus request cannot be processed normally.
Common Exception Codes:
- 01: Illegal Function - Unsupported function code
- 02: Illegal Data Address - Invalid register address
- 03: Illegal Data Value - Invalid data in request
- 04: Slave Device Failure - Device processing error
Example:
Exception Response:
Function Code = 131 (128 + 03)
Exception Code = 02 (Illegal Address)
Address Formats & Configuration Challenges
Traditional 5-Digit Addressing
Legacy addressing system where the first digit indicates data type:
- 0xxxx: Coils (00001-09999)
- 1xxxx: Discrete Inputs (10001-19999)
- 3xxxx: Input Registers (30001-39999)
- 4xxxx: Holding Registers (40001-49999)
Modern Protocol Data Unit (PDU) Addressing
Direct register addressing used in actual protocol messages:
- 0-based indexing: Address 0 = first register
- Function code defines type: No prefix needed
- Example: Traditional 40001 = PDU address 0
⚠️ Manufacturer Addressing Differences
Critical Configuration Challenge: Different manufacturers use different addressing conventions:
- 0-Based Manufacturers: First register = address 0
- 1-Based Manufacturers: First register = address 1
- Traditional Offset: Some subtract 1 from 5-digit addresses
- Direct Mapping: Others use 5-digit addresses directly
Data Format & Encoding Challenges
🔄 Byte Order (Endianness)
Problem: 16-bit and 32-bit values can be stored in different byte orders:
Big-Endian (Motorola/Network Order)
Most significant byte first
Value: 0x1234 = 4660 decimal
Little-Endian (Intel Order)
Least significant byte first
Value: 0x3412 = 13330 decimal
32-bit Values Across Two Registers
Four possible combinations for value 0x12345678:
- Big-Endian, High-Low: [0x1234][0x5678]
- Little-Endian, High-Low: [0x3412][0x7856]
- Big-Endian, Low-High: [0x5678][0x1234]
- Little-Endian, Low-High: [0x7856][0x3412]
📝 String Encoding Challenges
Problem: Text data stored in registers can use various encoding methods:
ASCII Encoding
Standard 7-bit character encoding (most common)
Register: 0x4142 ('A'=0x41, 'B'=0x42)
UTF-8 Encoding
Variable-length encoding for international characters
Manufacturer-Specific Encoding
Some use proprietary character sets or packing methods
- BCD (Binary Coded Decimal) for numeric strings
- Custom character mappings
- Null-terminated vs fixed-length strings
String Character Ordering
Different manufacturers pack characters differently:
⚠️ Initial Configuration Challenges
1. Address Mapping Issues
Symptom: Reading wrong registers or getting "illegal address" errors
Causes:
- Mixing 0-based and 1-based addressing
- Incorrect offset calculations
- Misunderstanding manufacturer documentation
Solution: Always verify with simple test reads and confirm with device documentation
2. Data Interpretation Problems
Symptom: Numeric values appear incorrect or text is garbled
Causes:
- Wrong endianness assumption
- Incorrect string encoding
- Multi-register value ordering
Solution: Test with known values and try different byte order combinations
3. Mixed Vendor Environments
Symptom: Some devices work perfectly while others don't
Causes:
- Different manufacturers use different conventions
- Legacy vs modern implementations
- Firmware version differences
Solution: Create device-specific configuration profiles
Configuration Best Practices
📋 Documentation Review
- Always check manufacturer's Modbus implementation guide
- Look for addressing convention explanations
- Note any special data format requirements
🧪 Test with Known Values
- Start with simple single register reads
- Use manufacturer's diagnostic tools if available
- Test with registers containing known static values
📝 Document Your Findings
- Record working address mappings
- Note byte order requirements
- Create device-specific configuration templates
🔧 Configurable Libraries
- Use Modbus libraries that support multiple addressing modes
- Implement configurable byte order handling
- Build abstraction layers for different vendors
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 Client Example
from pymodbus.client.sync import ModbusTcpClient
# Create client connection
client = ModbusTcpClient('192.168.1.100', port=502)
# Connect to server
connection = client.connect()
if connection:
print("Connected to Modbus server")
# Read holding registers
result = client.read_holding_registers(0, 10, unit=1)
if result.isError():
print("Error reading registers")
else:
print(f"Register values: {result.registers}")
# Write single register
client.write_register(0, 1234, unit=1)
# Close connection
client.close()
else:
print("Failed to connect")
JavaScript Client Example
const ModbusRTU = require("modbus-serial");
const client = new ModbusRTU();
async function modbusExample() {
try {
// Connect to Modbus TCP server
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);
console.log("Connected to Modbus server");
// 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 error:", error);
}
}
modbusExample();
C# Client Example
using EasyModbus;
class Program
{
static void Main()
{
ModbusClient modbusClient = new ModbusClient("192.168.1.100", 502);
try
{
modbusClient.Connect();
Console.WriteLine("Connected to Modbus server");
// 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}");
}
}
}
Modbus TCP Client Development
Client Responsibilities
- Initiate TCP connections to Modbus servers
- Format and send Modbus requests
- Handle server responses and errors
- Manage connection lifecycle
Implementation Steps
Establish TCP Connection
Connect to the server on port 502 (default Modbus TCP port)
Format Request Message
Create properly formatted Modbus TCP ADU with transaction ID, protocol ID, length, unit ID, function code, and data
Send Request
Transmit the formatted message to the server
Receive and Parse Response
Wait for server response and parse the returned data
Best Practices
Connection Management
- Implement connection pooling for multiple requests
- Handle connection timeouts gracefully
- Implement reconnection logic for lost connections
Error Handling
- Check for Modbus exception responses
- Validate response message format
- Implement retry mechanisms for failed requests
Performance
- Use persistent connections when possible
- Implement request queuing for high-throughput scenarios
- Consider async/await patterns for non-blocking operations
Modbus TCP Server Development
Server Responsibilities
- Listen for incoming TCP connections on port 502
- Parse and validate incoming Modbus requests
- Process requests and access data model
- Format and send appropriate responses
Data Model
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 Server Example (Python)
from pymodbus.server.sync import StartTcpServer
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=store, single=True)
# Server identification
identity = ModbusDeviceIdentification()
identity.VendorName = 'pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl = 'http://github.com/bashwork/pymodbus/'
identity.ProductName = 'pymodbus Server'
identity.ModelName = 'pymodbus Server'
identity.MajorMinorRevision = '1.0'
# Start server
print("Starting Modbus TCP Server on port 502...")
StartTcpServer(context, identity=identity, address=("localhost", 502))
Server Implementation Considerations
Threading
Handle multiple client connections simultaneously using threads or async programming
Data Persistence
Implement data storage mechanisms for maintaining register values across sessions
Security
Consider implementing access controls and connection limits for production environments
Monitoring
Add logging and monitoring capabilities to track client connections and requests
Modbus Gotchas
Common pitfalls and manufacturer-specific implementations that can cause confusion.
Decimal Number Representations
Different manufacturers represent decimal numbers in various ways, especially when negative values are involved. Here are the most common approaches:
1. IEEE 754 32-bit Float
Registers Used: 2 (32-bit total)
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 PLCs, SCADA systems, energy meters
| Example Value | Register 1 (Hex) | Register 2 (Hex) | Sign Bit | What Device Shows |
|---|---|---|---|---|
| -123.45 | 0xC2F6 | 0xE666 | 1 (negative) | -123.45°C |
| 1234.56 | 0x449A | 0x51EC | 0 (positive) | 1234.56 kW |
| -0.001 | 0xBA83 | 0x126F | 1 (negative) | -0.001 bar |
Pros: Handles very large and very small numbers, standard format
Cons: Can have tiny rounding errors, uses 2 registers
Note: The device automatically handles all the complex math - you just get the final decimal value
2. Scaled Signed Integer
Registers Used: 1 (16-bit)
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: Temperature sensors, pressure transmitters, simple devices
| Example Value | Scale Factor | Stored As (value × scale) |
Register Value (Hex) | Sign Check | What Device Shows |
|---|---|---|---|---|---|
| -123.45 | × 100 | -12,345 | 0xCFC7 (53,191) | 53,191 > 32,767 = Negative | -123.45°C |
| 25.6 | × 10 | 256 | 0x0100 (256) | 256 ≤ 32,767 = Positive | 25.6°C |
| -50.25 | × 100 | -5,025 | 0xEC37 (60,471) | 60,471 > 32,767 = Negative | -50.25 psi |
Pros: Exact decimal representation, efficient, single register
Cons: Limited range, need to know scale factor from manual
Scale Factor Examples: ×10 = 1 decimal place, ×100 = 2 decimal places, ×1000 = 3 decimal places
3. Packed BCD (Binary Coded Decimal)
Registers Used: 2-4 (depends on precision needed)
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: Flow meters, financial systems, older industrial devices
| Example Value | Register 1 (Hex) | Register 2 (Hex) | Sign Nibble | Digits | What Device Shows |
|---|---|---|---|---|---|
| -123.45 | 0x8123 | 0x4500 | 8 = Negative | 1,2,3 . 4,5 | -123.45 L/min |
| 987.65 | 0x0987 | 0x6500 | 0 = Positive | 9,8,7 . 6,5 | 987.65 gal |
| -0.12 | 0xF000 | 0x1200 | F = Negative | 0,0,0 . 1,2 | -0.12 mph |
Pros: No rounding errors, human-readable in hex, each digit stored exactly
Cons: Inefficient storage, more complex to parse
Sign Convention: Some manufacturers use F for negative, others use 8-F range
4. Offset Binary (Excess-K)
Registers Used: 1 (16-bit)
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: Temperature controllers, level sensors, pressure gauges
| Example Value | Offset Used | Scale Factor | Calculation | Register Value (Hex) | What Device Shows |
|---|---|---|---|---|---|
| -123.45°C | +200 | × 100 | (-123.45 + 200) × 100 = 7,655 | 0x1DE7 | -123.45°C |
| 75.2°C | +200 | × 100 | (75.2 + 200) × 100 = 27,520 | 0x6B80 | 75.2°C |
| -50.0 psi | +100 | × 10 | (-50 + 100) × 10 = 500 | 0x01F4 | -50.0 psi |
Pros: Avoids negative numbers completely, simple for device firmware
Cons: Need to know offset value from manual, not intuitive
Common Offsets: Temperature: +200°C, Pressure: +100 psi, Level: +50%
5. ASCII Decimal Strings
Registers Used: 3-6 (depends on number length)
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: Display meters, some legacy RTU devices
| Example Value | Register 1 | Register 2 | Register 3 | Sign Character | What Device Shows |
|---|---|---|---|---|---|
| -123.45 | 0x2D31 ("-1") | 0x3233 ("23") | 0x2E34 (".4") | 0x2D = "-" | -123.45 |
| 56.78 | 0x3536 ("56") | 0x2E37 (".7") | 0x3800 ("8\0") | No sign = "+" | 56.78 |
| -9.1 | 0x2D39 ("-9") | 0x2E31 (".1") | 0x0000 | 0x2D = "-" | -9.1 |
Pros: Human readable, flexible precision, exactly what you see
Cons: Inefficient, uses many registers, slow transmission
Characters: "0"-"9" = 0x30-0x39, "." = 0x2E, "-" = 0x2D, space/null = 0x00
💡 Tips for Handling Decimal Representations
- Always check documentation: Manufacturer manuals specify the exact format used
- Test with known values: Send known inputs and verify the register outputs
- Look for scaling info: Check for scale factors, decimal places, or unit multipliers
- Watch byte order: IEEE floats can be big-endian or little-endian
- Validate ranges: Check if the parsed values make sense for the application