Je dois lire un capteur toutes les cinq minutes, mais mon dessin ayant également d'autres tâches à accomplir, je ne peux pas me contenter delay()
des lectures. Il y a le tutoriel Blink sans délai suggérant que je code dans ce sens:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
Le problème, c’est qu’il millis()
va revenir à zéro après environ 49,7 jours. Étant donné que mon dessin doit durer plus longtemps, je dois m'assurer que le basculement ne provoque pas l'échec de mon dessin. Je peux facilement détecter la condition de basculement ( currentMillis < previousMillis
), mais je ne sais pas quoi faire alors.
C’est pourquoi ma question est la suivante: quel serait le moyen le plus simple / approprié de gérer le
millis()
roulement?
programming
time
millis
Edgar Bonet
la source
la source
previousMillis += interval
au lieu depreviousMillis = currentMillis
si je voulais une certaine fréquence de résultats.previousMillis += interval
si vous voulez une fréquence constante et que vous êtes sûr que votre traitement prend moins deinterval
, maispreviousMillis = currentMillis
pour vous garantir un délai minimum deinterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Réponses:
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é parmillis()
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 égalementmicros()
, à l'exception du fait qu'il semicros()
déroule toutes les 71,6 minutes et que lasetMillis()
fonction fournie ci-dessous n'affecte pasmicros()
.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 pensermillis()
à 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:
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 pendantdelay(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'expressiont2 > 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:Et voici la bonne:
La plupart des programmeurs en C écrivent les boucles ci-dessus sous forme de test, comme
et
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:
Ceci peut être simplifié comme:
Il est tentant de simplifier davantage
if (t1 - t2 < 0)
. Évidemment, cela ne fonctionne pas cart1 - t2
, calculé comme un nombre non signé, ne peut pas être négatif. Ceci, cependant, bien que non portable, fonctionne:Le mot clé
signed
ci-dessus est redondant (une simplelong
est toujours signée), mais cela permet de clarifier l'intention. La conversion en une longueur signée équivaut à un réglageLONG_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: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:
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:
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érieurloop()
: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: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 dehigh32
. 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.
la source
TL; DR version courte:
An
unsigned long
vaut 0 à 4 294 967 295 (2 ^ 32 - 1).Disons
previousMillis
donc 4 294 967 290 (5 ms avant le basculement) etcurrentMillis
10 (10 ms après le basculement). Vient ensuitecurrentMillis - previousMillis
16 (pas -4 294 967 280), puisque le résultat sera calculé comme un long non signé (ce qui ne peut pas être négatif, donc il roulera tout seul). Vous pouvez vérifier ceci simplement en:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Donc, le code ci-dessus fonctionnera parfaitement. L'astuce consiste à toujours calculer la différence de temps et non à comparer les deux valeurs de temps.
la source
unsigned
logique, de sorte que cela ne sert à rien ici!millis()
répète deux fois, mais il est très peu probable que cela se produise pour le code en question.previousMillis
a été stocké doit avoir été mesuré auparavantcurrentMillis
, donc sicurrentMillis
est plus petit qu'unpreviousMillis
retournement s'est produit. Il se trouve que, à moins que deux roulements ne surviennent, il n’est même pas nécessaire d’y penser.t2-t1
, et si vous pouvez garantir que le résultatt1
est mesuré avant,t2
il est équivalent à signé(t2-t1)% 4,294,967,295
, d’où le bouclage automatique. Agréable!. Mais que se passe-t-il s'il y a deux roulements, ou siinterval
> 4 294 967 295?Enveloppez le
millis()
dans une classe!Logique:
millis()
directement.Garder une trace des renversements:
millis()
. Cela vous aidera à savoir simillis()
a survolé.Crédits de minuterie .
la source
get_stamp()
51 fois. Comparer les retards au lieu des horodatages sera certainement plus efficace.J'ai adoré cette question et les bonnes réponses qu'elle a générées. Tout d’abord un bref commentaire sur une réponse précédente (je sais, je sais, mais je n’ai pas encore le représentant à commenter. :-).
La réponse d'Edgar Bonet était incroyable. Je code depuis 35 ans et j'ai appris quelque chose de nouveau aujourd'hui. Je vous remercie. Cela dit, je crois que le code pour "Et si je dois vraiment suivre de très longues durées?" sauf si vous appelez millis64 () au moins une fois par période de substitution. Vraiment tatillon et peu susceptible de poser problème dans une implémentation réelle, mais voilà.
Maintenant, si vous voulez vraiment que les horodatages couvrent un intervalle de temps raisonnable (64 bits de millisecondes représentent environ un demi-milliard d'années), il semble simple d'étendre l'implémentation existante de millis () à 64 bits.
Ces modifications apportées à attinycore / câblage.c (je travaille avec ATTiny85) semblent fonctionner (je suppose que le code des autres AVR est très similaire). Voir les lignes avec les commentaires // BFB et la nouvelle fonction millis64 (). Clairement, cela va être à la fois plus volumineux (98 octets de code, 4 octets de données) et plus lent, et comme Edgar l'a souligné, vous pouvez certainement atteindre vos objectifs avec une meilleure compréhension du calcul des entiers non signés, mais c'était un exercice intéressant. .
la source
millis64()
seul travail s’il est appelé plus fréquemment que la période de roulement. J'ai édité ma réponse pour souligner cette limitation. Votre version ne présente pas ce problème, mais elle présente un autre inconvénient: elle utilise l'arithmétique 64 bits dans un contexte d'interruption , ce qui augmente parfois le temps de latence dans la réponse à d'autres interruptions.