Zeitsteurung mit millis

Besser: Nicht dass überhaupt einer auf die Idee kommt, diese "Lösung" einzusetzen.

Gruß Tommy

genau, lieber von Anfang an richtig machen, sonst droht später nur Kaos und man weiß nicht warum. Das einzigste Limit was es gibt betrifft die maximale Wartezeit von 49 Tagen, also das maximale einstellbare Intervall oder Pause oder wie auch immer man millis nutzen möchte. Der µC selbst kann ewig laufen trotz Werteüberlauf und macht alles richtig.

Es ist aber ineffizient, wenn ich bei jedem Durchlauf des Loops zuerst rechne und dann vergleiche.
1x rechnen reicht aus.

Wenn mir der Überlauf tatsächlich wichtig ist, dann kann ich das ja abfangen:

static unsigned long nextMillis = 0;
unsigned long currentMillis = millis();

if (nextMillis < currentMillis) {
  //hier der doublecheck
  unsigned long lastMillis = nextMillis - delay;
  if (currentMillis - lastMillis >= delay) {
    //jetzt bin ich mir sicher, dass auch nach überlauf mein delay vergangen ist
    nextMillis = currentMillis + delay;
  }
}

Üblicherweise sind unsere Delays gegenüber der Loop Geschwindigkeit ziemlich lang. Das heißt die Loop läuft einige tausend male durch, bevor das delay überhaupt erreicht ist. Oder anders gesagt: die erste Condition wird viel öfter false liefern, als true.
Somit muss der Chip viel seltener rechnen. Ja der Code erzeugt im Bereich des Überlaufs zusätzliche Last, entlastet den Chip aber die ganze übrige Zeit. Wenn man davon ausgeht, dass so ein Delay sich im Bereich von Sekunden bewegt, dann ist die doppelte Prüfung nur für wenige Sekunden oder Minuten überhaupt Zusatzlast und ansonsten ist es von der Performance her ein Vorteil.

Natürlich kann man die Performanceaspekte auch komplett ignorieren. Das entspricht aber nicht meinem eigenen Anspruch an Code.

/edit
mir fällt auf, dass man das noch geschickter formulieren kann, wenn der Speicherbedarf nicht relevant ist:

static unsigned long nextMillis = 0;
static unsigned long prevMillis = 0;

unsigned long currentMillis = millis();

if (nextMillis < currentMillis && currentMillis - lastMillis >= delay) {
  lastMillis = currentMillis;
  nextMillis = currentMillis + delay;
  }
}
void loop() {
	static unsigned long lastMillis = 0;
	const unsigned long interval = 1000;
		
	if(millis() - lastMillis > interval) {
	// ... mache etwas

	lastMillis = millis();
	}
}

Gibt eigentlich sinnvoll nur dieses. Wie Doc bereits geschriebene hat, wenn die Interval nicht > 49Tage entspricht, geht das ganze durchaus ohne viel zu schreiben.

Mit AVR entlasten hat das ganze wenig zu tun. Der rechnet den millis() Wert im Hintergrund sowie über die ISR mit. Du lässt dir mit millis() lediglich den aktuellen Wert zurückgeben.

Es geht doch nicht um millis() an sich.

Es geht darum, ob ich in jedem Durchlauf "millis() - lastMillis" rechne oder ob ich das nur dann tue, wenn ich tatsächlich in den Block reinkomme.

OT: Um wie viel "Entlastet" du den AVR wenn du das - lastMillis weglässt ...
wie nutzt du die "gewonnen" Performance?

Da sehe ich mehr Ineffizienz in deinem Webserver. Da könntest du so richtig viel rausholen, aber auch dazu gilt - was macht man dann mit dem gesparten. Wenn es für dich so funktioniert ist ja alles gut. Aber für eine Zeitabfrage empehle ich dein Konstrukt nicht weiter.

@TO: wie Combie schon schrieb:

Bitte du auch "BlinkWithoutDelay" lesen.

Bei Lust könnte das ja mal einer auswerten. Habe keinen Arduino oder Logik Analyser hier. Der Effekt wird so geschwinden gering sein, dass man damit keinerlei andere Aktion zusätzlich ausführen kann.

Ich finde, um Effizienz, kann es erst in zweiter Linie gehen.

§1 ist immer die zuverlässige Funktion

Die "Entlastung" ist garantiert marginal und wirkt sich wahrscheinlich nichtmal merklich aus. Dafür müsste man vermutlich tausend solcher Delays einbauen, damit das auffällt.
Allerdings habe ich mir angewöhnt prinzipiell unnötige Aufrufe bzw. Operationen zu vermeiden. Man weiß nicht immer, wie teuer ein Aufruf tatsächlich ist oder ob er nicht irgendwann mal teuer wird. Das gehört zum Job einfach dazu.

Was den Webserver angeht: Warum muss der effizient sein? Der Webserver ist für genau 30sec aktiv und wenn dann niemand conntected hat (was zu 99,9% der Fall ist), dann schaltet er sich komplett ab.

Mir ist es total wumpe, wie jemand sein Delay codet. Ich habe erläutert, warum ich es für richtig halte, die rechenoperation nicht als Teil der condition zu machen und da geht es mir ums Prinzip und nicht um das bischen, was diese Operation jetzt ausmachen würde.
Wenn man sich die unnötige Ausführung der Operationen angewöhnt, dann macht man das ggf. später auch mit wirklich teuren Operationen und dann wundert man sich, warum die Kiste nicht mehr läuft.
Ich hab das im Job oft genug erlebt, dass Code zwar funktioniert hat, wenn dann aber auf einmal die Datenmenge gestiegen ist, wurde es sehr schnell sehr langsam. Da waren die Augen dann schnell ganz groß.

Hallo,

ich sehe aber jetzt nicht wo deine Variante Rechenzeit einspart. Die Subtraktion muss immer ausgeführt werden bedingt durch die UND Verknüpfung. Im dümmsten Fall dauert das alles sogar länger. Meinste nicht auch? Wäre es ODER wäre nur erstmal die erste Prüfung interessant.

if (nextMillis < currentMillis && currentMillis - lastMillis >= delay) {

Ich kenne nur die klassische Variante wie von sschulteworker gezeigt oder die andere die jurs irgendwann mal eingeführt hatte.

static unsigned long last_millis = 0;
const unsigned int interval = 2000;

if (millis() - last_millis < interval) return; // Zeit noch nicht erreicht, Funktion abbrechen
last_millis += interval;                       // addiere Wartezeit

Allen, auch deiner, ist gemeinsam das einmal gerechnet werden muss.

Edit:
deine erste Version ist laut meiner Meinung nach auch nicht Überlauf sicher. Deswegen auch der sinnfreie Doublecheck der dann immer zum tragen kommt und damit auch immer Rechenzeit kostet. Man gewinnt nichts. Man gewinnt nur komplizierteren Code der fehlerträchtig ist.

if (nextMillis < currentMillis) {

Ich wollte es wissen: ich hab mal den BlinkWithoutDelay um zwei Varianten erweitert.

V1 mit Subtraktion wie es viele hier machen würden
V2 mit einer Zielzeiterreichung

was besseres als die Micros davor und danach zu messen ist mir nicht eingefallen.

/*
  Blink without Delay

  Based on discussion in: http://forum.arduino.cc/index.php?topic=524248.msg3618724#new
*/

const int ledPin =  LED_BUILTIN;// the number of the LED pin
uint8_t ledState = LOW;             // ledState used to set the LED
unsigned long previousMillis = 0;        // will store last time LED was updated

// constants won't change:
const long interval = 1000;           // interval at which to blink (milliseconds)

uint32_t v1previousMillis = 0;
uint32_t v1before = 0;
uint32_t v1in = 0;
uint32_t v1ready = 0;
uint32_t v1total = 0;

uint32_t v2nextMillis = interval;    // uint32_t v2previousMillis = 0;   // same usage, but first nextMillis is 1 second
uint32_t v2before = 0;
uint32_t v2in = 0;
uint32_t v2ready = 0;
uint32_t v2total = 0;



void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
}

void loop() {
  // here is where you'd put code that needs to be running all the time.

  unsigned long currentMillis = millis();

  v1before = micros();
  if (currentMillis - v1previousMillis > interval) {
    v1in = micros();
    // save the last time

    v1previousMillis = currentMillis;

    // do what you want to do
    v1total = v1in - v1before;

    // Statistic
    Serial.print(F("V1: "));Serial.print(v1before);
    Serial.print(F(" to "));Serial.print(v1in);
    Serial.print(F(" =  "));Serial.print(v1total);Serial.println(F(" micros"));
  }

  v2before = micros();
  if (v2nextMillis < currentMillis) {                       // Never use this example in production systems
    v2in = micros();
    // calculate the next time 

    v2nextMillis = currentMillis + interval;

    // do what you want to do
    v2total = v2in - v2before;

    // Statistic
    Serial.print(F("V2: "));Serial.print(v2before);
    Serial.print(F(" to "));Serial.print(v2in);
    Serial.print(F(" =  "));Serial.print(v2total);Serial.println(F(" micros"));
  }


  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the LED
    previousMillis = currentMillis;
    // if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
    // set the LED with the ledState of the variable:
    digitalWrite(ledPin, ledState);
  }
}

V1/V2 sollten das gleiche machen.

Ergibt auf einem UNO Clone im Serial

V1: 1001480 to 1001484 =  4 micros
V2: 1002408 to 1002412 =  4 micros
V1: 2002960 to 2002964 =  4 micros
V2: 2003888 to 2003892 =  4 micros
V1: 3003400 to 3003404 =  4 micros
V2: 3004328 to 3004336 =  8 micros
V1: 4004872 to 4004876 =  4 micros
V2: 4005800 to 4005804 =  4 micros
V1: 5005328 to 5005332 =  4 micros
V2: 5006256 to 5006260 =  4 micros
V1: 6006800 to 6006804 =  4 micros
V2: 6007728 to 6007732 =  4 micros
V1: 7007248 to 7007252 =  4 micros
V2: 7008176 to 7008184 =  8 micros
V1: 8008720 to 8008724 =  4 micros
V2: 8009648 to 8009652 =  4 micros
V1: 9009160 to 9009164 =  4 micros
V2: 9010092 to 9010096 =  4 micros
V1: 10010632 to 10010636 =  4 micros
V2: 10011656 to 10011660 =  4 micros
V1: 11011084 to 11011092 =  8 micros
V2: 11012112 to 11012116 =  4 micros
V1: 12012564 to 12012568 =  4 micros
V2: 12013588 to 12013592 =  4 micros
V1: 13014028 to 13014032 =  4 micros
V2: 13015056 to 13015060 =  4 micros
V1: 14015500 to 14015504 =  4 micros
V2: 14016528 to 14016532 =  4 micros
V1: 15016976 to 15016984 =  8 micros
V2: 15018004 to 15018008 =  4 micros
V1: 16017424 to 16017432 =  8 micros
V2: 16018448 to 16018452 =  4 micros
V1: 17018896 to 17018904 =  8 micros
V2: 17019920 to 17019928 =  8 micros
V1: 18019340 to 18019344 =  4 micros
V2: 18020368 to 18020372 =  4 micros

V1 (mit Subtraktion) ist fast immer so "schnell" wie die V2.
Ausreißer gibt es nach beiden Seiten.

Einer der alten kann sicher erklären warum das so ist. Mir reicht die Beobachtung.
Ich bleib bei V1.

Zur Zeitersparnis:

if (nextMillis < currentMillis && currentMillis - lastMillis >= delay) {

nextMillis < currentMillis ist nur dann true, wenn die nächste Schaltzeit erreicht ist (also wenn das Delay abgelaufen ist). Also ist es die meiste Zeit false.
Wenn der erste Ausdruck schon false ist, dann wird der zweite Ausdruck gar nicht ausgeführt.

Man kann sich && und || auch so erklären:
&&

if(a) {
 if(b) {
  doIt();
 }
}

wenn der äußere Block false liefert, kommt man in den inneren Block gar nicht rein. Das zweite Statement wird nur ausgeführt, wenn das erste true ist.

||

if(a) {
  doIt();
} else if(b) {
  doIt();
}

wenn der erste Block true liefert, kommt man in den zweiten Block nicht mehr rein. Das zweite Statement wird nur ausgeführt, wenn das erste false ist.

Zum Test von noiasca:
Du betrachtest einen viel zu kleinen Bereich. Du musst das 1000x am Stück machen und dann die Zeit anschauen.

int repeat = 1000;
int i = 0;
v1before = micros();
while(i<repeat) {
  if (currentMillis - v1previousMillis > interval) {
  }
  i++;
}
v1after = micros();


i = 0;
v2before = micros();
while(i<repeat) {
  if (v2nextMillis < currentMillis) {
  }
  i++;
}
v2after = micros();

So kannst du dann beliebig skalieren. Auf 10000 erhöhen oder sonst was, um zu sehen, wie es sich auswirkt. Irgendwann sollte man mit der Zeit in einen messbaren Bereich kommen (4 Micros ist nicht messbar).
Was man natürlich mit drin hat, ist die Operation auf dem Zähler i. Das lässt sich aber nicht vermeiden, wenn man nicht 1000x das if statement hinschreiben will.

Hallo,

&& das stimmt, da hatte ich einen Denkfehler. Zum anderen, gut, kann man machen, ist aber noch nicht komplett effizient. Wenn man jetzt nicht hunderte verschiedene Intervalle hat, dann lässt man vom Timer im Intervall ein Flag setzen und fragt nur das Flag im Hauptprogramm ab. Wenn wahr führt man aus und setzt das Flag zurück. Dabei wird nichts mehr gerechnet.

Mir ist es total wumpe, wie jemand sein Delay codet. Ich habe erläutert, warum ich es für richtig halte, die rechenoperation nicht als Teil der condition zu machen und da geht es mir ums Prinzip und nicht um das bischen, was diese Operation jetzt ausmachen würde.

Dein Code ist fehlerhaft!

nextMillis = millis() + interval;
Führt zu einem Überlauf.
Und wenn das geschieht, liefern zeitlich nachfolgende Abfragen falsche Ergebnisse.

Wenn du das für dich tolerieren möchtest, ok.
Aber dann akzeptiere auch bitte, dass wir es hier, mit noch größerer Intensität, verhindern möchten, dass Andere das falsche lernen.

Allerdings habe ich mir angewöhnt prinzipiell unnötige Aufrufe bzw. Operationen zu vermeiden.

Das unterstütze ich!
Aber auch nur soweit, wie es nicht auf kaputten/fehlerhaften Code beruht.

Combie mein Code funktioniert einwandfrei, weil der Überlauf nie passieren kann. So lange läuft das Ding nicht.
Mir ist total bewusst, dass die "Lebensdauer" so nur 49 Tage beträgt.

Ich habe ja bereits gepostet, wie man das ändern müsste, damit es auch über 49 Tage hinaus sauber läuft. Der Code wird dadurch schlechter lesbar, wenn man das Pattern aber erstmal verinnerlicht hat, ist es normal.
Oder man macht sich ne Timerklasse dafür, die dann eine Methodenreferenz übergeben bekommt, die als Callback gerufen wird. So verschwindet der komplexe Code komplett.

@Doc
Wenn man gleiche Intervalle hat, dann spart man sich das Flag und ruft in dem Block einfach alle nötigen Methoden auf :wink:
Wenn die Intervalle unterschiedlich sind, dann muss man wieder jedes einzeln checken.

Hallo,

da muss ich combie recht geben, hatte es ja selbst schon erwähnt. Wenn du den Überlauf ignorierst, ist der Code nicht allgemein fehlerfrei. Wenn du das wiederrum korrigierst, verringert sich deine anvisierte Effizent. Kann man drehen wie man möchte. Code zu posten mit starken Einschränkungen ist nicht gut. Ich persönlich halte auch nichts davon einfachen Code aufzubauschen das der kaum noch lesbar ist. Nur um paar Takte einzusparen. Da sollte man dann völlig anders rangehen. Sonst wird das zu einer neuen Art von Löcher stopfen. Am Ende hat man nur noch Flicken vor sich.

Ich möchte dich aber nicht davon abhalten einfachen und effizenten Code zu erstellen. Vielleicht gibts ja nochmal irgendwann die Eierlegendewollmilschsau. Keine Ahnung.

so - könnte man, denn sonst optimiert wer das if weg:

  v1before = micros();
  while (i < repeat) {
    if (currentMillis - v1previousMillis > interval) {
      j++ ;
    }
    i++;
  }
  v1after = micros();
  Serial.println(j);

Ergibt dann:

const uint32_t repeat = 100;
V1: 8 to 0 =  88 micros
V2: 172 to 0 =  84 micros


const uint32_t repeat = 1000;
V1: 8 to 0 =    880 micros
V2: 964 to 0 =  828 micros


const uint32_t repeat = 10000;
V1: 8 to 0 =     8804 micros
V2: 8888 to 0 =  8188 micros


const uint32_t repeat = 100000;
V1: 8 to 0 =      81744 micros
V2: 81828 to 0 =  75472 micros


const uint32_t repeat = 1000000;
V1: 8 to 0 =       817444 micros
V2: 817528 to 0 =  754572 micros


const uint32_t repeat = 10000000;
V1: 8 to 0 =        8174408 micros
V2: 8174488 to 0 =  7545620 micros

sobald ich mal 100 verschiedene Zeitaufrufe in einer Loop brauche und ich mir die 4Microsekunden pro Loop nicht mehr leisten kann, melde ich mich ganz bestimmt wieder bei euch um das effizienter zu machen ...[/code]

Die Korrekur verringert die Effizienz nur im Grenzbereich des Überlaufs.
Nehmen wir an, das Delay soll 20sec betragen. Der Überlauf geschieht bei 4.294.967.295ms
Das sind 4.294.967 Sekunden.
Es sind in dem Fall also ca. 0,00004% der Laufzeit von dem Überlauf und somit von der Korrektur betroffen. Ich glaube die kurze Zeit kann man sich das in dem Fall unnötige Auftreten des zusätzlichen > Vergleichs leisten, wenn man umgekehrt dafür 99,9999% der Zeit eine unnötige Rechenoperation einspart.

Bei der Lesbarkeit stimme ich euch ja absolut zu. Die kann man aber deutlich verbessern, indem man den ganzen Mist in einer Klasse versteckt. Das ist dann auch besser, als die Variante mit der ständigen Rechenoperation (auch da muss man erstmal drüber nachdenken, was eigentlich passiert).

Im Endeffekt ist es wurscht. Der Impact ist so klein, dass man auch pauschal immer die Rechenoperation machen kann. Sauber ist es in meinen Augen damit aber eben nicht. Die supersaubere Lösung gibt es aber an der Stelle leider sowieso nicht, weil wir Nebenläufigkeit faken müssen, anstatt sie wirklich durch echte Threads zu lösen.

Noiasca danke fürs Ausprobieren. Man sieht, dass wir ca. 10% Unterschied in der Performance haben, was aber nicht relevant ist, weil der restliche Code im Normalfall um ein vielfaches langsamer sein wird, als unsere Schleife hier.
Der code lässt sich ca. 10000x pro Millisekunde ausführen. Somit zeigt sich, dass in der Praxis überhaupt kein relevanter Impact eintritt (was ich ja Eingangs auch schon sagte).

Somit ist auch praktisch bestätigt, dass es rein von der Performance nicht stört, wenn man in der Condition immer neu berechnet. Das gilt aber nur für die simple Subtraktion für die Delaysteuerung.

@combie
Du musst nicht gleich beleidigend werden, nur weil ich deine Meinung nicht teile oder weil du nicht verstehst, worum es mir geht.
Ich verstehe deinen Punkt absolut, ich habe aber das Gefühl, dass du mich nicht verstehst.

Mir geht es darum, dass man das Prinzip versteht. Sobald man in der Condition eine teure Operation ausführt, landet man in der Performancehölle und jeder sollte sich bewusst sein, was er da für ein Konstrukt bastelt und warum das Konstrukt im Falle für das Delay nicht weh tut.

Nehmen wir an, dass das Delay über ein Poti am Analogport gesteuert werden kann und ich lese jetzt den Analogport jedes mal in der Schleife ein (weil ich es ja so im Beispiel gesehen habe, dass man das Zeug in der Condition berechnet), dann wird das schon bedeutend langsamer. Allerdings wird das immer noch nicht auffallen.

Jetzt machen wir das Delay so, dass es auf der gemessenen Temperatur eines DS1820 basiert und messen die in der Condition. Ich hoffe spätestens jetzt wird klar, worauf ich vom Prinzip her raus will. Wenn ich da jedes mal auslese, dann bricht mir alles zusammen, weil der Dallas viele Millisekunden für die Aktion benötigt. Besser ich merke mir einfach den Wert und erst wenn die Bedingung wieder zutrifft lese ich für das nächste mal aus.

Oder nochmal in der Kurzfassung:
Haltet euch an das Beispielsketch für Blink ohne Delay, das ist safe. Aber seid euch bitte darüber im klaren, dass dieses Pattern nur da funktioniert, wo die Operation sehr schnell ist und wendet es nicht blind überall an.

Man kann den DS1820 auch nichtblockierend aufrufen. Erst die Messanforderung und später das Ergebnis abholen.
In der Performance-Hölle landet man nur, wenn man es falsch macht.

Gruß Tommy

Du musst zwangsweise die Daten über den Onewire schicken und die Daten holen. Das braucht zwangsweise Zeit. Mir geht es nicht um die Conversation Zeit, die muss man ja nicht abwarten.

Es geht mir auch überhaupt nicht um den konkreten Inhalt des Aufrufs. Nimm einen Datenbankzugriff, einen Websitedownload oder sonst irgendwas.

Wenn man in der Condition eine Operation ausführt, muss man sich damit befassen, ob die Operation teuer ist. Es ist nunmal ein Pattern, das man nicht einfach jederzeit überall drüberschmieren darf. Genau so, wie meine Variante die Einschränkung hat, dass sie nur 49 Tage Lebensdauer hat.

Jeder Code bzw. jedes Pattern hat seine Einschränkungen und muss sich nach dem Verwendungszweck und den Gegebenheiten richten.

das Pattern

loop() {
 if (tuWas()) {
 }
}

ist nur so lange gut, wie tuWas() schnell ist. Dessen sollte man sich bewusst sein. Nicht mehr und nicht weniger will ich hier vermitteln.