Réponse courte: n'essayez pas de "gérer" le basculement en millis, écrivez plutôt du code protégé contre le basculement. Votre exemple de code du didacticiel est correct. Si vous essayez de détecter le basculement afin de mettre en œuvre des mesures correctives, il y a des chances que vous fassiez quelque chose de mal. La plupart des programmes Arduino ne doivent gérer que des événements s'étendant sur des durées relativement courtes, telles que le rebond d'un bouton pendant 50 ms ou l'allumage d'un chauffage pendant 12 heures ... Ensuite, et même si le programme est conçu pour durer des années, le roulement en millis ne devrait pas être une préoccupation.
La bonne façon de gérer (ou plutôt d'éviter de devoir gérer) le problème du roulement est de penser au unsigned long
nombre renvoyé par
millis()
en termes d' arithmétique modulaire . Pour les mathématiciens, une certaine familiarité avec ce concept est très utile lors de la programmation. Vous pouvez voir le calcul en action dans l'article de Nick Gammon, millis (), débordement ... une mauvaise chose? . Pour ceux qui ne veulent pas passer à travers les détails de calcul, je propose ici une autre façon (espérons-le plus simple) d'y penser. Elle repose sur la simple distinction entre instants et durées . Tant que vos tests ne concernent que la comparaison des durées, ça devrait aller.
Note sur micros () : Tout ce que nous avons dit à propos millis()
s'applique également micros()
, à l'exception du fait qu'il se micros()
déroule toutes les 71,6 minutes et que la setMillis()
fonction fournie ci-dessous n'affecte pas micros()
.
Instants, horodatages et durées
Dans le temps, il faut distinguer au moins deux concepts différents: les instants et les durées . Un instant est un point sur l'axe du temps. Une durée est la durée d'un intervalle de temps, c'est-à-dire la distance dans le temps entre les instants qui définissent le début et la fin de l'intervalle. La distinction entre ces concepts n’est pas toujours très nette dans le langage courant. Par exemple, si je dis « Je serai de retour dans cinq minutes », alors « cinq minutes » est la durée estimée
de mon absence, alors que « dans cinq minutes » est l' instant
de mon prédit revenir. Il est important de garder la distinction à l’esprit, car c’est le moyen le plus simple d’éviter entièrement le problème du roulement.
La valeur de retour de millis()
pourrait être interprétée comme une durée: le temps écoulé depuis le début du programme jusqu'à maintenant. Cette interprétation, cependant, se décompose dès que des millis débordent. Il est généralement beaucoup plus utile de penser millis()
à renvoyer un
horodatage , c'est-à-dire une "étiquette" identifiant un instant particulier. On pourrait soutenir que cette interprétation est ambiguë car ces étiquettes sont réutilisées tous les 49,7 jours. Cependant, cela pose rarement un problème: dans la plupart des applications intégrées, tout ce qui s’est passé il ya 49,7 jours est une histoire ancienne qui ne nous intéresse pas. Ainsi, le recyclage des anciennes étiquettes ne devrait pas être un problème.
Ne pas comparer les horodatages
Essayer de savoir lequel des deux horodatages est supérieur à l'autre n'a pas de sens. Exemple:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Naïvement, on pourrait s’attendre à ce que la condition du if ()
soit toujours vraie. Mais ce sera réellement faux si des millis dépassent pendant
delay(3000)
. Considérer t1 et t2 comme des étiquettes recyclables est le moyen le plus simple d'éviter l'erreur: l'étiquette t1 a clairement été affectée à un instant antérieur à t2, mais elle sera réaffectée à un instant ultérieur dans 49,7 jours. Ainsi, t1 se produit à la fois avant et après t2. Cela devrait indiquer clairement que l'expression t2 > t1
n'a aucun sens.
Mais si ce ne sont que des étiquettes, la question évidente est: comment pouvons-nous faire des calculs de temps utiles avec eux? La réponse est: en nous limitant aux deux seuls calculs pertinents pour les horodatages:
later_timestamp - earlier_timestamp
donne une durée, à savoir la durée écoulée entre l’instant précédent et l’instant précédent. Il s'agit de l'opération arithmétique la plus utile impliquant des horodatages.
timestamp ± duration
donne un horodatage qui est quelque temps après (si vous utilisez +) ou avant (si -) l'horodatage initial. Pas aussi utile que ça en a l'air, puisque l'horodatage résultant ne peut être utilisé que dans deux types de calculs ...
Grâce à l'arithmétique modulaire, il est garanti que ces deux solutions fonctionneront parfaitement tout au long du roulement en millis, du moins tant que les retards impliqués sont inférieurs à 49,7 jours.
Comparer les durées c'est bien
Une durée est simplement la quantité de millisecondes écoulée au cours d'un intervalle de temps. Tant que nous n'avons pas besoin de gérer des durées de plus de 49,7 jours, toute opération ayant un sens physique doit également avoir un sens sur le plan informatique. On peut, par exemple, multiplier une durée par une fréquence pour obtenir un nombre de périodes. Ou nous pouvons comparer deux durées pour savoir laquelle est la plus longue. Par exemple, voici deux implémentations alternatives de delay()
. Tout d'abord, le 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;
}
}
Et voici la bonne:
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 plupart des programmeurs en C écrivent les boucles ci-dessus sous forme de test, comme
while (millis() < start + ms) ; // BUGGY version
et
while (millis() - start < ms) ; // CORRECT version
Bien qu’ils se ressemblent de manière trompeuse, la distinction timestamp / duration devrait indiquer clairement lequel est correct et lequel est correct.
Et si j'ai vraiment besoin de comparer les horodatages?
Mieux vaut essayer d'éviter la situation. Si cela est inévitable, il reste encore de l’espoir si on sait que les instants respectifs sont suffisamment proches: moins de 24,85 jours. Oui, notre délai maximum gérable de 49,7 jours vient d'être réduit de moitié.
La solution évidente consiste à convertir notre problème de comparaison d’horodatage en un problème de comparaison de durée. Supposons que nous ayons besoin de savoir si l'instant t1 est avant ou après t2. Nous choisissons un instant de référence dans leur passé commun et comparons les durées de cette référence jusqu'à t1 et t2. L’instant de référence est obtenu en soustrayant une durée suffisamment longue de t1 ou de 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
Ceci peut être simplifié comme:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Il est tentant de simplifier davantage if (t1 - t2 < 0)
. Évidemment, cela ne fonctionne pas car t1 - t2
, calculé comme un nombre non signé, ne peut pas être négatif. Ceci, cependant, bien que non portable, fonctionne:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
Le mot clé signed
ci-dessus est redondant (une simple long
est toujours signée), mais cela permet de clarifier l'intention. La conversion en une longueur signée équivaut à un réglage LONG_ENOUGH_DURATION
égal à 24,85 jours. L'astuce n'est pas portable car, selon la norme C, le résultat est défini par la mise en œuvre . Mais comme le compilateur gcc promet d'agir correctement , il fonctionne de manière fiable sur Arduino. Si nous souhaitons éviter le comportement défini par l'implémentation, la comparaison signée ci-dessus est mathématiquement équivalente à ceci:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
avec le seul problème que la comparaison regarde en arrière. Il est également équivalent, dans la mesure où les longueurs sont en 32 bits, à ce test à un seul bit:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Les trois derniers tests sont en fait compilés par gcc dans exactement le même code machine.
Comment puis-je tester mon croquis contre le roulement de millis
Si vous suivez les préceptes ci-dessus, vous devriez être tout bon. Si vous souhaitez néanmoins tester, ajoutez cette fonction à votre croquis:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
et vous pouvez maintenant voyager dans le temps dans votre programme en appelant
setMillis(destination)
. Si vous voulez que les débordements de millis se répètent encore et encore, comme Phil Connors revivant le jour de la marmotte, vous pouvez insérer ceci à l'intérieur loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
L'horodatage négatif ci-dessus (-3000) est converti implicitement par le compilateur en un signe long non signé correspondant à 3 000 millisecondes avant le basculement (il est converti en 4294964296).
Et si j'ai vraiment besoin de suivre de très longues durées?
Si vous devez activer et désactiver un relais trois mois plus tard, vous devez absolument suivre les débordements de millis. Il y a plusieurs façons de le faire. La solution la plus simple peut être simplement d’étendre millis()
à 64 bits:
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;
}
Il s’agit essentiellement de compter les événements de substitution et d’utiliser ce compte comme les 32 bits les plus significatifs d’un décompte en millisecondes de 64 bits. Pour que ce comptage fonctionne correctement, la fonction doit être appelée au moins une fois tous les 49,7 jours. Toutefois, s'il n'est appelé qu'une fois tous les 49,7 jours, il est possible que le contrôle (new_low32 < low32)
échoue et que le code ne contienne pas le nombre de high32
. Utiliser millis () pour décider quand faire le seul appel à ce code en un seul "wrap" de millis (une fenêtre spécifique de 49,7 jours) peut être très dangereux, en fonction de l'alignement des trames horaires. Pour des raisons de sécurité, si vous utilisez millis () pour déterminer quand faire les seuls appels à millis64 (), vous devez avoir au moins deux appels dans une fenêtre de 49,7 jours.
Gardez toutefois à l'esprit que l'arithmétique 64 bits coûte cher sur l'Arduino. Il peut être intéressant de réduire la résolution temporelle afin de rester à 32 bits.