ESP32 Modbus RTU to Redlion HMI

Thread Starter

mfellers

Joined May 22, 2021
11
Hopefully, I can convey the issue in a clear concise manner. I designed a micro PLC with an ESP32 DEVKIT V1 at heart just for personal projects and to learn as I've always been fascinated with electronics. It has (8) digital inputs, (6) digital outputs, RS-485 connection, LCD connection, I2C connection (for potential future multiplexers to expand IO), and power port for necessary 5V and connecting all power commons together (for common reference point for external DI's power supply).

The digital inputs/outputs work just fine at least so far. The problem I am having is with Modbus RTU over RS-485. Modbus RTU is relatively new to me and I'm having a hard time knowing if the problem is with my hardware design or if it has to do with software. Unfortunately, I do not have a analyzer to help troubleshoot to find if it is software or hardware. My setup is Redlion HMI (CR30000700000420) as the master and the ESP32 PLC as the slave. Attached is the KiCad file for the PCB I designed, Arduino IDE program, and RedLion HMI screen shots of how it is configured. Right now I only have the two devices on the RS-485 network with a cable run of about 50foot max (120Ohm terminating resistor at the ESP32 PLC). Currently I am just trying to read a digital input value from the ESP32 modbus table and display what state it is in on the Redlion HMI. I've set all values to true (in the setup routine) in the ESP32 modbus table which I believe will hold a (1) value at the offset location. I try to read this value from the Redlion HMI master and turn on a red light for value (1) and green light for value (0). Right now it does not read any value and indicator on redlion hmi is transparent (no value present). I've tried everything I can think of and still have not been able to communicate.

From the Redlion HMI I wired a standard ethernet cable (T-568B) down to terminal blocks. The black wire is wired from A to A and white wire is B to B and ran to a test station in my barn (roughly 50ft run).

I know this is somewhat of a complex issue and any help would be appreciated. Hopefully, I've supplied enough information

Code:
#include <ESPmDNS.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <ModbusRTU.h>
/********************************************************************/

/******************ESP32 PINOUT ASSIGNMENTS**********************************/
const int Input_Float_HH =13;
const int Input_Float_H =14;
const int Input_Float_L =15;
const int Input_Float_LL =16;
const int Input_Float_Refill =17;
const int Input_Start_RO_Cycle =18;
const int Input_Stop_RO_Cycle =19;
const int Input_8 =5;
const int Output_RO_Pump = 25;
const int Output_2 = 26; 
const int Output_3 = 27; 
const int Output_4 = 32; 
const int Output_5 = 33; 
const int Output_6 = 4;   
const int I2C_SCL = 22;
const int I2C_SDA = 21;
const int I2C_SCL2 = 12;
const int I2C_SDA2 = 2;
const int RS_485_TX = 1;
const int RS_485_RX = 3;
const int RS_485_CNTRL_Pin = 23;
/********************************************************************/
// WiFi
// Make sure to update this for your own WiFi network!
const char* ssid = ""; ... removed for privacy
const char* password = "";... removed for privacy
/********************************************************************/
/******************Global Variables**********************************/
int Read_Input_Float_HH =0;
int Read_Input_Float_H =0;
int Read_Input_Float_L =0;
int Read_Input_Float_LL =0;
int Read_Input_Float_Refill =0;
int Read_Input_Start_RO_Cycle =0;
int Read_Input_Stop_RO_Cycle =0;
int Read_Input_8 =0;
int Start_RO_Cycle = 0;
int Stop_RO_Cycle = 0;
int Start_RO_Pump = 0;
int Stop_RO_Pump = 0;
int Main_PLC_Permissive = 1; // Bit sent to Main PLC to know current state of RO System. 0 = No error; 1 = Error or just starting.
bool High_Float_Error = false;
bool High_High_Float_Error = false;
bool Refill_Float_Error = false;
bool Low_Float_Error = false;
bool Low_Low_Float_Error = false;
/******************Global Variables******************************/

/******************Modbus Stuff**********************************/
#define SLAVE_ID 1
#define RO_Pump_Coil  1
#define Float_HH_DI  1
#define Float_H_DI  2
#define Float_Refill_DI 3
#define Float_L_DI 4
#define Float_LL_DI 5
#define RO_Cycle_Start_DI 6
#define RO_Cycle_Stop_DI 7
#define Main_PLC_Permissive_DI 8 // Bit sent to Main PLC to know current state of RO System. 0 = No error; 1 = Error or just starting.
#define High_Float_Error_DI 9
#define High_High_Float_Error_DI 10
#define Refill_Float_Error_DI 11
#define Low_Float_Error_DI 12
#define Low_Low_Float_Error_DI 13
 
ModbusRTU mb;

/******************Modbus Stuff**********************************/
void setup() {
 
/******************INITIALIZE INPUTS*****************************/ 
  pinMode(Input_Float_HH, INPUT_PULLDOWN);
  pinMode(Input_Float_H, INPUT_PULLDOWN);
  pinMode(Input_Float_L, INPUT_PULLDOWN);
  pinMode(Input_Float_LL, INPUT_PULLDOWN);
  pinMode(Input_Float_Refill, INPUT_PULLDOWN);
  pinMode(Input_Start_RO_Cycle, INPUT_PULLDOWN);
  pinMode(Input_Stop_RO_Cycle, INPUT_PULLDOWN);
  pinMode(Input_8, INPUT_PULLDOWN);
/******************INITIALIZE INPUTS**********************************/

/******************INITIALIZE OUTPUTS*********************************/ 
  pinMode(Output_RO_Pump, OUTPUT);     // Initialize pin as an output
  digitalWrite(Output_RO_Pump, LOW);
  pinMode(Output_2, OUTPUT);
  digitalWrite(Output_2, LOW);
  pinMode(Output_3, OUTPUT);
  digitalWrite(Output_3, LOW);
  pinMode(Output_4, OUTPUT);
  digitalWrite(Output_4, LOW);
  pinMode(Output_5, OUTPUT);
  digitalWrite(Output_5, LOW);
  pinMode(Output_6, OUTPUT);
  digitalWrite(Output_6, LOW);
/******************INITIALIZE OUTPUTS*********************************/
 
/******************INITIALIZE SERIAL PORTS/MODBUS*********************/
  Serial.begin(9600, SERIAL_8N1);
  //Serial2.begin(9600, SERIAL_8N1, RS_485_RX, RS_485_TX, RS_485_CNTRL_Pin);
  mb.begin(&Serial);
  mb.slave(SLAVE_ID);
  mb.addCoil(RO_Pump_Coil, false);
  mb.addIsts(Float_HH_DI, true);
  mb.addIsts(Float_H_DI, true);
  mb.addIsts(Float_Refill_DI);
  mb.addIsts(Float_L_DI, true);
  mb.addIsts(Float_LL_DI, true);
  mb.addIsts(RO_Cycle_Start_DI, true);
  mb.addIsts(RO_Cycle_Stop_DI, true);
  mb.addIsts(Main_PLC_Permissive_DI, true);
  mb.addIsts(High_Float_Error_DI, true);
  mb.addIsts(High_High_Float_Error_DI, true);
  mb.addIsts(Refill_Float_Error_DI, true);
  mb.addIsts(Low_Float_Error_DI, true);
  mb.addIsts(Low_Low_Float_Error_DI, true);

/******************INITIALIZE SERIAL PORTS/MODBUS*********************/
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

  // Port defaults to 3232
  // ArduinoOTA.setPort(3232);

  // Hostname defaults to esp3232-[MAC]
   ArduinoOTA.setHostname("RO_System");

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";

      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });

  ArduinoOTA.begin();

  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
}

void loop() {
  ArduinoOTA.handle();
  mb.task();
  Read_Inputs();
  RO_System_Control();
  Set_Modbus_Tables();
  Alarm_Status();
  Set_Digital_Outputs();
  yield();
}

void Read_Inputs() {
 
  Read_Input_Float_HH = digitalRead(Input_Float_HH);
  Read_Input_Float_H = digitalRead(Input_Float_H);
  Read_Input_Float_L = digitalRead(Input_Float_L);
  Read_Input_Float_LL = digitalRead(Input_Float_LL);
  Read_Input_Float_Refill = digitalRead(Input_Float_Refill);
  Read_Input_Start_RO_Cycle = digitalRead(Input_Start_RO_Cycle);
  Read_Input_Stop_RO_Cycle = digitalRead(Input_Stop_RO_Cycle);
  if(Input_Start_RO_Cycle == 1){
    Start_RO_Cycle =1;
    Stop_RO_Cycle =0;
  }
  if(Input_Stop_RO_Cycle == 1){
    Start_RO_Cycle =0;
    Stop_RO_Cycle =1;
  }
}

void RO_System_Control(){
 
  if(Input_Start_RO_Cycle == 1 && Input_Stop_RO_Cycle == 0){
    if(Read_Input_Float_HH == 0 || Read_Input_Float_H == 0){
      Start_RO_Pump = 0;
      Stop_RO_Pump = 1;
      Main_PLC_Permissive = 0;
    }
    if(Read_Input_Float_Refill == 1){
      Start_RO_Pump = 1;
      Stop_RO_Pump = 0;
      Main_PLC_Permissive = 0;
    }
    if(Read_Input_Float_L == 1 || Read_Input_Float_LL == 1){
      Start_RO_Pump = 1;
      Stop_RO_Pump = 0;
      Main_PLC_Permissive = 0;
    }
    if(Read_Input_Float_LL == 1){
      Start_RO_Pump = 1;
      Stop_RO_Pump = 0;
      Main_PLC_Permissive = 1;
    }
  }
}

void Set_Modbus_Tables(){
   //***************SET DIGITAL OUTPUT MODBUS REGISTER********************** 
  //if(Output_RO_Pump == 1){
  //  mb.Coil(RO_Pump_Coil, true); // writes value of Output_RO_Pump to modbus register of RO_Pump_Coil
 // }
 // else{
  //  mb.Coil(RO_Pump_Coil, false); // writes value of Output_RO_Pump to modbus register of RO_Pump_Coil
  //}
 
//***************SET DIGITAL OUTPUT MODBUS REGISTER********************** 


//***************SET DIGITAL INPUT MODBUS REGISTER**********************
  //if(Read_Input_Float_HH == 1){
   // mb.Ists(Float_HH_DI,true); // writes value of Read_Input_Float_HH to modbus register of FLoat_HH_DI
  //}
  //else{
   // mb.Ists(Float_HH_DI,false); // writes value of Read_Input_Float_HH to modbus register of FLoat_HH_DI
 // }
  //if(Read_Input_Float_H == 1){
   //mb.Ists(Float_H_DI,true); // writes value of Read_Input_Float_H to modbus register of FLoat_H_DI
  //}
  //else{
   // mb.Ists(Float_H_DI,false); // writes value of Read_Input_Float_H to modbus register of FLoat_H_DI
 // }
 // if(Read_Input_Float_Refill == 1){
    //mb.Ists(Float_Refill_DI,true); // writes value of Read_Input_Float_Refill to modbus register of FLoat_Refill_DI
  //}
  //else{
    //mb.Ists(Float_Refill_DI,false); // writes value of Read_Input_Float_Refill to modbus register of FLoat_Refill_DI
  //}
  //if(Read_Input_Float_L == 1){
   // mb.Ists(Float_L_DI,true); // writes value of Read_Input_Float_L to modbus register of FLoat_L_DI
 // }
  //else{
  //  mb.Ists(Float_L_DI,false); // writes value of Read_Input_Float_L to modbus register of FLoat_L_DI
  //}
  //if(Read_Input_Float_LL == 1){
  //  mb.Ists(Float_LL_DI,true); // writes value of Read_Input_Float_LL to modbus register of FLoat_LL_DI
 // }
 // else{
    //mb.Ists(Float_LL_DI,false); // writes value of Read_Input_Float_LL to modbus register of FLoat_LL_DI
  //}
  //if(Read_Input_Start_RO_Cycle == 1){
   // mb.Ists(RO_Cycle_Start_DI,true); // writes value of Read_Input_Start_RO_Cycle to modbus register of RO_Cycle_Start_DI
 // }
  //else{
   // mb.Ists(RO_Cycle_Start_DI,false); // writes value of Read_Input_Start_RO_Cycle to modbus register of RO_Cycle_Start_DI
  //}
 // if(Read_Input_Stop_RO_Cycle == 1){
   // mb.Ists(RO_Cycle_Stop_DI,true); // writes value of Read_Input_Stop_RO_Cycle to modbus register of RO_Cycle_Stop_DI
  //}
  //else{
  //  mb.Ists(RO_Cycle_Stop_DI, false);  // writes value of Read_Input_Stop_RO_Cycle to modbus register of RO_Cycle_Stop_DI
  //}

 // if(Main_PLC_Permissive == true){
   // mb.Ists(Main_PLC_Permissive_DI, true);
  //}
  //else{
  //  mb.Ists(Main_PLC_Permissive_DI, false);
 // }
 // if(High_Float_Error == true){
   // mb.Ists(High_Float_Error_DI, true);
  //}
  //else{
  //  mb.Ists(High_Float_Error_DI, false);
  //}
 // if(High_High_Float_Error == true){
   // mb.Ists(High_High_Float_Error_DI, true);
  //}
  //else{
  //  mb.Ists(High_High_Float_Error_DI, false);
 // }
  //if(Refill_Float_Error == true){
   // mb.Ists(Refill_Float_Error_DI, true);
  //}
  //else{
  //  mb.Ists(Refill_Float_Error_DI, false);
  //}
 // if(Low_Float_Error == true){
   // mb.Ists(Low_Float_Error_DI, true);
  //}
  //else{
  //  mb.Ists(Low_Float_Error_DI, false);
  //}
  //if(Low_Low_Float_Error == true){
   // mb.Ists(Low_Low_Float_Error_DI, true);
  //}
  //else{
   // mb.Ists(Low_Low_Float_Error_DI, false);
  //}
 
//***************SET DIGITAL INPUT MODBUS REGISTER**********************

  
//***************INSTRUCTIONS TO RETURN VALUE OF A MODBUS REGISTER**********************
//bool Coil (offset word).... DIGITAL OUTPUT
//Hreg word (word offset).... ANALOG OUTPUT
//bool Ists (offset word).... DIGITAL INPUT
//IREG word (word offset).... ANALOG INPUT
//***************INSTRUCTIONS TO RETURN VALUE OF A MODBUS REGISTER**********************

//***************INSTRUCTIONS TO SET VALUE OF A MODBUS REGISTER*************************
//bool Coil (offset word, bool value).... DIGITAL OUTPUT
//bool Hreg (offset word, word value).... ANALOG OUTPUT
//bool Ists (offset word, bool value).... DIGITAL INPUT
//bool IREG (offset word, word value).... ANALOG INPUT
//***************INSTRUCTIONS TO SET VALUE OF A MODBUS REGISTER*************************
}

void Alarm_Status(){
 
  if(Read_Input_Float_HH == 0){
    High_High_Float_Error = true;
  }
  else{
    High_High_Float_Error = false;
  }
  if(Read_Input_Float_HH == 0 && (Read_Input_Float_Refill == 1 || Read_Input_Float_Refill == 1)){
    High_Float_Error = true;
  }
  else{
    High_Float_Error = false;
  }
  if((Read_Input_Float_H == 0 && Read_Input_Float_Refill == 0) || (Read_Input_Float_H == 0 && (Read_Input_Float_L == 1 || Read_Input_Float_LL == 1) && Read_Input_Float_Refill == 0) ){
    Refill_Float_Error = true;
  }
  else{
    Refill_Float_Error = false;
  }
  if((Read_Input_Float_H == 0 && Read_Input_Float_L == 0) || (Read_Input_Float_LL == 1 && Read_Input_Float_Refill == 1 && Read_Input_Float_L == 0)){
    Low_Float_Error = true;
  }
  else{
    Low_Float_Error = false;
  }
  if(Read_Input_Float_LL == 0 && (Read_Input_Float_L == 1 || Read_Input_Float_Refill == 1 || Read_Input_Float_H == 0 || Read_Input_Float_HH == 0)){
    Low_Low_Float_Error = true;
  }
  else{
    Low_Low_Float_Error = false;
  }
 
 
}

void Set_Digital_Outputs(){
  if(Start_RO_Pump == 1){
    digitalWrite(Output_RO_Pump, HIGH);
  }
  if(Stop_RO_Pump == 1 || Start_RO_Pump == 0){
    digitalWrite(Output_RO_Pump, LOW);
  }
}
 

Attachments

Thread Starter

mfellers

Joined May 22, 2021
11
I have not received my data analyzer yet but I did get Modbus to work on a very basic level between two ESP32 devices thanks to aemelianov (maintainer of library) posts on another topic. It seems I do not fully understand how the ModbusRTU library works.

If I use the following code I can write 99 to the slave and turn on a relay. However, if I use an if else or else statement in the master code it does not work how I would expect. I use an else statement to write a different value to the slave register which turns off the relay. I would expect to be able to turn on and off the relay based on what condition the pushbutton is in but that is not the case. If I add another pushbutton that writes to the register I can then turn on and off the relay as expected.

Master code which will work.
Master:
if(buttonState == 1)
  {
        if (!mb.slave())
        mb.writeHreg(1, REG1, 99,  cbWrite);
      
  }
Slave code which works.
slave code:
if(mb.Hreg(REG1)==99){
   digitalWrite(BUTTON1, HIGH);
  }

Master code which does not work:
if(buttonState == 1)
  {
        if (!mb.slave())
        mb.writeHreg(1, REG1, 99,  cbWrite);
      
  }
  else{
        mb.writeHreg(1, REG1, 50,  cbWrite); 
  }
slave code which does not work:
  if(mb.Hreg(REG1)==99){
   digitalWrite(BUTTON1, HIGH);
  }
  if(mb.Hreg(REG1)!=99){
    digitalWrite(BUTTON1,LOW);
  }
 

dendad

Joined Feb 20, 2016
4,451
Do you have a common 0V between the units?
Although RS485 is a two wire network, a common 0V is needed to prevent the signals from going out of the range of the transceivers.
Years ago, our first major RS485 installation in a wire drawing mill gave us great trouble as we just ran 2 wires. It turned out when the big motors ran, the 0V rails of the power supplies at each end of the line wandered apart by around 30V if I remember correctly. Well outside the common mode driver voltage range.
Running a 0V common between the units fixed it. Now, we always use shielded twisted pair comms cable with the shield connected to 0V at both ends.
Or if you want complete isolation, have a look at Mornsun devices. I've used them in the past. They have a range.
https://www.mornsun-power.com/html/products/48/rs-485-transceiver-module.html
 

Thread Starter

mfellers

Joined May 22, 2021
11
Thanks for the reply dendad! I'll look further into this transceiver. Initial glance looks pretty spiffy.... I was worried for a while that the reference could be out of whack but I am using the same power supply for each device and connected the two signal grounds together. I also installed a fail safe bias (10K between Vcc and signal A and 10K between gnd and signal B).

I also noticed that if I power the ESP on with the digital input that triggers the relay set at high during startup the library does send the register value of "99" to the slave which would turn on the relay. Once I disconnect the input from "1" to "0" then back to "1" it behaves as expected and turns on the relay. However, if I quickly set the inputs high and low to toggle the relay it does not work as it should. I have to slowly toggle it back and forth and sometimes it hangs up and I have to wait several seconds and be deliberate and it will work.

I did read on another post that when transmitting and receiving with an ESP32 there needs to be a delay before flushing the serial port otherwise it does not transmit the full packet based on the inherent design of the ESP32. Not sure if this has something to do with it or not. I did put a delay in the ModbusRTU.cpp file where a serial. flush was and it seemed to help a little but it could be my imagination. That was only for the send and I could not find where to put a delay for the receive. I also noticed this library should not require a RE/DE pin and auto negotiates but for me it will only work when the RE/DE is initialized.
 

dendad

Joined Feb 20, 2016
4,451
I am not the programmer, ("my favorite programming language is solder") but on our systems, the data packets have a checksum to ensure valid data is used. Do you have anything like that on yours? And the status is read back to check it worked.
On one installation, there were very occasional errors, and that was the baud rate changed, so the slave would no longer respond. It was noise corrupting a packet and the 8 bit checksum would line up once in a blue moon. Changing to a 16bit checksum fixed it.
The problem took ages to figure out!
 

Thread Starter

mfellers

Joined May 22, 2021
11
I finally received the logic analyzer that nsaspook mentioned and used Pulseview software. I'm not sure what I am looking at but seems to be a lot of errors. If anyone has any input feel free to share!!! Thanks for everyone's help so far!... The setup that I ran the software with is Redlion HMI as the master and ESP32 as the slave. I have been able to get basic communications to work between two ESP32 devices. I used a sample program that turned on and off a relay and seemed to work just fine.

Forgot to mention that I was trying to read digital input register 10002 at the slave when running the software. Slave ID should be 1.
 

Attachments

Thread Starter

mfellers

Joined May 22, 2021
11
So I didn't know what I was doing last night and the attached above is inaccurate. I've watched some tutorials and did some reseach. I took new samples from ESP32 to ESP32. I used the updated ModbusMaster.h which delays the serial.flush for complete packet transmission without loosing the last few bits. I am using <ModbusRTU.h> for the slave. The relay does toggle like it should but I can't do much more than that. Seems like the timing is off based on the results of the attached logic analyzer data.
 

Attachments

Thread Starter

mfellers

Joined May 22, 2021
11
Anyone have an idea why there is an invalid checksum? Almost seems as if the the enable pin is not behaving properly and not allowing the transmission to complete by a byte. Do I need a delay somewhere so it can fully transmit? Channel 0 is my enable pin of ESP32.... Channel 1 is "A" line and Channel 2 is "B" line.



1622297963255.png
1622298291497.png
 
Top