Lesson #9
#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:
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 type | Access | Size |
Coil | Read-write | 1bit |
Discrete input | Read | 1 bit |
Input register | Read | 16 bits |
Holding registers | Read-write | 16 bits |
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 502ModbusTCPSlave 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 begin
function before the registers mapping. Remember to begin the Ethernet before the ModbusTCPSlave object in the setup
.
// Init the EthernetEthernet.begin(mac, ip);
// Init the ModbusTCPSlave objectslave.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 valuesdiscreteInputs[0] = digitalRead(I0_7);inputRegisters[0] = analogRead(I0_0);// ...
// Update the ModbusTCPSlave objectslave.update();
// Update coils and holding registersdigitalWrite(Q0_0, coils[0]);// ...
Example software for Arduino based automation controller
/*Copyright (c) 2018 Boot&Work Corp., S.L. All rights reservedThis program is free software: you can redistribute it and/or modifyit under the terms of the GNU Lesser General Public License as published bythe 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 ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See theGNU Lesser General Public License for more details.You should have received a copy of the GNU Lesser General Public Licensealong 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 valuesuint8_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+ mappingint 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 objectModbusTCPSlave modbus(port);////////////////////////////////////////////////////////////////////////////////////////////////////void setup() {Serial.begin(9600UL);// Init variables, inputs and outputsfor (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 EthernetEthernet.begin(mac, ip);Serial.println(Ethernet.localIP());// Init ModbusTCPSlave objectmodbus.begin();modbus.setCoils(digitalOutputs, numDigitalOutputs);modbus.setDiscreteInputs(digitalInputs, numDigitalInputs);modbus.setHoldingRegisters(analogOutputs, numAnalogOutputs);modbus.setInputRegisters(analogInputs, numAnalogInputs);}////////////////////////////////////////////////////////////////////////////////////////////////////void loop() {// Update inputsfor (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 requestsmodbus.update();// Update outputsfor (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 number40009
has the address8
.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 abool
value and a holding register is represented by auint16_t
value.
On a multiple read/write function the address
argument is the first element address. On a multiple write function the values
argument 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 false
depending 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 ADDRESS0x03 ILLEGAL DATA VALUE0x04 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 reservedThis program is free software: you can redistribute it and/or modifyit under the terms of the GNU Lesser General Public License as published bythe 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 ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See theGNU Lesser General Public License for more details.You should have received a copy of the GNU Lesser General Public Licensealong 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 valuesuint8_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 objectModbusTCPMaster modbus;// Ethernet client object used to connect to the slaveEthernetClient slave;uint32_t lastSentTime = 0UL;uint32_t lastSentTimeReadInputs = 0UL;////////////////////////////////////////////////////////////////////////////////////////////////////void setup() {Serial.begin(9600UL);// Begin EthernetEthernet.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 onceif (!slave.connected()) {slave.stop();slave.connect(slaveIp, slavePort);if (slave.connected()) {Serial.println("Reconnected");}}// Send a request every 1000ms if connected to slaveif (slave.connected()) {if (millis() - lastSentTime > 1000) {// Set random valuesbool 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 treatmentSerial.println("Request fail");}lastSentTime = millis();}// Check available responses oftenif (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 responseSerial.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).
Views | |
---|---|
1788 | Total Views |
141 | Members Views |
1647 | Public Views |
Share by mail
Please login to share this webpage by email.