Ich muss alle fünf Minuten einen Sensor ablesen, aber da meine Skizze auch andere Aufgaben zu erledigen hat, kann ich nicht einfach delay()
zwischen den Ablesungen wechseln. Es gibt das Blink- Tutorial, das ohne Verzögerung vorschlägt, dass ich nach folgenden Grundsätzen codiere:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
Das Problem ist, dass millis()
es nach ungefähr 49,7 Tagen zu einem Rollover auf Null kommen wird. Da meine Skizze länger ausgeführt werden soll, muss ich sicherstellen, dass der Rollover meine Skizze nicht zum Scheitern bringt. Ich kann den Rollover-Zustand leicht erkennen ( currentMillis < previousMillis
), bin mir aber nicht sicher, was ich dann tun soll.
Also meine Frage: Was wäre der richtige / einfachste Weg, um mit dem
millis()
Überschlag umzugehen?
programming
time
millis
Edgar Bonet
quelle
quelle
previousMillis += interval
anstattpreviousMillis = currentMillis
wenn ich eine bestimmte Häufigkeit von Ergebnissen wollte.previousMillis += interval
wenn Sie eine konstante Frequenz wünschen und sicher sind, dass Ihre Verarbeitung weniger alsinterval
, aberpreviousMillis = currentMillis
für die Gewährleistung einer minimalen Verzögerung voninterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Antworten:
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
Zahlmillis()
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ßenmicros()
, mit Ausnahme der Tatsache, dassmicros()
alle 71,6 Minuten ein Rollover ausgeführt wird und diesetMillis()
unten angegebene Funktion keinen Einfluss hatmicros()
.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 , dassmillis()
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:
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äuftdelay(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 Ausdruckt2 > 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:Und hier ist der Richtige:
Die meisten C-Programmierer würden die obigen Schleifen in einer Terser-Form schreiben, wie z
und
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:
Dies kann vereinfacht werden als:
Es ist verlockend, weiter zu vereinfachen
if (t1 - t2 < 0)
. Dies funktioniert natürlich nicht, dat1 - t2
eine vorzeichenlose Zahl nicht negativ sein kann. Dies funktioniert jedoch, obwohl es nicht portabel ist:Das
signed
obige Schlüsselwort ist überflüssig (eine Ebenelong
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:mit dem einzigen problem, dass der vergleich rückwärts schaut. Es ist auch äquivalent zu diesem Einzelbittest, solange Longs 32-Bit sind:
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:
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ügenloop()
: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: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 bestehthigh32
. 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.
quelle
TL; DR Kurzfassung:
An
unsigned long
ist 0 bis 4 294 967 295 (2 ^ 32 - 1).Sagen wir
previousMillis
also 4.294.967.290 (5 ms vor dem Überschlag) undcurrentMillis
10 (10 ms nach dem Überschlag). DanncurrentMillis - previousMillis
ist es tatsächlich 16 (nicht -4.294.967.280), da das Ergebnis als vorzeichenloses Long berechnet wird (was nicht negativ sein kann, so dass es sich selbst dreht ). Sie können dies einfach überprüfen, indem Sie:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Der obige Code funktioniert also einwandfrei. Der Trick besteht darin, immer die Zeitdifferenz zu berechnen und nicht die beiden Zeitwerte zu vergleichen.
quelle
unsigned
Logik, so dass hier nichts zu gebrauchen ist!millis()
zweimal ein Rollover ausgeführt wird. Es ist jedoch sehr unwahrscheinlich, dass dies bei dem betreffenden Code der Fall ist.previousMillis
muss zuvor gemessen worden seincurrentMillis
, wenncurrentMillis
also ein kleinerer Wert alspreviousMillis
ein Überschlag aufgetreten ist. Es hat sich herausgestellt, dass Sie nicht einmal darüber nachdenken müssen, es sei denn, es sind zwei Überschläge aufgetreten.t2-t1
, und wenn Sie garantieren können ,t1
bevor gemessen wird ,t2
dann ist es gleichbedeutend mit unterzeichnet(t2-t1)% 4,294,967,295
, damit die Auto - Wraparound. Nett!. Was aber, wenn es zwei Überschläge gibt oderinterval
> 4.294.967.295?Wickeln Sie die
millis()
in einer Klasse!Logik:
millis()
direkt.Rückbuchungen nachverfolgen:
millis()
. Auf diese Weise können Sie feststellen, obmillis()
ein Überlauf aufgetreten ist.Timer Credits .
quelle
get_stamp()
51-maliger Beschädigung des Speichers beginnen . Der Vergleich von Verzögerungen anstelle von Zeitstempeln ist mit Sicherheit effizienter.Ich habe diese Frage geliebt und die tollen Antworten, die sie hervorgebracht hat. Zuerst ein kurzer Kommentar zu einer vorherigen Antwort (ich weiß, ich weiß, aber ich habe noch keinen Repräsentanten, um einen Kommentar abzugeben. :-).
Edgar Bonets Antwort war erstaunlich. Ich programmiere seit 35 Jahren und habe heute etwas Neues gelernt. Danke. Das heißt, ich glaube, der Code für "Was ist, wenn ich wirklich sehr lange Zeiträume verfolgen muss?" bricht ab, es sei denn, Sie rufen millis64 () mindestens einmal pro Rollover-Zeitraum auf. Wirklich pingelig, und es ist unwahrscheinlich, dass es in einer realen Implementierung zu Problemen kommt, aber los geht's.
Wenn Sie wirklich Zeitstempel für einen vernünftigen Zeitbereich benötigen (64-Bit-Millisekunden sind nach meiner Einschätzung etwa eine halbe Milliarde Jahre), scheint es einfach, die vorhandene millis () -Implementierung auf 64-Bit zu erweitern.
Diese Änderungen an attinycore / wiring.c (ich arbeite mit dem ATTiny85) scheinen zu funktionieren (ich gehe davon aus, dass der Code für andere AVRs sehr ähnlich ist). Siehe die Zeilen mit den Kommentaren // BFB und der neuen Funktion millis64 (). Es ist klar, dass es sowohl größer (98 Byte Code, 4 Byte Daten) als auch langsamer sein wird, und wie Edgar betonte, können Sie Ihre Ziele mit ziemlicher Sicherheit mit einem besseren Verständnis der vorzeichenlosen ganzzahligen Mathematik erreichen, aber es war eine interessante Übung .
quelle
millis64()
funktioniert nur, wenn es häufiger aufgerufen wird als die Überlaufzeit. Ich habe meine Antwort bearbeitet, um auf diese Einschränkung hinzuweisen. Ihre Version hat dieses Problem nicht, weist jedoch einen weiteren Nachteil auf: Sie führt 64-Bit-Arithmetik im Interrupt-Kontext aus , wodurch sich die Wartezeit beim Reagieren auf andere Interrupts gelegentlich erhöht.