Understanding Modbus Protocol: Understanding Modbus Protocol: Variants, Implementation, and Sample C Code​

Modbus, a widely used communication protocol in industrial automation, facilitates data exchange between devices. In this blog post, we’ll explore the Modbus protocol, its various flavors, how to use it, and provide a sample C code for implementation.

What is Modbus?

Modbus is a serial communication protocol developed in the late 1970s to enable communication between programmable logic controllers (PLCs) and other industrial devices. It was designed for the internal point-to-point network communications protocol between Modicon PLCs and programming panels to program the controllers. The protocol continues to be many developers’ first choice for its effortless, small footprint, and memory-mapped advantages. It is easy for the developers to implement the protocol due to an open system and royalty-free nature.

The protocol can be found in diverse automation industries, including building automation. It has evolved and become a standard in the automation industry, with serial (RS-232/RS-485) and Ethernet variants. The first implementation is the traditional implementation of Modbus over a serial line called Modbus Serial (Modbus RTU). The second implementation is more modern, operating over a TCP/IP network called Modbus TCP.

Modbus RTU vs Modbus TCP

The Modbus RTU version is most commonly used nowadays where small-footprint implementation is required, such as low-power microcontrollers. The Modbus RTU is used mainly as a point-point protocol in full-duplex or half-duplex client-server communication. In the most common half-duplex mode, the client starts the request, and the server responds to the client with the requested information. Technically, this is a single-thread driver design where a single communication thread coordinates the transmitting of the request and processing of the response from a remote server. In more complex full-duplex Modbus, RTU drivers implement at least two threads for a full-duplex to yield the highest throughput. In this driver design, the Transmit thread continuously sends requests one after another without waiting for a response. The response thread independently monitors the responses, checks for errors, debocks the frame, and copies over the data to internal memory. You can always tell good driver design by tracking the response time and throughput.

The second most common version is the Modbus TCP, an extension of the Modbus protocol that emerged with the growing prevalence of Ethernet in industrial automation. It was designed to facilitate communication between devices over TCP/IP networks, taking advantage of the reliability and ubiquity of Ethernet connections. In contemporary industrial automation, Modbus TCP has become a standard for communication between various devices, such as programmable logic controllers (PLCs), sensors, and human-machine interfaces (HMIs). Ethernet Connectivity, Scalability and Flexibility, Real-time Communication, Security Measures, and Integration with IoT and Industry 4.0 characterize its modern use.

One significant advantage of the Modbus TCP over Modbus RTU is authentic client-server architecture or multi-client responses (multi-master). Only one Client (Master) can talk on the wire in the Modbus RTU implementation, while multi-clients (masters) can talk to a single Modbus TCP server in the Modbus TCP implementation. In other words, the Modbus TCP supports a client-server architecture, allowing multiple devices to communicate seamlessly. The additional advantage of the Modbus TCP is the speed and communication distance. It operates over standard TCP/IP networks, making it compatible with existing IT infrastructure. The Modbus TCP enables faster and more efficient communication than its serial counterparts.

 
Variants of Modbus
  1. Modbus RTU (Remote Terminal Unit)
    • RTU is a binary protocol primarily used over serial communication (RS-232/RS-485)
    •  It uses a compact binary representation, making it efficient for communication in resource-constrained environments
  2. Modbus ASCII
    • ASCII is another serial variant, representing data in ASCII characters
    • While less commonly used than RTU, it can be useful for troubleshooting due to its human-readable format.
  3. Modbus TCP/IP
    • TCP/IP is the Ethernet-based variant, allowing Modbus communication over standard TCP/IP networks.
    • It leverages the client-server model, where the client initiates requests, and servers respond.
 
How to Use Modbus
  1. Addressing:
    • Devices in a Modbus network are assigned unique addresses.
    • For Modbus RTU, addresses typically range from 1 to 247.
  2. Function Codes:
    • Modbus messages include function codes defining the operation type to be performed (read, write, etc.).
    • Common function codes include 03 (Read Analog Output Holding Registers) and 06 (Write Single Register).
  3. Data Types:
    • Modbus supports various data types, such as coils (binary), input registers (16-bit integers), holding registers (read/write data), and input status (read-only binary).
 
Sample C Code for Modbus Implementation

Please see a simplified example of Modbus RTU implementation in C, which shows how to create a sample Modbus RTU frame and send it to the console screen in hex format. The code defines a function createModbusRTUFrame that takes the Modbus address, function code, start address, quantity, and a pointer to an array to store the Modbus RTU frame. The calculateCRC function is used to calculate the CRC16 checksum for the frame. The main function demonstrates creating a Modbus RTU Read Holding Registers frame with a specific address, function code, start address, and quantity. The resulting frame is then printed to the console as hex characters repressing bytes sent over the serial line.


Sample C Code to make Modbus RTU frame and send to to console (Transmit)
#include <stdio.h>
#include <stdint.h>

//Send Modbus RTU Frame to console
int main() {
    // Example: Create a Modbus RTU Read Holding Registers frame
    uint8_t modbusFrame[8];
    uint8_t modbusAddress = 0x01;
    uint8_t modbusFunction = 0x03;  // Read Holding Registers
    uint16_t modbusStartAddress = 0x0000;
    uint16_t modbusQuantity = 0x0002;

    createModbusRTUFrame(modbusAddress, modbusFunction, modbusStartAddress, modbusQuantity, modbusFrame);

    // Print the created Modbus RTU frame
    printf("Modbus RTU Frame:");
    for (int i = 0; i < 8; i++) {
        printf(" %02X", modbusFrame[i]);
    }
    printf("\n");

    return 0;
}

// Function to create Modbus RTU frame
void createModbusRTUFrame(uint8_t address, uint8_t function, uint16_t startAddress, uint16_t quantity, uint8_t *frame) {
    // Modbus RTU frame structure: [Address] [Function] [Data] [CRC_Low] [CRC_High]
    frame[0] = address;
    frame[1] = function;
    frame[2] = startAddress >> 8;  // High byte of start address
    frame[3] = startAddress & 0xFF;  // Low byte of start address
    frame[4] = quantity >> 8;  // High byte of quantity
    frame[5] = quantity & 0xFF;  // Low byte of quantity

    // Calculate CRC16
    uint16_t crc = calculateCRC(frame, 6);

    // Add CRC to the frame
    frame[6] = crc & 0xFF;  // Low byte of CRC
    frame[7] = crc >> 8;  // High byte of CRC
}


// Function to calculate CRC16 for Modbus RTU
uint16_t calculateCRC(uint8_t *data, int length) {
    uint16_t crc = 0xFFFF;

    for (int i = 0; i < length; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }

    return crc;
}
Receiving Modbus RTU data from Serial port

This example opens a serial port, reads data continuously, and prints the received Modbus RTU frame to the console. Make sure to replace “COM1” with the appropriate COM port name for your setup. This examples uses the classic Win32 API to open the communication port. Additionally, add proper error handling and consider implementing more robust logic based on your specific application needs.


Sample C Code to Receive Modbus RTU frame from Serial port (Receive)
#include <windows.h>
#include <stdio.h>

#define COM_PORT "COM1" // Change this to your COM port

//The sample code shows how to receive Modbus RTU frame from COM1 on Windows Platform
int main() {
    HANDLE hSerial = openSerialPort(COM_PORT);

    if (hSerial == NULL) {
        return 1;
    }

    // Assuming Modbus RTU frame size is 8 bytes (adjust as needed)
    const int bufferSize = 8;
    uint8_t modbusFrame[bufferSize];

    while (1) {
        int bytesRead = readFromSerialPort(hSerial, modbusFrame, bufferSize);

        if (bytesRead > 0) {
            // Validate CRC before processing the frame
            if (validateCRC(modbusFrame, bytesRead)) {
                // Process the received Modbus RTU frame
                processModbusFrame(modbusFrame, bytesRead);
            } else {
                fprintf(stderr, "CRC validation failed\n");
                // Handle CRC validation failure as needed
            }
        }
    }

    CloseHandle(hSerial);

    return 0;
}


// Function to open the serial port
HANDLE openSerialPort(const char* portName) {
    HANDLE hSerial = CreateFile(portName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hSerial == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "Error opening serial port %s\n", portName);
        return NULL;
    }

    DCB dcbSerialParams = { 0 };
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

    if (!GetCommState(hSerial, &dcbSerialParams)) {
        fprintf(stderr, "Error getting serial port state\n");
        CloseHandle(hSerial);
        return NULL;
    }

    // Update DCB structure for Modbus RTU
    dcbSerialParams.BaudRate = CBR_9600;
    dcbSerialParams.ByteSize = 8;
    dcbSerialParams.StopBits = ONESTOPBIT;
    dcbSerialParams.Parity = NOPARITY;

    if (!SetCommState(hSerial, &dcbSerialParams)) {
        fprintf(stderr, "Error setting serial port state\n");
        CloseHandle(hSerial);
        return NULL;
    }

    return hSerial;
}

// Function to read data from the serial port
int readFromSerialPort(HANDLE hSerial, uint8_t* buffer, int bufferSize) {
    DWORD bytesRead;

    if (!ReadFile(hSerial, buffer, bufferSize, &bytesRead, NULL)) {
        fprintf(stderr, "Error reading from serial port\n");
        return -1;
    }

    return bytesRead;
}

// Function to calculate CRC16 for Modbus RTU
uint16_t calculateCRC(uint8_t *data, int length) {
    uint16_t crc = 0xFFFF;

    for (int i = 0; i < length; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }

    return crc;
}

// Function to validate CRC of Modbus RTU frame
int validateCRC(uint8_t *frame, int length) {
    if (length < 2) {
        fprintf(stderr, "Frame too short for CRC validation\n");
        return 0;  // Frame too short
    }

    uint16_t receivedCRC = frame[length - 2] | (frame[length - 1] << 8);
    uint16_t calculatedCRC = calculateCRC(frame, length - 2);

    return (receivedCRC == calculatedCRC);
}

// Function to process Modbus RTU frame
void processModbusFrame(uint8_t *frame, int length) {
    // Your Modbus RTU frame processing logic here
    // Example: Print the contents of the frame
    printf("Processed Modbus RTU Frame:");
    for (int i = 0; i < length; i++) {
        printf(" %02X", frame[i]);
    }
    printf("\n");
}
Summary

Please note that this is a basic example. In a real-world application, you would need to handle serial communication interruptions, time-outs, inter-character delays, inter-frame delays, platform specific serial interface access, and implement error-checking and response parsing.

The Modbus protocol has become a widely adopted and versatile solution in modern industrial automation. Its compatibility, scalability, and real-time capabilities make it a go-to choice for engineers and system integrators in building robust and efficient automation systems.

Conclusion

Understanding its variants, addresses, function codes, and data types is crucial to successful implementation. The C code I’ve provided offers a starting point for integrating Modbus communication into your projects. A good understanding of the protocol chosen is critical to designing a Modbus driver for error-free and high-throughput communication. Here at Quantum Bit Solutions, we monitor all these parameters to ensure the best possible throughput and provide the values so you can optimize your device.

Facebook
Twitter
WhatsApp
Email

Leave a Reply

Your email address will not be published. Required fields are marked *