Kurze Antwort: Versuchen Sie nicht, den Millis-Rollover zu „handhaben“, sondern schreiben Sie stattdessen einen rollover-sicheren Code. Ihr Beispielcode aus dem Tutorial ist in Ordnung. Wenn Sie versuchen, den Rollover zu erkennen, um Korrekturmaßnahmen zu ergreifen, ist die Wahrscheinlichkeit groß, dass Sie etwas falsch machen. Die meisten Arduino-Programme müssen nur Ereignisse verwalten, die sich über eine relativ kurze Zeitspanne erstrecken, z. B. das Entprellen einer Taste für 50 ms oder das Einschalten einer Heizung für 12 Stunden. Der Millis-Rollover sollte kein Problem sein.
Der richtige Weg, das Rollover-Problem zu verwalten (oder besser gesagt, es zu vermeiden), besteht darin, sich die zurückgegebene unsigned long
Zahl
millis()
in modularen Arithmetiken vorzustellen . Für Mathematiker ist eine gewisse Kenntnis dieses Konzepts beim Programmieren sehr hilfreich. Sie können die Mathematik in Aktion in Nick Gammons Artikel millis () overflow sehen ... eine schlechte Sache? . Für diejenigen, die die rechnerischen Details nicht durchgehen möchten, biete ich hier eine alternative (hoffentlich einfachere) Denkweise an. Es basiert auf der einfachen Unterscheidung zwischen Zeitpunkten und Dauern . Solange Ihre Tests nur Vergleichsdauern beinhalten, sollten Sie in Ordnung sein.
Anmerkung zu micros () : Alles, was hier über gesagt wird, millis()
gilt gleichermaßen micros()
, mit Ausnahme der Tatsache, dass micros()
alle 71,6 Minuten ein Rollover ausgeführt wird und die setMillis()
unten angegebene Funktion keinen Einfluss hat micros()
.
Zeitpunkte, Zeitstempel und Dauer
Im Umgang mit der Zeit müssen wir mindestens zwei verschiedene Konzepte unterscheiden: Momente und Dauer . Ein Moment ist ein Punkt auf der Zeitachse. Eine Dauer ist die Länge eines Zeitintervalls, dh der zeitliche Abstand zwischen den Zeitpunkten, die den Beginn und das Ende des Intervalls definieren. Die Unterscheidung zwischen diesen Begriffen ist in der Alltagssprache nicht immer sehr scharf. Wenn ich zum Beispiel sage, dass ich in fünf Minuten zurück sein werde , ist „ fünf Minuten “ die geschätzte
Dauer meiner Abwesenheit, während „ in fünf Minuten “ der Zeitpunkt ist
von meiner vorhergesagten Rückkehr. Es ist wichtig, die Unterscheidung im Auge zu behalten, da dies der einfachste Weg ist, das Problem des Überschlags vollständig zu vermeiden.
Der Rückgabewert von millis()
könnte als Dauer interpretiert werden: die Zeit, die seit dem Start des Programms bis jetzt vergangen ist. Diese Interpretation bricht jedoch zusammen, sobald Millis überläuft. Es ist im Allgemeinen weitaus nützlicher, sich vorzustellen , dass millis()
ein
Zeitstempel zurückgegeben wird , dh ein „Etikett“, das einen bestimmten Zeitpunkt identifiziert. Es könnte argumentiert werden, dass diese Interpretation unter der Mehrdeutigkeit dieser Etiketten leidet, da sie alle 49,7 Tage wiederverwendet werden. Dies ist jedoch selten ein Problem: In den meisten eingebetteten Anwendungen ist alles, was vor 49,7 Tagen passiert ist, eine alte Geschichte, die uns egal ist. Daher sollte das Recycling der alten Etiketten kein Problem darstellen.
Vergleichen Sie keine Zeitstempel
Es macht keinen Sinn, herauszufinden, welcher der beiden Zeitstempel größer ist als der andere. Beispiel:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Naiv würde man erwarten, dass die Bedingung der if ()
immer wahr ist. Aber es wird tatsächlich falsch sein, wenn Millis während überläuft
delay(3000)
. T1 und t2 als wiederverwertbare Etiketten zu betrachten, ist der einfachste Weg, um den Fehler zu vermeiden: Das Etikett t1 wurde eindeutig einem Zeitpunkt vor t2 zugewiesen, aber in 49,7 Tagen wird es einem zukünftigen Zeitpunkt zugewiesen. Somit geschieht t1 sowohl vor als auch nach t2. Dies sollte deutlich machen, dass der Ausdruck t2 > t1
keinen Sinn ergibt.
Wenn es sich jedoch nur um Etiketten handelt, ist die offensichtliche Frage: Wie können wir mit ihnen nützliche Zeitberechnungen durchführen? Die Antwort lautet: indem wir uns auf die beiden einzigen Berechnungen beschränken, die für Zeitstempel sinnvoll sind:
later_timestamp - earlier_timestamp
ergibt eine Dauer, nämlich die Zeitdauer, die zwischen dem früheren und dem späteren Zeitpunkt verstrichen ist. Dies ist die nützlichste Rechenoperation mit Zeitstempeln.
timestamp ± duration
Gibt einen Zeitstempel zurück, der einige Zeit nach (wenn + verwendet wird) oder vor (wenn -) dem ursprünglichen Zeitstempel liegt. Nicht so nützlich, wie es sich anhört, da der resultierende Zeitstempel nur für zwei Arten von Berechnungen verwendet werden kann ...
Dank der modularen Arithmetik funktioniert beides garantiert über den gesamten Millis-Rollover hinweg, zumindest solange die Verzögerungen kürzer als 49,7 Tage sind.
Das Vergleichen der Dauer ist in Ordnung
Eine Dauer ist nur die Anzahl der Millisekunden, die in einem bestimmten Zeitintervall verstrichen sind. Solange wir nicht länger als 49,7 Tage arbeiten müssen, sollte jede Operation, die physikalisch sinnvoll ist, auch rechnerisch sinnvoll sein. Wir können zum Beispiel eine Dauer mit einer Frequenz multiplizieren, um eine Anzahl von Perioden zu erhalten. Oder wir können zwei Zeiträume vergleichen, um zu wissen, welcher länger ist. Zum Beispiel sind hier zwei alternative Implementierungen von delay()
. Zunächst der Buggy:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
Und hier ist der Richtige:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
Die meisten C-Programmierer würden die obigen Schleifen in einer Terser-Form schreiben, wie z
while (millis() < start + ms) ; // BUGGY version
und
while (millis() - start < ms) ; // CORRECT version
Obwohl sie täuschend ähnlich aussehen, sollte die Unterscheidung zwischen Zeitstempel und Dauer deutlich machen, welcher fehlerhaft und welcher korrekt ist.
Was ist, wenn ich Zeitstempel wirklich vergleichen muss?
Versuchen Sie besser, die Situation zu vermeiden. Wenn es unvermeidlich ist, gibt es noch Hoffnung, wenn bekannt ist, dass die jeweiligen Momente nahe genug sind: näher als 24,85 Tage. Ja, unsere maximal handhabbare Verzögerung von 49,7 Tagen wurde gerade halbiert.
Die naheliegende Lösung besteht darin, unser Zeitstempel-Vergleichsproblem in ein Dauer-Vergleichsproblem umzuwandeln. Angenommen, wir müssen wissen, ob der Zeitpunkt t1 vor oder nach t2 liegt. Wir wählen einen Referenzzeitpunkt in ihrer gemeinsamen Vergangenheit und vergleichen die Zeitdauern von dieser Referenz bis sowohl t1 als auch t2. Der Referenzzeitpunkt wird durch Subtrahieren einer ausreichend langen Dauer von entweder t1 oder t2 erhalten:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
Dies kann vereinfacht werden als:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Es ist verlockend, weiter zu vereinfachen if (t1 - t2 < 0)
. Dies funktioniert natürlich nicht, da t1 - t2
eine vorzeichenlose Zahl nicht negativ sein kann. Dies funktioniert jedoch, obwohl es nicht portabel ist:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
Das signed
obige Schlüsselwort ist überflüssig (eine Ebene long
ist immer signiert), aber es hilft, die Absicht klar zu machen. Das Konvertieren in eine vorzeichenbehaftete Long- LONG_ENOUGH_DURATION
Position entspricht einer Einstellung von 24,85 Tagen. Der Trick ist nicht portierbar, da gemäß dem C-Standard das Ergebnis in der Implementierung definiert ist . Aber da der GCC-Compiler verspricht, das Richtige zu tun , funktioniert er zuverlässig auf Arduino. Wenn wir implementierungsdefiniertes Verhalten vermeiden möchten, ist der oben angegebene Vergleich mathematisch äquivalent zu:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
mit dem einzigen problem, dass der vergleich rückwärts schaut. Es ist auch äquivalent zu diesem Einzelbittest, solange Longs 32-Bit sind:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Die letzten drei Tests werden von gcc tatsächlich in genau denselben Maschinencode kompiliert.
Wie teste ich meine Skizze gegen den Millis-Rollover?
Wenn Sie die obigen Vorschriften befolgen, sollten Sie alle gut sein. Wenn Sie dennoch testen möchten, fügen Sie diese Funktion Ihrer Skizze hinzu:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
und Sie können jetzt Ihr Programm per Zeitreise aufrufen
setMillis(destination)
. Wenn Sie möchten, dass es immer wieder durch den Millis-Überlauf geht, wie Phil Connors, der den Groundhog Day noch einmal durchlebt, können Sie dies hier einfügen loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
Der negative Zeitstempel über (-3000) wird vom Compiler implizit in einen vorzeichenlosen Wert konvertiert, der 3000 Millisekunden vor dem Rollover entspricht (er wird in 4294964296 konvertiert).
Was ist, wenn ich wirklich sehr lange Zeiträume erfassen muss?
Wenn Sie ein Relais drei Monate später ein- und ausschalten müssen, müssen Sie die Millis-Überläufe wirklich nachverfolgen. Es gibt viele Möglichkeiten, dies zu tun. Die einfachste Lösung könnte darin bestehen, einfach millis()
auf 64 Bit zu erweitern:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
Dies zählt im Wesentlichen die Rollover-Ereignisse und verwendet diese Zählung als die 32 höchstwertigen Bits einer 64-Bit-Millisekunden-Zählung. Damit diese Zählung ordnungsgemäß funktioniert, muss die Funktion mindestens alle 49,7 Tage einmal aufgerufen werden. Wenn es jedoch nur einmal alle 49,7 Tage aufgerufen wird, ist es in einigen Fällen möglich, dass die Prüfung (new_low32 < low32)
fehlschlägt und der Code eine Zählung von nicht besteht high32
. Die Verwendung von millis (), um zu entscheiden, wann der einzige Aufruf dieses Codes in einem einzigen "Wrap" von millis (einem bestimmten 49,7-Tage-Fenster) erfolgen soll, kann abhängig von der Ausrichtung der Zeitrahmen sehr gefährlich sein. Wenn Sie mithilfe von millis () ermitteln, wann die einzigen Aufrufe von millis64 () erfolgen sollen, sollten aus Sicherheitsgründen mindestens zwei Aufrufe in jedem 49,7-Tage-Fenster erfolgen.
Beachten Sie jedoch, dass 64-Bit-Arithmetik auf dem Arduino teuer ist. Es kann sich lohnen, die Zeitauflösung zu reduzieren, um bei 32 Bit zu bleiben.