Arduino : Comment gérer le temps ?

Niveau : débutant

Jay M

Vous avez compris comment piloter une broche, mais cela ne présente que peu d'intérêt si l'on ne peut pas définir « précisément » quand ou pour combien de temps cette broche doit rester dans un état donné : par exemple allumer une LED pendant deux secondes ou effectuer une action si aucune activité n'a été détectée pendant 30 secondes. La gestion du temps est donc souvent un élément essentiel d'un programme sur microcontrôleur, et bonne nouvelle, Arduino propose des fonctions pour traiter le temps.

Commentez

- Titre : Arduino : Comment gérer le temps ?

- Auteur : Jay M

- Parution : 21 juillet 2021

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2021 Team Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Les Cahiers Pratiques Arduino, une série de tutoriels pour débuter

Sommaire

- I. L’attente active

- II. Mesurer le temps qui passe

- III. Mesure du temps dans un cas particulier

- IV. Gestion asynchrone

- V. Pour aller plus loin

I - L’attente active

La première fonction que l'on utilise s'appelle delay().

Comme son nom l'indique, cette fonction crée un délai dans le code, c'est-à-dire que le microcontrôleur ne va rien faire pendant un certain temps : on arrête le programme. Pour cela on indique pendant combien de temps attendre en millisecondes, en paramètre de la fonction, sous forme d'un nombre de type unsigned long.

Par exemple pour faire clignoter une LED il faut l'allumer, attendre un moment dans cet état, l'éteindre, attendre un moment dans cet état puis recommencer. On pourrait donc coder cela et utiliser la loop() pour boucler.

const byte brocheLed = 3;         // le N° de broche de la LED

void setup() {
pinMode(brocheLed, OUTPUT);     // on met la broche en sortie
}

void loop()
{
digitalWrite(brocheLed, HIGH);  // allume la LED
delay(1000);                    // bloquer pendant 1000 millisecondes (1s)
digitalWrite(brocheLed, LOW);   // éteint la LED
delay(1000);                    // bloquer pendant 1000 millisecondes (1s)
}

La fonction delay() ne vous permet pas d'attendre moins d'une milliseconde. Votre Arduino fonctionne assez vite, avec une fréquence en MHz (16 MHz pour un UNO) et donc 1 milliseconde, c'est long pour le microcontrôleur : le temps d'effectuer 16 000 instructions de base sur un UNO !

Si vous souhaitez avoir un temps de blocage du code inférieur à la milliseconde, il existe la fonction delayMicroseconds(). Cette fonction a un rôle similaire et prend en paramètre le temps de blocage en microsecondes (en millièmes de milliseconde, soit en millionièmes de seconde). Le paramètre est de type unsigned int, mais il ne faut pas dépasser 16 383 µs et il ne faut pas non plus descendre sous 3 µs.

II - Mesurer le temps qui passe

Une autre caractéristique du temps, c'est qu'il s'écoule inexorablement. Votre code pourrait avoir à mesurer ce temps qui passe. Par exemple, vous avez une voiture qui fait des tours de circuit et vous voulez mesurer le temps au tour.

Votre Arduino est configuré pour capturer ce temps qui passe depuis qu'il a été démarré (à froid – première alimentation ou à chaud après un reset).

Cette fonction qui vous dit depuis combien de temps votre programme fonctionne s'appelle millis(). La valeur retournée est de type unsigned long, ça veut dire que le compteur peut aller jusqu'à 4 294 967 295, ce qui représente 49 jours 17 heures 2 minutes 47 secondes et 295 millisecondes. Si votre Arduino est donc allumé depuis plus de 50 jours environ, le compteur recommencera à 0.

Pour mesurer combien de temps s'est écoulé entre deux moments, vous pourriez mémoriser dans une variable la valeur de millis() au début puis la valeur de millis() à la fin, et la différence c'est la durée en millisecondes.

unsigned long debut;
unsigned long fin;
unsigned long duree;

void setup() {
Serial.begin(115200);                       // configuration de la voie série à 115200 bauds
}

void loop() {
debut = millis();
...                                         // ici vous effectuez un traitement assez long
fin = millis();
duree = fin - debut;                        // calcul de la durée
Serial.print(F("Durée de traitement : "));
Serial.print(duree);
}

La granularité de la mesure sera de l'ordre de la milliseconde. Si vous souhaitez obtenir une mesure plus précise, il existe la fonction micros() qui vous retourne le nombre de microsecondes écoulées depuis le lancement du programme. C'est aussi un unsigned long, mais comme on compte en microsecondes, la valeur revient à 0 mille fois plus vite, au bout de 1 heure, 11 minutes, 34 secondes et 967 millisecondes et 295 microsecondes. Sur les Arduino à 16 Mhz la résolution de la valeur retournée par micros() est de 4 microsecondes. C'est la durée minimale que vous serez capable de mesurer en utilisant cette approche.

III - Mesure du temps dans un cas particulier

Si le temps que vous souhaitez mesurer est le temps pendant lequel une broche prend une valeur donnée (HIGH ou LOW), il existe la fonction pulseIn() qui va faire cela pour vous. Elle fonctionne bien pour une durée dans un état donné entre 10 microsecondes et 3 minutes. La fonction dispose d'un paramètre optionnel qui indique au bout de combien de temps abandonner (timeout) si les fronts attendus ne se sont pas produits.

Par exemple, imaginez que vous souhaitiez mesurer le temps pendant lequel un bouton est appuyé, vous pourriez avoir le code suivant :

const byte brocheBouton = 2;
unsigned long duree;

void setup() {
Serial.begin(115200);                 // configuration de la voie série à 115200 bauds
pinMode(brocheBouton, INPUT_PULLUP);  // broche en entrée, activation du pullup (pin HIGH par défaut)
}

void loop() {
duree = pulseIn(brocheBouton, LOW);   // mesure de la durée LOW, après une transition HIGH -> LOW
Serial.print(duree);                  // affichage du résultat
Serial.println(F(" µs"));
}

IV - Gestion asynchrone

Comme nous l'avons dit précédemment, lorsque vous utilisez la fonction delay() ou delayMicroseconds() tout le programme s’arrête. Ce n'est pas toujours un souci, si le programme n'a rien d'autre à effectuer, mais dans certains cas c'est gênant par exemple si vous souhaitez continuer à surveiller une entrée et répondre rapidement.

Par exemple, nous voulons faire clignoter une LED à une certaine fréquence, sauf si un bouton est appuyé. On pourrait modifier le programme précédent de clignotement ainsi :

const byte brocheLed = 3;         // le N° de broche de la LED
const byte brocheBouton = 2;      // le N° de broche du bouton

void setup() {
pinMode(brocheLed, OUTPUT);     // on met la broche en sortie
pinMode(brocheBouton, INPUT_PULLUP);  // broche en entrée, activation du pullup (pin HIGH par défaut)
}

void loop()
{
if (digitalRead(brocheBouton) == HIGH) {  // si le bouton n'est pas appuyé
digitalWrite(brocheLed, HIGH);          // allume la LED
delay(1000);                            // bloquer pendant 1000 millisecondes (1s)
digitalWrite(brocheLed, LOW);           // éteint la LED
delay(1000);                            // bloquer pendant 1000 millisecondes (1s)
}
}

Le souci cependant c'est que si on appuie sur le bouton juste après avoir allumé la LED, rien ne va se passer et si on ne le tient pas appuyé plus de 2 secondes, le code ne verra pas la broche à LOW lors du prochain tour de boucle : notre programme n'est pas réactif, l'expérience utilisateur est mauvaise.

Pour corriger cela, il faut écrire un programme qui ne va pas bloquer le microcontrôleur.

On va donc commencer par écrire un clignotement qui ne bloque pas, sans delay(). Il suffit de mémoriser le moment de dernier changement d'état de la LED (debut) et laisser la LED dans cet état jusqu'à ce qu'une certaine durée se soit écoulée. À ce moment, on inverse à nouveau l'état de la LED, on note ce nouveau moment et on recommence.

const byte brocheLed = 3;         // le N° de broche de la LED

unsigned long debut;

void inverserLed() {
// on inverse l'état de la broche
if (digitalRead(brocheLed) == HIGH) {
digitalWrite(brocheLed, LOW);           // éteint la LED
} else {
digitalWrite(brocheLed, HIGH);          // allume la LED
}

// on mémorise le moment de changement d'état
debut = millis();
}

void setup() {
pinMode(brocheLed, OUTPUT);           // on met la broche en sortie
inverserLed();                        // on active la LED
}

void loop()
{
unsigned long maintenant = millis();
if (maintenant - debut >= 1000) {     // si plus d'une seconde s'est écoulée depuis le début du changement d'état
inverserLed();                      // alors on inverse l'état
}
}

Tout le secret est donc dans le test :

if (maintenant - debut >= 1000)

que l'on répète dans la boucle. Le calcul dit : « si la durée écoulée entre le début et maintenant dépasse 1000 millisecondes alors… ».

Vous remarquez qu'il n'y a pas de else. Donc si le temps n'est pas écoulé, on ne fait rien et la boucle va juste reboucler et on va continuer à tester si c'est le bon moment. Ça ne change donc pas trop du code précédent, on clignote, mais la grande différence, c'est que l'on peut donc effectuer d'autres traitements si le temps n'est pas écoulé.

Par exemple, on pourrait dire « si le bouton est appuyé, éteindre la LED et ne plus clignoter ».

const byte brocheLed = 13;         // le N° de broche de la LED
const byte brocheBouton = 2;      // le N° de broche du bouton

unsigned long debut;

void inverserLed() {
// on inverse l'état de la broche
if (digitalRead(brocheLed) == HIGH) {
digitalWrite(brocheLed, LOW);           // éteint la LED
} else {
digitalWrite(brocheLed, HIGH);          // allume la LED
}

// on mémorise le moment de changement d'état
debut = millis();
}

void setup() {
pinMode(brocheLed, OUTPUT);           // on met la broche en sortie
pinMode(brocheBouton, INPUT_PULLUP);  // broche en entrée, activation du pullup (pin HIGH par défaut)
inverserLed();                        // on active la LED
}

void loop()
{
if (digitalRead(brocheBouton) == HIGH) {  // si le bouton n'est pas appuyé on regarde s'il faut clignoter
unsigned long maintenant = millis();
if (maintenant - debut >= 1000) {     // si plus d'une seconde s'est écoulée depuis le début du changement d'état
inverserLed();                      // alors on inverse l'état
}
} else {                                // le bouton est appuyé
digitalWrite(brocheLed, LOW);         // on s'assure que la LED est éteinte
}
}

Dans ce code, on va regarder si c'est le moment de clignoter seulement si le bouton n'est pas appuyé, sinon on éteint la LED. Comme nous n'avons plus de delay(), le code ne bloque jamais et donc notre programme sera maintenant très réactif : dès que vous pressez le bouton, la LED s'éteint et dès que vous le relâchez, elle se remet à clignoter.

Une erreur fréquente est de prendre un int ou unsigned int comme type de variable pour représenter le temps (debut, maintenant). Sur un UNO ou MEGA un int ne peut pas aller au-delà de 32 767 et un unsigned int au-delà de 65 535 – soit environ 33 ou 66 secondes. Donc si vous testez votre code pendant quelques secondes, vous n'allez rien déceler, mais en production, très vite votre code aura un comportement anormal. Il est donc important de toujours prendre un unsigned long (que l'on note aussi uint32_t pour unsigned int sur 32 bits) pour représenter le temps.

Vous remarquerez que le test sur la durée est écrit sous la forme maintenant - debut >= 1000. On voit souvent des codes avec maintenant >= debut + 1000 et c'est mathématiquement similaire dans le cas général. Cependant, il faut retenir la première formulation et bannir la seconde si votre code doit tourner longtemps. En effet, quand vous approcherez des ~50 jours fatidiques, le unsigned long qui permet de stocker debut va s'approcher de sa valeur maximale. Rajouter 1000 à cette valeur va donc faire déborder ce qui est représentable et on se retrouvera avec une valeur proche de zéro.

La valeur de millis() (maintenant) sera elle très grande (proche des 50 jours) et donc le test maintenant >= debut + 1000 sera tout de suite vrai, sans avoir attendu les 1000 ms. En prenant l'approche par soustraction, même lorsque maintenant a débordé et retourné à 0 et que debut est proche de 50 jours, maintenant - debut représentera bien la bonne durée, et le test sera valable.

V - Pour aller plus loin

Nous avons décrit la gestion du temps au niveau du microcontrôleur, mais pas forcément ce que vous dit votre montre : « Samedi 13 mars 2021 à 20h30 ».

Votre Arduino n'a pas de notion du jour et de l'heure, il sait juste depuis quand il a été allumé.

Si vous avez besoin de gérer cela, il vous faudra un composant externe, une horloge, appelée RTC (Real Time Clock). Celles que l'on trouve le plus fréquemment sont les DS1307 et DS3231/DS3232. Si vous avez le choix, laissez de côté la DS1307 (elle dérive trop vite et ne sera pas précise sur plusieurs semaines/mois) et préférez une DS3231 (ou DS3232).

Il existe des alternatives : si votre Arduino dispose d'une connexion à Internet, vous pouvez interroger un serveur de temps (serveur NTP) ou si un GPS est connecté sur votre montage, il est généralement possible d'obtenir l'heure grâce aux satellites GPS.

Et si vous avez encore des questions, n’hésitez pas à ouvrir une discussion dans le forum Arduino.