Arduino · 12 ottobre 2012 2

Come gestire l’overflow di millis()

Millis() è una funzione predefinita di Arduino che restituisce il numero di millisecondi (da qui il suo nome) trascorsi dall’avvio dello sketch. Questa funzione si basa su un registro interno a 32 bit aggiornato continuamente da una routine agganciata al timer 0. Essendo un registro a 32 bit, può contenere un valore massimo pari a 232-1, cioè 4.294.967.295. Questo numero può sembrare grande, e generalmente è più che sufficiente per gestire sketch su Arduino che vengono accesi per poco tempo. Ma se la vostra scheda deve restare sempre accesa, dopo un po’ di tempo questo registro raggiungerà il massimo valore che può contenere. E poi? E poi andrà in overflow, che in informatica significa che il valore da contenere è superiore a quello gestibile dal contenitore. Questo accade dopo 49,7 giorni (4.294.967.295 ms corrispondono a 1193,05 ore, che sono appunto 49,7 giorni). Quindi il registro ripartirà da zero, con tutti i problemi che ciò può comportare se il vostro sketch misura il trascorrere del tempo facendo affidamento proprio al valore restituito da millis(). Ma c’è il modo per gestire l’overflow di millis() e far sì che il vostro sketch possa continuare a funzionare senza problemi.

Il modo per gestire l’overflow risiede nel modo in cui vengono gestiti i numeri in memoria. Le variabili vengono memorizzate in celle di memoria con dimensioni fisse. I tipi char o byte, ad esempio, occupano 1 byte di memoria, ossia 8 bit; un tipo int ne occupa 2, ossia 16 bit; un tipo long ne occupa 4, ossia 32 bit. Un tipo long può, come abbiamo detto, contenere un valore che va da 0 a 232-1. Ed i numeri negativi, vi chiederete? Essi sono gestiti sempre usando la stessa quantità di bit ma sacrificandone uno, il più significativo (quello più a sinistra) per indicare il segno. Ecco come il compilatore distingue fra numeri senza segno e numeri con segno. Un tipo long è un intero con segno che può contenere un numero che varia da -2.147.483.648 a +2.147.483.647 mentre un tipo unsigned long può contenere un numero che varia da 0 a +4.294.967.295. Il tipo long è difatti un tipo signed long e come tale sacrifica il 32° bit per contenere il segno: in questo modo il numero è a tutti gli effetti un numero a 31 bit, difatti 231 da proprio 2.147.483.648.

Nei numeri con segno entra in gioco un modo differente per memorizzare il valore in memoria, un modo detto complemento a 2. Il numero viene memorizzato con i suoi bit negati, ossia di valore opposto a quello che rappresentano: viene cioè effettuata un’operazione NOT su ognuno dei bit del valore, ed infine viene aggiunto 1. Prendiamo il caso di un tipo char, che è un numero ad 8 bit con segno. Il valore 1 è rappresentato così:

00000001

mentre il valore -1 è rappresentato così:

NOT di 00000001 -> 11111110 + 00000001 = 11111111

Se ad una variabile che contiene 0 sottriaiamo 1, otteniamo -1. Usando la rappresentazione binaria ed un tipo signed char, otteniamo:

00000000 – 00000001 = 11111111

Se ad una variabile che contiene -1 aggiungiamo 1, otteniamo 0. Rappresentiamo l’operazione con i numeri binari:

11111111 + 00000001 = 100000000 -> 00000000

Come possiamo vedere, il risultato darebbe un numero a 9 bit ma, siccome la nostra variabile è ad 8 bit, il 9° bit viene troncato ed il risultato finale è dato dagli 8 bit meno significativi, che rappresentano il valore 0. Questo ci suggerisce il modo per gestire l’overflow del registro di millis().

Come detto, millis() restituisce il valore di un registro a 32 bit di tipo unsigned, che può al massimo contenere il numero 4.294.967.295. Normalmente si usa pianificare operazioni con millis() sommando al valore restituito da millis() un intervallo prefissato. Ad esempio:

void loop() {
    if (millis() > precedenteIntervallo) {
        //codice da eseguire
        precedenteIntervallo += 1000;
    }
}

Prendiamo il caso che siamo entrati nel blocco precedente, con millis() che abbia restituito 4.294.967.001 e che precedenteIntervallo sia uguale a 4.294.967.000: il test è vero ed il codice viene eseguito. Terminato il codice, aggiungiamo a precedenteIntervallo (anch’esso di tipo unsigned long) il valore 1.000, che darebbe come risultato 4.294.968.000. Però 4.294.968.000 è un numero a 33 bit, perciò verrà troncato ed il risultato memorizzato sarà 704. Al successivo test, verrà eseguito il seguente controllo:

4.294.967.001 > 704

Il risultato sarà positivo. E verrà  (erroneamente) eseguito il blocco di codice nuovamente, e verrà eseguito fino a che il registro che tiene il conto dei millisecondi non andrà in overflow e ripartirà da zero. Si può anche verificare il caso che il blocco di codice non venga eseguito più per molto tempo. Prendiamo il seguente codice:

void setup() {
    precedenteIntervallo = millis();
}
void loop() {
    if ((millis() + 1000) > precedenteIntervallo) {
        //codice da eseguire
        precedenteIntervallo = millis();
    }
}

Mettiamo che ad un certo punto millis() restituisca 4.294.966.001 e che precedenteIntervallo sia pari a 4.294.967.000: il test risulterà vero perché(4.294.966.001 + 1.000) > 4.294.967.000. Al termine del blocco di codice, a precedenteIntervallo verrà assegnato il valore di millis(), che mettiamo adesso valga 4.294.967.000 perché il nostro codice ha eseguito delle operazioni che hanno preso molto tempo. Al test successivo verrà eseguito il seguente confronto:

(4.294.967.001 + 1.000) > 4.294.967.000

Però  4.294.967.001 + 1.000 restituirebbe 4.294.968.001, che però è un numero a 33 bit: esso verrà perciò troncato in un numero a 32 bit con valore 705. Il test sarà quindi:

705 > 4.294.967.000

Ovviamente risulterà falso. E lo sarà per 49,7 giorni, finché millis() non raggiungerà nuovamente il valore di 4.294.966.001 per cui, sommando 1.000 il confronto sarà 4.294.967.001 > 4.294.967.000 ed il confronto sarà positivo.

Soluzione 1

Capito come si può manifestare il problema dell’overflow di millis(), e capito come vengono memorizzati i numeri in memoria, possiamo usare i numeri negativi nel nostro test. Per far ciò costringiamo il compilatore a convertire (cast) un tipo di dato in un altro. Abbiamo visto che -1 è rappresentato in una variabile ad 8 bit come 11111111. Ed un numero molto grande come ad esempio l’unsigned long 4.294.967.000? Esso sarà rappresentato così:

11111111 11111111 11111110 11011000

Ma questa rappresentazione è identica anche se usiamo una variabile di tipo signed long, solo che cambia il suo significato quando trattiamo il numero in decimale. Essa infatti diventa -296.

Ecco il trucco. Basta cambiare nel primo esempio il confronto in modo da verificare se la differenza fra il valore attuale di millis() e l’intervallo, trasformata in un tipo signed long, è inferiore a 0: se ciò è vero, significa che millis() è ripartito da 0 ed il test restituirà un numero negativo fino a quando millis() non avrà un valore maggiore di zero e di intervallo. Ecco un esempio di codice che non soffre del problema dell’overflow di millis():

void setup() {
    intervallo = millis() + 1000;
}
void loop() {
    if ((long)(millis() - intervallo) >= 0) {
        //codice da eseguire
        intervallo += 1000;
    }
}

La differenza (convertita in una variabile di tipo signed) fra millis() e overflow, nel momento in cui quest’ultimo va in overflow, diventa negativa e resta tale finché anche millis() non va in overflow ed il suo valore supera quello di intervallo. In questo momento la differenza torna pari a 0 o positiva per cui sappiamo che millis() ha superato nuovamente intervallo.

Per sincerarsi che il codice funziona come detto, possiamo attendere 49 giorni oppure manipolare il valore del registro che viene letto da millis(). Carichiamo il seguente sketch ed apriamo il monitor seriale. Dopo pochi secondi vedremo che il valore stampato, da molto grande, torna ad essere molto piccolo: ciò accade quando si ha l’overflow di intervallo, gestito tramite il casting dei tipi di dato. Per modificare il registro che contiene il valore dei millisecondi dobbiamo modificare la variabile timer0_millis, dichiarandola nel nostro sketch con la parola chiave extern, che significa che essa è stata dichiarata altrove. Per modificarla, blocchiamo gli interrupt e poi li riattiviamo subito dopo, per evitare di andare a scriverci sopra mentre essa viene incrementata dal timer 0.

//la dichiarazione seguente serve per dire all'interprete
//che timer0_millis è dichiarato altrove
extern unsigned long timer0_millis;
static unsigned long myTime;
void setup() {
    Serial.begin(19200);
    delay(2000);
    cli(); //blocco gli interrupt
    timer0_millis = 4294950000UL; //cambio il valore del registro
    sei(); //riattivo gli interrupt
    myTime = millis() + 1000;
}
void loop() {
    if ((long)(millis() - myTime) >= 0) {
        Serial.println(millis(), DEC);
        myTime += 1000;
    }
}

Soluzione 2

Un’altra soluzione al problema, suggerita dall’utente lesto del forum di arduino.cc, può essere quella di invertire il controllo con l’intervallo. Normalmente nel codice si esegue un controllo di questo tipo:

MILLIS + INTERVALLO > TEMPO_PRECEDENTE

questo tipo di controllo, come detto, ricade nel campo di pertinenza dell’overflow di millis.

Si può usare invece un controllo come il seguente:

MILLIS - TEMPO_PRECEDENTE > INTERVALLO

In questo modo la differenza fra il valore fornito da millis() e la precedente registrazione sarà sempre un numero compreso fra 0 ed intervallo. Facciamo un esempio.

if (millis() - tempo_precedente > intervallo) {
    tempo_precedente = millis();
    ....
}

Ammettiamo che tempo_precedente valga  4.294.967.000 e che intervallo valga 1000. Ad un certo punto millis() va in overflow e riparte da zero. Il confronto diventa:

0 - 4294967000 > 1000

Si tenderebbe a pensare che il confronto diventi:

-4294967000 > 1000

ma usando interi di tipo unsigned la sottrazione in realtà restituisce come valore 296. Questo perché un unsigned non può trattare numeri negativi per cui il risultato è in realtà dato dal massimo valore contenibile, 232 ossia 4.294.967.296, meno 4.294.967.000, per cui 296. A questo punto il confronto è diventato:

296 > 1000

che ovviamente è falso. Solo quando millis supera il valore di 704 il confronto diventa vero, perché:

705 - 4294967000 =  -4294966295
-4294966295 => 1001
1001 > 1000 = TRUE