Cómo comunicar un PLC M-Duino y un PLC ESP32 con Modbus RTU - Parte 2

Aprende a comunicar PLCs industriales basados en código abierto utilizando el protocolo Modbus. Sin necesidad de bibliotecas adicionales.
14 de diciembre de 2022 por
Cómo comunicar un PLC M-Duino y un PLC ESP32 con Modbus RTU - Parte 2
Boot & Work Corp. S.L., Bernat Brunet Pedra
Placa ESP32

Introducción

En este post es mostrarte lo fácil que es comunicar controladores utilizando el protocolo Modbus, sin necesidad de librerías adicionales. 

Como continaucioón de la Parte 1, en la Parte 2, el ESP32 actuará como controlador maestro, y el PLC industrial M-Duino será el esclavo, justo lo contrario que en la Parte 1. La parte 2 se centra en el PLC maestro, el controlador esclavo utilizará las librerías de Industrial Shields para simplificar la explicación.

Para obtener más información sobre cómo programar el controlador esclavo sin bibliotecas adicionales, consulta la Parte 1.

Requisitos

Cómo conectar controladores industriales

Para conectarlos correctamente, necesitarás un par de cables trenzados. Usando la configuración Half Duplex en el PLC M-Duino, conecta ambos cables a RS485 B- y A+. Estos dos cables deben estar conectados a los pines RS485 de ESP32s también, así:

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

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

Recuerda que para trabajar con RS485 y el PLC ESP32, necesitarás configurar los interruptores. Apagando el tercer interruptor habilitarás la comunicación RS485. Comprueba la serigrafía del PLC si tienes dudas.

Código M-Duino

En este caso, el M-Duino será el controlador esclavo, así que para implementar su programa solo tienes que abrir el IDE de Arduino e ir a Archivo -> Ejemplos -> Tools40 -> Modbus -> ModbusRTUSlave. (Puedes encontrar el repositorio de Tools40 aquí).

La única modificación necesaria aquí es cambiar esta línea:

RS485.begin(RS485_RATE, HALFDUPLEX, SERIAL_8E1);

por esta otra:

RS485.begin(RS485_RATE, HALFDUPLEX, SERIAL_8N1);

Esto configurará la comunicación RS485 para tener 8 bits de datos, sin paridad y 1 bit de parada..

Además, el código utiliza el mapeado de M-Duino 21+. Si tienes cualquier otro modelo, cambia la definición del array digitalOutputsPins para que coincida con todas tus salidas digitales. Por ejemplo, para M-Duino 58+ PLC, el array debería ser así:

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
};

Las otras matrices no se utilizarán, por lo que no tiene sentido modificarlas.

Este código comprobará si hay peticiones Modbus y actualizará sus salidas digitales y analógicas en función de los mensajes recibidos automáticamente. Sube el código a tu PLC M-Duino usando lasplacas Industrial Shields, y pasemos al código del ESP32.

Código ESP32

Tal como has visto en la Parte 1, el mensaje Modbus request ADU es así:

Código ESP32

Para enviar ADUs como en la imagen de arriba, puedes utilizar el siguiente ejemplo. Siguiendo la estructura de la Parte 1, se pueden hacer dos posibles mensajes de petición: writeSimpleCoil o 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();
    }
}

En primer lugar, se inicializan 3 variables globales. A continuación, se inician los objetos Serial y RS485 con sus respectivas tasas de baudios. La velocidad de transmisión RS485 debe ser la misma que en el código M-Duino, y también la paridad (SERIAL_8N1). Como esta paridad es la predeterminada, no es necesario especificarla.

Después de la función setup(), está la función crc16(). Esta función se utiliza para calcular la redundancia CRC para todo el mensaje de la petición. Debido a que la redundancia CRC se puede calcular utilizando diferentes algoritmos, es importante hacer las mismas operaciones que el controlador esclavo basado en código abierto.

A continuación, se definen las funciones cuya finalidad es realizar la petición correspondiente. Cada una de ellas tiene el mismo tamaño que las vistas en la parte 1 (writeSingleCoil() con un tamaño de 8 bytes y writeMultipleCoils() con un tamaño de 10 bytes, y tienen la misma utilidad que las implementadas en la librería ModbusRTUMaster. Por último, está la función sendRequest() que envía la petición correspondiente al puerto RS485.

En la función loop(), se realiza una petición cada segundo. Puede ser writeSingleCoil() o writeMultipleCoils(), dependiendo del valor dentro de la segunda condicional "if"

Por un lado, si esa condicional es verdadera, entonces se elige writeSingleCoil. Con esto, cada segundo se encenderá una nueva salida digital para un num total salidas. Como en este caso se utiliza un PLC M-Duino 58+, el número total es 14, por lo que un bucle será de 14 segundos en total, uno por cada salida digital.

Cambia ese valor para ajustar el número total de salidas digitales si tienes cualquier otro modelo. Una vez finalizado un bucle, el coilValue conmutará y luego repetirá la secuencia pero apagando todas las salidas. Esta secuencia se repetirá infinitamente.

Por otro lado, si el valor condicional es falso, entonces se selecciona writeMultipleCoils. En este caso, se genera una matriz de números aleatorios y, a continuación, se realiza una petición, solicitando al controlador esclavo que active todas las salidas digitales, en función del valor de cada elemento de la matriz.

Para una matriz como la siguiente { 1 , 0 , 1 , 1 , 0 }, las salidas digitales número 0, 2 y 3 se encenderán y las salidas 1 y 4 se apagarán. Recuerda cambiar el valor de num, dependiendo del número de salidas digitales de tu PLC para automatización industrial.

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

Conclusiones

En estos dos posts, has aprendido cómo se realizan las peticiones en la comunicación del protocolo Modbus para entender las aplicaciones Modbus más simples y sencillas, pero esto es sólo el principio.

Después de leer la Parte 1 y la Parte 2, ya estás preparado para dar el salto al uso de librerías Modbus más complejas y utilizar el protocolo en todo su potencial.

Buscar en nuestro blog

Cómo comunicar un PLC M-Duino y un PLC ESP32 con Modbus RTU - Parte 2
Boot & Work Corp. S.L., Bernat Brunet Pedra 14 de diciembre de 2022
Compartir

¿Estás buscando tu Controlador Lógico Programable ideal?

Echa un vistazo a esta comparativa de producto de varios controladores industriales basados en Arduino.

Comparamos entradas, salidas, comunicaciones y otras especificaciones con las de los equipos de otras marcas destacadas.


Industrial PLC comparison >>>