Risposta breve: non provare a "gestire" il rollover del millis, scrivi invece un codice sicuro. Il tuo codice di esempio dal tutorial va bene. Se si tenta di rilevare il rollover per implementare misure correttive, è probabile che si stia facendo qualcosa di sbagliato. La maggior parte dei programmi Arduino deve solo gestire eventi che durano relativamente brevemente, come il rimbalzo di un pulsante per 50 ms o l'accensione di un riscaldatore per 12 ore ... Quindi, e anche se il programma è destinato a funzionare per anni alla volta, il rollover in millis non dovrebbe essere un problema.
Il modo corretto di gestire (o meglio, evitare di dover gestire) il problema del rollover è quello di pensare al unsigned long
numero restituito
millis()
in termini di aritmetica modulare . Per i matematicamente inclini, una certa familiarità con questo concetto è molto utile durante la programmazione. Puoi vedere la matematica in azione nell'articolo millis () di Nick Gammon traboccare ... una cosa negativa? . Per coloro che non vogliono passare attraverso i dettagli computazionali, offro qui un modo alternativo (speriamo più semplice) di pensarci. Si basa sulla semplice distinzione tra istanti e durate . Fintanto che i test prevedono solo il confronto delle durate, dovresti andare bene.
Nota su micros () : tutto quanto detto qui si millis()
applica allo stesso modo micros()
, tranne per il fatto che micros()
passa ogni 71,6 minuti e la setMillis()
funzione fornita di seguito non ha alcun effetto micros()
.
Istanti, timestamp e durate
Quando abbiamo a che fare con il tempo, dobbiamo fare la distinzione tra almeno due concetti diversi: istanti e durate . Un istante è un punto sull'asse temporale. Una durata è la lunghezza di un intervallo di tempo, ovvero la distanza temporale tra gli istanti che definiscono l'inizio e la fine dell'intervallo. La distinzione tra questi concetti non è sempre molto chiara nel linguaggio di tutti i giorni. Ad esempio, se dico " Tornerò tra cinque minuti ", allora " cinque minuti " è la durata stimata
della mia assenza, mentre " tra cinque minuti " è l' istante
del mio previsto ritorno. È importante tenere presente la distinzione, poiché è il modo più semplice per evitare del tutto il problema del rollover.
Il valore di ritorno di millis()
potrebbe essere interpretato come una durata: il tempo trascorso dall'inizio del programma fino ad ora. Questa interpretazione, tuttavia, si interrompe non appena traboccano i millis. In genere è molto più utile pensare di millis()
restituire un
timestamp , ovvero una "etichetta" che identifica un particolare istante. Si potrebbe sostenere che questa interpretazione risente del fatto che queste etichette sono ambigue, poiché vengono riutilizzate ogni 49,7 giorni. Questo, tuttavia, raramente è un problema: nella maggior parte delle applicazioni integrate, tutto ciò che è accaduto 49,7 giorni fa è una storia antica a cui non ci importa. Pertanto, il riciclaggio delle vecchie etichette non dovrebbe essere un problema.
Non confrontare i timestamp
Cercare di scoprire quale tra due timestamp è maggiore dell'altro non ha senso. Esempio:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Ingenuamente, ci si aspetterebbe che la condizione del if ()
sia sempre vera. Ma in realtà sarà falso se traboccano millis durante
delay(3000)
. Pensare a t1 e t2 come etichette riciclabili è il modo più semplice per evitare l'errore: l'etichetta t1 è stata chiaramente assegnata a un istante prima di t2, ma in 49,7 giorni verrà riassegnata a un istante futuro. Pertanto, t1 avviene sia prima che dopo t2. Ciò dovrebbe chiarire che l'espressione t2 > t1
non ha senso.
Ma, se si tratta di semplici etichette, la domanda ovvia è: come possiamo fare dei calcoli del tempo utile con loro? La risposta è: limitandoci ai soli due calcoli che hanno senso per i timestamp:
later_timestamp - earlier_timestamp
produce una durata, ovvero la quantità di tempo trascorso tra l'istante precedente e l'istante successivo. Questa è l'operazione aritmetica più utile che coinvolge i timestamp.
timestamp ± duration
genera un timestamp che è trascorso qualche tempo dopo (se si utilizza +) o prima (se -) del timestamp iniziale. Non utile come sembra, poiché il timestamp risultante può essere utilizzato solo in due tipi di calcoli ...
Grazie all'aritmetica modulare, entrambi sono garantiti per funzionare bene attraverso il rollover dei millis, almeno fintanto che i ritardi in questione sono inferiori a 49,7 giorni.
Il confronto delle durate va bene
Una durata è solo la quantità di millisecondi trascorsi durante un certo intervallo di tempo. Finché non abbiamo bisogno di gestire durate superiori a 49,7 giorni, qualsiasi operazione che abbia un senso fisico dovrebbe avere senso anche dal punto di vista computazionale. Ad esempio, possiamo moltiplicare una durata per una frequenza per ottenere un numero di periodi. Oppure possiamo confrontare due durate per sapere quale è più lunga. Ad esempio, ecco due implementazioni alternative delay()
. Innanzitutto, quello difettoso:
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;
}
}
Ed ecco quello corretto:
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;
}
}
La maggior parte dei programmatori C scriverà i loop di cui sopra in una forma terser, come
while (millis() < start + ms) ; // BUGGY version
e
while (millis() - start < ms) ; // CORRECT version
Sebbene appaiano ingannevolmente simili, la distinzione data / ora dovrebbe chiarire quale è difettoso e quale è corretto.
Cosa succede se devo davvero confrontare i timestamp?
Meglio cercare di evitare la situazione. Se è inevitabile, c'è ancora speranza se si sa che i rispettivi istanti sono abbastanza vicini: più vicini di 24,85 giorni. Sì, il nostro ritardo gestibile massimo di 49,7 giorni è stato appena dimezzato.
La soluzione ovvia è convertire il nostro problema di confronto data / ora in un problema di confronto durata. Diciamo che dobbiamo sapere se t1 istantaneo è prima o dopo t2. Scegliamo alcuni istanti di riferimento nel loro passato comune e confrontiamo le durate da questo riferimento fino a t1 e t2. L'istante di riferimento si ottiene sottraendo una durata abbastanza lunga da t1 o t2:
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
Questo può essere semplificato come:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
È allettante tentare di semplificare ulteriormente if (t1 - t2 < 0)
. Ovviamente, questo non funziona perché t1 - t2
, essendo calcolato come un numero senza segno, non può essere negativo. Questo, sebbene non sia portatile, funziona:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
La parola chiave signed
sopra è ridondante (una pianura long
è sempre firmata), ma aiuta a chiarire l'intento. La conversione in un long firmato equivale a un'impostazione LONG_ENOUGH_DURATION
pari a 24,85 giorni. Il trucco non è portatile perché, secondo lo standard C, il risultato è l' implementazione definita . Ma dal momento che il compilatore gcc promette di fare la cosa giusta , funziona in modo affidabile su Arduino. Se desideriamo evitare comportamenti definiti dall'implementazione, il confronto sopra firmato è matematicamente equivalente a questo:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
con l'unico problema che il confronto guarda indietro. È anche equivalente, purché siano lunghi 32 bit, a questo test a singolo bit:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Gli ultimi tre test sono in realtà compilati da gcc nello stesso identico codice macchina.
Come testare il mio schizzo con il rollover in millis
Se segui i precetti di cui sopra, dovresti essere tutto buono. Se vuoi comunque provare, aggiungi questa funzione al tuo schizzo:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
e ora puoi viaggiare nel tempo chiamando il tuo programma
setMillis(destination)
. Se vuoi che passi attraverso i millesimi traboccanti ripetutamente, come Phil Connors che rivive il Giorno della marmotta, puoi metterlo dentro loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
Il timestamp negativo sopra (-3000) viene convertito implicitamente dal compilatore in un lungo senza segno corrispondente a 3000 millisecondi prima del rollover (viene convertito in 4294964296).
E se avessi davvero bisogno di tenere traccia di durate molto lunghe?
Se devi attivare un relè e disattivarlo tre mesi dopo, devi davvero tenere traccia dei millesimi di troppo pieno. Ci sono molti modi per farlo. La soluzione più semplice potrebbe essere semplicemente quella di estendere millis()
a 64 bit:
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;
}
Questo sta essenzialmente contando gli eventi di rollover e usando questo conteggio come i 32 bit più significativi di un conteggio dei millisecondi a 64 bit. Affinché questo conteggio funzioni correttamente, è necessario chiamare la funzione almeno una volta ogni 49,7 giorni. Tuttavia, se viene chiamato una sola volta per 49,7 giorni, in alcuni casi è possibile che il controllo (new_low32 < low32)
fallisca e che il codice non contenga high32
. L'utilizzo di millis () per decidere quando effettuare l'unica chiamata a questo codice in un singolo "a capo" di millis (una finestra specifica di 49,7 giorni) potrebbe essere molto pericoloso, a seconda di come si allineano i tempi. Per sicurezza, se si utilizza millis () per determinare quando effettuare le uniche chiamate a millis64 (), dovrebbero esserci almeno due chiamate in ogni finestra di 49,7 giorni.
Tieni presente, tuttavia, che l'aritmetica a 64 bit è costosa sull'Arduino. Potrebbe valere la pena ridurre la risoluzione temporale per rimanere a 32 bit.