[Bericht] Der (Millis) Ueberlauf im Test

Schönen guten Tag!

Hier im Forum kursieren einige Methoden, wie man ein "blink without delay" hin bekommt, oder auch andere Wartezeiten.

Drei davon habe ich hier mal grafisch aufbereitet. Ok, das eigentliche malen, das macht der Serielle Plotter, der Arduino IDE.

Da das Problem mit uint32_t schlecht darstellbar ist, wg. der 49 Tage Wartezeit, habe ich es auf byte runtergebrochen. Darum auch die Casts, um wirklich in dem Zahlenbereich zu bleiben. Bei unsigned long sind die Casts nicht nötig.

Wie auch immer, ob uint8_t oder uint32_t, das Problembild ist im Grunde gleich.

Was sehen wir in den Diagrammen?
In der blauen Kurve sehen wir die langsam ansteigende Zeit. Beim Überlauf gehts wieder mit Null los.
Die rote Treppenstufe zeigt die gemerkte Zeit. An jeder Flanke beginnt ein neues Zeit Intervall.

Die kleine grüne Rechteckkurve(?) zeigt unseren Aktor. Z.B. eine blinkende LED oder die Anforderung für unsere 27KW Klimaanlage. Und genau diese fette Klimaanlage verträgt keine Hakler an der Stelle. Wir(nur ich?) wollen da ein sauberes, symmetrisches, von Überläufen unbeeindrucktes, Rechteck sehen.

Resultat der Geschichte:
Drei Verfahren getestet, nur eins davon kommt unbeeindruckt durch den Überlauf.

Die Modulo Methode:

if(not (jetztzeit % interval))


Die Methode mit Addition:

if(jetztzeit > byte(zeitmerker + interval))


Die einzig richtige Methode:

if(byte(jetztzeit - zeitmerker) >= interval)


Zu guter Letzt der Testcode:

#include <Streaming.h> // die Lib findest du selber ;-)
Stream &cout = Serial; // cout Emulation für "Arme"

byte jetztzeit  =     0; // macht sich blaue Rampe
byte zeitmerker =     0; // macht sich rote Treppe
byte interval   =    60; // Zeit zwischen grüne Flanken
bool signal     = false; // macht sich grünes Rechteck


void setup() 
{
  Serial.begin(9600);
  cout << F("jetztzeit,zeitmerker,signal") << endl;
}

void loop() 
{
  jetztzeit++;

  // if(not (jetztzeit % interval)) // fehlverhalten beim Ueberlauf
  // if(jetztzeit > byte(zeitmerker + interval))// fehlverhalten vor dem Ueberlauf
  if(byte(jetztzeit - zeitmerker) >= interval) // der wahre Jakob
  {
    zeitmerker = jetztzeit;
    signal = not signal;
  }
  
  cout << jetztzeit << "," << zeitmerker << "," << (signal * 20) << endl;

 delay(1);
}

Viel Freude damit, ich euch wünsche.

5 Likes

:+1: :+1: :+1:

Hallo combie,
danke für deine sehr ausführlich und gut verständliche Beschreibung.

Schön, dass es euch gefällt!
Und ja, Bilder sagen oft mehr, als Tausend Worte.
Um das nicht zu verwässern, habe/hatte ich etwas mit Worten gespart.
Das hole ich mal hier nach...

Die "richtige" Methode:
Genau dann, wenn der Überlauf des Zeitgebers uns einen Streich spielen möchte, wird dieser durch den Unterlauf bei der Subtraktion kompensiert.
Also: Unterlauf neutralisiert Überlauf!

Das fehlt bei der "Addition" Methode:
Egal wie man es anstellt, mit einem Überlauf bei der Addition, kann man nicht den Überlauf des Zeitgebers kompensieren.

Vom Rechen- bzw. Speicheraufwand sind beide Methoden gleichwertig.

Da fällt die "Modulo" Variante aus dem Rahmen:
Diese spart eine Variable ein. Benötigt an der Stelle also weniger Speicher.
Zur Strafe unterliegt sie starken Einschränkungen.

  1. Wenn es nicht wirklich alle 1 ms an die Reihe kommt, können Intervall Enden übersehen werden.
  2. Die Intervallzeiten müssen Zweierpotenzen sein, damit die Ausgabe im Überlauf symmetrisch wird/bleibt.

Ganz nebenbei, gehört Modulo zu den Divisionen, welche die teuersten Rechenoperationen auf den kleinen AVR sind.

Klasse! Das sollte man in die Dokumentation als Tutorial aufnehmen.

Man darf natürlich nicht vergessen, ob der Überlauf überhaupt kritisch ist. Wenn eine blinkende LED aller 49 Tage mal ein blinken außerhalb des Taktes macht, wird dass kaum jemand bemerken. Da ist der Modulo wahrscheinlich besser.

Gewöhne dir solchen Mist erst gar nicht an. Vor allen Dingen wenn man weiß wie man es richtig macht. Im Laufe der Zeit etablieren sich gewisse Standardverfahren die man einfach nur anwenden sollte.

Nunja...
Jedes mal, wenn irgendeiner deiner Loop Durchgänge länger als 1ms braucht, kann dir das auf die Füße fallen.

Ich sage:
1 Fehler pro 49 Tage ist zuviel, da unnötig.
Aber das Risiko jede ms einmal einzugehen, sollte auch dir zuviel sein.

Ganz schlimm wird es dann auch noch auf einem z.B. 3,3V Pro Mini.
Da fällt sowieso fast jede 2te ms unter den Tisch. Dann ist das Versagen der Modulo Methode quasi unausweichlich.

So, jetzt habe ich auch endlich mal die Zeit gefunden, den Beweis dafür anzutreten.
Einzige Änderung im Programm:

// jetztzeit++; // ersetzt
jetztzeit = millis(); // mit den unteren 8 Bit des echten Millis Wertes.

Das geänderte Programm, mit den echten millis() auf einem 8MHZ Pro Mini


Das geänderte Programm, mit den echten millis() auf einem 16MHZ UNO


Die "wahrhaftige" Methode liefert auf beiden gute Ergebnisse. VIEL symmetrischere.
Dagegen ist die "Modulo" Methode eher ein Zufallsgenerator
:hole:

Die Modulo Variante macht einen X == Y Vergleich, wenn da die falsche ms übersprungen wird, gehts schief.


unsigned long last;

void setup() {
  Serial.begin(115200);
  last = millis();
}

void loop() {
  auto now = millis();
  if (now == last) {
    return;
  }
  if ((now - 1) == last) {
    last = now;
    return;
  }
  Serial.println(now);
  last = now;
}

Hallo Rintin,

das haste dir jetzt ausgedacht? Dieser falsche Code taucht nirgendswo auf. Auch sehe ich darin kein Modulo. Bist du dir im Klaren was dein return bewirkt? Tipp. Vergiss das ganz schnell.

Um zu erreichen, dass loop (höchstens) einmal je ms drankommen soll, könnte @Rintin evtl. so was wollen:

Das ist gut so, denn modulo ist Mist. Außer der Compiler kann es wegoptimieren und ohne modulo-Division machen.

Hallo,

na logisch ist Modulo hier Mist, Ich hatte nichts anderes geschrieben. Nur das war nicht mein Anliegen. Mein Anliegen war daraufhin zuweisen das Rintin was von Modulo erzählt aber kein Modulo im Code steht.
Weiterhin steht die Frage im Raum was das return bewirken soll? Wo springt das return hin? Ich vermute das ist so nicht gewollt. Also weg damit.

Natürlich beendet return; den aktuellen loop-Durchlauf (und startet also einen neuen).
Der Rest von loop wird also nur (max.) einmal je ms durchlaufen.
Die Frage ist eher, wofür das gut sein soll.

Ok... war schon spät... es ging mir hauptsächlich um das X == Y.

Jetzt überlegt mal was gibt der Code auf der Seriellen aus?
Und was gibt der wirklich aus?

Ja, das ist das Problem mit dem Modulo.
Zumindest in dem Bereich wo kein Überlauf stattfindet.

Im Überlauf zeigt es dann nochmal sein zweites Gesicht, welches auch nicht schöner ist.

Und wie oft wird auf Serial was ausgegeben?

Ist der Sinn deines zweiten return; ( in #10) , dass nur Stolpern protokolliert werden soll ?

Ich hätte übrigens nicht gedacht, dass ein loop-Durchlauf dieser "Größe" so häufig >1ms dauert.

43
86
128
171
214
256
299
342
384

(Dein Code auf meinem Uno)

Macht er ja nicht. Mit dem return bricht er vorher ab und durch das jeweilige neu setzen von last kommt er nur ca. alle 40ms unten an, weil erst nach dieser Zeit der Durchläufe sich ein Unterschied von 2ms zwischen now und last ergibt.
(Seins in Kurzform)

  if ((now == last) || ((now - 1) == last)) {
    last = now;
    return;
  }

Dann erklär bitte einmal was es macht.