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