June 17, 2025
Modbus: Bridging the Gap in Industrial Networking

Modbus: Bridging the Gap in Industrial Networking

What is modbus

Modbus is a widely used communication protocol in industrial automation, enabling devices to communicate over serial lines and Ethernet. Developed in the late 1970s by Modicon (now part of Schneider Electric), it has become a standard for connecting industrial electronic devices. Its simplicity, open nature, and reliability have made it the go-to choice for numerous applications.

Key Features of Modbus

Open Standard

One of the main advantages of Modbus is that it is an open protocol. This means that manufacturers can implement it without licensing fees, fostering widespread adoption across various industries.

Versatile Communication Modes

Modbus supports several communication modes:

  • Modbus RTU (Remote Terminal Unit): This mode uses binary coding and is suitable for serial communication (RS-232 or RS-485). It is efficient for transmitting data over long distances.
  • Modbus ASCII: Similar to RTU but uses ASCII characters, making it easier to read but less efficient than RTU.
  • Modbus TCP/IP: This version runs over Ethernet, enabling faster communication and integration with modern network infrastructure.

Master-Slave Architecture

Modbus operates on a master-slave architecture. The master device initiates communication by sending requests, while the slave devices respond. This structure ensures organised communication and prevents data collisions.

Data Types and Addressing

Modbus supports various data types, including:

  • Coils: Single-bit values (ON/OFF).
  • Discrete Inputs: Single-bit values that are read-only.
  • Input Registers: 16-bit read-only values.
  • Holding Registers: 16-bit read/write values.

Each data type has a unique addressing scheme, allowing for efficient data retrieval and manipulation.

Applications of Modbus

  • Energy and Utilities : In the energy sector, Modbus facilitates communication between smart meters, substations, and control centers, supporting efficient energy management and grid monitoring.
  • Building Management Systems : Modbus is used in HVAC systems, lighting controls, and energy management systems, enabling centralized control and data acquisition.
  • Industrial Automation : In manufacturing plants, Modbus connects PLCs (Programmable Logic Controllers), sensors, actuators, and HMIs (Human-Machine Interfaces) for real-time monitoring and control.
  • Water and Wastewater Management : Modbus helps in the control and monitoring of pumps, valves, and treatment processes, ensuring optimal performance and compliance with regulations.

Advantages of Modbus over other protocols

  • Simplicity: Its straightforward implementation makes it accessible to developers and integrators.
  • Interoperability: Being an open standard, devices from different manufacturers can communicate seamlessly.
  • Scalability: Modbus can easily be expanded to accommodate more devices and complex networks.
  • Cost-Effectiveness: Low implementation and maintenance costs contribute to its popularity.

Coding Time

Enough of theory lets start coding

Theory may look complicated but it is quite easy to implement in Arduino.

Hardware Required .

  • You will need to pair Arduino or esp devices . One will act as master and other will act as slave.
    • In this setup, I am using two Arduino Mega boards, which offer multiple built-in UART ports. One port is dedicated to debugging, while the other handles Modbus communication. If you’re using a board with only a single serial port, you can alternatively utilize a SoftwareSerial port for additional communication.
  • Pair of RS485 modules .

Circuit Connections

The circuit connection may vary depending on the RS485 module you have but in general this is how you connect your MCU with the RS485 modules . In this example we are using full duplex mode.

Master RTU

uint8_t deviceAddress = 0x11;
uint8_t functionCode = 0x01;
uint8_t offsetH = 0x00;
uint8_t offsetL = 0x13;
uint8_t readLengthL = 0x00; 
uint8_t readLengthH = 0x25;  

uint8_t readcount = 10; 
uint16_t readByte =0;


void setup()
{

	Serial2.begin(9600);
	Serial.begin(9600);

}


void loop()
{
	//sendModbusQuery
	readModbusCoil_request(deviceAddress,functionCode,offsetH,offsetL,readLengthH,readLengthL);
	delay(500);

	if (Serial2.available())
	{
		uint8_t reader[readcount] ={0};

		Serial2.readBytes(reader,readcount);

		if((reader[0] == deviceAddress) && (reader[1] == functionCode)){

			Serial.print("Byte 1 is : ");  
			Serial.print(reader[3],HEX);

			Serial.print(" Byte 2 is : ");  
			Serial.print(reader[4],HEX);

			Serial.print(" Byte 3 is : ");
			Serial.print(reader[5],HEX);

			Serial.print(" Byte 4 is : ");
			Serial.print(reader[6],HEX);

			Serial.print(" Byte 5 is : ");
			Serial.println(reader[7],HEX);



		}

	}

}


void readModbusCoil_request(uint8_t deviceAddressP, uint8_t functionCodeP, uint8_t offsetHP ,uint8_t offsetP, uint8_t readLengthHP, uint8_t readLengthLP ){
	
	// just for calculating CRC 
	uint16_t calCRC =0;
	uint8_t CRCLow = 0;
	uint8_t CRCHigh =0;

  	uint8_t subreader [6]= {deviceAddressP,functionCodeP,offsetHP,offsetLP,readLengthHP,readLengthLP};

	// Following Code may look like (form masking prespective that calCRC & 0x00FF = Low byte and vice versa 
	// but Our CRC Function return Bytes in revese order So here we are Correcting that inconsistency 
	
	calCRC = ModRTU_CRC(subreader,6);

	CRCHigh = calCRC & 0x00FF;

	CRCLow = (calCRC &  0xFF00)>>8;

	Serial2.write(deviceAddressP);
	Serial2.write(functionCodeP);
	Serial2.write(offsetHP);
	Serial2.write(offsetLP);
	Serial2.write(readLengthHP);
	Serial2.write(readLengthLP);
	Serial2.write(CRCHigh);
	Serial2.write(CRCLow);
	
}


uint16_t ModRTU_CRC(uint8_t buf[], uint8_t len)
{
	uint16_t crc = 0xFFFF;
	
	
	for (uint16_t pos = 0; pos < len; pos++) {
		crc ^= (uint16_t)buf[pos];          // XOR byte into least significant byte of crc
		
		for (uint16_t i = 8; i != 0; i--) {    // Loop over each bit
			if ((crc & 0x0001) != 0) {      // If the LSB is set
				crc >>= 1;                    // Shift right and XOR 0xA001
				crc ^= 0xA001;
			}
			else                            // Else LSB is not set
			crc >>= 1;                    // Just shift right
		}
	}
	// Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
	return crc;
}

Explanation

  • Sends a Modbus request to read coils.
  • Waits for half a second to allow for a response.
  • Checks if there are any bytes available from the slave.
  • If a valid response is received (matching the expected device address and function code), it reads and prints specific bytes from the response to the serial monitor.

Master Request Packet Structure

Slave_address | Function_Code | offset_High_Byte | offset_Low_Byte | readLength_High | readLength_Low | CRC_High | CRC_Low

Slave Address (1 byte)

  • This byte specifies the address of the slave device that the master wants to communicate with. In a Modbus network, each slave must have a unique address ranging from 1 to 247.

Function Code (1 byte)

The function code indicates the type of action the master wants the slave to perform. For example, common function codes include:

  • 0x01 Read a coil [Coils are binary outputs (ON/OFF) in Modbus terminology. Each coil can be individually addressed and can be read to determine its current state (either 0 for OFF or 1 for ON).]
  • 0x03: Read Holding Registers
  • 0x04: Read Input Registers
  • 0x06: Write Single Register

Offset High Byte (1 byte)

  • This byte represents the high byte of the starting address (or offset) of the data that the master wants to read from the slave. In Modbus, addresses are typically 16 bits (2 bytes), so this is part of that 16-bit address.

Offset Low Byte (1 byte)

  • This byte represents the low byte of the starting address. Together with the high byte, these two bytes define the starting address for the read operation.

Read Length High Byte (1 byte)

  • This byte indicates the high byte of the number of registers (or data items) the master wants to read from the slave. This is used to specify how much data is to be retrieved.

Read Length Low Byte (1 byte)

  • This byte represents the low byte of the number of registers to be read. Again, together with the high byte, it defines the total length of the data request.

CRC High Byte (1 byte)

  • The CRC (Cyclic Redundancy Check) is a two-byte checksum used to verify the integrity of the data in the packet. The high byte is the first part of the CRC.

CRC Low Byte (1 byte)

  • This is the second part of the CRC. Both CRC bytes are calculated based on the entire packet (excluding the CRC itself) to ensure that the data has not been corrupted during transmission.

Modbus Slave

/************************************************************************/
/* This code is only meant for Mode 1  (Read Coil)

IN this mode data is composed of 1 bit
but data is transmitted as a byte                                                                   */
/************************************************************************/

int my_address = 0x11;

uint8_t lowCRC  =  0;
uint8_t highCRC =  0;

uint16_t calCRC =0;

void setup()
{
	Serial.begin(9600);
}

void loop()
{
  
		if (Serial.available())
		{
			unsigned char reader[8] = {0};
			
			unsigned char subreader[6] = {0};
			
			Serial.readBytes(reader,8);
			
			if (reader[0] == my_address)
			{
						
				for (int i = 0;i<6;i++)
				{
					subreader[i] = reader[i];
				}
				
				calCRC = ModRTU_CRC(subreader,6);
				
				highCRC = calCRC & 0x00FF;
				
				lowCRC  = (calCRC &  0xFF00)>>8;
				
				
				if ((highCRC == reader[6]) && (lowCRC == reader[7]) )
				{
					
					Serial.write(reader[0]);
					Serial.write(reader[1]);
					
					// before sending data we have to send size of data 
					Serial.write(0x05); // Since data is composed of 37bits (37 / 8 = 5 bytes approx)   
					
					// each byte contains 8 bits (coils) 
					Serial.write(0xCD); // Coils 27 - 20 (1100 1101)
					Serial.write(0x6B); // Coils 35 - 28 (0110 1011)
					Serial.write(0xB2); // Coils 43 - 36 (1011 0010)
					Serial.write(0x0E); // Coils 51 - 44 (0000 1110)
				
					Serial.write(0x1B); // 3 space holders & Coils 56 - 52 (0001 1011)
	
					Serial.write(0x45); // CRC High
					Serial.write(0xE6); // CRC Low 
					
					
				}
				
			}
	
			
		}
  
	  
	   
	   //Serial.print("Low CRC Byte : ");
	   //Serial.print(lowCRC,HEX);
	   //Serial.print("  & High CRC Byte : ");
	   //Serial.println(highCRC,HEX);
	     

}

uint16_t ModRTU_CRC(byte buf[], uint8_t len)
{
	uint16_t crc = 0xFFFF;
	
	
	for (uint16_t pos = 0; pos < len; pos++) {
		crc ^= (uint16_t)buf[pos];          // XOR byte into least significant byte of crc
		
		for (uint16_t i = 8; i != 0; i--) {    // Loop over each bit
			if ((crc & 0x0001) != 0) {      // If the LSB is set
				crc >>= 1;                    // Shift right and XOR 0xA001
				crc ^= 0xA001;
			}
			else                            // Else LSB is not set
			crc >>= 1;                    // Just shift right
		}
	}
	// Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
	return crc;
}

Explanation :

  • In this we checks if there are bytes available to read from the serial.
  • Reads 8 bytes into an array called reader.
  • If the first byte (address) matches the predefined address (my_address), it processes the request.
  • Copies the first 6 bytes into another array called subreader for CRC calculation.
  • Computes the CRC for the subreader array.
  • Checks if the calculated CRC matches the last two bytes of the reader array (to ensure the data is valid).
  • If the CRC is valid, it sends back a response, which includes:
    • The original address and function code.
    • The size of the data to be sent (5 bytes in this case).
    • The actual coil data (represented as bytes).
    • The CRC for the response.

Modbus Slave Packet

  • Size of Response of is not fixed because master can request Bytes ranging from [1-255]
  • Slave request Packet : Slave_address | Function_Code | No_of_ data_ bytes [not no of registers] after Slave_address byte and Function Code byte except CRC bytes|Data byte 1 | Data Byte 2 |…… | CRC_High | CRC_Low

Using the Library

Now that we have a grasp of the fundamentals of Modbus RTU, we can begin utilizing the Arduino library from this GitHub repository to simplify our tasks.

#include <ArduinoRS485.h> 
#include <ArduinoModbus.h>

int counter = 0;

void setup() {
  Serial.begin(9600);
  while (!Serial);

  Serial.println("Modbus RTU Client Kitchen Sink");

  // start the Modbus RTU client
  if (!ModbusRTUClient.begin(9600)) {
    Serial.println("Failed to start Modbus RTU Client!");
    while (1);
  }
}

void loop() {
  writeCoilValues();

  readCoilValues();

  readDiscreteInputValues();

  writeHoldingRegisterValues();

  readHoldingRegisterValues();

  readInputRegisterValues();

  counter++;

  delay(5000);
  Serial.println();
}

void writeCoilValues() {
  // set the coils to 1 when counter is odd
  byte coilValue = ((counter % 2) == 0) ? 0x00 : 0x01;

  Serial.print("Writing Coil values ... ");

  // write 10 Coil values to (slave) id 42, address 0x00
  ModbusRTUClient.beginTransmission(42, COILS, 0x00, 10);
  for (int i = 0; i < 10; i++) {
    ModbusRTUClient.write(coilValue);
  }
  if (!ModbusRTUClient.endTransmission()) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");
  }

  // Alternatively, to write a single Coil value use:
  // ModbusRTUClient.coilWrite(...)
}

void readCoilValues() {
  Serial.print("Reading Coil values ... ");

  // read 10 Coil values from (slave) id 42, address 0x00
  if (!ModbusRTUClient.requestFrom(42, COILS, 0x00, 10)) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");

    while (ModbusRTUClient.available()) {
      Serial.print(ModbusRTUClient.read());
      Serial.print(' ');
    }
    Serial.println();
  }

  // Alternatively, to read a single Coil value use:
  // ModbusRTUClient.coilRead(...)
}

void readDiscreteInputValues() {
  Serial.print("Reading Discrete Input values ... ");

  // read 10 Discrete Input values from (slave) id 42, address 0x00
  if (!ModbusRTUClient.requestFrom(42, DISCRETE_INPUTS, 0x00, 10)) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");

    while (ModbusRTUClient.available()) {
      Serial.print(ModbusRTUClient.read());
      Serial.print(' ');
    }
    Serial.println();
  }

  // Alternatively, to read a single Discrete Input value use:
  // ModbusRTUClient.discreteInputRead(...)
}

void writeHoldingRegisterValues() {
  // set the Holding Register values to counter

  Serial.print("Writing Holding Registers values ... ");

  // write 10 coil values to (slave) id 42, address 0x00
  ModbusRTUClient.beginTransmission(42, HOLDING_REGISTERS, 0x00, 10);
  for (int i = 0; i < 10; i++) {
    ModbusRTUClient.write(counter);
  }
  if (!ModbusRTUClient.endTransmission()) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");
  }

  // Alternatively, to write a single Holding Register value use:
  // ModbusRTUClient.holdingRegisterWrite(...)
}

void readHoldingRegisterValues() {
  Serial.print("Reading Input Register values ... ");

  // read 10 Input Register values from (slave) id 42, address 0x00
  if (!ModbusRTUClient.requestFrom(42, HOLDING_REGISTERS, 0x00, 10)) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");

    while (ModbusRTUClient.available()) {
      Serial.print(ModbusRTUClient.read());
      Serial.print(' ');
    }
    Serial.println();
  }

  // Alternatively, to read a single Holding Register value use:
  // ModbusRTUClient.holdingRegisterRead(...)
}

void readInputRegisterValues() {
  Serial.print("Reading input register values ... ");

  // read 10 discrete input values from (slave) id 42,
  if (!ModbusRTUClient.requestFrom(42, INPUT_REGISTERS, 0x00, 10)) {
    Serial.print("failed! ");
    Serial.println(ModbusRTUClient.lastError());
  } else {
    Serial.println("success");

    while (ModbusRTUClient.available()) {
      Serial.print(ModbusRTUClient.read());
      Serial.print(' ');
    }
    Serial.println();
  }

  // Alternatively, to read a single Input Register value use:
  // ModbusRTUClient.inputRegisterRead(...)
}

Modbus Exception

Modbus slave can also send error message if master sends invalid request . In Modbus these error response messages are called exceptions .

Exception Packet : Slave_address | Function_Code |Exception Code | CRC_High | CRC_Low

You can read more about these exceptions here

Limitations of Modbus

While Modbus is robust, it has some limitations:

  • Limited Data Throughput: In high-speed applications, Modbus may not be fast enough due to its polling mechanism.
  • No Built-in Security: Standard Modbus lacks encryption and authentication features, making it vulnerable to cyber threats. However, Modbus TCP/IP can be enhanced with additional security layers.

Conclusion

Modbus remains a cornerstone of industrial communication protocols, thanks to its simplicity, reliability, and flexibility. As industries continue to evolve with the advent of the Industrial Internet of Things (IIoT), Modbus is adapting, ensuring its relevance in modern automation landscapes. Understanding its principles, advantages, and limitations is crucial for engineers and technicians working in automation and control systems. Whether you’re implementing a new system or maintaining an existing one, Modbus offers a robust solution for your communication needs.

Leave a Reply

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