Problema de comunicación Modbus RTU con ESP32

Hola a todos.
Estoy intentando comunicar mi ESP32(maestro) con una bomba para el control de riego (esclavo). Para la comunicación estoy usando el integrado Max485.

La bomba es el modelo ITC DOSTEC-AC, cuyas características son:

Mi intención es escribir una consigna de caudal según como dice el manual Modbus de la bomba, para ello debe escribirse el valor de la consigna de caudal en los registros de memoria según la siguiente imagen.
image

Por ejemplo, para un caudal de 90 l/h, la trama Modbus sería:

Petición (ESP32)
-Id. esclavo = 0x01
-Función = 0x10
-Reg. ADDR HI = 0x12
-Reg. ADDR LO = 0x02
-Num Reg HI = 0x00
-Num Reg LO = 0x02
-Bits count = 0x04
-Value 1 HI = 0x00
-Value 1 LO = 0x00
-Value 2 HI = 0x23
-Value 2LO = 0x28
-CRC HI = 3A
-CRC LO = 44

Aclaración: Consigna de caudal = 0x00002328 = 90.000 * 10^-2 = 90 l/h

Para la creación del código he utilizado la librería ModbusMaster, pero cuando le pido a la placa de ESP32 que comunique con la bomba, es imposible. La bomba no responde.

El código.ino es el siguiente:

#include <ModbusMaster.h>

/*------------------------------------------------VARIABLES GLOBALES---------------------------------------------*/

uint8_t StatusPin_DE_RE = 14;                     // Habilita y desabilida la transmisión del MAX485 IC.
uint8_t id_slave = 1;                             // Direccion del esclavo remoto.                 
uint16_t MAR = 4684;                              // Memory Address Register (MAR).
uint16_t NMR = 2;                                 // Number of Memory Records (NMR).
uint16_t ModbusTimeout = 1000;                    // Modbus timeout [milliseconds].
uint32_t u32_ConsignaCaudal = 9000;               // Consigna de Caudal = 90 l/h.

/*A continuación definiremos las varialbles para la configuración del puerto Serial para el ESP32 Dev Module.
  Por defecto, en el archivo HardwareSerial.cpp se utiliza: HardwareSerial Serial(0);
  Los pines rxPin & txPin tienen que ser menor que 0 para asignar correctamente los pines Rx y Tx en el ESP32.*/
   
unsigned long baudios = 9600;                     // Tasa de bits/segundo.
uint32_t config = SERIAL_8N1;                     // Serial -> 8 bits de datos, 1 bits de parada, 0 bits de paridad.
int8_t rxPin= -1;                 
int8_t txPin= -1; 
bool invert=false;
unsigned long timeout_ms = 20000UL;
uint8_t rxfifo_full_thrhd = 112;

// Creación de una instancia del objeto ModbusMaster
  ModbusMaster node;

/*------------------------------------------------FUNCIONES--------------------------------------------------------*/ 

// Función que habilita los pines RE y DE para activar el modo de transmisión de datos.
void preTransmission(){ digitalWrite(StatusPin_DE_RE, 1); }

// Función que deshabilita los pines RE y DE para activar el modo de recepción de datos.
void postTransmission() { digitalWrite(StatusPin_DE_RE, 0); }

// Funcion que convierte un numero uint32_t en dos numeros uint16_t
// Se utiliza ya que para añadir la variable u32_ConsignaCaudal al buffer de transmisión implementado en la clase 
// <ModbusMaster.h> es necesario descomponerlo en dos registros/numeros de 16 bits.
// uint8_t  setTransmitBuffer(uint8_t, uint16_t);
void u32_to_u16(const uint32_t u32)
{
  uint16_t u16_ArrayTmp[2];                  // Array auxiliar para setear el buffer de transmision
  Serial.print(" Consigna de caudal = ");
  Serial.print(u32/100);
  Serial.println(" l/h");
  delay(2000);

  Serial.println("Iniciando descomposicion uint32_t --> uint16_t...");
  u16_ArrayTmp[0] = (u32 & 0xffff0000) >> 16;
  u16_ArrayTmp[1] = (u32 & 0x0000ffff);
  delay(100);

  for(int i=0; i<2; i++)
  {
    Serial.print("u16_ArrayTmp[");
    Serial.print(i);
    Serial.print("] = ");
    Serial.println(u16_ArrayTmp[i]);
    delay(1000);
  }  

  // Se añanden los valores de 8 bits de la consigna de caudal al buffer de transmision y se comprueba que ha sido correcto.
  if(node.setTransmitBuffer(0, u16_ArrayTmp[0]) == node.ku8MBSuccess){ Serial.println("MSB adjuntado correctamente al Buffer de transmision.");}
  delay(1000);
  if(node.setTransmitBuffer(1, u16_ArrayTmp[1]) == node.ku8MBSuccess){ Serial.println("LSB adjuntado correctamente al Buffer de transmision.");}
  else{Serial.println("Error al añadir los datos al Buffer de transmision!!! :(");}   
}

// Funcion de detección de errores. Devuelve que tipo de erros es.
bool getResultMsg(uint8_t result)
{
  String tmpstr2;

  switch (result) {
    case node.ku8MBSuccess:
      return true;
      break;
    case node.ku8MBIllegalFunction:
      tmpstr2 = "Illegal Function";
      break;
    case node.ku8MBIllegalDataAddress:
      tmpstr2 = "Illegal Data Address";
      break;
    case node.ku8MBIllegalDataValue:
      tmpstr2 = "Illegal Data Value";
      break;
    case node.ku8MBSlaveDeviceFailure:
      tmpstr2 = "Slave Device Failure";
      break;
    case node.ku8MBInvalidSlaveID:
      tmpstr2 = "Invalid Slave ID";
      break;
    case node.ku8MBInvalidFunction:
      tmpstr2 = "Invalid Function";
      break;
    case node.ku8MBResponseTimedOut:
      tmpstr2 = "Response Timed Out";
      break;
    case node.ku8MBInvalidCRC:
      tmpstr2 = "Invalid CRC";
      break;
    default:
      tmpstr2 = "Unknown error: " + String(result);
      break;
  }
  Serial.println(tmpstr2);
  return false;
}

/*------------------------------------------------PARAMETROS DE CONFIGURACIÓN----------------------------------------------------------------*/
void setup() 
{
  // Configuración de pines.
  pinMode(StatusPin_DE_RE, OUTPUT);
  delay(50);

  // Iniciar en modo de transmision.
  digitalWrite(StatusPin_DE_RE, 1);
  delay(50);

  // Inicializa los pines Rx y Tx para que puedan ser usados como puerto serial.
  // void HardwareSerial::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert, unsigned long timeout_ms)
  Serial.begin(baudios, config , rxPin, txPin, invert, timeout_ms);
  delay(50);  
  
  // Se limpian los Buffer de Respuesta y transmision.
  node.clearResponseBuffer();
  node.clearTransmitBuffer();
  delay(100);

  // Asigna la ID de esclavo Modbus y el puerto serie.
  // void ModbusMaster::begin(uint8_t slave, Stream &serial)
  node.begin(id_slave, Serial);   
  delay(100);

  // Las devoluciones de llamada nos permiten configurar correctamente el transceptor RS485
  node.postTransmission(postTransmission);
  node.preTransmission(preTransmission);
  
  
}

/*------------------------------------------------FUNCIÓN LOOP--------------------------------------------------------------------*/

void loop() 
{ 
  uint32_t  result; 
  u32_to_u16(u32_ConsignaCaudal);
  delay(500); 

  result = node.writeMultipleRegisters(MAR, NMR);
  delay(500);
  
  // Si la petición ha sido correcta y no ha fallado tendremos un buffer con los
  // registros, en el primero esta la parte alta del valor y en el segundo la baja.
  if( result == node.ku8MBSuccess)
  {
    Serial.println("Peticion correcta.");
  }

  else 
  {
    Serial.println("Error en la orden de writeMultipleRegisters!!! ");
    delay(1000);
    Serial.print("Tipo de Error = ");
    Serial.println(result);
    delay(1000);
    Serial.print(". Se identifica con: ");    
    getResultMsg(result);
    delay(2000);
  }
    
}

He comprobado la conexión del cableado y parece estar todo bien.
Pin Tx (ESP32) --> DI (Max485)
Pin Rx (ESP32)--> R0 (Max485)
Pin 14 (ESP32)--> DE & RE (Max485) (eneable)
A (Max485) --> A (Bomba)
B (Max485) --> B (Bomba)

Agradeceré cualquier sugerencia de que puede estar pasando o que hago mal.
Muchas Gracias!!

No entendí si la trama que pusiste fue solo como ejemplo pero creo que la trama para escribir el caudal debería ser

Id. esclavo = 0x01
Función = 0x10
Reg. ADDR HI = 0x12
Reg. ADDR LO = 0x4C
Num Reg HI = 0x00
Num Reg LO = 0x02
Value 1 HI = 0x00
Value 1 LO = 0x00
Value 2 HI = 0x23
Value 2LO = 0x28
CRC HI = 0x81
CRC LO = 0xFD

Al menos eso interpreto. (Verifica si el CRC es correcto)

Saludos

A lo dicho por @Gatul yo añado lo siguiente:

El módulo MAX485 se alimenta generalmente a una tensión de 5VDC. En teoria debería poder funcionar correctamente a 3.3VDC (la del ESP32) pero para ese tipo de aplicaciones es conveniente usar un chip de 3.3V como el MAX3485E

En cuanto a la conexión falta el GND. Sin una referencia de tensión díficilmente va a funcionar. He revisado el manual de la bomba y tienes a disposición las siguientes salidas:

SALIDA DETALLE
33 +5VDC
34 H(B)
35 L(A)
36 (-) GND

Esa información la puedes encontrar en la página 33 del manual.

Por lo tanto debes llevar el GND del max485 a la salida 36.

También habría que consultar si esas salidas están hay para conectar directamente a una señal rs485 o simplemente estan pensadas para que compres un módulo aparte para la realizar la comunicación. Los fabricantes suelen hacer cosas como esas para vender mas.

Por último, indicarte que algunos fabricantes eléctricos suelen denominar a los pines A/B de forma que para el MAX485 estan invertidos. Es decir, A de la bomba puede ser B del max485; y lo mismo ocurre con B. Aunque esto creo que ya es menos probable.

Tienes razón, escribí mal la trama Modbus.

Para comprobar que estaba enviando la trama Modbus correctamente me descargue el Software "Serial Port Monitor", que me permite monitorear y registrar cualquier información que pase por los puertos serie del ordenador. Así que analice el puerto serie que esta conectado a mi Maestro (ESP32). Dándome los siguientes resultados.

Id. esclavo = 0x01
Función = 0x10
Reg. ADDR HI = 0x12
Reg. ADDR LO = 0x4C
Num Reg HI = 0x00
Num Reg LO = 0x02
Bits count = 0x04
Value 1 HI = 0x00
Value 1 LO = 0x00
Value 2 HI = 0x23
Value 2LO = 0x28
CRC HI = 0x3A
CRC LO = 0x44

El CRC es el correcto. Al parecer no hay problema ya que la librería implementa correctamente la función "writeMultipleRegisters".

Comprobé que un gran numero de llamadas eran erróneas hasta llegar a una exitosa como se puede ver a continuación.

y esto es lo que se muestra en el monitor serial:

Consigna de caudal = 90 l/h
Iniciando descomposicion uint32_t --> uint16_t...
u16_ArrayTmp[0] = 0
u16_ArrayTmp[1] = 9000
MSB adjuntado correctamente al Buffer de transmision.
LSB adjuntado correctamente al Buffer de transmision.
...L.....#(:DError en la orden de writeMultipleRegisters!!! 

Pero aún así la bomba sigue sin dar respuesta.

¿Puede ser que el problema venga de los tiempos de llamada y respuesta en la trama Modbus?

Nunca hay problema en los tiempos de llamada y respuesta. Modbus es muy preciso en eso.
Si la trama y la conexión electrica estan bien, todo funciona perfectamente. Asi que si has logrado un exito parcial, hay que seguir por esa línea de trabajo. Siempre es especial comunicarse con un dispositivo Modbus.
Yo he usado MAX485 común desde un ESP8266 sin problemas. Asi que funciona, pero me dió trabajo.

Veo que 4684 (0x124C) te respondió OK.
Eso quiere decir ademas que acertaste con el tipo de mensaje.
En este caso el manual dice:
Funciones soportadas: READ INPUT REGISTERS (0x04), WRITE SINGLE REGISTER

  • (0x06), WRITE MULTIPLE REGISTERS (0x10)*
    En este caso es un Write Multiple registers o sea 2 registros MSB y LSB. Pero eso ya lo has conseguido.

Que son los demas mensajes de tu monitor Serie?
Otra cosa, porque repites tantas veces la misma petición? Si la consigna recibe un OK deja de enviarla.
Tienes que armar una máquina de estados.
Envias algo, recibes OK o BAD y entonces actuas pero no de otro modo.

Así fue cuando lo probé, el Max485 lo alimente a 5VDC, y el GND del Max485 lo lleve a la salida 36 de la bomba. También probé las conexiones A-->L(A) y B-->H(B) y viceversa. La bomba seguía sin dar respuesta.

Para tratar de encontrar el error, comprobé que estaba enviando bien la trama Modbus como le he comentado a gatul.

Además para simplificar el problema, he intentado comunicarme con el pc a través del Software "Modbus Poll" y un convertidor usb RS485. Mi intención ahora es enviar la consigna de caudal mediante el ESP32(maestro) y que el Modbus Poll haga de esclavo.

Para ello la configuración en Modbus Poll fue la siguiente:

Nota: Tengo en cuenta que cada valor de memoria corresponde a 8 bits. Por lo tanto, para escribir la consigna de caudal será necesario 4 direcciones de memoria.

Hasta aquí todo bien, también comprobé que el Software Modbus Poll hace la transmisión correctamente, es decir, envía la trama Modbus (mensaje de transmisión Tx) cada 2 segundos.

Pero tras poner todo en marcha, no obtenía respuesta (Rx) del ESP32, es extraño ya que comprobé en el Software "Serial Port Monitor" que si enviaba cada cierto tiempo una respuesta, y además se me enciende cada cierto periodo de tiempo el led del USB-RS485 correspondiente a Rx. Pero en el Modbus Poll no me aparece por pantalla.

Este problema me tiene loco. ¿Puede ser que el problema venga de los tiempos de llamada y respuesta en la trama Modbus?

Agradeceré cualquier sugerencia de que puede estar pasando o que hago mal.
Muchas Gracias!!

Durante la ejecución de mi programa he ido añadiendo varios "Serial.print" como método de comprobación de que se estaban haciendo bien los pasos, por ejemplo en mi salida por pantalla.

 Consigna de caudal = 90 l/h   
//Corresponde a la variable de consigna de caudal que quiero escribir en los registros de memoria de mi bomba. 
//(Recuerda: Consigna de caudal = 0x00002328 = 90.000 * 10^-2 = 90 l/h).

Iniciando descomposicion uint32_t --> uint16_t...   
//Me indica que voy a proceder la descomposición de mi variable de consigna de caudal de un 
//numero uint32_t a un numero uint16_t. Esto se utiliza ya que para añadir la variable 
//u32_ConsignaCaudal al buffer de transmisión implementado en la clase <ModbusMaster.h> es 
//necesario descomponerlo en dos registros/numeros de 16 bits.

u16_ArrayTmp[0] = 0
u16_ArrayTmp[1] = 9000
//Aquí muestro el resultado de la descomposición en dos numero uint_16, añadidos a un array 
//temporal creado en esta función en especifico.

MSB adjuntado correctamente al Buffer de transmision.
LSB adjuntado correctamente al Buffer de transmision.
//Aqui compruebo que se han añadido correctamente al buffer de transmisión de la clase ModbusMaster.

...L.....#(:DError en la orden de writeMultipleRegisters!!! 
Tipo de Error = 226
. Se identifica con: Response Timed Out
//Aquí muestro que la petición no ha sido correcta y que corresponde con ese tipo de error 
//(según está en la clase ModbusMaster)

Investigando en los archivos .h y .cpp de la librería, me he encontrado que se debe a un error debido al CRC.

//Excepción CRC de respuesta inválida de ModbusMaster.
//El CRC en la respuesta no coincide con el calculado.
if ((millis() - u32StartTime) > ku16MBResponseTimeout)
    {
      u8MBStatus = ku8MBResponseTimedOut;
    }
  }

//En mi caso ku8MBResponseTimedOut = 1000 ms

Es por eso, que tras analizar la trama en el Software "Serial Port Monitor" me aparecen varias tramas como [BAD] hasta que se muestra un [OK].

¿Piensas que una maquina de estados hará que no se produzca este error en el calculo del CRC y sea más eficiente mi aplicación? ¿O por el contrario el problema es otro?

No consigo dar con la solución a este problema. Un saludo!!!

Me parece que ahí tienes un error de interpretación.
Lo que lees/escribes son registros de 16 bits.
Si bien es cierto que ocupan 4 bytes, tu lees/escribes a nivel de registro entonces para leer/escribir la consigna caudal debes poner 0x02 en lugar de 0x04.

Fijate que tu ejemplo con Modbus Poll es igual al ejemplo Lectura de varios registros, de la documentación de la bomba, pero el ejemplo es para leer caudal (2 registros) y contador de ciclos (2 registros).

Saludos

Es cierto, cada registro en Modbus Poll es de 8 bits, por eso cogí 4 registros (4684, 4685, 4686, 4687). Sin embargo, en el manual de la bomba los registros son de 16 bits (MSB, LSB).

Mi duda ahora es, ¿cómo consigo tener 2 registros de 16 bits en Modbus Poll?

Mi configuración ahora es la siguiente:

A ver si configurándolo de esa manera puedo recibir la orden del ESP32 y que me aparezca Rx en el monitor del Modbus Poll.

Sigo aquí atascado. Una vez que confirme esto, podré pasar a hacer pruebas con la bomba.

Gracias, y un saludo a todos.

En el manual tienes los siguientes comandos:

  • Lectura (individual o multiple): 0x04
  • Escritura (1 registro): 0x06
  • Escritura (múltiple): 0x10

El comando 0x03 que usas en MBP y se ve en la captura de pantalla no sería admitido por la bomba, según entiendo.

Saludos

Lo que yo estoy intentando ahora es comunicar mi ESP32 con Modbus Poll, ya que no podré realizar pruebas con la bomba hasta dentro de un par de dias.

Por eso utilizo, las siguientes funciones:

-Esp32: (Write Multiple Registers --> 0x10)
-Modbus Poll: (Read Holding Registers --> 0x04)

La cosa es... que para el ESP32 utilizo 2 registros de 16 bits y para Modbus Poll 4 registros de 8 bits.

Quisiera saber como cambiar en Modbus Poll para tener así 2 registros de 16 bits.

Para así verificar el trafico de comunicación entre Tx y Rx, de momento solo obtengo esto:

En eso no te puedo ayudar, no he usado Modbus Poll y de hecho no he usado Modbus en absoluto (por ahora).

Saludos

Gracias de todos modos.
Un saludo.

modbus

En la ventana de "read/write definition" cuando creas la consulta, abajo verás un combobox que pone "display", ahí puedes indicar el tamaño de las variables a visualizar. Puedes seleccionar long
o inverse long que será un entero largo (32bits). Juega con los valores de dirección y cantidad para
lograr el efecto deseado.

Tienes otro MAX485? podrias poner el Modbus Poll como monitor y ver lo que comunican el dispositivo y el ESP32.

Buenas a todos.

He avanzado un poco con el tema tratando de buscar el error.

Conseguí utilizar 2 registros de 16 bits en Modbus Poll, el mensaje de transmisión Modbus que envía el software parece ser el correcto, el cual es el siguiente.

Como se ve en la imagen, el software (que simula a la bomba) es el esclavo, y quiere leer el valor de consigna de caudal que arroja el ESP32, en este caso caudal = 90.000.

Id. esclavo = 0x01
Funcion = 0x04  (Read Input Register)  
Reg. ADDR HI = 0x12
Reg. ADDR LO = 0x4C
Num Reg HI = 0x00
Num Reg LO = 0x02
CRC HI = B5
CRC LO = 64

Hasta aquí todo correcto.

Volví a conectar mi ESP32(maestro) con Modbus Poll(esclavo).

Tras ponerlo en marcha e investigar de donde procedía el error que me daba en los archivos .h & .cpp de la libreria ModbusMaster, encontre que el error es el siguiente.

Invalid slave id

Este error corresponde con la línea 759 del archivo .cpp

Seguí investigando y comprobando que se hacía todo correctamente. El array que contiene la trama Modbus es el "u8ModbusADU[256]".
Lo comprobé y era correcto, siendo este:

u8ModbusADU[1] = 1  (dec) = 0x01 (Hex) = Id. Esclavo
u8ModbusADU[2] = 16 (dec) = 0x10 (Hex) = Función
u8ModbusADU[3] = 18 (dec) = 0x12 (Hex) = Reg. ADDR HI
u8ModbusADU[4] = 76 (dec) = 0x4C (Hex) = Reg. ADDR LO
u8ModbusADU[5] = 0  (dec) = 0x00 (Hex) = Num. Reg. HI
u8ModbusADU[6] = 2  (dec) = 0x02 (Hex) = Num. Reg. LO
u8ModbusADU[7] = 4  (dec) = 0x04 (Hex) = Byte count
u8ModbusADU[8] = 0  (dec) = 0x00 (Hex) = Value 1 HI
u8ModbusADU[9] = 0  (dec) = 0x00 (Hex) = Value 1 LO
u8ModbusADU[10]= 35 (dec) = 0x23 (Hex) = Value 2 HI
u8ModbusADU[11]= 40 (dec) = 0x28 (Hex) = Value 2 LO
u8ModbusADU[12]= 58 (dec) = 0x3A (Hex) = CRC HI
u8ModbusADU[13]= 68 (dec) = 0x44 (Hex) = CRC LO

Es a partir de la línea 704 del archivo .cpp cuando empieza a borrar elementos y hace que mi Id.Esclavo tome el valor de 0. No soy un experto en lenguaje Arduino u C++. Así que si me pueden ayudar a comprender este fragmento de código de la librería ModbusMaster me sería de gran ayuda para avanzar a la hora de encontrar mi error.

El fragmento de código que quiero entender es el siguiente, procedente del archivo .cpp, a partir de la linea 704:

// flush receive buffer before transmitting request
  while (_serial->read() != -1);

  // transmit request
  if (_preTransmission)
  {
    _preTransmission();
  }
  for (i = 0; i < u8ModbusADUSize; i++)
  {
    _serial->write(u8ModbusADU[i]);
  }
  
  u8ModbusADUSize = 0;
  _serial->flush();    // flush transmit buffer
  if (_postTransmission)
  {
    _postTransmission();
  }
  
  // loop until we run out of time or bytes, or an error occurs
  u32StartTime = millis();
  while (u8BytesLeft && !u8MBStatus)
  {
    if (_serial->available())
    {
#if __MODBUSMASTER_DEBUG__
      digitalWrite(__MODBUSMASTER_DEBUG_PIN_A__, true);
#endif
      u8ModbusADU[u8ModbusADUSize++] = _serial->read();
      u8BytesLeft--;
#if __MODBUSMASTER_DEBUG__
      digitalWrite(__MODBUSMASTER_DEBUG_PIN_A__, false);
#endif
    }
    else
    {
#if __MODBUSMASTER_DEBUG__
      digitalWrite(__MODBUSMASTER_DEBUG_PIN_B__, true);
#endif
      if (_idle)
      {
        _idle();
      }
#if __MODBUSMASTER_DEBUG__
      digitalWrite(__MODBUSMASTER_DEBUG_PIN_B__, false);
#endif
    }

Un saludo a todos y gracias.

Vaya, parece que el problema es más complejo de lo que parecía.

Echad un vistazo a este post. Creo que puede tener relación con mi error.

Gracias, un saludo.