Come posso gestire il rollover millis ()?


73

Ho bisogno di leggere un sensore ogni cinque minuti, ma poiché il mio schizzo ha anche altri compiti da svolgere, non riesco solo delay()tra le letture. Esiste il tutorial Blink senza indugio che suggerisce il codice in queste righe:

void loop()
{
    unsigned long currentMillis = millis();

    // Read the sensor when needed.
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        readSensor();
    }

    // Do other stuff...
}

Il problema è che millis()tornerà a zero dopo circa 49,7 giorni. Poiché il mio schizzo è destinato a durare più a lungo di quello, devo assicurarmi che il rollover non faccia fallire il mio schizzo. Posso facilmente rilevare la condizione di rollover ( currentMillis < previousMillis), ma non sono sicuro di cosa fare.

Quindi la mia domanda: quale sarebbe il modo corretto / più semplice per gestire il millis()rollover?


5
Nota editoriale: questa non è esattamente una mia domanda, piuttosto un tutorial in un formato domanda / risposta. Ho assistito a molta confusione su Internet (incluso qui) su questo argomento e questo sito sembra il luogo ovvio in cui cercare una risposta. Questo è il motivo per cui sto fornendo questo tutorial qui.
Edgar Bonet,

2
Vorrei fare previousMillis += intervalinvece previousMillis = currentMillisse volessi una certa frequenza di risultati.
Jasen,

4
@Jasen: Esatto! previousMillis += intervalse desideri una frequenza costante e sei sicuro che l'elaborazione richieda meno di interval, ma previousMillis = currentMillisper garantire un ritardo minimo di interval.
Edgar Bonet,

Abbiamo davvero bisogno di una FAQ per cose come questa.

Uno dei "trucchi" che uso è quello di alleggerire il carico sull'arduino usando l'int più piccolo che contiene l'intervallo. Ad esempio, per intervalli di massimo 1 minuto, scrivouint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87 il

Risposte:


95

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 longnumero 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 > t1non 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:

  1. later_timestamp - earlier_timestampproduce 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.
  2. timestamp ± durationgenera 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 signedsopra è ridondante (una pianura longè sempre firmata), ma aiuta a chiarire l'intento. La conversione in un long firmato equivale a un'impostazione LONG_ENOUGH_DURATIONpari 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.


2
Quindi, stai dicendo che il codice scritto nella domanda funzionerà correttamente?
Jasen,

3
@Jasen: esattamente! Mi sembra più di una volta che le persone stiano cercando di "risolvere" il problema che non esisteva in primo luogo.
Edgar Bonet,

2
Sono contento di averlo trovato. Ho già fatto questa domanda.
Sebastian Freeman,

1
Una delle risposte migliori e più utili su StackExchange! Molte grazie! :)
Falko,

Questa è una risposta così sorprendente alla domanda. Torno a questa risposta praticamente una volta all'anno perché sono paranoico di invasioni di confusione.
Jeffrey Cash,

17

TL; DR Versione corta:

An unsigned longè compreso tra 0 e 4.294.967.295 (2 ^ 32 - 1).

Quindi diciamo che previousMillis4.294.967.290 (5 ms prima del rollover) ed currentMillisè 10 (10 ms dopo il rollover). Quindi currentMillis - previousMillissono effettivi 16 (non -4.294.967.280) poiché il risultato verrà calcolato come un long senza segno (che non può essere negativo, quindi si girerà). Puoi verificarlo semplicemente:

Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16

Quindi il codice sopra funzionerà perfettamente. Il trucco è calcolare sempre la differenza temporale e non confrontare i due valori temporali.


Che ne dici di un 15ms prima del rollover e un 10ms dopo il rollover (ovvero 49.7 giorni dopo ). 15> 10 , ma il timbro da 15ms ha quasi un mese e mezzo. 15-10> 0 e 10-15> 0 unsigned logica, quindi non serve a niente qui!
ps95,

@ prakharsingh95 10ms-15ms diventerà ~ 49,7 giorni - 5ms, che è la differenza corretta. La matematica funziona fino a quando non viene millis()ripetuta due volte, ma è molto improbabile che si verifichi il codice in questione.
BrettAM,

Lasciami riformulare. Supponiamo di avere due timestamp 200ms e 10ms. Come si dice quale è (sono) capovolto?
ps95,

@ prakharsingh95 Quella memorizzata previousMillisdeve essere stata misurata in precedenza currentMillis, quindi se currentMillisè inferiore a previousMillisun rollover. La matematica capita che a meno che non si siano verificati due rollover, non è nemmeno necessario pensarci.
BrettAM,

1
Ah ok. se lo fai t2-t1, e se puoi garantire che t1viene misurato prima, t2allora equivale a firmato (t2-t1)% 4,294,967,295 , quindi l'auto wraparound. Bello!. E se ci fossero due rollover o se intervalfosse> 4.294.967.295?
ps95,

1

Avvolgi la millis()classe!

Logica:

  1. Usa gli ID anziché millis()direttamente.
  2. Confronta le inversioni usando gli ID. Questo è pulito e indipendente dal rollover.
  3. Per applicazioni specifiche, per calcolare la differenza esatta tra due ID, tenere traccia di inversioni e timbri. Calcola la differenza

Tenere traccia delle inversioni:

  1. Aggiorna periodicamente un timbro locale più velocemente di millis(). Questo ti aiuterà a scoprire se millis()ha sorvolato.
  2. Il periodo del timer determina l'accuratezza
class Timer {

public:
    static long last_stamp;
    static long *stamps;
    static int *reversals;
    static int count;
    static int reversal_count;

    static void setup_timer() {
        // Setup Timer2 overflow to fire every 8ms (125Hz)
        //   period [sec] = (1 / f_clock [sec]) * prescale * (255-count)
        //                  (1/16000000)  * 1024 * (255-130) = .008 sec


        TCCR2B = 0x00;        // Disable Timer2 while we set it up

        TCNT2  = 130;         // Reset Timer Count  (255-130) = execute ev 125-th T/C clock
        TIFR2  = 0x00;        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
        TIMSK2 = 0x01;        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
        TCCR2A = 0x00;        // Timer2 Control Reg A: Wave Gen Mode normal
        TCCR2B = 0x07;        // Timer2 Control Reg B: Timer Prescaler set to 1024

        count = 0;
        stamps = new long[50];
        reversals = new int [10];
        reversal_count =0;
    }

    static long get_stamp () {
        stamps[count++] = millis();
        return count-1;
    }

    static bool compare_stamps_by_id(int s1, int s2) {
        return s1 > s2;
    }

    static long long get_stamp_difference(int s1, int s2) {
        int no_of_reversals = 0;
        for(int j=0; j < reversal_count; j++)
        if(reversals[j] < s2 && reversals[j] > s1)
            no_of_reversals++;
        return stamps[s2]-stamps[s1] + 49.7 * 86400 * 1000;       
    }

};

long Timer::last_stamp;
long *Timer::stamps;
int *Timer::reversals;
int Timer::count;
int Timer::reversal_count;

ISR(TIMER2_OVF_vect) {

    long stamp = millis();
    if(stamp < Timer::last_stamp) // reversal
        Timer::reversals[Timer::reversal_count++] = Timer::count;
    else 
        ; // no reversal
    Timer::last_stamp = stamp;    
    TCNT2 = 130;     // reset timer ct to 130 out of 255
    TIFR2 = 0x00;    // timer2 int flag reg: clear timer overflow flag
};

// Usage

void setup () {
    Timer::setup_timer();

    long s1 = Timer::get_stamp();
    delay(3000);
    long s2 = Timer::get_stamp();

    Timer::compare_stamps_by_id(s1, s2); // true

    Timer::get_stamp_difference(s1, s2); // return true difference, taking into account reversals
}

Crediti timer .


9
Ho modificato il codice per rimuovere gli errori maaaaany che ne hanno impedito la compilazione. Questa roba ti costerà circa 232 byte di RAM e due canali PWM. Inizierà anche a corrompere la memoria dopo get_stamp()51 volte. Confrontare i ritardi anziché i timestamp sarà sicuramente più efficiente.
Edgar Bonet,

1

Ho adorato questa domanda e le grandi risposte che ha generato. Prima un breve commento su una risposta precedente (lo so, lo so, ma non ho ancora il rappresentante per commentare. :-).

La risposta di Edgar Bonet è stata sorprendente. Ho programmato per 35 anni e ho imparato qualcosa di nuovo oggi. Grazie. Detto questo, credo che il codice per "E se avessi davvero bisogno di tenere traccia di durate molto lunghe?" si interrompe a meno che non si chiami millis64 () almeno una volta per periodo di rollover. Davvero nitido, e probabilmente non sarà un problema in un'implementazione del mondo reale, ma il gioco è fatto.

Ora, se davvero volevi che i timestamp coprissero qualsiasi intervallo di tempo sano (64 bit di millisecondi sono circa mezzo miliardo di anni secondo i miei calcoli), sembra semplice estendere l'implementazione millis () esistente a 64 bit.

Queste modifiche a attinycore / cablaggio.c (sto lavorando con ATTiny85) sembrano funzionare (suppongo che il codice per altri AVR sia molto simile). Vedi le righe con i commenti // BFB e la nuova funzione millis64 (). Chiaramente sarà più grande (98 byte di codice, 4 byte di dati) e più lento, e come ha sottolineato Edgar, quasi sicuramente puoi raggiungere i tuoi obiettivi con una migliore comprensione della matematica di numeri interi senza segno, ma è stato un esercizio interessante .

volatile unsigned long long timer0_millis = 0;      // BFB: need 64-bit resolution

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
    // copy these to local variables so they can be stored in registers
    // (volatile variables must be read from memory on every access)
    unsigned long long m = timer0_millis;       // BFB: need 64-bit resolution
    unsigned char f = timer0_fract;

    m += MILLIS_INC;
    f += FRACT_INC;
    if (f >= FRACT_MAX) {
        f -= FRACT_MAX;
        m += 1;
    }

    timer0_fract = f;
    timer0_millis = m;
    timer0_overflow_count++;
}

// BFB: 64-bit version
unsigned long long millis64()
{
    unsigned long long m;
    uint8_t oldSREG = SREG;

    // disable interrupts while we read timer0_millis or we might get an
    // inconsistent value (e.g. in the middle of a write to timer0_millis)
    cli();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}

1
Hai ragione, il mio millis64()funziona solo se viene chiamato più frequentemente del periodo di rollover. Ho modificato la mia risposta per sottolineare questa limitazione. La tua versione non presenta questo problema, ma presenta un altro svantaggio: esegue l'aritmetica a 64 bit in un contesto di interrupt , che a volte aumenta la latenza nel rispondere ad altri interrupt.
Edgar Bonet,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.