How to communicate M-Duino PLC and ESP32 PLC with Modbus RTU - Part 2

Learn how to communicate open source-based industrial PLCs using Modbus protocol. No additional libraries needed.
December 14, 2022 by
How to communicate M-Duino PLC and ESP32 PLC with Modbus RTU - Part 2
Boot & Work Corp. S.L., Bernat Brunet Pedra
ESP32 board

Introduction

The aim of this post is to show how easy it is to communicate controllers using the Modbus protocol without the need of additional libraries. 

As a continuation of Part 1, in Part 2, the ESP32 will act as the master controller, and the M-Duino industrial PLC will be the slave, just the opposite of Part 1. Part 2 is focused on the master PLC, the slave controller will use the Industrial Shields libraries to simplify the explanation.

For more information about how to program the slave controller without extra libraries, check Part 1.

Requirements

Connection between industrial controllers

To connect them properly, you will need a pair of twisted pair cables. Using the Half Duplex configuration on the M-Duino PLC, connect both wires to RS485 B- and A+. These two wires must be connected to the RS485 pins of ESP32s as well, like so:

M-Duino PLC pin B- to ESP32 PLC pin B-

M-Duino PLC pin A+ to M-Duino PLC pin A+

Remember that to work with RS485 and ESP32 PLC you will need to configure its switches. Turning off the third switch will enable RS485 communication. Check the serigraphy of the PLC if you have any doubts.

M-Duino code

In this case the M-Duino will be the slave controller, so to implement its program just open the Arduino IDE and go to File -> Examples -> Modbus -> ModbusRTUSlave. (You can find the Modbus repository here).

The only modification needed here is to change this line:

RS485.begin(RS485_RATE, HALFDUPLEX, SERIAL_8E1);

for this one:

RS485.begin(RS485_RATE, HALFDUPLEX, SERIAL_8N1);

This will set the RS485 communication to have 8 data bits, no parity and 1 stop bit.

Also, the code uses M-Duino 21+ mapping. If having any other model, change the definition of the array digitalOutputsPins to match all your digital outputs. For example, for M-Duino 58+ PLC, the array should be like this:

int digitalOutputsPins[] = {
#if defined(PIN_Q2_3)
    Q0_0, Q0_1, Q0_2, Q0_3, Q0_4, Q1_0, Q1_1, Q1_2, Q1_3, Q1_4, Q2_0, Q2_1, Q2_2, Q2_3,
#endif
};

The other arrays will not be used, so it makes no sense to modify them.

This code will check for Modbus requests and update its digital and analogue outputs depending on the messages  received automatically. Upload the code to your M-Duino PLC using the Industrial Shields boards, and let's jump into the ESP32 code.

ESP32 code

As seen in Part 1, the Modbus request ADU message is like so:

ESP32 code

To send ADUs like in the image above, you can use the following example. Following the structure from Part 1, two possible request messages can be done: writeSimpleCoil or writeMulipleCoils.

/* 
    Copyright (c) 2018 Boot&Work Corp., S.L. All right reserved
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU Lesser General Public License for more details.
    You should have received a copy of the GNU Lesser General Public License
    along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <RS485.h>
uint32_t lastSentTime = 0UL;
static bool coilValue = true;
static int n = 0;


/////////////////////////////////////////////////////////
void setup() {
    Serial.begin(9600UL);
    RS485.begin(38400);
}

/////////////////////////////////////////////////////////
uint16_t crc16(const uint8_t *ptr, uint16_t len) {
    int i;
    uint16_t crc = 0xffff;
    
    while(len--) {
        crc ^= *ptr++;
        for (i = 0; i < 8; ++i) {
            crc = (crc & 1) ? ((crc >> 1) ^0xA001) : (crc >> 1);
        }
    }

    return ((crc >> 8) & 0xff) | ((crc << 8) & 0xff00);
}

/////////////////////////////////////////////////////////
void sendWriteSingleCoil(uint8_t slave_address, uint16_t address, bool value) {
    uint8_t aux[8];
    
    aux[0] = slave_address;
    aux[1] = 0x05;
    aux[2] = (address >> 8);
    aux[3] = (address >> 0);
    aux[4] = (value) ? 255 : 0;
    aux[5] = 0;

    uint16_t crc = crc16(aux, 6);

    aux[6] = (crc >> 8);
    aux[7] = (crc >> 0);

    sendRequest(aux, 8);
}

/////////////////////////////////////////////////////////

void sendWriteMultipleCoils(uint8_t slave_address, uint16_t address, const bool *values, uint16_t quantity) {
    uint8_t aux[10];
    uint16_t val = 0;
    
    aux[0] = slave_address;
    aux[1] = 0x0F;
    aux[2] = (address >> 8);
    aux[3] = (address >> 0);
    aux[4] = (quantity >> 8);
    aux[5] = (quantity >> 0);

    for (int i = 0; i < quantity; i++) {
        val += pow(2,i)*(*values++);
    }

    aux[6] = (val >> 8) + 1;
    aux[7] = (val >> 0);

    uint16_t crc = crc16(aux, 8);

    aux[8] = (crc >> 8);
    aux[9] = (crc >> 0);

    sendRequest(aux, 10);
}

/////////////////////////////////////////////////////////
void sendRequest(const uint8_t *mess, uint8_t length) {
    for (int i = 0; i < length; i++) {
        RS485.write(*mess++);
    }
}

/////////////////////////////////////////////////////////
void loop() {
    uint16_t num = 14;
    
    // send a request every 1000 ms
    if (millis() - lastSentTime > 1000) {
        // change the value inside the next if to make a writeSingleCoil request or writeMultipleCoils
        if (0) {
            sendWriteSingleCoil(31, n++, coilValue);
            if (n >= num) {
                n = 0;
                coilValue = !coilValue;
            }
        }
        else {
            bool values[num];
            for (int i = 0; i < num; ++i) {
                values[i] = random() & 0x01;
            }
            sendWriteMultipleCoils(31, 0, values, num);
        }    
        lastSentTime = millis();
    }
}

First, 3 global variables are initialised. Then, Serial and RS485 objects begin with their respective baudrates. The RS485 baudrate must be the same as in the M-Duino code, and also the parity (SERIAL_8N1). As this parity is the default, it is not necessary to specify it.

After the setup() function, there is the crc16() function. This function is used to calculate the CRC redundancy for the entire message of the request. Because the CRC redundancy can be calculated using different algorithms, it is important to make the same operations as the slave open source based controller.

Then, Then, the functions are defined, the purpose of which is to carry out the corresponding request. Each of them has the same size as those seen in part Part 1 (writeSingleCoil() with an 8 bytes size and writeMultipleCoils() with a size of 10 bytes, and have the same utility as those implemented in the ModbusRTUMaster library. Finally, there is the sendRequest() function that sends the corresponding request to the RS485 port.

In the loop() function, a request is done every second. It can be either writeSingleCoil() or writeMultipleCoils(), depending on the value within the second "if" conditional. 

On the one hand, if that conditional is true, then writeSingleCoil is chosen. With this, every second a new digital output will be turned on for a total of num outputs. As in our case an M-Duino 58+ PLC is used, the total number is 14, so one loop will be 14 seconds in total, one for each digital output.

Change that value to adjust your total numbers of digital outputs if you have any other model. Once a loop is done, the coilValue will toggle and then repeat the sequence but turning off all outputs. This sequence will repeat infinitely.

On the other hand, if the conditional value is false, then writeMultipleCoils is selected. In this case, an array of random numbers is generated and then a request is made, asking the slave controller to trigger all digital outputs, depending on the value of each element of the array.

For an array like the following: { 1 , 0 , 1 , 1 , 0 }, digital outputs number 0, 2 and 3 will turn on and outputs 1 and 4 will turn off. Remember to change the value of num, depending on the number of digital outputs of your PLC for industrial automation.

How to communicate M-Duino PLC and ESP32 PLC with Modbus RTU - Part 2

Conclusions

In these two posts, you have learned how requests are made in Modbus protocol communication to understand the simplest and most straightforward Modbus applications, but this is just the beginning.

After reading Part 1 and Part 2, you are now ready to jump into using more complex Modbus libraries and use the protocol to its full potential!

​Search in our Blog

How to communicate M-Duino PLC and ESP32 PLC with Modbus RTU - Part 2
Boot & Work Corp. S.L., Bernat Brunet Pedra December 14, 2022
Share this post

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.


Industrial PLC comparison >>>