Ardu? No!Initiation ≫ millis()

millis()

Nous savons maintenant faire un programme qui fait clignoter la LED de la carte avec une période d'une seconde:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // Configuration de la broche de la led
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH); // Allumer la LED
  delay(500);
 
  digitalWrite(LED_BUILTIN, LOW); // Éteindre la LED
  delay(500);
}

Nous savons aussi afficher une suite de nombres aléatoire pleine vitesse (sans temporisation):

void setup() {
 
  Serial.begin(115200); // Pour pouvoir afficher dans la console
}

void loop() {
  Serial.println(random(100)); // Affiche un nombre de 0 à 99
}

Nous allons maintenant fusionner ces deux programmes. Cela nous arrivera de temps en temps. Il n'est pas rare de remettre ensemble deux bouts de codes qui ont permis de tester séparément plusieurs fonctions.

Les initialisations

Pour les initialisations, cela ne doit pas poser de problèmes, il faut faire les deux initialisation dans un ordre ou dans l'autre cela n'a pas d'importance. Par exemple:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // Configuration de la broche de la led
  Serial.begin(115200); // Pour pouvoir afficher dans la console
}

Fusion des loop()

Tout d'abord on ne peut pas mettre deux fois la fonction loop(), pas plus que deux fois setup().

Mettre les codes l'un après l'autre ne fonctionnera pas, car les deux codes qui nous intéressent n'ont pas de fin. Si on mettait les deux codes l'un derrière l'autre, seul le premier serait exécuté. Et le but, c'est bien d'afficher des nombres pendant que la LED clignote. Il faudrait donc exécuter les deux programmes en même temps. Mais cela n'est pas possible car le microcontrôleur exécute les instructions les unes à la suite des autres. Il nous reste donc plus qu'à exécuter un petit bout de code de l'un, et un petit bout de code de l'autre. Cela pourrait faire:

void loop() {
  digitalWrite(LED_BUILTIN, HIGH); // Allumer la LED
  delay(500);

  digitalWrite(LED_BUILTIN, LOW); // Éteindre la LED
  delay(500);

  Serial.println(random(100)); // Affiche un nombre de 0 à 99
}

Si vous faites cela, vous verrez effectivement la LED clignoter (à la bonne vitesse) et des chiffres s'afficher, mais les nombres ne vont s'afficher qu'à la vitesse de 1 nombre par seconde. On est loin de la vitesse que l'on avait avant. Cela est dû au clignotement dont une itération (une boucle) dure une seconde. On peut aller deux fois plus vite en mettant un Serial.print derrière chacun des delay(500).

Dans cet exemple, le problème vient de la fonction delay(500); qui dure longtemps devant les autres instructions.

Code bloquant

On va dire que l'instruction delay(500) est bloquante (on parlera aussi de code bloquant). delay() est bloquante parce que tant qu'elle n'est pas terminée et qu'elle est longue à s'exécuter, on reste bloqué dans cette instruction, on ne peut pas faire autre chose. Pour voir comment s'en sortir, c'est à dire rendre le clignotement non bloquant, on va prendre un parallèle avec la vie courante.

Sur une planète qui tourne vite, on demande à une personne d'allumer un réverbère et de l'éteindre toutes les minutes. C'est l'équivalent de notre clignotement de LED. Pour compter les minutes, on lui demande ce compter lentement jusqu'à 60. Pendant qu'elle compte, elle ne peut pas faire autre chose. C'est le delay(500). En même temps, on voudrait qu'elle mette une croix chaque fois qu'elle voit une nouvelle étoile. Bien entendu, ce sont des consignes simples mais cela ne fonctionne pas vraiment car pendant que notre personne compte, elle ne voit plus les étoiles. Que pouvons nous faire? On ne peut pas lui demander de passer son temps à compter sans arrêt. On va utiliser une montre. La personne va regarder l'heure de temps en temps pour voir si il est temps d'allumer ou d'éteindre le réverbère. Si la personne n'a pas un QI très élevé et pour simplifier les choses, on va alors lui demander de regarder l'heure à chaque fois qu'elle met une croix. C'est ce que nous allons faire pour notre programme.

millis()

Il existe des périphériques que l'on peut acheter en plus, et qui donnent l'heure absolue utilisables avec des Arduino. Mais pour l'instant, nous n'avons pas besoin d'une grande précision. Il existe aussi un bout de code dans la bibliothèque de base qui compte les millisecondes ou les microsecondes, depuis la démarrage de la carte. C'est cette horloge que l'on utilise en général pour faire ce type de tâche.

L'horloge système n'a pas une très bonne précision, mais dans la plupart des cas cela suffit largement. Si elle dérive d'une minute par jour, ce n'est pas important pour un clignotement.

Le nombre de millisecondes écoulées depuis le démarrage du système est donné par la fonction millis() que l'on appelle sans paramètres:

temps = millis();

La valeur retournée est un unsigned long et il y a donc un débordement au bout de 232 millisecondes, soit environ 49 jours (tous les 49 jours environ, ce temps repart à 0).

Pour avoir un temps en microsecondes, il faut utiliser micros() à la place. Cette fonction déborde (revient à 0) toutes les 70 minutes environ. Notez aussi que si delay() attend le nombre de millisecondes que l'on lui demande en paramètre, pour delayMicroseconds(), c'est en microsecondes qu'il faut donner le temps, avec une limite de 16383µs.

millis() est comme un chronomètre que l'on ne peut pas remettre à 0. Si on veut attendre 500ms, il est nécessaire de noter le temps t0 au départ et attendre que millis()-t0 soit plus grand que 500ms. On fait exactement comme avec une montre pour compter 10 minutes. Le test pour savoir si le temps est écoulé est:

millis() - t0 >= duree

Il y a 2 pièges à éviter. D'abord ne jamais faire un test d'égalité car:
- si les tests sont trop espacés on risque de ne jamais tomber sur la bonne valeur (si vous regardez votre montre de temps en temps pour savoir si la cantine est ouverte, il y a peu de chance que vous puissiez observer midi, zéro minutes, zéro secondes)
- millis() s'incrémente de 1 ou de 2, et certaines valeurs de millis() n’existeront jamais.

Le deuxième piège à éviter est de ne pas comparer des intervalles de temps. Si on calcule l'instant t1 du deuxième événement et que l'on fait:

if (millis() >= t1) ...

cela fonctionnera correctement tant qu'il n'y a pas débordement de millis(). Par contre en cas de remise à 0 du compteur de temps interne, il va y avoir un problème: si millis() vaut 49 jours et que l'on veuille attendre 1 jour, l'événement doit être à 50 jours. A cause du débordement, la date calculée sera de 1 jour, et la comparaison sera immédiatement vraie. On peut montrer qu'avec les intervalles, cela reste correct même si le compteur de temps repasse à 0.

millis() - t0 >= duree

Prenons un cas où l'on dépasse le temps, par exemple t0 vaut 45 jours et duree vaut 10 jours. Tant que millis() est inférieur à 49 jours, la condition n'est pas vraie, tout est parfait. Quand millis() vient de déborder et vaut moins de 4 jours, millis() vaut 49 jours de moins que la valeur souhaitée. La quantité millis() - t0 devrait être négative (par exemple 3-45 soit -42), mais nous avons demandé des variables non signées. Il y a débordement de l'opération et le résultat sera donc plus grand de 49 jours (au lieu de -42, on aura +7). On a donc quand même le bon résultat (49 jours en plus à cause de l'opération et 49 jours en mois pour débordement de millis(), bilan nul!). Et ainsi cela fonctionne parfaitement.

Clignotement et millis()

C'est évidemment plus compliqué de faire un code non bloquant. Nous allons regarder si il est temps de faire quelque chose (allumer ou éteindre la LED) et si ce n'est pas le cas, nous continuerons le code sans rien faire.

Il faut donc mémoriser plusieurs choses:
- dernierChangement: temps du dernier changement
- demiPeriode: nos 500ms entre chaque action.

Pour l'état de la LED actuel, nous pouvons le lire directement par digitalRead(LED_BUILTIN). Cela évite de se poser la question de la représentation HIGH et LOW. Pour l'instant ce sont des entiers, mais cela pourait changer.

Voici une ébauche de code pour loop():

void loop() {
  // On s'occupe de la LED
  if (millis() - dernierChangement >= demiPeriode) {
    // Changement de l'état de la LED
    ...
  }
  
  // On s'occupe des nombres affichés
  Serial.println(random(100)); // Affiche un nombre de 0 à 99
}

Analysons ce code. La première partie fait un test et éventuellement change l'état de la LED. C'est donc assez rapide. Le code n'est pas bloqué pendant 500ms. Puis la deuxième partie affiche un nombre. Cela prend un peu plus de temps à cause de l'affichage (il faut envoyer des caractères les uns après les autres en attenant entre deux envois que le premier soit parti). On fait donc sans arrêt des boucles qui affichent un nombre au hasard et quand il le faut, on change en plus l'état de la LED. C'est bien ce que l'on voulait.

Pour le changement d'état de la LED, on peut faire:

if (digitalRead(LED_BUILTIN) == HIGH) // Si la LED est allumée
  digitalWrite(LED_BUILTIN, LOW); // Éteint la led si elle était allumée
else // Si la LED est éteinte
  digitalWrite(LED_BUILTIN, HIGH); // Allume la led si elle était éteinte
dernierChangement += demiPeriode; // Nouveau départ

La dernière ligne permet d'augmenter dernierChangement des 500ms. Certains utilisent plutôt la forme:

dernierChangement = millis();

Ce qui permet aussi un nouveau départ. C'est presque équivalent car ces instructions sont exécutées 500ms après le temps dernierChangement. Il y a une toute petite différence, la première forme est indépendante du temps de traitement de la condition et du changement de l'état de la LED. dernierChangement prendra les valeurs 0, 500, 1000, 1500... Avec la deuxième expression, si la première fois etatDeLaLed valait 0, le test du changement attendrait 500ms. millis() vaudrait alors 500 (ms). Mais le test et le changement peuvent prendre un peu de temps et quand on change la valeur de dernierChangement, millis() peut avoir augmenté. dernierChangement prendrait alors la valeur 501. Le deuxième test se fera 500 ms après le changement de dernierChangement et pas exactement toutes les 500ms. Mais c'est vraiment un détail.

loop() devient alors:

void loop() {
  // On s'occupe de la LED
  if (millis() - dernierChangement >= demiPeriode) {
    // Changement de l'état de la LED
    if (digitalRead(LED_BUILTIN) == HIGH) // Si la LED est allumée
      digitalWrite(LED_BUILTIN, LOW); // Éteint la led si elle était allumée
    else // Si la LED est éteinte
      digitalWrite(LED_BUILTIN, HIGH); // Allume la led si elle était éteinte
    dernierChangement += demiPeriode; // Nouveau départ
  }
  
  // On s'occupe des nombres affichés
  Serial.println(random(100)); // Affiche un nombre de 0 à 99
}

Les déclarations

N'oublions pas les déclarations des variables!

dernierChangement est un temps que l'on va comparer à millis() qui retourne un unsigned long. Il faut donc déclarer:

unsigned long dernierChangement;

• En fait demiPeriode vaut toujours 500. On pourrait donc parfaitement remplacer demiPeriode par 500. C'est plus rapide à écrire, mais cela a deux inconvénients:
- c'est un peu moins compréhensible
- si on voit que la LED clignote trop vite ou trop lentement, il faut changer tous les 500 en une autre valeur. En la définissant:

unsigned long demiPeriode = 500UL;

un changement de valeur ne se fera qu'une seule fois dans la déclaration. Notez que j'ai mis un UL pour insister que la valeur numérique est sur 32 bits. Ici, si on ne le faisait pas (si on mettait juste 500), cela fonctionnerait aussi car le compilateur recevant le nombre 500, le transformerait automatiquement en 32 bits pour pouvoir le mettre dans demiPeriode.

On va quand même dire que c'est dommage d'utiliser une variable pour si peu (cela occupe de la mémoire). En fait pour nous, c'est une constante. Si on la définit comme tel, c'est comme si on mettait le nombre 500 partout. Cela ne prend de la place que dans le code, et pas dans l'espace des variables. Pour dire que c'est une constante, on peut utiliser:

const unsigned long DEMI_PERIODE = 500UL;

Notez que pour les constantes, on utilise normalement des majuscules. Ce n'est pas obligatoire (le compilateur s'en moque), mais c'est une convention qu'il vaut mieux respecter. Profitez en pour changer demiPeriode en DEMI_PERIODE dans loop().

Une autre solution consisterait à dire au préprocesseur (c'est un programme qui fait quelques changement dans le code source juste avant le compilateur) qu'il faudra remplacer tous les "DEMI_PERIODE" en "500UL". Ainsi quand le compilateur passera il verra que des 500UL. Cela se fait en écrivant:

#define DEMI_PERIODE 500UL

Il n'y a pas d'affectation, ni de point-virgule. Mais la forme avec const permet d'insister sur le type de constante qui est utilisée.

Il ne vous reste plus qu'à assembler les différents morceaux pour faire le programme qui fait clignoter la LED pendant qu'il affiche des nombres aléatoires. Aide?const unsigned long DEMI_PERIODE = 500UL; unsigned long dernierChangement; void setup() { pinMode(LED_BUILTIN, OUTPUT); // Configuration de la broche de la led Serial.begin(115200); // Pour pouvoir afficher dans la console } void loop() { // On s'occupe de la LED if (millis() - dernierChangement >= DEMI_PERIODE) { // Changement de l'état de la LED if (digitalRead(LED_BUILTIN) == HIGH) // Si la LED est allumée digitalWrite(LED_BUILTIN, LOW); // Éteint la led si elle était allumée else // Si la LED est éteinte digitalWrite(LED_BUILTIN, HIGH); // Allume la led si elle était éteinte dernierChangement += DEMI_PERIODE; // Nouveau départ } // On s'occupe des nombres affichés Serial.println(random(100)); // Affiche un nombre de 0 à 99 }