#9 Ejemplos: control de entrada-salida a distancia mediante Ethernet

                Al leer este capítulo hay que tener en cuenta una serie de puntos:  

                Puedes practicar con una placa Arduino. En los diferentes PLCs utilizamos placas Arduino originales, por este motivo no es necesario que practiques con nuestros equipos.

                (Recuerda: Para nosotros un PLC funciona como una placa Arduino, nuestros PLCs utilizan placas Arduino originales por lo que puedes probar este curso utilizando placas Arduino originales).

                Introducción 

                Este es un nuevo capítulo del curso de programación de Arduino para uso industrial. En este capítulo veremos un ejemplo sobre cómo gestionar las entradas y salidas de un PLC Arduino de forma remota usando Ethernet, relacionado con el capítulo anterior. Así, hablaremos de este ejemplo de control y del protocolo Ethernet. 

                Hay muchos protocolos Ethernet diferentes; UDP, por ejemplo, pero el más utilizado es TCP, del que veremos un ejemplo práctico. Nuestro equipo de programación desarrolló un nuevo protocolo, diseñado para funcionar mejor en nuestros equipos y creado para nuestros clientes, llamado SimpleComm.

                Explicación teórica

                En este capítulo, vamos a ver un ejemplo práctico sobre cómo gestionar las entradas y salidas de un PLC mediante Ethernet. Vamos a utilizar el protocolo TCP para establecer este tipo de comunicación pero, respecto a esto, tenemos que recordar el modelo OSI explicado en el capítulo anterior y, relacionándolo con nuestro modelo concreto, podemos ver un esquema como el siguiente:

                7 capas del modelo OSI - Lección 9 - Programación de Arduino en entornos industriales

                Ejemplo 1

                This type of communication is based in a master and a slave. You can find more information about this kind of relation in this post -->  Configuración del protocolo RS-485 en Arduino IDE
                :

                El ejemplo práctico que veremos en este capítulo se basa en dos entradas de nuestro blog, divididas en partes salva y maestra:

                Slave

                En este ejemplo, como hemos visto en los párrafos anteriores, el protocolo de la capa de aplicación utilizado es Modbus. En primer lugar, tenemos que ver algunos aspectos de este para entender el código de una mejor manera.


                Tipo de objetoAccesoTamaño
                BobinaRead-write1bit
                Entrada discretaRead1 bit
                Registro de entradaRead16 bits
                Registros de retenciónRead-write 16 bits
                Normalmente las bobinas a se utilizan para escribir valores digitales en una salida. Las entradas discretas se utilizan para leer las entradas digitales. Los registros se utilizan para comunicar datos entre los dispositivos y también suelen utilizarse para las E/S analógicas. 
                En este caso, veremos cómo utilizar la Librería Slave de Tools40.

                Requisitos


                Funciones de Modbus TCP Slave


                El módulo ModbusTCPSlave implementa las capacidades de Modbus TCP Slave.

                #include <ModbusTCPSlave.h>

                ModbusTCPSlave slave;

                El puerto TCP por defecto es el  502 pero puedes cambiarlo con:

                // Set the TCP listening port to 510 instead of 502
                ModbusTCPSlave slave(510);

                Para asignar las bobinas, las entradas discretas, los registros de retención y las direcciones de los registros de entrada con los valores de las variables deseadas, el módulo utiliza cuatro matrices de variables:

                bool coils[NUM_COILS];
                bool discreteInputs[NUM_DISCRETE_INPUTS];
                uint16_t holdingRegistesr[NUM_HOLDING_REGISTERS];
                uint16_t inputRegisters[NUM_INPUT_REGISTERS];

                Las longitudes de estas matrices dependen de la aplicación y de los usos de los registros. Obviamente, los nombres de las matrices también dependen de tus preferencias.

                Para asociar las matrices de registros con la biblioteca es posible utilizar estas funciones en setup:

                slave.setCoils(coils, NUM_COILS);
                slave.setDiscreteInputs(discreteInputs, NUM_DISCRETE_INPUTS);
                slave.setHoldingRegisters(holdingRegisters, NUM_HOLDING_REGISTERS);
                slave.setInputRegisters(inputRegisters, NUM_INPUT_REGISTERS);

                No es necesario tener todo tipo de mapeo de registros para que funcione, sólo los utilizados por la aplicación.

                Para iniciar el servidor ModbusTCP, llama a la función begin después de la asignación de registros. También es posible llamar a la función begin antes de los registros. Recuerda que debes comenzar la Ethernet antes del objeto ModbusTCPSlave en setup.

                // Init the Ethernet
                Ethernet.begin(mac, ip);

                // Init the ModbusTCPSlave object
                slave.begin();

                En este momento, el servidor ModbusTCP está funcionando y lo único importante que hay que hacer es actualizar el objeto ModbusTCPSlave a menudo en la función loop , y los valores de mapeo de los registros para actualizar las variables, entradas y salidas.

                // Update discrete inputs and input registers values
                discreteInputs[0] = digitalRead(I0_7);
                inputRegisters[0] = analogRead(I0_0);
                // ...

                // Update the ModbusTCPSlave object
                slave.update();

                // Update coils and holding registers
                digitalWrite(Q0_0, coils[0]);
                // ...

                Ejemplo de software para el controlador de automatización basado en Arduino

                /*
                   Copyright (c) 2018 Boot&Work Corp., S.L. All rights 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 <ModbusTCPSlave.h>
                #if defined(MDUINO_PLUS)
                #include <Ethernet2.h>
                #else
                #include <Ethernet.h>
                #endif
                // Ethernet configuration values
                uint8_t mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE };
                IPAddress ip(10, 10, 10, 4);
                int port = 502;
                // Modbus registers mapping
                // This example uses the M-Duino21+ mapping
                int digitalOutputsPins[] = {
                #if defined(PIN_Q0_4)
                  Q0_0, Q0_1, Q0_2, Q0_3, Q0_4,
                #endif
                };
                int digitalInputsPins[] = {
                #if defined(PIN_I0_6)
                  I0_0, I0_1, I0_2, I0_3, I0_4, I0_5, I0_6,
                #endif
                };
                int analogOutputsPins[] = {
                #if defined(PIN_A0_7)
                  A0_5, A0_6, A0_7,
                #endif
                };
                int analogInputsPins[] = {
                #if defined(PIN_I0_12)
                  I0_7, I0_8, I0_9, I0_10, I0_11, I0_12,
                #endif
                };
                #define numDigitalOutputs int(sizeof(digitalOutputsPins) / sizeof(int))
                #define numDigitalInputs int(sizeof(digitalInputsPins) / sizeof(int))
                #define numAnalogOutputs int(sizeof(analogOutputsPins) / sizeof(int))
                #define numAnalogInputs int(sizeof(analogInputsPins) / sizeof(int))
                bool digitalOutputs[numDigitalOutputs];
                bool digitalInputs[numDigitalInputs];
                uint16_t analogOutputs[numAnalogOutputs];
                uint16_t analogInputs[numAnalogInputs];
                // Define the ModbusTCPSlave object
                ModbusTCPSlave modbus(port);
                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void setup() {
                  Serial.begin(9600UL);
                  // Init variables, inputs and outputs
                  for (int i = 0; i < numDigitalOutputs; ++i) {
                    digitalOutputs[i] = false;
                    digitalWrite(digitalOutputsPins[i], digitalOutputs[i]);
                  }
                  for (int i = 0; i < numDigitalInputs; ++i) {
                    digitalInputs[i] = digitalRead(digitalInputsPins[i]);
                  }
                  for (int i = 0; i < numAnalogOutputs; ++i) {
                    analogOutputs[i] = 0;
                    analogWrite(analogOutputsPins[i], analogOutputs[i]);
                  }
                  for (int i = 0; i < numAnalogInputs; ++i) {
                    analogInputs[i] = analogRead(analogInputsPins[i]);
                  }
                  // Init Ethernet
                  Ethernet.begin(mac, ip);
                  Serial.println(Ethernet.localIP());
                  // Init ModbusTCPSlave object
                  modbus.begin();
                  modbus.setCoils(digitalOutputs, numDigitalOutputs);
                  modbus.setDiscreteInputs(digitalInputs, numDigitalInputs);
                  modbus.setHoldingRegisters(analogOutputs, numAnalogOutputs);
                  modbus.setInputRegisters(analogInputs, numAnalogInputs);
                }
                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void loop() {
                  // Update inputs
                  for (int i = 0; i < numDigitalInputs; ++i) {
                    digitalInputs[i] = digitalRead(digitalInputsPins[i]);
                  }
                  for (int i = 0; i < numAnalogInputs; ++i) {
                    analogInputs[i] = analogRead(analogInputsPins[i]);
                  }
                  // Process modbus requests
                  modbus.update();
                  // Update outputs
                  for (int i = 0; i < numDigitalOutputs; ++i) {
                    digitalWrite(digitalOutputsPins[i], digitalOutputs[i]);
                  }
                  for (int i = 0; i < numAnalogOutputs; ++i) {
                    analogWrite(analogOutputsPins[i], analogOutputs[i]);
                  }
                }


                Master

                Ahora, teniendo en cuenta la misma información de Modbus que vimos en la parte de Esclavo y los mismos Requisitos, vamos a ver la parte de maestro:

                Funciones Master Modbus TCP

                The functions to read and write slave values are:

                readCoils(client, slave_address, address, quantity);
                readDiscreteInputs(client, slave_address, address, quantity);
                readHoldingRegisters(client, slave_address, address, quantity);
                readInputRegisters(client, slave_address, address, quantity);
                writeSingleCoil(client, slave_address, address, value);
                writeSingleRegister(client, slave_address, address, value);
                writeMultipleCoils(client, slave_address, address, values, quantity);
                writeMultipleRegisters(client, slave_address, address, values, quantity);

                Donde

                • client es el EthernetClient conectado al esclavo.
                • slave_address es la dirección del esclavo Modbus TCP.
                • address es la dirección de la bobina, de la entrada digital, del registro de retención o del registro de entrada. Normalmente, esta dirección es el número de la bobina, de la entrada digital, del registro de retención o del registro de entrada menos 1: el número del registro de retención 40009 tiene la dirección 8.
                • quantity es el número de bobinas, entradas digitales, registros de retención o registros de entrada a leer/escribir.
                • value es el valor dado de los registros de bobina o de retención en una operación de escritura. Dependiendo de la función, el tipo de datos cambia. Una bobina está representada por un valor bool y un registro de retención está representado por un valor uint16_t .

                En una función de lectura/escritura múltiple el address es la dirección del primer elemento. En una función de escritura múltiple el argumento valueses un array de valores a escribir.

                Es importante notar que estas funciones no son de bloqueo, por lo que no devuelven el valor leído. Devuelven true o false dependiendo del estado actual del módulo. Si hay una petición Modbus pendiente o el cliente no está conectado, devuelven false.

                // Read 5 holding registers from address 0x24 of slave with address 0x10if (master.readHoldingRegisters(client, 0x10, 0x24, 5)) {
                // OK, the request is being processed
                } else {
                // ERROR, the master is not in an IDLE state
                }

                Existe la función available() para comprobar las respuestas del esclavo.

                ModbusResponse response = master.available();
                if (response) {
                // Process response
                }

                El ModbusResponse implementa algunas funciones para conseguir la información de respuesta:

                hasError();
                getErrorCode();
                getSlave();
                getFC();
                isCoilSet(offset);
                isDiscreteInputSet(offset);
                isDiscreteSet(offset);
                getRegister(offset);
                ModbusResponse response = master.available();
                if (response) {
                if (response.hasError()) {
                // There is an error. You can get the error code with response.getErrorCode()
                 } else {
                // Response ready: print the read holding registers
                for (int i = 0; i < 5; ++i) {
                   Serial.println(response.getRegister(i);
                  }
                 }
                }

                Los posibles errores de código son:

                0x01 ILLEGAL FUNCTION
                0x02 ILLEGAL DATA ADDRESS
                0x03 ILLEGAL DATA VALUE
                0x04 SERVER DEVICE FAILURE


                Software

                Después de ver las bases de la Librería Modbus TCP Master, podemos proceder a desarrollar un código para comunicarnos con otro dispositivo Modbus en nuestra red. En este código se muestra cómo leer registros y cómo escribir bobinas desde otro esclavo Modbus TCP/IP. Este ejemplo escribirá números aleatorios en las bobinas digitales cada segundo y también leerá 6 valores del esclavo cada 500 milisegundos. 


                /*
                   Copyright (c) 2018 Boot&Work Corp., S.L. All rights 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 <ModbusTCPMaster.h>
                #if defined(MDUINO_PLUS)
                #include <Ethernet2.h>
                #else
                #include <Ethernet.h>
                #endif

                // Ethernet configuration values
                uint8_t mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
                IPAddress ip(10, 10, 10, 3);
                IPAddress slaveIp(10, 10, 10, 4);
                uint16_t slavePort = 502;

                // Define the ModbusTCPMaster object
                ModbusTCPMaster modbus;

                // Ethernet client object used to connect to the slave
                EthernetClient slave;
                uint32_t lastSentTime = 0UL;
                uint32_t lastSentTimeReadInputs = 0UL;

                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void setup() {
                  Serial.begin(9600UL);
                  // Begin Ethernet
                  Ethernet.begin(mac, ip);
                  Serial.println(Ethernet.localIP());
                  // NOTE: it is not necessary to start the modbus master object
                }

                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void loop() {
                  // Connect to slave if not connected
                  // The ethernet connection is managed by the application, not by the library
                  // In this case the connection is opened once
                  if (!slave.connected()) {
                    slave.stop();
                    slave.connect(slaveIp, slavePort);
                    if (slave.connected()) {
                      Serial.println("Reconnected");
                    }
                  }
                  // Send a request every 1000ms if connected to slave
                  if (slave.connected()) {
                    if (millis() - lastSentTime > 1000) {
                      // Set random values
                      bool values[5];
                      for (int i = 0; i < 5; ++i) {
                        values[i] = random() & 0x01;
                      }
                      // Send a Write Multiple Coils request to the slave with address 31
                      // It requests for setting 5 coils starting in address 0
                      // IMPORTANT: all read and write functions start a Modbus transmission, but they are not
                      // blocking, so you can continue the program while the Modbus functions work. To check for
                      // available responses, call modbus.available() function often.
                      if (!modbus.writeMultipleCoils(slave, 31, 0, values, 5)) {
                        // Failure treatment
                        Serial.println("Request fail");
                      }
                      lastSentTime = millis();
                    }
                    // Check available responses often
                    if (modbus.isWaitingResponse()) {
                      ModbusResponse response = modbus.available();
                      if (response) {
                        if (response.hasError()) {
                          // Response failure treatment. You can use response.getErrorCode()
                          // to get the error code.
                          Serial.print("Error ");
                          Serial.println(response.getErrorCode());
                        } else {
                          Serial.println("Done");
                        }
                      }
                    }
                    
                    if (millis() - lastSentTimeReadInputs > 500) {
                      // Send a Read Input Registers request to the slave with address 31
                      // It requests for 6 registers starting at address 0
                      // IMPORTANT: all read and write functions start a Modbus transmission, but they are not
                      // blocking, so you can continue the program while the Modbus functions work. To check for
                      // available responses, call master.available() function often.
                      if (!modbus.readInputRegisters(slave, 31, 0, 6)) {
                        // Failure treatment
                      }
                      lastSentTimeReadInputs = millis();
                    }
                    
                    if (modbus.isWaitingResponse()) {
                      ModbusResponse response = modbus.available();
                      if (response) {
                        if (response.hasError()) {
                          // Response failure treatment. You can use response.getErrorCode()
                          // to get the error code.
                        } else {
                          // Get the input registers values from the response
                          Serial.print("Input registers values: ");
                          for (int i = 0; i < 6; ++i) {
                            Serial.print(response.getRegister(i));
                            Serial.print(',');
                          }
                          Serial.println();
                        }
                      }
                    }
                  }
                }


                Ejemplo 2

                Siguiendo la parte del ejemplo anterior del Esclavo y, cambiando un poco el código del Maestro para hacerlo más interactivo con el usuario, aquí tenemos otro ejemplo:

                Arquitectura de la aplicación

                M-Duino master tiene un menú interactivo en serie que permite al usuario controlar la aplicación. El menú tiene 6 opciones. Las primeras cuatro opciones son para controlar dos salidas del esclavo, la quinta opción es para obtener las entradas analógicas o registros del esclavo y la última opción, la sexta, es para obtener las entradas digitales o discretas del esclavo.

                Utilizamos las funciones writeSingleCoil(), readInputRegisters() y readDiscreteInputs() para comunicarnos con el esclavo. A continuación, ejecutando un pequeño bucle dependiendo del mensaje que hayamos enviado, leemos los valores utilizando response.getRegister() o response.isDiscreteInputSet().

                El resto del código es sólo configuraciones de Ethernet y comandos de serie para depurar y hacer la aplicación más interactiva.

                Software

                /*
                   Copyright (c) 2018 Boot&Work Corp., S.L. All rights 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 <ModbusTCPMaster.h>
                #if defined(MDUINO_PLUS)
                #include <Ethernet2.h>
                #else
                #include <Ethernet.h>
                #endif
                // Ethernet configuration values
                uint8_t mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
                IPAddress ip(10, 10, 10, 3);
                IPAddress slaveIp(10, 10, 10, 4);
                uint16_t slavePort = 502;
                // Define the ModbusTCPMaster object
                ModbusTCPMaster modbus;
                //
                bool registerSet = 0;
                bool discreteSet = 0;
                // Ethernet client object used to connect to the slave
                EthernetClient slave;
                uint32_t lastSentTime = 0UL;
                uint32_t lastSentTimeReadInputs = 0UL;
                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void setup() {
                  Serial.begin(9600UL);
                  // Begin Ethernet
                  Ethernet.begin(mac, ip);
                  Serial.println(Ethernet.localIP());
                  // NOTE: it is not necessary to start the modbus master object
                  mainUI();
                }
                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void loop() {
                  // Connect to slave if not connected
                  // The ethernet connection is managed by the application, not by the library
                  // In this case the connection is opened once
                  if (!slave.connected()) {
                    Serial.println("Slave not connected");
                    slave.stop();
                    slave.connect(slaveIp, slavePort);
                    if (slave.connected()) {
                      Serial.println("Reconnected");
                    }
                  }
                  // Send a request every 1000ms if connected to slave
                  if (slave.connected()) {
                    //Serial.println("Slave connected");
                  if (Serial.available()) {
                      byte chosenOption= Serial.read();
                  bool value;
                  byte address;
                  switch(chosenOption){
                      case '1': //set Q0_0 to high
                          value = 1;
                          address = 0;
                          if (!(modbus.writeSingleCoil(slave, 0, address, value))) {
                            // Failure treatment
                            Serial.println("Request fail");
                          }
                          Serial.println("Q0_0 set to HIGH");
                          break; 
                      case '2': //set Q0_0 to low
                          value = 0;
                          address = 0;
                          if (!(modbus.writeSingleCoil(slave, 0, address, value))) {
                            // Failure treatment
                            Serial.println("Request fail");
                          }
                          Serial.println("Q0_0 set to LOW");
                          break;
                      case '3': //set Q0_1 to high
                          value = 1;
                          address = 1;
                          if (!(modbus.writeSingleCoil(slave, 0, address, value))) {
                            // Failure treatment
                            Serial.println("Request fail");
                          }
                          Serial.println("Q0_1 set to HIGH");
                          break;
                      case '4':
                          value = 0;
                          address= 1;
                          if (!(modbus.writeSingleCoil(slave, 0, address, value))) {
                            // Failure treatment
                            Serial.println("Request fail");
                          }
                          Serial.println("Q0_1 set to LOW");
                          break;
                      case '5':
                          if (!modbus.readInputRegisters(slave, 0, 0, 6)) {
                            // Failure treatment
                            Serial.println("Error requesting registers");
                          }else{registerSet = true;}
                          break;
                      case '6':
                          if (!modbus.readDiscreteInputs(slave, 0, 0, 7)) {
                          // Failure treatment
                          Serial.println("Error requesting discrete input");
                          }else{discreteSet = true;} 
                          break;
                  }
                  mainUI();
                    }
                    if (modbus.isWaitingResponse()) {
                      ModbusResponse response = modbus.available();
                      if (response) {
                        if (response.hasError()) {
                          // Response failure treatment. You can use response.getErrorCode()
                          // to get the error code.
                        }else if (registerSet){
                          // Get the input registers values from the response
                          Serial.print("Input registers values: ");
                          for (int i = 0; i < 6; ++i) {
                            Serial.print(response.getRegister(i));
                            Serial.print(',');
                          }
                          registerSet = false; 
                        } else if(discreteSet) {
                          // Get the input registers values from the response
                          Serial.print("Input discrete values: [");
                          for (int i = 0; i < 7; ++i) {
                            Serial.print(response.isDiscreteInputSet(i));
                            Serial.print(',');
                          }
                          Serial.println(']');
                          discreteSet = false;
                        }
                      }
                    }
                  }
                }
                ////////////////////////////////////////////////////////////////////////////////////////////////////
                void mainUI(){
                    Serial.println("********************Modbus Test*********************");
                    Serial.println("Chose an option:");
                    Serial.println("1. Set Q0_0 to HIGH");
                    Serial.println("2. Set Q0_0 to LOW");
                    Serial.println("3. Set Q0_1 to HIGH");
                    Serial.println("4. Set Q0_1 to LOW");
                    Serial.println("5. Print slave input analog values");
                    Serial.println("6. Print slave input digital values");
                    Serial.println("****************************************************");
                }

                Enlaces útiles

                Material para practicar

                Arduino Leonardo o Arduino Mega ((También puedes usar un Arduino UNO si lo consigues).

                (Los dispositivos utilizados con el Arduino Mega y el Arduino Leonardo montados en su interior han sido: De los PLCs Ethernet; el M-Duino 21. De los PLCs 20I/Os; el Ardbox Relay).


                Por favor, responde a esta encuesta. Nos ayudará a mejorar el curso.

                Empezar la encuesta >>

                Vistas
                209 Número de vistas
                15 Vistas de miembros
                194 Vistas públicas
                Compartir en redes sociales
                Compartir enlace
                Usar enlace permanente en las redes sociales
                Compartir por correo

                Por favor iniciar sesión para compartir esto webpage por correo.