Forum: Mikrocontroller und Digitale Elektronik D1Mini Interrupt Routine zählt nicht hoch.


von Klaus R. (klara)


Lesenswert?

Hallo,
ich möchte mit einem D1Mini S0 Stromzähler auslesen und die Werte in 
eine Datenbank ablegen.

Es sollen 4 Zähler ausgelesen werden. Im Array counter[4] werden die 
Impulse addiert. Nach 28 Sekunden wird der Zählerstand zur Datenbank 
übertragen. Es genügt hier ein Wertebereich von 0 bis 255.

// Zuordnung der GPIOs
#define Kanal1 14  // D5
#define Kanal2 12  // D6
#define Kanal3 13  // D7
#define Kanal4 5   // D1

byte counter[4];  // 0 bis 255
byte count_wert[4];

//Die ISR darf keine Parameter annehmen und zurückgeben!
ICACHE_RAM_ATTR void interruptRoutine1(){
  interruptRoutine(0);
}

Aus der Interrupt Routine rufe ich die eigentliche Zähler - Funtion auf.
Die Interrupts werden ausgelöst. In der Form des nachfolgenden Codes 
steht
counter[btKanalNr] immer auf 0. Auch wenn ich die Zeile 
"counter[btKanalNr] = 0;" auskommentiere tut sich nichts.

Füge ich am Ende der Funktion ein "counter[btKanalNr]++;" so wird 
tatsächlich um 1 hochgezählt. Das Verückte dabei ist jedoch, es bleibt 
dann dabei bei 1! Und wenn ich 2 mal direkt hintereinander inkrementiere 
dann bleibt es bei 2.

void interruptRoutine(int btKanalNr) {
  pulsIstZeit[btKanalNr] = millis();
  if (pulsIstZeit[btKanalNr] - pulsStartIst[btKanalNr] >= endPrellZeit) 
{
    pulsStartIst[btKanalNr] = pulsIstZeit[btKanalNr];
    if (counter[btKanalNr] = 255) {
      counter[btKanalNr] = 0;
    } else {
      counter[btKanalNr]++;
    }
  }
}

void setup() {

 pinMode(Kanal1, INPUT_PULLUP);
 .....

  // ---- Starte Interrupts ----------
  attachInterrupt(digitalPinToInterrupt(Kanal1), interruptRoutine1, 
FALLING);
 .....

}

Ist counter[btKanalNr] mal z.B. auf 2 gesetzt worden, so wird der Wert 
auch tatsächlich der Versand - Funktion übergeben. Weitere Impulse 
werden aber nicht hinzuaddiert. Es bleibt bei 2.

Wie komme ich da weiter?
mfg Klaus

von Andreas B. (andreas-ab)


Lesenswert?

1
 if (counter[btKanalNr] = 255) {

sollte wohl
1
 if (counter[btKanalNr]==255) {

heißen. Ich schreibe es gerne so:
1
 if (255 == counter[btKanalNr]) {

Denn dann findet der Compiler den Fehler, falls man fälschlicherweise 
nur ein einzelnes "=" schreibt.

: Bearbeitet durch User
von Klaus R. (klara)


Lesenswert?

Hallo Andreas,
das könntes es sein. Ich denke da einfach noch in VB.NET. Ich melde mich 
später.
mfg Klaus

von Klaus R. (klara)


Lesenswert?

Hallo Andreas,
ich wollte einen Vergleich durchführen und habe eine Zuweisung codiert.
1
 if (counter[btKanalNr] = 255) {

Was das Programm dann wirklich macht kann ich nicht so recht 
nachvollziehen. Jedenfalls war das Verhalten verwirrend.

Mit der korrekten Schreibweise läuft das Programm einwandfrei.
1
 if (counter[btKanalNr] == 255) {

Dein Tipp die Parameter zu tauschen ist zumindest für Anfänger sehr 
hilfreich weil der Compiler dann eher einen Schreibfehler entdeckt.
1
 if (255 == counter[btKanalNr]) {

Die Ardurino IDE ist eben nicht mit Visual Studio von Microsoft zu 
vergleichen.

Nochmals, vielen Dank.
mfg Klaus

von Andreas B. (andreas-ab)


Lesenswert?

>Was das Programm dann wirklich macht kann ich nicht so recht
nachvollziehen. Jedenfalls war das Verhalten verwirrend.

Du könntest das auch einfach ausprobieren, eine Dummy if() Anweisung mit 
solch einer Zuweisung statt eines Vergleichs z.B. in der loop() einbauen 
und mit printf() Debug alles ausgeben was dich interessiert.

Einen weiteren Fallstrick sehe ich allerdings in deiner 
Interrupt-Service-Routine noch. Beim den ESPs mit externem seriellen 
Flash müssen die ISR mit der Compiler Directive IRAM_ATTR 
(obsolet=ICACHE_RAM_ATTR) versehen sein, damit der zugehörige Code eben 
nicht wie üblich aus dem Flash abläuft, sondern vorher ins RAM gecached 
wird und von dort abläuft (siehe auch Doku: 
https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/spi_flash/spi_flash_concurrency.html#iram-safe-interrupt-handlers 
). Für die IRS von oben stimmt das, aber es ist auch zwingend notwendig 
für Funktionen die innerhalb der IRS aufgerufen werden. In deinem Fall 
also auch für interruptRoutine(). Desweiteren findet sich in der Doku, 
dass die "globalen" Variablen auf welche die IRS zugreift per DRAM_ATTR 
Direktive abgesichert werden sollen.

von Klaus R. (klara)


Lesenswert?

Hallo Andreas,
ich verwende nicht einen ESP32 sondern einen D1 Mini - ESP8266.
Anbei zeige ich Dir noch mal die betroffenen aktuellen Sourcen.
1
unsigned long pulsStartIst[4];
2
unsigned long pulsIstZeit[4];
3
const byte endPrellZeit = 100;
4
5
//Die ISR darf keine Parameter annehmen und zurückgeben!
6
ICACHE_RAM_ATTR void interruptRoutine1() {
7
  interruptRoutine(0);
8
}
9
ICACHE_RAM_ATTR void interruptRoutine2() {
10
  interruptRoutine(1);
11
}
12
ICACHE_RAM_ATTR void interruptRoutine3() {
13
  interruptRoutine(2);
14
}
15
ICACHE_RAM_ATTR void interruptRoutine4() {
16
  interruptRoutine(3);
17
}
18
19
void interruptRoutine(int btKanalNr) {
20
  pulsIstZeit[btKanalNr] = millis();
21
22
  if (pulsStartIst[btKanalNr] > 0) {
23
    if (pulsIstZeit[btKanalNr] - pulsStartIst[btKanalNr] >= endPrellZeit) {
24
       pulsStartIst[btKanalNr] = pulsIstZeit[btKanalNr];
25
      if (counter[btKanalNr] == 255) {
26
        counter[btKanalNr] = 0;
27
      } else {
28
        counter[btKanalNr]++;
29
      } 
30
    } else {
31
      //Serial.print("Geprellt.");
32
      //Serial.print("\n");
33
    }
34
  } else {
35
    pulsStartIst[btKanalNr] = pulsIstZeit[btKanalNr];    
36
  }
37
}
38
void setup() {
39
....
40
 //Vaiablen initialisieren.
41
  for (byte n = 0; n <= 3; n++) {
42
    counter[n] = 0;
43
    count_wert[n] = 0;
44
    pulsStartIst[n] = 0;
45
    pulsIstZeit[n] = 0;
46
  }
47
48
 pinMode(Kanal1, INPUT_PULLUP);
49
  pinMode(Kanal2, INPUT_PULLUP);
50
  pinMode(Kanal3, INPUT_PULLUP);
51
  pinMode(Kanal4, INPUT_PULLUP);
52
53
  // ---------------------- Starte Interrupts ---------------------------------------------
54
  attachInterrupt(digitalPinToInterrupt(Kanal1), interruptRoutine1, FALLING);
55
  attachInterrupt(digitalPinToInterrupt(Kanal2), interruptRoutine2, FALLING);
56
  attachInterrupt(digitalPinToInterrupt(Kanal3), interruptRoutine3, FALLING);
57
  attachInterrupt(digitalPinToInterrupt(Kanal4), interruptRoutine4, FALLING);

Also ICACHE_RAM_ATTR ist demnach schon obsolet ...
1
ICACHE_RAM_ATTR void interruptRoutine1() {
2
  interruptRoutine(0);
3
}

... und ist durch IRAM_ATTR zu ersetzen.
1
IRAM_ATTR void interruptRoutine1() {
2
  interruptRoutine(0);
3
}

Ferner muß ich noch die Funktion interruptRoutine mit IRAM_ATTR 
aufrufen.
1
IRAM_ATTR void interruptRoutine(int btKanalNr) {
2
  pulsIstZeit[btKanalNr] = millis();
3
4
  if (pulsStartIst[btKanalNr] > 0) {
5
    if (pulsIstZeit[btKanalNr] - pulsStartIst[btKanalNr] >= endPrellZeit) {
6
       pulsStartIst[btKanalNr] = pulsIstZeit[btKanalNr];
7
      if (counter[btKanalNr] == 255) {
8
        counter[btKanalNr] = 0;
9
      } else {
10
        counter[btKanalNr]++;
11
      } 
12
    } else {
13
      //Serial.print("Geprellt.");
14
      //Serial.print("\n");
15
    }
16
  } else {
17
    pulsStartIst[btKanalNr] = pulsIstZeit[btKanalNr];    
18
  }
19
}

Andreas B. schrieb:
> Desweiteren findet sich in der Doku,
> dass die "globalen" Variablen auf welche die IRS zugreift per DRAM_ATTR
> Direktive abgesichert werden sollen.
1
DRAM_ATTR unsigned long pulsStartIst[4];
2
DRAM_ATTR unsigned long pulsIstZeit[4];

Ich nehme mal an das die Konstante nicht mit DRAM_ATTR zu deklarieren 
ist.
1
const byte endPrellZeit = 100;

Ich nutze als Quelle gerne https://randomnerdtutorials.com .
Dort wird DRAM_ATTR wohl nirgendwo eingesetzt. Das heißt natürlich auch 
wiederum nichts.

Bitte prüfe mal.
mfg Klaus

von Wastl (hartundweichware)


Lesenswert?

Klaus R. schrieb:
> Anbei zeige ich Dir noch mal die betroffenen aktuellen Sourcen.

.... und als altes, erfahrenes Mitglied hier im Forum
berücksichtigen wir auch immer brav die Hinweise zum Posten
von Sourcecode, gelle?
1
Wichtige Regeln - erst lesen, dann posten!
2
Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

von Klaus R. (klara)


Lesenswert?

Wastl schrieb:
> gelle?

Aber natürlich. Aber so lang ist der Auszug doch nicht.😉
mfg Klaus.

: Bearbeitet durch User
von Andreas B. (andreas-ab)


Lesenswert?

>Bitte prüfe mal.

Hhmm, ich hab die Schaltung grad nicht da und auch keine Ardunino IDE 
installiert. (ich verwende für ESPs VisualStudioCode  + Platformio 
Extension)

Aber Du könntest doch am lebenden Objekt selber testen ob der Code ohne 
Warnungen durch den Compiler/Linker läuft und ob das Programm dann genau 
tut was Du erwartest.

Wie gesagt, die Info bezüglich DRAM seht so in den Espressif Doku. Ich 
kann mir vorstellen, dass die FW auch ohne DRAM Directive funktioniert 
und du gar nix merkst. Aber es ist eben auch wahrscheinlich, das die SW 
sproadisch Quatsch macht und du nicht weißt warum.

Noch ein kleiner Hinweis, was du evtl. verbessern könntest. Die millis() 
Routine liefert einen unsigned long zurück, d.h. nach ca. 50 Tage gibt 
es einen Überlauf. Durch Umstellen der Entprellung kannst du das Problem 
vermeiden. Einfach mal nach overflow+millis() googlen oder hier z.B. 
eine Erklärung: 
https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/

von Pit S. (pitschu)


Lesenswert?

Variablen, die in einem ISR-kontext verändert werden (z.B. deine 
counter) müssen 'volatile' gekennzeichnet werden, damit der Compiler 
nicht weg-optimiert.

von Klaus R. (klara)


Lesenswert?

Pit S. schrieb:
> Variablen, die in einem ISR-kontext verändert werden (z.B. deine
> counter) müssen 'volatile' gekennzeichnet werden, damit der Compiler
> nicht weg-optimiert.

Danke, daran hatte ich gar nicht mehr gedacht.
mfg Klaus

von Klaus R. (klara)


Lesenswert?

Andreas B. schrieb:
> Noch ein kleiner Hinweis, was du evtl. verbessern könntest. Die millis()
> Routine liefert einen unsigned long zurück, d.h. nach ca. 50 Tage gibt
> es einen Überlauf. Durch Umstellen der Entprellung kannst du das Problem
> vermeiden. Einfach mal nach overflow+millis() googlen oder hier z.B.
> eine Erklärung:
> 
https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/

Der Überlauf wird tatsächlich vorkommen. Der Prozessor soll solange 
laufen wie Strom vorhanden ist und selbst dann hängt er noch mit anderen 
Verbrauchern an einer USV dran.

In der Tat ist es ja so, daß der D1Mini selber nur rollierend Werte von 
0 bis 255 sendet. Der S0 Stromzähler liefert 1000 Impulse pro 1 kWh.
Das sind bei 1 kW = 0,277 Impulse/s.
Mein Programm für die Einarbeitung in die Datenbank liest schon seit 
über 10 Jahren einen anderen S0 Stromzähler aus. Diesen Überlauf habe 
ich schon  behandelt.

Bei D1Mini muß ich mir jedoch noch ein neues Konzept einfallen lassen. 
Gut das Du mich nochmal auf diese Problematiken hingewiesen hast, denn 
ich war schon sehr zufrieden.

Ich liebe Teamwork und hoffe Dir auch mal so helfen zu können.
mfg Klaus

von Klaus R. (klara)


Lesenswert?

Hallo Andreas,
1
DRAM_ATTR  byte counter[4];  // 0 bis 255
2
DRAM_ATTR  byte count_wert[4];

Erzeugt eine Fehlermeldung:
Compilation error: 'DRAM_ATTR' does not name a type; did you mean 
'IRAM_ATTR'?

1
void IRAM_ATTR gpio_isr_handler(void* arg)
2
{
3
  DRAM_ATTR  byte counter[4];  // 0 bis 255
4
  DRAM_ATTR  byte count_wert[4];
5
}

Erzeugt diese Fehlermeldung:
Compilation error: 'DRAM_ATTR' was not declared in this scope; did you 
mean 'IRAM_ATTR'?

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/memory-types.html#how-to-place-code-in-iram

Im oben angegeben Link wird an anderer Stelle "esp_attr.h" eingebunden.
1
#include <ESP8266WiFi.h>
2
#include <ESP8266WiFiMulti.h>
3
#include <ESP8266HTTPClient.h>
4
#include <WiFiClient.h>
5
#include "esp_attr.h"

Fehlermeldung:
Compilation error: esp_attr.h: No such file or directory

Wenn es das ist was mir fehlt, wie/wo binde ich das File ein?
mfg Klaus

von Sven K. (svenk)


Lesenswert?

Hallo Klaus,

versuche mal Krokodilmaul statt Gänsefüßchen wie bei den anderen 
includes darüber.

Um esp_attr.h

Gruß Sven

: Bearbeitet durch User
von Andreas B. (andreas-ab)


Lesenswert?

ich befürchte, dass das ESP8266_RTOS_SDK nicht in den Arduino Libs 
enthalten ist. Daher fehlt das Include File natürlich. Dann hab ich für 
den ESP8266 leider auch keine gute Lösung. Sorry für den falschen 
Hinweis, dann wird es wohl auch ohne gehen, so wie in anderen 
Beispielen, die du im Netz gefunden hast.

von Klaus R. (klara)


Lesenswert?

Andreas B. schrieb:
> ESP8266_RTOS_SDK

Sven K. schrieb:
> Hallo Klaus,
>
> versuche mal Krokodilmaul statt Gänsefüßchen wie bei den anderen
> includes darüber.
>
> Um esp_attr.h
>
> Gruß Sven

Dein Tipp genügt manchmal, hier aber nicht. Der Header ist wirklich 
nicht greifbar.
mfg Klaus.

von Klaus R. (klara)


Lesenswert?

Andreas B. schrieb:
> ich befürchte, dass das ESP8266_RTOS_SDK nicht in den Arduino Libs
> enthalten ist. Daher fehlt das Include File natürlich. Dann hab ich für
> den ESP8266 leider auch keine gute Lösung. Sorry für den falschen
> Hinweis, dann wird es wohl auch ohne gehen, so wie in anderen
> Beispielen, die du im Netz gefunden hast.

Ja, man müßte wohl mit dem SDK und RTOS arbeiten. Das geht mir zu weit. 
Aber es funktionierte ja schon zuvor zufriedenstellend.
Besten Dank.
mfg Klaus

von Klaus R. (klara)


Lesenswert?

Pit S. schrieb:
> Variablen, die in einem ISR-kontext verändert werden (z.B. deine
> counter) müssen 'volatile' gekennzeichnet werden, damit der Compiler
> nicht weg-optimiert.

Ich habe mir die Referenz von 'volatile' angeschaut.

https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/volatile/
1
Declaring a variable volatile is a directive to the compiler. The compiler is software which translates your C/C++ code into the machine code, which are the real instructions for the Atmega chip in the Arduino.
2
3
Specifically, it directs the compiler to load the variable from RAM and not from a storage register, which is a temporary memory location where program variables are stored and manipulated. Under certain conditions, the value for a variable stored in registers can be inaccurate.
4
5
A variable should be declared volatile whenever its value can be changed by something beyond the control of the code section in which it appears, such as a concurrently executing thread. In the Arduino, the only place that this is likely to occur is in sections of code associated with interrupts, called an interrupt service routine.

Ich verstehe das so, daß 'volatile' so etwas wie 'DRAM_ATTR' bewirkt.
Zumindest wird das Programm dadurch sicherer.
mfg Klaus

von Axel S. (a-za-z0-9)


Lesenswert?

Klaus R. schrieb:
> Ich habe mir die Referenz von 'volatile' angeschaut.
...

> Ich verstehe das so, daß 'volatile' so etwas wie 'DRAM_ATTR' bewirkt.

Nein. Der Unterschied ist zum einen, daß volatile ein Schlüsselwort 
ist, das jeder C/C++ Compiler verstehen muß. DRAM_ATTR ist hingegen ein 
Makro, dessen Bedeutung nicht vom Standard festgelegt wird. Es sieht aus 
wie eine Festlegung der Speicherklasse.

Google (https://www.google.com/search?&q=DRAM_ATTR) verrät, daß es sich 
bei DRAM_ATTR um ein Attribut zur Steuerung der Lokalisierung der 
Variable im DRAM handelt.

> Zumindest wird das Programm dadurch sicherer.

Auch nicht. volatile verbietet dem Compiler bloß einige Optimierungen. 
Spezifisch die Optimierung, daß der Compiler davon ausgehen darf, daß 
die Variable nur innerhalb des betrachteten Programmcodes verändert 
werden kann. Deswegen muß er die Variable jedes Mal wieder vom Speicher 
lesen. Auch wenn er bereits eine Kopie in einem Register hat.

Einfach alle Variablen als volatile zu deklarieren, macht das Programm 
nicht sicherer, sondern nur langsamer.

: Bearbeitet durch User
von Rolf (rolf22)


Lesenswert?

Axel S. schrieb:
> Deswegen muß er die Variable jedes Mal wieder vom Speicher
> lesen. Auch wenn er bereits eine Kopie in einem Register hat.

Um es präziser zu sagen: Der Compiler muss für die Variable einen Platz 
im Arbeitsspeicher (egal, ob Flash, RAM oder Register) fest reservieren. 
Und: Der Compiler muss dafür sorgen, dass der aktuelle Wert bei jedem 
Zugriff darauf von diesem festen Ort gelesen bzw. dorthin geschrieben 
wird.

Mit Registern hat das nichts zu tun. Theoretisch könnte der Compiler 
auch ein Register dafür fest reservieren; praktisch tut er das nur bei 
Prozessoren mit extrem vielen Registern. Wichtig ist in jedem Fall nur 
die oben erwähnte Zugriffstaktik, egal ob Register oder Arbeitsspeicher:

volatile int aa;   Foo() { int bb;  bb = aa; if (bb == aa) { x = 99; } }

Ohne 'volatile' wäre es dem Compiler erlaubt, die 'if'-Bedingung 
wegzuoptimieren, und das würde schieflaufen, falls eine Interruptroutine 
den Wert von 'aa' nach der Zuweisung und vor dem Auswerten der 
if-Bedingung verändern würde. Ob bzw. wann so ein Interrupt kommt, kann 
man aber nicht vorhersagen.
Das Beispiel ist natürlich nicht sinnvoll, aber es zeigt, dass ein 
Wegoptimieren mancher Teile ungeahnte Folgen haben kann.

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.