Sporadische Timeouts bei ModbusRTU Slaves

Hallo zusammen,

ich habe ein Problem mit der zuverlässigen Antwort meiner ModbusRTU-Slaves.

In unserem Wohnhaus sind in jedem Raum in einer Unterputzdose proMinis untergebracht die dort mit je einem DHT22 die Lufttemperatur und die relative Luftfeuchte messen, sie optional auf einem OLED Display darstellen und per ModbusRTU einem Master zur Archivierung zur Verfügung stellen sollen.

Die letzten sechs Jahre werkelte ein selbst geschriebenes Python-Skript auf einem Raspi 3B um die Slaves anzusprechen und die Messwerte in eine MySQL Datenbank zu schreiben. Und auch damals schon kam es immer wieder mal vor, dass bei Abfrage diverser Slaves ein Timeout auftrat. Die Fehlerhäufigkeit ist dabei relativ gleichmäßig auf alle Slaves verteilt. Im Python-Skript hatte ich dazu einen „faulen“ Workaround geschaffen der einfach jeden Slave bis zu drei Mal angesprochen hat.

Vor kurzem habe ich nun auf Iobroker umgestellt und lasse die Slaves mit einem ModbusRTU Adapter abfragen. Hier ist natürlich kein Workaround möglich und das ist auch gut so – ich denke mittlerweile, dass es sinnvoller ist die Ursache für die Timeouts zu beheben, wenn denn möglich.

Es kommt allerdings auch mit dem schlankestmöglichen Code immer wieder zu Timeouts und mir gehen langsam die Ideen aus woran das noch liegen könnte.

Der aktuelle Testaufbau auf dem Schreibtisch umfasst einen Slave mit dem unten angehängten Code. Also keine langen Leitungen, selbst die OLED Ansteuerung habe ich schon mal auskommentiert um zu sehen ob der OLED-Code zu irgendwelchen Latenzen führt. Aber, keine Besserung. Selbst wenn wirklich nur der DHT abgefragt wird, kommt es immer wieder mal zu Timeouts. Master ist in diesem Fall der PC mit der QModMaster Software zum Abfragen der Leseregister.

#include <Arduino.h>
#include <DHT_Async.h>
#include <ModbusRTUSlave.h>
// #include <U8g2lib.h>


/*
/*-----( Declare Constants and Pin Numbers )-----*/
#define RELEASE 3.01          //NICHT KOMPATIBEL MIT LOCHRASTER-PROTOTYP!!!
#define BAUD 19200
#define RX_PIN 12             // Serial Receive pin RO on MAX485-Board
#define TX_PIN 10             // Serial Transmit pin DI on MAX485-Board
#define DE_RE_PIN 11          // MAX485-Board Direction control DE_RE
#define DHT_SENSOR_PIN 2      // Sensor angeschlossen an pin
#define DHT_TYPE DHT_TYPE_22  // Sensortyp = DHT22
#define OLED_5V 3             //Spannungsversorgung OLED +5V fuer Abschaltung OLED ### Aktuell nicht verwendet ###
#define ID_Bit0 4             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define ID_Bit1 5             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define ID_Bit2 6             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define ID_Bit3 7             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define ID_Bit4 8             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define ID_Bit5 9             //mit 6Bit wird der ProMini codiert --> ID vergeben
#define VCC A0                //Messe Versorgungsspannung


SoftwareSerial mySerial(RX_PIN, TX_PIN);
ModbusRTUSlave modbus(mySerial, DE_RE_PIN);  // serial port, driver enable pin for rs-485
uint16_t inputRegisters[6];

// Initialization DHT22
DHT_Async dht_sensor(DHT_SENSOR_PIN, DHT_TYPE);

// Initialization OLED Display
// U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);

void setup() {
  //Define all needed Digital IN/OUT´s
  pinMode(OLED_5V, OUTPUT);        // Vcc for OLED Display
  pinMode(DE_RE_PIN, OUTPUT);      // Modbus direction switch
  pinMode(ID_Bit0, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(ID_Bit1, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(ID_Bit2, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(ID_Bit3, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(ID_Bit4, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(ID_Bit5, INPUT_PULLUP);  // SlaveID 6bit in summary max 63 slaves
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  //Initialize all needed OUTPUT´s
  digitalWrite(OLED_5V, HIGH);  // switches the OLED constantly on
  delay(150);

  // Initialize modbus instance
  uint8_t SlaveAdress = getSlaveAdress();
  modbus.configureInputRegisters(inputRegisters, 6);  // unsigned 16 bit integer array of input register values, number of input registers
  modbus.begin(SlaveAdress, BAUD, SERIAL_8N1);        // ModbusRTUSlave::begin(uint8_t unitId, unsigned long baud, uint32_t config, unsigned long preDelay; unsigned long postDelay)

  // Start OLED Driver instance
  // u8g2.begin();

  Serial.begin(9600);

  // OLED_StartScreen();
}

void loop() {

  static float temperature;
  static float humidity;
  static byte keepAliveFeedback;
  static byte dht_error;


  int voltage = CheckVoltage(VCC);
  bool sampled = measure_environment(&temperature, &humidity, &dht_error);

  // --- Modbus Communication ---
  inputRegisters[0] = (int)(temperature * 10);
  inputRegisters[1] = (int)(humidity * 10);
  inputRegisters[2] = keepAliveFeedback;        // Send keep-alive frmo 0 to 99
  inputRegisters[3] = voltage;
  inputRegisters[4] = dht_error;                //DHT Error incremented in error case
  inputRegisters[5] = (int)(RELEASE * 100);
  modbus.poll();

  

  // OLED Data
  // keepAliveFeedback = OLED_Measured_Values(temperature, humidity, 0);
  keepAliveFeedback = 123;
}

/*
byte OLED_Measured_Values( float Val_1, float Val_2, int ERR_ALL) {

  //TXT_1 Benennung des Messwerts
  //Val_1 Temperaturwert
  //Val_2 Lufteuchtigkeitswert

  static byte ScreenDelay = 1;
  static byte keepAlive = 0;

  static unsigned long flap_screen = millis();


  // Flap the screen after a defined time 
  if (millis() - flap_screen > 1000ul) {
    flap_screen = millis();
    ++ScreenDelay;

    //Wechsel nach X-Sekunden wieder auf den Screen1
    if (ScreenDelay >= 17) {
      ScreenDelay = 5;
    }

    //Zaehle einen keep-alive-timer hoch
    ++keepAlive;
  }


  //Bedingung zur Anzeige des ersten Screens
  if (ScreenDelay == 5) {

    u8g2.firstPage();
    do {
      u8g2.setFont(u8g2_font_helvB10_tf);
      u8g2.setCursor(2, 12);
      u8g2.print("Temperatur:");

      u8g2.setFont(u8g2_font_fub35_tn);
      u8g2.setCursor(0, 64);
      u8g2.print(Val_1, 1);
    } while (u8g2.nextPage());

    ++ScreenDelay;

  }

  if (ScreenDelay == 11) {

    u8g2.firstPage();
    do {
      u8g2.setFont(u8g2_font_helvB10_tf);
      u8g2.setCursor(1, 12);
      u8g2.print("Luftfeuchtigkeit:");

      u8g2.setFont(u8g2_font_fub35_tn);
      u8g2.setCursor(0, 64);
      u8g2.print(Val_2, 1);
    } while (u8g2.nextPage());

    ++ScreenDelay;

  }
  return keepAlive;
}
*/


/*
void OLED_StartScreen(){

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_helvB10_tf);
    u8g2.setCursor(2, 12);
    u8g2.print("Slave-ID:   "); u8g2.print(getSlaveAdress());
    u8g2.setCursor(2, 30);
    u8g2.print("Release:    "); u8g2.print(RELEASE);
    u8g2.setCursor(2, 48);
    u8g2.print("Voltage:    "); u8g2.print(CheckVoltage(VCC)/100); u8g2.print("V");
  } while ( u8g2.nextPage() );

  delay(2000);
  return;
}
*/


static bool measure_environment(float *temperature, float *humidity, byte *dht_error) {

  static unsigned long measurement_timestamp = millis();
  static float tmp_temp = 99.9;
  static float tmp_humi = 99.9;
  static bool firstRun = true;


  /* Measure once every four seconds. */
  if (millis() - measurement_timestamp > 4000ul || firstRun) {
    if (dht_sensor.measure(temperature, humidity)) {
      measurement_timestamp = millis();

      if(isnan(*temperature) || isnan(*humidity)){
        *temperature = tmp_temp;                      // 99.9 means, we never saw a real measure but NAN was delivered
        *humidity = tmp_humi;                         // 99.9 means, we never saw a real measure but NAN was delivered
        *dht_error = 10;                              // error-code 10 means that delivered value is NAN
      }else{
        // eine Messung temporär zwischenspeichern
        tmp_temp = *temperature;
        tmp_humi = *humidity;
        *dht_error = 0;                              // error-code 0 means everything is fine
      }

      firstRun = false;
      return (true);
    }
  }
  return (false);
}

byte getSlaveAdress() {

  byte ID_Byte = 0;

  //Codiereingänge lesen und entsprechende Bits im Adress_Byte setzen
  bitWrite(ID_Byte, 0, digitalRead(ID_Bit0));  //least-significant (rightmost) bit
  bitWrite(ID_Byte, 1, digitalRead(ID_Bit1));
  bitWrite(ID_Byte, 2, digitalRead(ID_Bit2));
  bitWrite(ID_Byte, 3, digitalRead(ID_Bit3));
  bitWrite(ID_Byte, 4, digitalRead(ID_Bit4));
  bitWrite(ID_Byte, 5, digitalRead(ID_Bit5));
  bitWrite(ID_Byte, 6, 0);  //Ungenutzes Nibble initialisieren
  bitWrite(ID_Byte, 7, 0);  //Ungenutzes Nibble initialisieren

  return ID_Byte;
}

int CheckVoltage(byte pin) {

  float rawVoltage = map(analogRead(pin), 0, 1023, 0, 500);
  int Voltage = (int)rawVoltage;

  return Voltage;
}

QModbusMaster_Error_Timeout

Vielleich kann mir hier jemand helfen?

da würde ich zunächst wirklich einen schlanken Testcode machen, z.B. nur einen digitalPin einlesen bzw. ein Register mit einem Fixwert od. Sekunden seit Systemstart zurückliefern.

Mit einem einfachen Sketch kannst du überprüfen ob das Problem beim Slave oder beim Master liegt.

Danke dir für die schnelle Rückmeldung. Ich teste das Ganze dann jetzt also mal mit:

#include <ModbusRTUSlave.h>

/*-----( Declare Constants and Pin Numbers )-----*/
#define BAUD 19200
#define RX_PIN 12             // Serial Receive pin RO on MAX485-Board
#define TX_PIN 10             // Serial Transmit pin DI on MAX485-Board
#define DE_RE_PIN 11          // MAX485-Board Direction control DE_RE

SoftwareSerial mySerial(RX_PIN, TX_PIN);
ModbusRTUSlave modbus(mySerial, DE_RE_PIN);  // serial port, driver enable pin for rs-485
uint16_t inputRegisters[1];

void setup() {
  //Define all needed Digital IN/OUT´s
  pinMode(DE_RE_PIN, OUTPUT);      // Modbus direction switch


  // Initialize modbus instance
  uint8_t SlaveAdress = 11;
  modbus.configureInputRegisters(inputRegisters, 1);  // unsigned 16 bit integer array of input register values, number of input registers
  modbus.begin(SlaveAdress, BAUD, SERIAL_8N1);        // ModbusRTUSlave::begin(uint8_t unitId, unsigned long baud, uint32_t config, unsigned long preDelay; unsigned long postDelay)


  // Serial.begin(9600);

}

void loop() {

   // --- Modbus Communication ---
  inputRegisters[0] = 123;
  modbus.poll();

}

Das sieht mir dann allerdings irgendwie nach einer quasi iterativen Fehlersuche aus in der dann der Code wieder Stück für Stück aufgebaut und lange getestet werden muss?

Wie kann man denn an der Stelle testen ob das Problem am Master liegt?

klar musst du das schrittweise testen, wie willst sonst auf die Ursache kommen.

na jetzt befeuere mal deinen Slave und schau ob bzw. wodurch du Timeouts bekommst.

Nun, über den heutigen Tag habe ich mehrere Tests gemacht und dabei hat sich nun herausgestellt, das sobald die DHT_Async Funktion zum Auslesen des DHTs verwendet wird, der Timeout auftaucht. Dabei hab ich extra auf die "Async" Bibliothek zurück gegriffen um derartigen Timeoutproblemen aus dem Weg zu gehen.

Jetzt stellt sich mir die Frage, was ich tun kann damit der DHT nicht die ModbusRTU Funktionalität einschränkt?

Du könntest mit verschieden DHT libraries experimentieren, ob die ein ähnliches Problem haben.

Ganz wirst du das Problem nicht ausschließen können. Irgendwann kollidieren das Auslesen des DHT und ein eintreffender Request immer - und du hast nur einen CPU-Kern, um beides zu bearbeiten. Da kommst du nur mit einer Mehrkern-MCU weiter (ESP32 z.B.), oder mit einer, die die Serielle mit einer eigenständigen Logik betreibt und einen separaten Puffer hat, der groß genug für den Request ist.

Könnte man den Modbus-Part nicht auch "irgendwie" in einer ISR unterbringen und damit bevorzugt behandeln?

"nein"

überleg dir eher:

  • warum musst du die Temperatur so oft vom Sensor lesen
  • warum musst du die Temperatur so oft auf dem Master einlesen

Die aktuelle Abtastrate ist nur beispielhaft um zu sehen ob das Auslesen ohne Timeouts klappt oder nicht. Natürlich muss im Produktivbetrieb nicht sekundenweise ausgelesen werden.

Eine deutlich geringere Abtastrate behebt aber das eigentliche Problem nicht, es verringert halt nur die Anzahl Timeouts.

Du könntest versuchen, einen Interrupt der Seriellen dafür zu benutzen, ein "lies keine Daten vom DHT22"-Flag zu setzen. Dadurch wird Modbus priorisiert und am Ende der Requestbearbeitung setzt du das Flag wieder zurück. Wenn du aber gerade im Datenlesen steckst, nutzt dir das auch nichts, es wird trotzdem zu (weniger) Kollisionen kommen.

ich würde das am Master behandeln.
Wenn der Request in ein Timeout läuft (weil der Slave anderwärtig beschäftigt ist), dann Fehlercounter erhöhen und in einem kürzerem Intervall wieder probieren.
bei mehreren (5?) konsekutiven Fehlern den Slave "aufgeben" oder wieder im normalen Intervall fortsetzen.

wobei es mich jetzt fast interessieren würde ...
wie schnell schafft man ein DHT lesen
wie schnell kommt es zu einem Timeout bei einem 19200 baud modbus ...

Master ist der Iobroker mit dem Adapter "Modbus". Die Adapterinstanz soll alle verfügbaren Adressen auslesen. Da habe ich an der Stelle leider keinen Einfluss auf die Vorgehensweise des Auslesens. Also solch ein Workaround wie ich ihn seinerzeit in Python realisiert hatte "lese ModbusAdresse XY bei Timeout bis zu drei mal" geht mit dem Iobroker in der Form nicht.

Zusätzlich dazu arbeitet der Iobroker die Liste an Slaves nicht chronologisch weiter ab wenn ein Slave einen Timeout verursacht hat. Iobroker fängt dann wieder von Adresse 0 an statt in der Liste mit dem nächsten Slave weiter zu machen.

Und natürlich müllen mir die Timeouts dann das Logfile im Iobroker zu.

Schon ein eigenartiges Verhalten.

das würde beim Ersteller der Software als Feature Request einwerfen.

Ein Versuch wäre es auch noch am Slave die HW Serial für Modbus zu verwenden, dann hast wenigstens einen Eingangsbuffer auf HW-Ebene.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.