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 widely used today for small-footprint implementations, such as low-power microcontrollers. It primarily functions as a point-to-point protocol in full-duplex or half-duplex client-server communication. In the common half-duplex mode, the client initiates the request, and the server responds with the requested information. This is a single-thread driver design where one communication thread handles both the request transmission and response processing.
In more complex full-duplex Modbus RTU drivers, at least two threads are used to achieve the highest throughput. The Transmit thread continuously sends requests without waiting for responses, while the Response thread independently monitors responses, checks for errors, deadlocks the frame, and transfers the data to internal memory. Good driver design can be identified by tracking 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
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
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.
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
Addressing:
- Devices in a Modbus network gets unique addresses.
- For Modbus RTU, addresses typically range from 1 to 247.
Function Codes:
- Modbus messages include function codes that match the operation type to be performed (read, write, etc.).
- Common function codes include 03 (Read Analog Output Holding Registers) and 06 (Write Single Register).
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
I’ve included 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 calculate CRC 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 the 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.