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 Monitor
Current Temperature: 23.5°C
Online
1
Request: Read Temperature
Function: 04, Register: 30001
2
Response: Temperature Data
Value: 2350 (23.5°C)
Temperature Sensor Unit
(Modbus Server - IP: 192.168.1.100)
🌡️ 23.5°C
Register 30001: 2350
(Temperature × 100)
How it Works:
  1. Temperature Measurement: The sensor continuously measures temperature (23.5°C)
  2. Data Storage: Value is stored in Input Register 30001 as integer 2350 (temp × 100)
  3. Client Request: HMI Display Unit sends Modbus request to read the temperature register
  4. Server Response: Temperature Sensor Unit responds with raw value 2350
  5. Data Processing: HMI divides by 100 to get 23.5°C
  6. 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)

Request: Unit ID + Function Code + Starting Address + Quantity

02 - Read Discrete Inputs

Read status of discrete inputs

Request: Unit ID + Function Code + Starting Address + Quantity

03 - Read Holding Registers

Read values from holding registers

Request: Unit ID + Function Code + Starting Address + Quantity

04 - Read Input Registers

Read values from input registers

Request: Unit ID + Function Code + Starting Address + Quantity

05 - Write Single Coil

Write a single coil (discrete output)

Request: Unit ID + Function Code + Address + Value

06 - Write Single Register

Write a single holding register

Request: Unit ID + Function Code + Address + Value

Message Format

Modbus TCP/IP ADU (Application Data Unit)

Transaction ID
2 bytes
Protocol ID
2 bytes (0x0000)
Length
2 bytes
Unit ID
1 byte
Function Code
1 byte
Data
N bytes

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
Impact: Same logical register may have different addresses depending on manufacturer documentation and implementation!

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

0x120x34

Value: 0x1234 = 4660 decimal

Little-Endian (Intel Order)

Least significant byte first

0x340x12

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)

Example: "AB" in one register
Register: 0x4142 ('A'=0x41, 'B'=0x42)
UTF-8 Encoding

Variable-length encoding for international characters

Challenge: Multi-byte characters may span registers
Manufacturer-Specific Encoding

Some use proprietary character sets or packing methods

Examples:
  • BCD (Binary Coded Decimal) for numeric strings
  • Custom character mappings
  • Null-terminated vs fixed-length strings
String Character Ordering

Different manufacturers pack characters differently:

High-byte first: "AB" → 0x4142
Low-byte first: "AB" → 0x4241

⚠️ 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

1

Establish TCP Connection

Connect to the server on port 502 (default Modbus TCP port)

2

Format Request Message

Create properly formatted Modbus TCP ADU with transaction ID, protocol ID, length, unit ID, function code, and data

3

Send Request

Transmit the formatted message to the server

4

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

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 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

Modbus TCP Quick Quiz

Question 1 of 8