IUT GEII Toulon
Philippe Arlotto

Accueil > Arduino > Utilisez correctement la fonction millis()

Utilisez correctement la fonction millis()

vendredi 15 mai 2015, par philippe Arlotto

Le compteur de millisecondes disponible avec la fonction millis() est très pratique pour réaliser des temporisations. Cependant il faut l’utiliser correctement car sinon vos programmes risquent de ne plus fonctionner correctement au bout d’une cinquantaine de jours. C’est un bug vicieux car comme il n’apparaît qu’après une longue durée de fonctionnement, il risque fort de passer inaperçu en phase de test.

Le compteur de millisecondes est au format unsigned long int donc sur 32bits pour le compilateur avr-gcc. Il est initialisé au reset et revient donc à zéro au bout de 2³² ms soit 4294967296 ms et donc 4294967296/(1000*3600*24) = 49,7 jours.
Si on ne prend pas de précaution les temporisations utilisant ce compteur ne seront pas valables après environ 50 jours. Si on s’y prend correctement le retour à zéro du compteur ne pose aucun problème et les temporisations pourront fonctionner indéfiniment.

L’exemple de base de l’IDE arduino blinkWithoutDelay indique la bonne façon de faire et il vaut mieux ne pas faire différemment.
Voyons pourquoi.

Imaginons qu’on souhaite déclencher une action périodiquement toutes les 30 secondes.

On déclare une variable pour stocker la valeur du compteur au moment au fait l’action.
On déclare une variable contenant l’intervalle convertie en millisecondes [1] .
Ces deux variables sont de type unsigned long pour pouvoir contenir des valeurs sur 32bits.

  1. unsigned long previousMillis=0 ;
  2. unsigned long interval = 30000L;

Télécharger

On peut initialiser previousMillis à zéro ou bien avec millis().
Dans la boucle infinie (fonction loop), on compare la valeur courante du compteur (millis()) à sa valeur lors de la dernière action ( previousMillis ). Lorsque l’écart dépasse l’intervalle [2], on déclenche l’action et on sauve la valeur du compteur dans previousMillis [3].

  1. void loop() {
  2. .....
  3. if( millis() - previousMillis >= interval) {
  4. previousMillis = millis();
  5. DoAction() ;
  6. }
  7. ....
  8. }

Télécharger

Tant que previousMillis est inférieur à millis() on comprend aisément le fonctionnement. On va voir qu’en procédant ainsi, avec une soustraction , le retour à zéro ne pose aucun problème .
Imaginons que la dernière action est été faite 25s avant le retour à zéro. Il faut donc que la prochaine action soit déclenchée 5s après le passage à zéro pour respecter l’intervalle de 30s. Donc lorsque le compteur de millisecondes vaudra 5000.
Notre programme fonctionne donc (sans reset) depuis 2³² - 25000 millisecondes. milis() retourne donc la valeur 4294967296 - 25000 = 4294942296.
Donc PreviousMillis =  4294942296 = 0xFFFF9E58
Quand le compteur passe à zéro, la soustraction millis() - previousMillis donne :

0 - 0xFFFF9E58 = -0xFFFF9E58
or -0xFFFF9E58 = inverse logique de 0xFFFF9E58 +1 = 0x000061A7+1 =0x000061A8 = 25000

Donc millis() - previousMillis reste bien inférieure à 30000. Et ce jusqu’à ce que le compteur vaille 5000. On aura alors :
5000 - 4294942296 = 0x00001388 - 0xFFFF9E58 = 0x00013880 + x000061A8 = 0x00007530 = 30000
Donc millis() - previousMillis >= interval sera vraie. L’action sera bien déclenchée au bon moment et previousMillis vaudra alors 5000. On repart pour 50 jours sans problème.

Ce qu’il ne faut pas faire :
On pourrait avoir l’idée de calculer la valeur que doit avoir le compteur lorsque l’action doit être déclenchée et de déclencher l’action lorsque le compteur atteint cette valeur.

  1. unsigned int actionTime = 30000L ;
  2. unsigned long interval = 30000L;

Télécharger

Dans la boucle infinie, on "attend" que le compteur ait la bonne valeur puis on calcule la date de la prochaine action en ajoutant l’intervalle.

  1. void loop() {
  2. .....
  3. if( millis() >= actionTime ) {
  4. actionTime = millis() + interval ;
  5. DoAction() ;
  6. }
  7. ....
  8. }

Télécharger

Cet algorithme a l’air correct et le drame c’est qu’il fonctionne correctement pendant presque 50 jours.
Imaginons comme précédemment que l’action est été faite 25s avant le retour à zéro. La prochaine action doit être faite lorsque le compteur atteindra 5000.
La valeur de actionTime est alors :

actionTime =  (2³² - 25000) + 30000
actionTime = 0xFFFF9E58 + 0x00007530 = 0x100001388

mais comme il s’agit de valeur 32 bits, le poids fort n’est pas conservé et
actionTime = 0x00001388 = 5000
Mais au moment où actionTime prend cette valeur le compteur possède une valeur supérieure (0xFFFF9E58). Donc une nouvelle action est déclenchée juste après.
Puis actionTime vaudra 5001 et une autre action sera déclenchée... et il en sera ainsi jusqu’à ce que le compteur repasse à zéro et on reprendra un fonctionnement à peu près normal. Dans l’intervalle (ici pendant 5 seconde) l’action aura été déclenchée de nombreuses fois. On n’est plus vraiment dans le cas d’une action toute les 30 secondes !!!

Il n’est pas nécessaire d’attendre 50 jours pour tester votre programme. Il suffit d’écrire une fonction qui retourne la valeur de millis() augmentée d’une valeur proche de la valeur maximum et de l’appeler à la place de millis(). Dans l’exemple on part de 0xFFFEB3F8 qui est 85 secondes avant le retour à 0.

  1. /*
  2.  * Exemple showing millis() rollover
  3.  
  4. 15 May 2015
  5.   by Philippe Arlotto
  6.  */
  7. #include <Arduino.h>
  8. extern HardwareSerial Serial;
  9. unsigned long previousMillis=0xFFFEB3F8 ; // 85s before rollover
  10. unsigned long interval = 30000L;
  11. bool x = 0 ;
  12. int n=0;
  13. unsigned long testMillis(); // function to test millis() rollover
  14.  
  15.  
  16. void setup() {
  17. // initialize digital pin 13 as an output.
  18. pinMode(13, OUTPUT);
  19. digitalWrite(13,0);
  20. // initialize serial port
  21. Serial.begin(9600);
  22. Serial.print("previousMillis at start=");
  23. Serial.print(testMillis(),DEC);
  24. Serial.print(" 0x"); Serial.println(testMillis(),HEX);
  25.  
  26. }
  27.  
  28. // the loop function runs over and over again forever
  29. void loop() {
  30. if( testMillis() - previousMillis >= interval) {
  31. previousMillis = testMillis();
  32. //Action
  33. x=1-x ;
  34. digitalWrite(13,x);
  35. n++;
  36. Serial.print("action "); Serial.print(n); Serial.print(" previousMillis=");
  37. Serial.print(testMillis(),DEC);
  38. Serial.print(" 0x"); Serial.println(testMillis(),HEX);
  39. // End of Action
  40. }
  41.  
  42. }
  43.  
  44. unsigned long testMillis(){
  45.  
  46. return 0xFFFEB3F8 + millis() ;
  47. }

Télécharger

Et le résultat qui prouve que ça fonctionne correctement :

previousMillis at start=4294882296 0xFFFEB3F8
action 1 previousMillis=4294912296 0xFFFF2928
action 2 previousMillis=4294942296 0xFFFF9E58
action 3 previousMillis=5000 0x1388
action 4 previousMillis=35000 0x88B8
action 5 previousMillis=65000 0xFDE8
action 6 previousMillis=95000 0x17318
action 7 previousMillis=125000 0x1E848
action 8 previousMillis=155000 0x25D78
action 9 previousMillis=185000 0x2D2A8

Maintenant la mauvaise solution :

  1. unsigned long actionTime = testMillis() ;
  2.  
  3. void badLoop() {
  4. if( testMillis() >= actionTime ) {
  5. actionTime = testMillis() + interval ;
  6. //Action
  7. x=1-x ;
  8. digitalWrite(13,x);
  9. n++;
  10. Serial.print("action "); Serial.print(n); Serial.print(" done at ");
  11. Serial.print(testMillis(),DEC);
  12. Serial.print(" 0x"); Serial.print(testMillis(),HEX);
  13. Serial.print(" next due at "); Serial.print(actionTime,DEC);
  14. Serial.print(" 0x"); Serial.println(actionTime,HEX);
  15.  
  16. // End of Action
  17. }
  18. }
  19.  
  20. }

Télécharger

ça marche au début quand la valeur du compteur est assez faible :

actionTime at start=0 0x0
action 1 done at 0 0x0 next due at 30000 0x7530
action 2 done at 30000 0x7530 next due at 60000 0xEA60
action 3 done at 60000 0xEA60 next due at 90000 0x15F90
action 4 done at 90000 0x15F90 next due at 120000 0x1D4C0

Mais après un certain temps :

actionTime at start=4294882296 0xFFFEB3F8
action 1 done at 4294882297 0xFFFEB400 next due at 4294912297 0xFFFF2929
action 2 done at 4294912297 0xFFFF2929 next due at 4294942297 0xFFFF9E59
action 3 done at 4294942297 0xFFFF9E59 next due at 5001 0x1389
action 4 done at 4294942313 0xFFFF9E76 next due at 5002 0x138A
action 5 done at 4294942380 0xFFFF9EB9 next due at 5066 0x13CA
action 6 done at 4294942446 0xFFFF9EFC next due at 5133 0x140D
action 7 done at 4294942513 0xFFFF9F3E next due at 5199 0x144F
action 8 done at 4294942579 0xFFFF9F81 next due at 5266 0x1492
action 9 done at 4294942646 0xFFFF9FC3 next due at 5333 0x14D5

Dès que le compteur est revenu à zéro les actions sont quasiment faites en permanence (5001,5002,5066,5133,...). Mais le pire c’est qu’ensuite ça reprend le fonctionnement normal :

action 360 done at 4294966937 0xFFFFFEA7 next due at 29621 0x73B5
action 361 done at 4294967007 0xFFFFFEEC next due at 29691 0x73FB
action 362 done at 4294967077 0xFFFFFF32 next due at 29761 0x7441
action 363 done at 4294967146 0xFFFFFF78 next due at 29831 0x7487
action 364 done at 4294967216 0xFFFFFFBD next due at 29900 0x74CC
action 365 done at 4294967285 0x3 next due at 29970 0x7512
action 366 done at 29970 0x7512 next due at 59970 0xEA42
action 367 done at 59970 0xEA42 next due at 89970 0x15F72

Entre-temps il y a juste eu 362 actions en trop !!! [4]

En résumé il faut donc :

  • utiliser des variables de type unsigned long.
  • travailler par soustraction entre la valeur du compteur et la valeur qu’il avait lors de la dernière action.
  • Tester votre programme en ajoutant un valeur proche du débordement.

Notes

[1ou une constante si on ne souhaite pas modifier l’intervalle

[2L’écart ne pourra pas dépasser de plus de 1ms si votre boucle dure moins de 1ms, si elle dure plus, le retard maximum pourra être égal à la durée de la boucle

[3Il est évident que la durée de l’action doit être très inférieure à l’intervalle

[4la situation est pire quand l’intervalle est plus important : Si vous voulez nourrir vos poissons toutes les 24 heures, vous allez ouvrir la réserve de nourriture dans l’aquarium pendant 24 heures. Quand le fonctionnement normal reprendra la réserve sera vide...

SPIP | Se connecter | Plan du site | Suivre la vie du site RSS 2.0
Habillage visuel © Andreas Viklund sous Licence free for any purpose