Modbus TCP between 2 Industruinos

expand I/O channels over Ethernet

Tom

The Modbus protocol is a common industrial communication platform available over RS485 as Modbus RTU, and over Ethernet as Modbus TCP. We can use Modbus as an easy way to communicate between Industruinos, and also many other sensors can be connected. Sensors are usually Modbus Slaves, and the Industruino can be used as Master or as Slave. A good summary on Modbus is here.

In this example, we will use one Industruino as Master, another one as Slave, so that the Master can control/read all I/O channels, in fact doubling the number of channels. You can go further and add another Industruino as Slave in the same way. We will use the official Arduino Modbus library, and Modbus TCP via Ethernet. Earlier we have provided a similar example using Modbus RTU (RS485) and a different library.

Modbus TCP can co-exist with normal 'internet' traffic on the same LAN; to illustrate this, the master in our example makes a TCP connection to a dummy server e.g. to post data.

The Arduino library uses the uncommon terminology of Client to denote the Master, and Server to denote the Slave. Strangely, this ArduinoModbus library also requires the ArduinoRS485 library to be installed.

Below if the full code for Modbus Master and Slave Industruinos, using the Ethernet2 and UC1701 libraries. It also uses the Adafruit_SleepyDog library for watchdog timer.

Please note that if you have ArduinoModbus library v1.0.4 you may need to add a line in modbus.c as described here, or downgrade the library to v1.0.3. The below example was tested on v1.0.1

Both Master and Slave display the Modbus connection status on the LCD. The Slave fills 8 registers with a seconds timer, and the Master displays these 8 registers. For digital channels you could also use the coilWrite() and coilRead() functions.

Note: you can also test the below Slave sketch with a laptop as Modbus TCP Master; i used Modpoll on my Linux system to poll the Industruino slave with command:

> ./modpoll -c 8 -r 1 -m tcp 192.168.1.11 -t 3

This is the MASTER sketch:

/*
  Industruino Modbus TCP example master(client)
  hardware: 
  >Industruino D21G (INDIO or PROTO)
  >Ethernet module
  
  ArduinoModbus TCP Client Toggle = MASTER
  needs IP address of server = slave to send requests

  notes: 
  >using new Ethernet library did not work
  >modbus.connected() is slow to fail

  read 8 input registers of slave
  show on LCD
  post the results to a dummy server
    
  Tom Tobback - March 2020
*/

////////////////// RTC MAC ////////////////////////////////////////
#include <Wire.h>                               // for RTC MAC
byte mac[6];                                    // read from RTC

////////////////// ETHERNET //////////////////////////////////////
#include <SPI.h>                                // for Ethernet
#include <SD.h>                                 // for FRAM to work
#include <Ethernet2.h>                          // use Industruino version
EthernetClient client_modbus;
EthernetClient client_tcp;
IPAddress industruino_ip(192, 168, 1, 11);  // this is client=master

//////////////////// INDUSTRUINO LCD //////////////////////////////
#include <UC1701.h>
static UC1701 lcd;
#define LCD_PIN 26

/////////////////////// WATCHDOG TIMER ////////////////////////////
#include <Adafruit_SleepyDog.h>
const unsigned int watchdog_interval = 10000;     // 10 seconds: if no wdt reset within this interval, wdt will reset the unit

/// MODBUS TCP
#include <ArduinoRS485.h> // ArduinoModbus depends on the ArduinoRS485 library
#include <ArduinoModbus.h>
ModbusTCPClient modbusTCPClient(client_modbus);
int32_t modbus_reg[8] = {-1, -1, -1, -1, -1, -1, -1, -1};   // will be -1 if read failed
IPAddress server(192, 168, 1, 10); // server=slave

/// DATA SERVER
#define TCP_SERVER "httpbin.org"
#define TCP_SERVER_PORT 80
unsigned long last_tcp;

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

void setup() {

  // setup LCD
  pinMode(LCD_PIN, OUTPUT);                     // LCD backlight
  digitalWrite(LCD_PIN, HIGH);                  // LCD backlight ON
  lcd.begin();
  lcd.print("ModbusTCPclient=master");

  //Initialize serial and wait for port to open:
  SerialUSB.begin(115200);
  delay(2000);

  SerialUSB.println("==========================");
  SerialUSB.println("Modbus TCP Client = MASTER");
  SerialUSB.println("==========================");

  // start watchdog timer
  Watchdog.enable(watchdog_interval);
  SerialUSB.println("Watchdog timer started");

  // read MAC from eeprom at rtc
  readMACfromRTC();             // MAC stored in RTC eeprom
  // start Ethernet with fixed IP
  Ethernet.begin(mac, industruino_ip);
  SerialUSB.print("Ethernet started with IP: ");
  SerialUSB.println(Ethernet.localIP());
  lcd.setCursor(0, 1);
  lcd.print("IP :");
  lcd.print(Ethernet.localIP());

  // set timeouts for ethernet connections
  client_modbus.setTimeout(1000);
  client_tcp.setTimeout(1000);
  // and need to set the timeout of the ModbusClient
  modbusTCPClient.setTimeout(1000);
}

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

void loop() {

  Watchdog.reset();                          // needed to avoid watchdog reset

  // connect to slave
  SerialUSB.print("modbus>> checking connection.. ");
  if (!modbusTCPClient.connected()) {
    // client not connected, start the Modbus TCP client
    SerialUSB.println();
    SerialUSB.print("modbus>> not connected, attempting to connect to Modbus TCP server.. ");
    lcd.setCursor(0, 2);
    lcd.print("Modbus connecting..");
    lcd.setCursor(0, 2);
    if (!modbusTCPClient.begin(server)) {
      SerialUSB.println("FAIL");
      lcd.print("Modbus FAIL        ");
    } else {
      modbusTCPClient.setTimeout(1000);  // needed here?
      SerialUSB.println("connected");
      lcd.print("Modbus connected   ");
    }
  } else {
    SerialUSB.println("OK");
  }

  // try to read registers
  if (modbusTCPClient.connected()) {  // otherwise read gets stuck
    SerialUSB.println("modbus>> read input registers");
    for (int i = 0; i < 8; i++) {
      modbus_reg[i] = modbusTCPClient.inputRegisterRead(i);
    }
  }

  // display on LCD
  for (int i = 0; i < 4; i++) {
    lcd.setCursor(0, 3 + i);
    if (modbus_reg[i] == -1) lcd.print("n/a");
    else lcd.print(modbus_reg[i]);
    lcd.print("     ");
  }
  for (int i = 4; i < 8; i++) {
    lcd.setCursor(60, 3 + i - 4);
    if (modbus_reg[i] == -1) lcd.print("n/a");
    else lcd.print(modbus_reg[i]);
    lcd.print("     ");
  }
  lcd.setCursor(100, 7);
  lcd.print(millis() / 1000);

  // connect to a web server
  if (millis() - last_tcp > 5000) {
    doGET();
    last_tcp = millis();
  }
  delay(10);
}

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

//////////////////////////// MAC ADDRESS ////////////////////////////////////////////
// the RTC has a MAC address stored in EEPROM - 8 bytes 0xf0 to 0xf7
void readMACfromRTC() {
  Wire.begin();                                 // I2C for RTC MAC
  SerialUSB.print("Reading MAC from RTC EEPROM: ");
  int mac_index = 0;
  for (int i = 0; i < 8; i++) {   // read 8 bytes of 64-bit MAC address, 3 bytes valid OUI, 5 bytes unique EI
    byte m = readByte(0x57, 0xf0 + i);
    SerialUSB.print(m, HEX);
    if (i < 7) SerialUSB.print(":");
    if (i != 3 && i != 4) {       // for 6-bytes MAC, skip first 2 bytes of EI
      mac[mac_index] = m;
      mac_index++;
    }
  }
  SerialUSB.println();
  SerialUSB.print("Extracted 6-byte MAC address: ");
  for (int u = 0; u < 6; u++) {
    SerialUSB.print(mac[u], HEX);
    if (u < 5) SerialUSB.print(":");
  }
  SerialUSB.println();
}

//////////////////////////// MAC ADDRESS /////////////////////////////////////////////
// the RTC has a MAC address stored in EEPROM
uint8_t readByte(uint8_t i2cAddr, uint8_t dataAddr) {
  Wire.beginTransmission(i2cAddr);
  Wire.write(dataAddr);
  Wire.endTransmission(false); // don't send stop
  Wire.requestFrom(i2cAddr, 1);
  return Wire.read();
}

//////////////////////////////////////////////////////////////////////////////////////
void doGET() {

  SerialUSB.print("tcp>> connecting to ");
  SerialUSB.print(TCP_SERVER);
  SerialUSB.print(" .. ");
  lcd.setCursor(0, 7);
  lcd.print("TCP connecting..");
  if (client_tcp.connect(TCP_SERVER, TCP_SERVER_PORT)) {
    SerialUSB.println(" connected");
    // example for HTTP GET REQUEST
    client_tcp.println("GET /ip HTTP/1.1");
    client_tcp.print("Host: ");
    client_tcp.println(TCP_SERVER);
    client_tcp.println("User-Agent: arduino-ethernet");
    client_tcp.println("Connection: close");
    client_tcp.println();
    /*
      // this block outputs the reply
      unsigned long timestamp = millis();
      while (client.available() || (millis() - timestamp < 1000)) {
      if (client.available()) {
        SerialUSB.write(client.read());
      }
      }
    */
    // this block just looks for '200 OK' in the reply
    lcd.setCursor(0, 7);
    if (client_tcp.find("200 OK")) {
      SerialUSB.println("tcp>> server replies OK");
      lcd.print("reply OK        ");
    } else {
      SerialUSB.println("tcp>> no OK received from server");
      lcd.print("no reply        ");
    }
    client_tcp.stop();
  } else {
    SerialUSB.println(" connection failed");
  }
}

and the SLAVE sketch:

/*
  Industruino Modbus TCP example slave(server)
  hardware: 
  >Industruino D21G (INDIO or PROTO)
  >Ethernet module

  ArduinoModbus TCP Server LED = SLAVE
  responds to client = master

  set 8 input registers from 0x00
  changing every second

  Tom Tobback - March 2020
*/

////////////////// RTC MAC ////////////////////////////////////////
#include <Wire.h>                               // for RTC MAC
byte mac[6];                                    // read from RTC

////////////////// ETHERNET //////////////////////////////////////
#include <SPI.h>                                // for Ethernet
#include <SD.h>                                 // for FRAM to work
#include <Ethernet2.h>                          // use Industruino version
EthernetServer server(502);
IPAddress industruino_ip (192, 168, 1, 10);    // this is server=slave

//////////////////// INDUSTRUINO LCD //////////////////////////////
#include <UC1701.h>
static UC1701 lcd;
#define LCD_PIN 26

/// MODBUS TCP
#include <ArduinoRS485.h> // ArduinoModbus depends on the ArduinoRS485 library
#include <ArduinoModbus.h>
ModbusTCPServer modbusTCPServer;

byte c = 0;
unsigned long last_change = millis();
unsigned long modbus_timeout = 1000;  // timeout for modbus connection
unsigned long last_modbus_connection;

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

void setup() {

  // setup LCD
  pinMode(LCD_PIN, OUTPUT);                     // LCD backlight
  digitalWrite(LCD_PIN, HIGH);                  // LCD backlight ON
  lcd.begin();
  lcd.print("ModbusTCPserver=slave");

  //Initialize serial and wait for port to open:
  SerialUSB.begin(115200);
  delay(2000);

  SerialUSB.println("Modbus TCP Server = SLAVE");

  readMACfromRTC();             // MAC stored in RTC eeprom
  Ethernet.begin(mac, industruino_ip);
  SerialUSB.print("Ethernet started with IP: ");
  SerialUSB.println(Ethernet.localIP());
  lcd.setCursor(0, 1);
  lcd.print("IP :");
  lcd.print(Ethernet.localIP());

  // start the TCP server
  server.begin();

  // start the Modbus TCP server
  if (!modbusTCPServer.begin()) {
    SerialUSB.println("Failed to start Modbus TCP Server!");
    while (1);
  }

  // configure input registers at address 0x00
  modbusTCPServer.configureInputRegisters(0x00, 8);
}

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

void loop() {

  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    // a new client connected
    //SerialUSB.println("new client=master found");
    // let the Modbus TCP accept the connection
    modbusTCPServer.accept(client);
    last_modbus_connection = millis();  // for the timeout
    lcd.setCursor(0, 2);
    lcd.print("connected    ");
    if (client.connected()) modbusTCPServer.poll();
  } else {
    //    SerialUSB.println("no client connected");
    // only fail if no connection for timeout
    if (millis() - last_modbus_connection > modbus_timeout) {
      lcd.setCursor(0, 2);
      lcd.print("not connected");
    }
  }

  // show counter on LCD
  lcd.setCursor(0, 4);
  lcd.print(c);
  lcd.print("   ");
  // increase counter every second
  if (millis() - last_change > 1000) {
    c++;
    SerialUSB.println(c);
    last_change = millis();
  }

  // set the input registers
  modbusTCPServer.inputRegisterWrite(0x00, 0 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x01, 1 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x02, 2 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x03, 3 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x04, 4 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x05, 5 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x06, 6 + 10 * c);
  modbusTCPServer.inputRegisterWrite(0x07, 7 + 10 * c);

}

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

//////////////////////////// MAC ADDRESS ////////////////////////////////////////////
// the RTC has a MAC address stored in EEPROM - 8 bytes 0xf0 to 0xf7
void readMACfromRTC() {
  Wire.begin();                                 // I2C for RTC MAC
  SerialUSB.print("Reading MAC from RTC EEPROM: ");
  int mac_index = 0;
  for (int i = 0; i < 8; i++) {   // read 8 bytes of 64-bit MAC address, 3 bytes valid OUI, 5 bytes unique EI
    byte m = readByte(0x57, 0xf0 + i);
    SerialUSB.print(m, HEX);
    if (i < 7) SerialUSB.print(":");
    if (i != 3 && i != 4) {       // for 6-bytes MAC, skip first 2 bytes of EI
      mac[mac_index] = m;
      mac_index++;
    }
  }
  SerialUSB.println();
  SerialUSB.print("Extracted 6-byte MAC address: ");
  for (int u = 0; u < 6; u++) {
    SerialUSB.print(mac[u], HEX);
    if (u < 5) SerialUSB.print(":");
  }
  SerialUSB.println();
}

//////////////////////////// MAC ADDRESS /////////////////////////////////////////////
// the RTC has a MAC address stored in EEPROM
uint8_t readByte(uint8_t i2cAddr, uint8_t dataAddr) {
  Wire.beginTransmission(i2cAddr);
  Wire.write(dataAddr);
  Wire.endTransmission(false); // don't send stop
  Wire.requestFrom(i2cAddr, 1);
  return Wire.read();
}