Skip to Content

Modbus RTU with Arduino and ESP32 based PLCs

RS-485 wiring, board setup, master and slave sketches and the function reference of the Industrial Shields Modbus library
February 11, 2025 by
Modbus RTU with Arduino and ESP32 based PLCs
Boot & Work Corp. S.L, Martí Schnabel

To use Modbus RTU on an Arduino or ESP32 based PLC, connect the RS-485 port of the PLC to the bus (A+ to A+, B- to B-), open it with RS485.begin(baudrate, HALFDUPLEX, SERIAL_8E1) and create a ModbusRTUMaster or ModbusRTUSlave object from the Industrial Shields Modbus library. The master sends a request with a function such as readHoldingRegisters(slave, address, quantity) and reads the answer later with available(), because the library does not block.

This guide works for the M-Duino family, the Ardbox family with hardware RS-485, the ESP32 PLC family and the ESP32 PLC 14. It covers the wiring, the board selection in the Arduino IDE, a tested master sketch, a slave sketch, the function reference of the library and the errors you will see first. If you need the protocol itself (frame, function codes, CRC), read the Modbus RTU frame and function codes guide first.

How does a Modbus RTU transaction work on a PLC?

Modbus RTU is a master and slave protocol on a serial line, usually RS-485. Only the master starts a transaction. Each slave has an address from 1 to 247 and answers only the requests with its address.

  1. Step 1RequestThe master sends the slave address, a function code, the first register, the quantity and a CRC.
  2. Step 2SilenceA pause of at least 3.5 characters marks the end of the frame (1.75 ms above 19200 baud).
  3. Step 3AnswerThe slave checks the CRC and answers with the data, or with an exception code.
  4. Step 4Next slaveThe master reads the answer, or gives up after its timeout, and asks the next slave.

The Industrial Shields library follows these steps for you. Your sketch only sends the request and checks for the answer in loop().

Hardware and software you need

Wire the RS-485 port of each PLC family

Power the PLC with 12 to 24 Vdc, connect the bus and set the communication switches. The full switch tables for each model are in the RS-485 tutorial for Arduino and ESP32 PLCs.

PLC familyRS-485 terminalsHalf duplex (2 wires)Full duplex (4 wires)
M-DuinoA+, B-, Y+, Z-A+ to A+, B- to B-. Bottom switch 1 OFFA+ to Y+, B- to Z-, Y+ to A+, Z- to B-. Bottom switch 1 ON
Ardbox HF+ with hardware RS-485A+, B-, Y+, Z-A+ to A+, B- to B-Same as M-Duino. HD/FD with right top switch 1 (Analog) or 2 (Relay)
ESP32 PLCA+, B-A+ to A+, B- to B-. Switch 3 OFFNot available
ESP32 PLC 14A+, B-A+ to A+, B- to B-. No switchNot available

A Modbus RTU bus is almost always half duplex: one twisted pair, all devices in a line, and a 120 Ω termination resistor at each end. Connect the signal ground (GND) of the devices too when they have no common reference. The RS-485 basics explain why.

Select the board and the model in the Arduino IDE

PLCTools > BoardTools > Model
M-DuinoIndustrial Shields AVR Boards > M-Duino [Extension] familyM-Duino [Module]
ArdboxIndustrial Shields AVR Boards > Ardbox [Extension] familyArdbox [Module] HF+ w/HW RS-485
ESP32 PLCIndustrial Shields ESP32 Boards > ESP32 PLC FamilyESP32 PLC [Module]
ESP32 PLC 14Industrial Shields ESP32 Boards > 14 IOS PLC FamilyNo model menu

The ready-made sketches are in File > Examples > Modbus (or File > Examples > Tools40 > Modbus in older installations). There is one master example for each function code and one slave example.

Modbus RTU master example: read holding registers

This sketch reads 4 holding registers (40001 to 40004) from slave 1 every second and prints them on the serial monitor. It compiles without warnings for an M-Duino 21+ with the Industrial Shields AVR boards 1.2.1 and Modbus library 1.1.0. On an Ardbox HF+ with hardware RS-485 and on an ESP32 PLC the sketch does not change: their RS485 library has the same begin(baudrate, duplex, config).

// Modbus RTU master: read 4 holding registers from slave 1 every second
// M-Duino, Ardbox HF+ w/HW RS-485 and ESP32 PLC (Industrial Shields Modbus library 1.1.0)
#include <RS485.h>
#include <ModbusRTUMaster.h>

const uint32_t BAUDRATE = 19200UL;   // same on master and slaves
const uint8_t SLAVE_ID = 1;          // address of the slave (1 to 247)
const uint16_t FIRST_REG = 0;        // register 40001 = address 0
const uint16_t NUM_REGS = 4;

ModbusRTUMaster master(RS485);
uint32_t lastRequest = 0;

void setup() {
  Serial.begin(115200UL);
  RS485.begin(BAUDRATE, HALFDUPLEX, SERIAL_8E1);  // 8 data bits, even parity, 1 stop bit
  master.begin(BAUDRATE);                         // sets the internal t3.5 and t1.5 timings
  master.setTimeout(200);                         // response timeout in ms (default 1000)
}

void loop() {
  // 1. Send a request only when the master is idle
  if (!master.isWaitingResponse() && millis() - lastRequest >= 1000UL) {
    if (!master.readHoldingRegisters(SLAVE_ID, FIRST_REG, NUM_REGS)) {
      Serial.println("Request not sent");
    }
    lastRequest = millis();
  }

  // 2. Poll for the answer: the functions do not block
  if (master.isWaitingResponse()) {
    ModbusResponse response = master.available();

    if (master.hasException()) {               // timeout, CRC error, wrong slave...
      Serial.print("Communication error: ");
      Serial.println(master.getExceptionMessage());
      master.clearException();
    }

    if (response) {
      if (response.hasError()) {               // the slave answered with an exception code
        Serial.print("Slave exception: ");
        Serial.println(response.getErrorMessage());
      } else {
        for (uint16_t i = 0; i < NUM_REGS; i++) {
          Serial.print(response.getRegister(i));
          if (i < NUM_REGS - 1) Serial.print(", ");
        }
        Serial.println();
      }
    }
  }
}

Three details that cause most of the questions:

  • The baud rate goes twice. RS485.begin() opens the port and master.begin() uses the same value to compute the 3.5 and 1.5 character times. If you leave master.begin() empty, it assumes 19200.
  • Call setTimeout() after begin(). begin() sets the response timeout back to 1000 ms.
  • A false from readHoldingRegisters() means "not sent". The master is still busy with the previous request, or the slave address is 0 or above 247. It never means that the slave failed.

The Modbus library needs a hardware serial port. Older M-Duino models without a "+" in the name drive RS-485 through a software serial port, so the library cannot use it. On those models the examples of the library use the RS-232 port instead (ModbusRTUMaster master(RS232);).

Before you upload, check that baud rate, parity and stop bits are the same in the master and in every slave. 8E1 (even parity) is the Modbus default. Many meters and drives come set to 8N1 or 8N2, so read their manual. Then upload with Sketch > Upload.

Modbus RTU slave example: a PLC that answers the master

When the slave is another PLC, it shares four arrays with the library: coils, discrete inputs, holding registers and input registers. The library answers the master from these arrays, and your sketch keeps them up to date. This slave uses the pin names of an M-Duino 21+. Change them for your model.

// Modbus RTU slave with address 1: 2 coils, 2 discrete inputs, 2 holding and 2 input registers
#include <RS485.h>
#include <ModbusRTUSlave.h>

const uint32_t BAUDRATE = 19200UL;
const uint8_t SLAVE_ID = 1;

bool coils[2];                  // FC 01, 05, 15  -> Q0_0, Q0_1
bool discreteInputs[2];         // FC 02          <- I0_0, I0_1
uint16_t holdingRegisters[2];   // FC 03, 06, 16  -> set points written by the master
uint16_t inputRegisters[2];     // FC 04          <- analog inputs I0_7, I0_8 (0 to 1023)

ModbusRTUSlave slave(RS485, SLAVE_ID);

void setup() {
  RS485.begin(BAUDRATE, HALFDUPLEX, SERIAL_8E1);
  slave.setCoils(coils, 2);
  slave.setDiscreteInputs(discreteInputs, 2);
  slave.setHoldingRegisters(holdingRegisters, 2);
  slave.setInputRegisters(inputRegisters, 2);
  slave.begin(BAUDRATE);
}

void loop() {
  discreteInputs[0] = digitalRead(I0_0);
  discreteInputs[1] = digitalRead(I0_1);
  inputRegisters[0] = analogRead(I0_7);
  inputRegisters[1] = analogRead(I0_8);

  slave.update();               // answers one request when it arrives, never blocks

  digitalWrite(Q0_0, coils[0]);
  digitalWrite(Q0_1, coils[1]);
}

Index 0 of each array is address 0 for the master. In this slave, the master writes Q0_0 with writeSingleCoil(1, 0, true) and reads I0_7 with readInputRegisters(1, 0, 1). The ModbusRTUSlave example of the library does the same with all the inputs and outputs of the first zone of the PLC. Since library version 1.1.0 it detects the PLC model, so it is no longer limited to the M-Duino 21+.

Want a full M-Duino to ESP32 PLC example with both sketches? See how to communicate an M-Duino PLC and an ESP32 PLC with Modbus RTU.

Function reference of the Modbus RTU master

All read and write functions send a request and return at once. They return true if the request was queued and false if the master was busy or a parameter was wrong. The data arrives later through master.available().

FunctionFCWhat it doesRead the answer with
readCoils(slave, addr, quantity)01Reads 1 to 2000 coils (read and write bits)response.isCoilSet(i)
readDiscreteInputs(slave, addr, quantity)02Reads 1 to 2000 discrete inputs (read-only bits)response.isDiscreteInputSet(i)
readHoldingRegisters(slave, addr, quantity)03Reads 1 to 125 holding registers (16-bit, read and write)response.getRegister(i)
readInputRegisters(slave, addr, quantity)04Reads 1 to 125 input registers (16-bit, read-only)response.getRegister(i)
writeSingleCoil(slave, addr, value)05Sets one coil to true or falseresponse.hasError()
writeSingleRegister(slave, addr, value)06Writes one holding register (0 to 65535)response.hasError()
writeMultipleCoils(slave, addr, values, quantity)15Writes a bool array to consecutive coilsresponse.hasError()
writeMultipleRegisters(slave, addr, values, quantity)16Writes a uint16_t array to consecutive holding registersresponse.hasError()

Other functions of the master: isWaitingResponse(), available(), setTimeout(ms), hasException(), getExceptionMessage() and clearException(). The response also has getSlave(), getFC(), getErrorCode() and getErrorMessage().

Addresses start at 0. Register 40001 in a device manual is address 0, and 40009 is address 8. If a value comes out shifted by one register, this is the reason. The write single register tutorial shows the other function codes with an M-Duino.

Errors you will see first and what they mean

What the sketch printsWhere it comes fromWhat to check
Timeout exceptionmaster.getExceptionMessage(): no answer within the timeoutSlave address, baud rate and parity, A and B swapped, HD/FD switch, slave powered
CRC exceptionThe answer arrived damagedTermination at both ends, cable length, common ground, two masters on the bus
Illegal function (01)response.getErrorMessage(): the slave does not support that function codeUse the function code from the device manual (03 or 04)
Illegal data address (02)The register does not exist in the slaveRegister number minus 1, and the quantity
Illegal data value (03)The slave rejects the quantity or the valueRange of the value, quantity per request
Request not sentThe read or write function returned falseWait for isWaitingResponse() to be false; slave address 1 to 247

For intermittent faults on a bus that worked before, follow the RS-485 troubleshooting checklist.

Modbus RTU in real PLC projects

In the Modbus temperature network case study, local M-Duino PLCs poll Modbus RTU thermometers on RS-485 and a main PLC collects the readings on Modbus TCP. The PLC Solution Library has code for the same jobs: a Modbus RTU master on an ESP32 PLC with timeouts and 32-bit values, speed control of a frequency drive and a Modbus TCP to RTU gateway on an M-Duino.

See every frame before you wire anything. In the Modbus network simulator, an M-Duino 21 polls four Modbus RTU slaves on RS-485 and serves the data on Modbus TCP. Switch a slave off, swap the word order or change the baud rate, and find the fault in the frames.

Open the Modbus network simulatorAsk an engineer about your Modbus network

Ardbox Q industrial PLC with Arduino UNO Q

Ardbox Q

Modbus RTU with Arduino UNO Q in a DIN-rail PLC

Ardbox Q is the first industrial PLC with Arduino UNO Q. It has an RS-485 port for Modbus RTU, is programmed with the Arduino IDE and adds Debian Linux on a separate processor for data and dashboards.

See Ardbox Q How Ardbox Q works

FAQ about Modbus RTU on Arduino and ESP32 PLCs

Can one PLC be Modbus RTU master and slave at the same time?

Not on the same bus, because a Modbus RTU bus has one master. A PLC can be master on its RS-485 port and slave on another serial port, or slave on Modbus TCP over Ethernet. That is how a gateway works.

How many slaves can a PLC poll on one RS-485 bus?

Modbus allows addresses 1 to 247. RS-485 allows 32 unit loads per segment without a repeater, and more with 1/4 or 1/8 unit load transceivers. In practice the polling time limits it: each request waits for its answer or its timeout.

Which baud rate should I use?

The one the slaves support. 9600 and 19200 baud are the most common in meters and drives. Higher rates shorten the cycle but need a good cable and termination.

Does Modbus RTU work on the RS-232 port?

Yes. Create the master with ModbusRTUMaster master(RS232); and open the port with RS232.begin(baudrate, SERIAL_8E1). RS-232 connects only two devices over a short distance.

How do I read a 32-bit value or a float?

Read two consecutive registers and join them. The word order depends on the device: some send the high word first, others the low word. If the value looks wrong but there is no error, swap the two words.

What is the difference with Modbus TCP?

Modbus TCP carries the same function codes over Ethernet, with an IP address and a unit identifier instead of the slave address and the CRC. See Modbus TCP master on Arduino and ESP32 PLCs and Modbus RTU vs Modbus TCP vs OPC UA.

​Search in our Blog

Modbus RTU with Arduino and ESP32 based PLCs
Boot & Work Corp. S.L, Martí Schnabel February 11, 2025
Share this post
Tags

Looking for your ideal Programmable Logic Controller?

Take a look at this product comparison with other industrial controllers Arduino-based. 

We are comparing inputs, outputs, communications and other features with the ones of the relevant brands.

PLC Comparison