#9 Examples: control input-output remotely using Ethernet

                When reading this chapter you have to take into account a series of points:  

                You can practice with an Arduino board. In the different PLCs we use original Arduino boards, for this reason it is not necessary that you practice with our equipment.

                (Remember: For us a PLC works like an Arduino board, our PLCs use original Arduino boards so you can try this course using original Arduino boards).

                Introduction 

                This is a new chapter about the Arduino programming course for industrial use. In this chapter we will see an example about how to manage the inputs and outputs of an Arduino PLC remotely using Ethernet, related to the previous chapter. So, we will be talking about this example of control and the Ethernet protocol. 

                There are a lot of different Ethernet protocols; UDP, for example, but the most used is TCP, which we will see a practical example of it. Our programming team developed a new protocol, designed to perform better in our equipment and created for our clients, called SimpleComm.

                Theoretical explanation

                In this chapter, we will see a practical example about how to manage the inputs and outputs of a PLC using Ethernet. We are going to use the TCP protocol to establish this kind of communication but, regarding this, we have to remember the OSI model explained in the previous chapter and, relating this with our specific model, we can see an scheme like this:

                7 Layers of the OSI Model - Lesson 9 - Programming Arduino on Industrial Environments

                Example 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 -->  Configuration of RS-485 Protocol on Arduino IDE:

                The practical example that we will see in this chapter is based in two posts of our blog, divided in slave and master parts:

                Slave

                In this example, as we saw in the previous paragraphs, the Application layer protocol used is Modbus. First of all, we have to see some aspects of this to understand the code in a better way.


                Object typeAccessSize
                CoilRead-write1bit
                Discrete inputRead1 bit
                Input registerRead16 bits
                Holding registersRead-write 16 bits
                Normally coils a are used to write digital values to an outputs. Discrete inputs are used to read digital inputs. Registers are use to communicate data between the devices and also usually used for analog I/Os. 
                In this case, we will see how to use the Slave Library of Tools40.

                Requirements


                Modbus TCP Slave functions


                The ModbusTCPSlave module implements the Modbus TCP Slave capabilities.

                #include <ModbusTCPSlave.h>

                ModbusTCPSlave slave;

                The default TCP port is the 502 but you can change it with:

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

                To map the coils, discrete inputs, holding registers and input registers addresses with the desired variables values, the module uses four variables arrays:

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

                The lengths of these arrays depend on the application and the registers usages. Obviously, the names of the arrays also depend on your preferences.

                To associate the registers arrays with the library it is possible to use these functions in the setup:

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

                It is not required to have all kind of registers mapping to work, only the used by the application.

                To start the ModbusTCP server, call the begin function after the registers mapping. It is also possible to call the beginfunction before the registers mapping. Remember to begin the Ethernet before the ModbusTCPSlave object in the setup.

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

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

                At this time, the ModbusTCP server is running and the only important thing to do is to update the ModbusTCPSlave object often in the loop function, and tret the registers mapping values to update variables, inputs and outputs.

                // 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]);
                // ...

                Example software for Arduino based automation controller

                /*
                   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

                Now, taking into account the same Modbus information that we saw in the Slave part and the same Requirements, we are going to see the master part:

                Modbus TCP master functions

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

                Where

                • client is the EthernetClient connected to the slave.
                • slave_address is the Modbus TCP slave address.
                • address is the coil, digital input, holding register or input register address. Usually this address is the coil, digital input, holding register or input register number minus 1: the holding register number 40009 has the address 8.
                • quantity is the number of coils, digital inputs, holding registers or input registers to read/write.
                • value is the given value of the coil or holding registers on a write operation. Depending on the function the data type changes. A coil is represented by a bool value and a holding register is represented by a uint16_t value.

                On a multiple read/write function the address argument is the first element address. On a multiple write function the valuesargument is an array of values to write.

                It is important to notice that these functions are non-blocking, so they don't return the read value. They return true or falsedepending on the current module state. If there is a pending Modbus request or the client is not connected, they return 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
                }

                There is the available() function to check for responses from the slave.

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

                The ModbusResponse implements some functions to get the response information:

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

                The possible error codes are:

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


                Software

                After seeing the bases of the Modbus TCP Master Library, we can proceed to develop a code to communicate with another Modbus device in our network. In this code, it is showed how to read registers and how to write coils from another Modbus TCP/IP slave. This example will write random numbers to digital coils every second and also will read 6 values from the slave every 500 milliseconds. 


                /*
                   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();
                        }
                      }
                    }
                  }
                }


                Example 2

                Following the previous Slave part of the example and, changing the Master's code a little bit in order to make it more interactive with the user, here we have another example:

                Application Architecture

                M-Duino master has a an interactive serial menu that allow the user to control the application. The menu has 6 options. The first four options are to control two outputs of the slave, the fifth option is to get the analog inputs or registers from the slave and the last option, sixth, is to get the digital inputs or discrete inputs from the slave.

                We use writeSingleCoil(), readInputRegisters() and readDiscreteInputs() functions to communicate to the slave. Then just executing a short for loop depending of which message we have sent, we read the values using response.getRegister() or response.isDiscreteInputSet().

                The rest of the code is just Ethernet configurations and Serial commands to debug and make the application more interactive.

                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("****************************************************");
                }

                Useful Links

                Material to do practices

                Arduino Leonardo or Arduino Mega (You also can use an Arduino UNO if you get it).

                (The devices used with the Arduino Mega and Arduino Leonardo assembled inside have been: From the Ethernet PLCs; the M-Duino 21. From the 20I/Os PLC; The Ardbox Relay).


                Please, answer this survey. It will help us to improve the course.

                Start Survey >>

                Commenting is not enabled on this course.