S0-Zähler mit Arduino Nano auslesen, wie Fehlimpulse verhindern?

Hallo,

ich versuche gerade mein allererstes Arduino Nano Projekt umzusetzen, und zwar das Auslesen eines S0 Zählers (1000 Pulse pro kW). Dabei habe ich dasselbe Referenzprojekt genommen wie der Autor dieses älteren Threads hier, und auch die gleiche Schaltung wie der Verfasser des Beitrags:

Auch bei mir ist es so, dass ich alle paar Sekunden, manchmal auch nur alle paar Minuten einen Interrupt bekomme obwohl kein Zählimpuls generiert wurde. Nun wird in dem Posting oben eine Schaltungsalternative genannt, aber nur verbal beschrieben, und ich bin mir nicht sicher ob ich den Vorschlag richtig verstanden habe, und vor allem wie groß der Widerstand eigentlich sein sollte. Daher habe ich das mal so aufgezeichnet, wie ich das als Laie verstanden habe, und bitte um sachdienliche Korrekturen und Hinweise, wie ich die Schaltung so störsichier wie möglich machen kann:

Derzeit versuche ich, die Störimpulse durch mein Programm etwas zu filtern und abzumildern, auch hier mal mein "Sketch" basierend auf dem ursprünglichen Code, aber mit einigen Abwandlungen. Auch hier bin ich, als wie gesagt absoluter Neuling auf dem Gebiet, für Vorschläge und Korrekturen sehr dankbar :slight_smile:

#include <EEPROM.h>
#define SAVEMULT 10
#define ERRORTHRESHOLD 25
#define SAVETHRESHOLD 100
int interval = 5;  //Counter refresh and save interval
int savemultiplier = 0;

long counter;
long prevSavedCounter;
int pulseCount, crontimer;
byte prevLEDStatus;
 
void setup() 
{
  Serial.begin(9600);
  attachInterrupt(0, onPulse, FALLING);
  counter = read_counter(0);
  prevSavedCounter = counter;
  pinMode(LED_BUILTIN, OUTPUT);
  prevLEDStatus = LOW;
  //save_counter(0, 40047100); 
}

void loop() {
  if(millis()/1000 > crontimer+interval)
  {
    prevLEDStatus = LOW;
    digitalWrite(LED_BUILTIN, LOW); 
    crontimer = millis()/1000;
    if (savemultiplier > SAVEMULT)
    {
      long counterdiff = counter - prevSavedCounter; 
      if (counterdiff < ERRORTHRESHOLD)
      {
        counter = prevSavedCounter;
        Serial.print("Counter difference too low, error signals assumed, reset counter to prev value, diff = ");
        Serial.println(counterdiff);
      }
      else if (counterdiff > SAVETHRESHOLD)
      {
        save_counter(0, counter);
        prevSavedCounter = counter;
        Serial.print("Counter difference high enough to save, so saving, diff = ");
        Serial.println(counterdiff);
        Serial.print("Saved to EEPROM: ");
        Serial.println(counter);
      }
      else
      {
        Serial.print("Counter difference too low to save, wait for next saving cycle, diff = ");
        Serial.println(counterdiff);
      }  
      savemultiplier = 0;
    }
    pulseCount = 0; 
    savemultiplier++;
    
    Serial.println(counter);
  }
}


void save_counter(int adr, long lo) 
{
  byte by;
  for(int i=0;i< 4;i++) 
  {
    by = (lo >> ((3-i)*8)) & 0x000000ff; 
    EEPROM.write(adr+i, by);
  }
} 


long read_counter(int adr) 
{
  long lo=0;

    for(int i=0;i< 3;i++){
      lo += EEPROM.read(adr+i);
      lo = lo << 8;
    }
    lo += EEPROM.read(adr+3);
    return lo;
} 

void onPulse()
{
    pulseCount++;
    counter++;
    if (prevLEDStatus == LOW)
    {
      digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
      prevLEDStatus = HIGH;
    }
    else 
    {
      digitalWrite(LED_BUILTIN, LOW); 
      prevLEDStatus = LOW;
    }
}

Der Interrupt wird ausgelöst wenn eine fallende (Spannungs-) Flanke erkannt wird. Daraus ist zu schließen, dass der Widerstand als Pullup- Widerstand gedacht ist, damit das Potential am Pin stabil auf Betriebsspannung gehalten wird, bis der S0 Anschluss auf GND gezogen wird.

Von daher passt das so. Die Größe des Widerstandes beeinflusst in gewisser Weise die „Störfestigkeit“. Je kleiner, desto höher. Allerdings fließt dann auch mehr Strom. 10K dürfte ein guter Ausgangswert sein. Musst Du probieren.

1 Like

Hallo
eventuell hilft auch noch ein 100nF Kondensator zwischen Eingang und GND.

Käme darauf an, wie lang ein Impuls ist. Das sollte, unabhängig von der aktuellen Leistung immer gleich lang sein und so kurz, dass selbst bei Höchstlast die Pause zischen zwei Pulsen länger als die Pulsdauer ist.
100nF * 10k (= 1 ms) bräuchte also Pulse > 1..2 ms. Bei längeren Pulsen gingen auch größere Kondensatoren.

Dass das Mist ist, weißt du ?

millis() liefert unsigned long, die Division / 1000 dann ebenfalls.
Das passt nicht in eine int - Variable, jedenfalls nicht bei einem AVR-Nano.

Und wenn das repariert ist, dauert es nach einem millis-Überlauf ( knapp 50 Tage ) mindestens wieder so lange, bis das --wenn überhaupt-- jemals wieder true wird.

1 Like

Ändere den Widerstand in 3,3K und verzichte auf die Verwendung von Interrupts.
1 KWh löst bei den meissten Zählern am S0 1000Impulse aus.
Das schaffst Du locker in ganz normaler Umlauftechnik.
Da die Dinger nicht prellen, ist ein entprellen und warten auf irgendwas unnötig.
Den Pegelwechsel bekommst Du mit einer bool mit - fertig.

1 Like

Hallo ,
Ich wollte die Frequenz mal ausrechnen. Dabei habe ich dann bemerkt das es 1000Pulse/KWh heißen muss.

Danke für den Hinweis mit Millis, was wäre denn eine denkbare Alternative damit es langzeitstabil wird?

Zum Thema Interrupt: Könnte Ich zusätzlich zur Fallenden auch einen Interrupt erhalten bei steigender Flanke, und prüfen ob die Zeit zwischen den flanken mindestens 30ms beträgt,wie es der S0 Standard vorsieht oder mache ich es mir zu einfach? Wie würdet ihr das Problem mit Umlauftechnik genau angehen?

Du machst es dir zu kompliziert.

Mach es ohne Interrupt, wie bereits vorgeschlagen.
Falls da tatsächlich Störungen entstehen, mach den Pullup kleiner, und nimm einen Kondensator. Oder verdrillte oder gar abgeschirmte Leitungen (GND am Arduino mit dem Schirm verbinden)

Den millis-Überlauf in einer Zyklus-Steuerung fängt

static unsigned long altMillis;  
if ( millis() - altMillis > interval ) {
    altMillis += interval;
    // was immer einmal im Intervall gemacht werden soll
}

ab, denn bei einem Überlauf bleibt die Differenz richtig und die Addition funktioniert auch.

Und schreib nicht zu oft in den EPROM. Wenn der Arduino ausfällt, werden dir während der Ausfallzeit in jedem Fall Impulse fehlen, also versuch erst gar nicht, den Zählerstand parallel zu halten.

1 Like

Danke für die vielen hilfreichen, konstruktiven Vorschläge an alle Schreibenden. Ich habe mal versucht eure Vorschläge umzusetzen und:
1.) das Überlaufproblem mit millis() zu fixen
2.) den Code umzubauen von Interrupt auf klassische Schleifenlogik.

Leider kann ich erst wriklich testen, wenn mein Zähler wieder zählt, das tut er im Moment nicht (es handelt sich um einen reinen Zähler für meine Wärmepumpe, da wird quasi nur gezählt wenn selbige gerade läuft. Irgendwelche Ideen wie man das S0 Signal zum Testen simulieren könnte)?

Hier mal mein aktueller Sketch, mit der Bitte um Meinungen und evtl. Verbesserungsvorschläge. Danke :slight_smile:

#include <EEPROM.h>
#define SAVEMULT 10
#define ERRORTHRESHOLD 15
#define SAVETHRESHOLD 100
#define DESIREDPULSETIME 25000
int interval = 5000;  //Counter refresh and save interval
int savemultiplier = 0;

long counter;
long prevSavedCounter;
int crontimer;
unsigned long time_now = 0;
byte prevLEDStatus;
unsigned long starttime = 0;
unsigned long endtime = 0;
unsigned long pulsetime = 0;
bool counting = false; 
void setup() 
{
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP);
  counter = read_counter(0);
  prevSavedCounter = counter;
  pinMode(LED_BUILTIN, OUTPUT);
  prevLEDStatus = LOW;
}

void loop() 
{
  if ( digitalRead(2) == HIGH && counting==false)
  {    
    starttime=millis();
    counting=true;
    digitalWrite(LED_BUILTIN, HIGH);
  }
  if (digitalRead(2) == LOW  && counting==true) 
  {    
    counting = false;
    endtime=millis();
    pulsetime = endtime - starttime;
    if (pulsetime > DESIREDPULSETIME)
    {
      counter++;
      Serial.print("Long signal detected, counting. Signal length = ");
    }
    else
    {
      Serial.print("Short signal detected, NOT counting. Signal length = ");      
    }
    Serial.println(pulsetime);
    digitalWrite(LED_BUILTIN, LOW); 
  }
  if(millis() > time_now + interval)
  {
    prevLEDStatus = LOW;
    digitalWrite(LED_BUILTIN, LOW); 
    time_now = millis();
    if (savemultiplier > SAVEMULT)
    {
      long counterdiff = counter - prevSavedCounter; 
      if (counterdiff < ERRORTHRESHOLD)
      {
        counter = prevSavedCounter;
        Serial.print("Counter difference too low, error signals assumed, reset counter to prev value, diff = ");
        Serial.println(counterdiff);
      }
      else if (counterdiff > SAVETHRESHOLD)
      {
        save_counter(0, counter);
        prevSavedCounter = counter;
        Serial.print("Counter difference high enough to save, so saving, diff = ");
        Serial.println(counterdiff);
        Serial.print("Saved to EEPROM: ");
        Serial.println(counter);
      }
      else
      {
        Serial.print("Counter difference too low to save, wait for next saving cycle, diff = ");
        Serial.println(counterdiff);
      }  
      savemultiplier = 0;
    }        
    Serial.println(counter);
    savemultiplier++;
  }
}


void save_counter(int adr, long lo) 
{
  byte by;
  for(int i=0;i< 4;i++) 
  {
    by = (lo >> ((3-i)*8)) & 0x000000ff; 
    EEPROM.write(adr+i, by);
  }
} // eepromWriteLong


long read_counter(int adr) 
{
  long lo=0;

    for(int i=0;i< 3;i++){
      lo += EEPROM.read(adr+i);
      lo = lo << 8;
    }
    lo += EEPROM.read(adr+3);
    return lo;
} // eepromReadLong

Wenn es billig sein soll. Oder sowas mit einem Microcontroller selber programmieren (PWM Signal ausgeben lassen).

1 Like

Hallo,
die Frequenzen die da bei dem S0 Signal auftreten sind ja recht niedrig. Du könntest mit einem freien Ausgang ein "Blinksignal" erzeugen bei dem der H Wert Konstant 30ms lang ist und die L Zeit Variabel. Als Vorlage kannst Du das Beispiel "BinkWithoutDelay" nehmen .

Da must Du jetzt mal ein bisschen rechnen.
Dein Zähler gibt 1000Imp/KWh aus.
Nehmen wir mal an deine Wärmepumpe läuft mit 2000 KW dann sind das in einer Stunde 2000 Impulse. Das macht dann je Sekunde 2000/3600=0,55555 Imp/s. Damit eine Periodendauer von 1/0,55555 = 1,8000s Davon ziehst Du die 30ms für die H zeit ab dann ergibt das 1770 ms L Zeit. Der Blinker blinkt mit 30ms H und 1770 L Zeit.

Schau die auch mal Eprom put und get an, das ist einfacher. Eventuell erweiterst Du auch Dein System um eine RTC dann hast Du einen Bezug zur Uhrzeit.

1 Like

Mach das nicht!

An Stelle von .write verwende .update
Dann wird nur geschrieben, wenn sich der Inhalt geändert hat.
Der EEprom ist nicht dauerhaft beschreibbar

Und das hier:

sind magische Zahlen...
Verwende keine Zahlen, wenn nicht notwendig.
Gib dem PIN einen Namen und weise diesem die Nummer zu.
Dann verwendest Du den Namen im Code.
Und wenn Du mal den PIN änderst, machst Du das in der Definition 1x und musst nicht im gesamten Code danach suchen....

2 Likes

An dieser Stelle würde ich put() bevorzugen. Das verwendet intern auch update() und man hat das Gehampel mit den Bytes nicht.

EEPROM.put(adr, lo);
2 Likes

Hallo zusammen,

erstmal: vielen Dank für die guten Verbesserungen und Vorschläge, ich habe meinen Code mal entsprechend angepasst (siehe unten), und auch selbst noch zwei Fehler gefunden und gefixt:

1.) #define DESIREDPULSETIME 25000 -> das ist natürlich um den Faktor 1000 falsch, die Signale sind per Definition um die 30ms lang, ich habe micros mit millis verwechselt, es sollte also #define DESIREDPULSETIME 25 heißen

2.) Durch den Pullup(?) habe ich HIGH und LOW vertauscht. if ( digitalRead(MEASUREPIN) == HIGH && counting==false) muss also zu if ( digitalRead(MEASUREPIN) == LOW && counting==false) werden, und der Gegenfall ebenso. Dann passen die Messungen auch ganz gut.

Klar, Magic Numbers sind Schrott, habe ich korrigiert, und den Speichercode auf put und get umgebaut. Interessanterweise hat put in Verbindung mit meinem alten EEPROM.read-Code nicht funktioniert, da kamen dann andere Werte beim Lesen an, ich habe es nicht weiter untersucht, evtl. speichert put mit einer anderen Byte-Order?

Was ich auf jeden Fall sagen kann: mit diesem Code habe ich keine Fehlauslösungen mehr, ohne dass ich verdrahtungsseitig etwas geändert hätte. :slight_smile:

Noch ein paar Gedanken:

  • zum Thema Flash Lebensdauer: ja, das ist suboptimal, mein Code schreibt derzeit pro 100 gezählte Wattstunden einmal ins EEPROM. Bei einer Lebensdauer von 100000 Schreibzyklen (sind da alle Nanos gleich? Auch die billigen China-Nachbauten?) wären das immerhin 10000 kWh, also etwa drei-vier Jahre in meinem Anwendungsfall.
    Was ich mir überlegt habe: gibt es fertige Bibliotheken oder Code für ein einfaches Wear-Leveling? Sonst würde ich es prinzipiell so implementieren:
    Zähler start mit 0 initialisieren, und bei jedem Schreibvorgang um (start + sizeof(long)) Modulo (EEPROM.length) inkrementieren und dort abspeichern. Damit würde man den Speicher (1024 Byte?) gleichmäßig ausnutzen und hätte die 256fache Schreibmenge zur Verfügung. Man müsste beim Start allerdings einmal über den Speicher iterieren (maximal 256 Reads, so wie ich es verstehe belasten Reads den Speicher nicht) und dort wo man den größten Wert findet ist die Startadresse (mein Zähler rählt nur Vorwärts also ist das eine valide Annahme, oder?). Habe ich noch nicht kodiert, mich würden eure Meinungen dazu interessieren.

  • Ein echtes "Nice Have" Feature wäre, wenn man dem Nano per Serieller Schnittstelle auch ein paar Kommandos geben könnte, zum Beispiel einen neuen Zählerstand zu setzen, falls der intern aus welchen Gründen mal massiv abweichen sollte. Ist das überhaupt möglich, und gibt's da evtl Beispielcode wie man per Putty oder ähnlichem nicht nur Daten von Seriell abholt, sondern auch sendet?

Hier noch mein aktueller Sketchstand:

#include <EEPROM.h>
#define MEASUREPIN 2
#define SAVEMULT 10
#define ERRORTHRESHOLD 15
#define SAVETHRESHOLD 100
#define DESIREDPULSETIME 25
int interval = 5000;  //Counter refresh and save interval
int savemultiplier = 0;

long counter;
long prevSavedCounter;
int crontimer;
unsigned long time_now = 0;
byte prevLEDStatus;
unsigned long starttime = 0;
unsigned long endtime = 0;
unsigned long pulsetime = 0;
bool counting = false; 
void setup() 
{
  Serial.begin(9600);
  pinMode(MEASUREPIN, INPUT_PULLUP);
  counter = read_counter(0);
  prevSavedCounter = counter;
  pinMode(LED_BUILTIN, OUTPUT);
  prevLEDStatus = LOW;
  //some tests...
  //save_counter(0, (long)4021400);
  //save_counter(0, (long)1256789012);
}

void loop() 
{
  if ( digitalRead(MEASUREPIN) == LOW && counting==false)
  {    
    starttime=millis();
    counting=true;
    digitalWrite(LED_BUILTIN, HIGH);
  }
  if (digitalRead(MEASUREPIN) == HIGH  && counting==true) 
  {    
    counting = false;
    endtime=millis();
    pulsetime = endtime - starttime;
    if (pulsetime > DESIREDPULSETIME)
    {
      counter++;
      Serial.print("Long signal detected, counting. Signal length = ");
    }
    else
    {
      Serial.print("Short signal detected, NOT counting. Signal length = ");      
    }
    Serial.println(pulsetime);
    digitalWrite(LED_BUILTIN, LOW); 
  }
  if(millis() > time_now + interval)
  {
    prevLEDStatus = LOW;
    digitalWrite(LED_BUILTIN, LOW); 
    time_now = millis();
    if (savemultiplier > SAVEMULT)
    {
      long counterdiff = counter - prevSavedCounter; 
      if (counterdiff < ERRORTHRESHOLD)
      {
        counter = prevSavedCounter;
        Serial.print("Counter difference too low, error signals assumed, reset counter to prev value, diff = ");
        Serial.println(counterdiff);
      }
      else if (counterdiff > SAVETHRESHOLD)
      {
        save_counter(0, counter);
        prevSavedCounter = counter;
        Serial.print("Counter difference high enough to save, so saving, diff = ");
        Serial.println(counterdiff);
        Serial.print("Saved to EEPROM: ");
        Serial.println(counter);
      }
      else
      {
        Serial.print("Counter difference too low to save, wait for next saving cycle, diff = ");
        Serial.println(counterdiff);
      }  
      savemultiplier = 0;
    }        
    Serial.println(counter);
    savemultiplier++;
  }
}


void save_counter(int adr, long lo) 
{
  EEPROM.put(adr, lo);
} 


long read_counter(int adr) 
{
  long lo=0;

    EEPROM.get(adr, lo);
    return lo;
} // eepromReadLong

Ja.
Du hast "big endian" gespeichert, put() nimmt die Bytes so wie sie im Speicher stehen - also "little endian".

1 Like

Hallo,
ich würde das auf 1KWh ändern , genauer willst Du das letztlich doch nicht haben.
Du könntest einen ESP verwenden und einen Webserver aufbauen. dann kannst Du damit den Zählerstand auslesen und wenn Du willst auch setzten. Zudem hättest Du gleich Datum/Uhrzeit mit drin (NTP)

1 Like

Ja klar ist das möglich.
Serial.read() liest 1 Zeichen, das aber vom PC normalerweise erst gesendet wird, wenn du die Enter-Taste drückst. Am einfachsten besteht dein "Protokoll" aus 1 Kommando-Buchstaben, dem zu übertragenden Wert und dem Zeilenende-Zeichen, das du mit überträgst.
Außerdem gibt es Komfort-Funktionen, die z.B. gleich eine ganze mehrstellige Zahl liefern.
Guckstu https://docs.arduino.cc/language-reference/funktionen/communication/stream/

1 Like

Danke nochmals für alle Tipps, ich muss jetzt leider nochmal auf die schaltungstechnischen Details zurückkommen, da mir jetzt nach längferem Testen aufgefallen ist, dass leider ein paar (ca. 3 Prozoent) der Impulse verloren gehen, mein Zähler zählt also ca. 3 Prozent weniger Impulse, als laut Display eigentlich ankommen müssten.

Nun die Frage, woran liegt das. Am (internen) Pullup? Oder ist mein Widerstand an S0+ mit 5,7 kOhm zu groß gewählt? Kann ich den theoretisch ganz weglassen wenn ich intern den Pullup verwende oder grille ich damit meinen Ardu?

Habt ihr Ideen, warum mir doch relativ viele Impulse durch die Lapen gehen?

Mach mal deine DESIREDPULSETIME kleiner oder mach einen Test, welche Pulsdauern du erkennst ( Min, Max ) ...
Siehst du deine Meldung

Short signal detected...

im Seriellen Monitor?

Vorher hattest du Bedenken, dass zu zu viele Pulse erkennst, richtig?

Einen Pullup ganz weglassen geht nur, wenn du den eingebauten verwendest
pinMode (pin, INPUT_PULLUP);
Der Ist ca. 20k .. 30k, also etwas störungsempfindlicher.

kurzschliessen erzeugt einen Kurzschluss :wink:
Was du (unter ca. 200 Ohm) genau grillst, hängt davon ab, wo die 5V herkommen.

Wenn Dir Pulse fehlen, dann hast Du ein Timingproblem.

Du brauchst auf nix warten!
Sobald Du Dich entschieden hast, welche Flanke Du auswertest, geht es los.
Nicht eine bestimmte Pausen- und schon gar nicht eine bestimmte pulslänge abwarten.
Det S0 ist prellfrei.
Da gibt es nur 2 Flankenwechsel

1 Like