La gestion du temps avec Arduino

J'ai pas le temps, mon esprit.

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Tutoriels Arduino | Mots clefs : Arduino Genuino Temps

Cet article n'a pas été mis à jour depuis un certain temps, son contenu n'est peut être plus d'actualité.


Dans ce tutoriel, nous allons voir ensemble comment gérer le temps avec une carte Arduino. Nous verrons comment faire une temporisation d'une durée définie, ainsi que les méthodes utilisables pour obtenir le temps depuis le démarrage d'un programme Arduino. En bonus, nous verrons comment gérer le débordement (aka "rollover") de l'horloge interne des cartes Arduino.

Sommaire

Bonjour à toutes et à tous !

Dans mon précédent article, nous avons vu ensemble comment faire clignoter une LED (Diode electro Luminescente). Dans ce précédent article, nous avons utilisé une fonction nommée delay() pour faire une temporisation de une seconde. Il est donc temps de vous en dire plus sur cette fonction et sur ses copines ;)

Nota Bene

Les durées avant "débordement" annoncées dans les derniers chapitres de cet article sont valables uniquement pour les cartes Arduino "classiques" (UNO, Mega2560, Leonardo) et les cartes Arduino compatibles ayant une fréquence processeur de 16MHz.

Définition : fonction bloquante et fonction non bloquante

Avant de commencer, nous devons voir ensemble deux notions fondamentales en informatique : les fonctions bloquantes et les fonctions non bloquantes.

Une fonction "bloquante" est une fonction qui bloque l'exécution du programme durant son déroulement. Tant que la fonction n'est pas terminée, le reste du programme est en attente.

Une fonction "non bloquante" est une fonction qui ne bloque pas l'exécution du programme durant son déroulement. Ce type de fonction est très utile pour faire du temps réel.

Houston, c'est pas clair votre truc !

D'un point de vue théorique, en informatique, toutes les fonctions sont bloquantes. Une fonction prend zéro ou plus paramètres, fait un traitement et retourne ou non une valeur de retour.

Ce qui pemet de différentier une fonction dite "bloquante" d'une fonction dite "non bloquante", c'est la façon de faire le traitement de la fonction.

Illustration fonctions bloquantes / non bloquantes

Fonction bloquante VS fonction non bloquante

Si une fonction attend quelque chose avant de faire son traitement pour une quelconque raison, on dit que cette fonction est bloquante.

Si au contraire, la fonction vérifie qu'une information ou une condition est bonne avant de faire son traitement, puis fais le traitement sans attendre quoi que se soit, on dit qu'il s'agit d'une fonction non bloquante.

Pour illustrer cette différence, prenons un exemple. Vous avez un programme avec une fonction qui permet à l'utilisateur de faire un choix avec une série de boutons et un bouton "valider".

La version bloquante de la fonction consisterait à attendre que le bouton "valider" soit appuyé avant de lire les autres boutons et de retourner le choix de l'utilisateur.

La version non bloquante de la fonction consisterait à vérifier que le bouton "valider" est appuyé, et dans ce cas (et uniquement dans ce cas), de lire les autres boutons et de retourner le choix de l'utilisateur. Dans le cas où le bouton ne serait pas appuyé, la fonction retournerait une valeur spéciale pour prévenir le code parent qu'il va falloir rappeler la fonction plus tard.

Dans ce tutoriel, nous allons étudier quatre fonctions différentes, deux bloquantes et deux non bloquantes. Gardez bien en tête les deux définitions ci-dessus, elles sont essentielles pour bien comprendre les cas d'usages des fonctions que nous allons étudier.

Faire une temporisation bloquante

Dans mon précédent article, j'ai utilisé la fonction delay() pour faire une temporisation de une seconde afin de faire clignoter une LED. L'idée était simple : allumer la LED, attendre une seconde, éteindre la LED, attendre une seconde, etc.

En programmation Arduino, il existe deux moyens de faire une temporisation en fonction de la durée désirée de celle-ci.

Faire une temporisation bloquante de N millisecondes

Pour faire une temporisation entre 1 milliseconde et 4 294 967 296 millisecondes (~50 jours), on utilise la fonction delay().

La fonction delay() permet de mettre en pause le programme pendant un certain nombre de millisecondes. C'est une fonction bloquante.

1
delay(millisecondes);

La fonction delay() accepte un unique paramètre obligatoire qui correspond à la durée en millisecondes de la temporisation. Cette fonction accepte uniquement des nombres entiers.

La fonction delay() a eu précision de une milliseconde, pas plus. Cela signifie que pour faire des temporisations de N millisecondes + M microsecondes, il faudra faire usage de la seconde fonction présentée ci-dessous en duo avec delay().

Tout est dans le timing

La fonction delay() est relativement précise, mais celle-ci n'est pas parfaite. De plus, avant et après l'appel à delay() se trouve forcément du code qui mettra un certain temps à s'exécuter.

La temporisation réalisée par delay() peut avoir quelques microsecondes d'erreur, ce qui est négligeable dans la plupart des cas. Le code avant ou après l'appel à delay() peut par contre lui induire une erreur de temporisation pouvant poser problème.

Si vous enchainez une série de digitalWrite() avant et après un delay(), gardé en tête que vos temporisations ne seront pas parfaites et qu'il faudra parfois faire des ajustements.

Ceci étant dit, il est rare qu'un programme demande des timings à quelques microsecondes près. Et si tel est le cas, il existe de bien meilleures façons de gérer ces contraintes de timing qu'avec un simple delay().

N.B. Pendant la durée de la temporisation, le programme est mis en pause, mais certaines fonctionnalités matérielles continuent de fonctionner en arriére plan, comme les horloges, les ports de communication, etc. Cela signifie que pendant une temporisation, il est possible de recevoir des données d'un port série par exemple, mais il faudra attendre la fin de la temporisation pour que le programme puisse traiter ces données.

Faire une temporisation bloquante de N microsecondes

Pour faire une temporisation entre 1 microseconde et 16 383 microsecondes (~16 millisecondes), on utilise la fonction delayMicroseconds().

La fonction delayMicroseconds() permet de mettre en pause le programme pendant un certain nombre de microsecondes. Comme delay(), c'est là aussi une fonction bloquante.

1
delayMicroseconds(microsecondes);

La fonction delayMicroseconds() accepte un unique paramètre obligatoire qui correspond à la durée en microsecondes de la temporisation. Cette fonction accepte uniquement des nombres entiers.

La fonction delayMicroseconds() a une granularité de 4 microsecondes. Cela signifie qu'une temporisation de N microsecondes peut avoir plus ou moins 3 microsecondes d'erreurs.

La fonction delayMicroseconds() est utile pour réaliser des temporisations très précises, inférieures à une quinzaine de millisecondes et supérieures à une dizaine de microsecondes.

Tout est dans le timing, le retour

Avec delay(), les erreurs induites par le code avant et après la temporisation sont souvent négligeables. Avec delayMicroseconds(), ce n'est plus du tout négligeable.

Imaginez que vous voulez faire clignoter une LED à 1 kilohertz (soit 1 000 hertz) précisément. Cela correspond à une attente de 500 microsecondes entre chaque changement d'état, pour un total de 1 milliseconde par cycle allumé / éteint.

Il serait tentant de faire ceci :

1
2
3
4
5
6
void loop() {
  digitalWrite(13, HIGH);
  delayMicroseconds(500);
  digitalWrite(13, LOW);
  delayMicroseconds(500);
}

Capture oscilloscope signal ~1KHz

Signal ~1KHz généré par la carte Arduino

En théorie, cela serait parfait, mais digitalWrite() et loop() demandent un certain temps pour s'exécuter et en pratique, le signal ne fait pas du tout 1KHz. Dans cet exemple, il faudrait plutôt des delayMicroseconds(492) pour obtenir un signal à 1KHz exactement.

Si vous utilisez delayMicroseconds(), gardez bien en tête que des ajustements seront nécessaires pour obtenir un timing parfait. Pour faire ces ajustements, un oscilloscope vous sera d'une grande aide.

L'extrait de code ci-dessus est disponible en téléchargement sur cette page (le lien de téléchargement en .zip contient le projet Arduino prêt à l'emploi).

N.B. La fonction delayMicroseconds() est rarement utilisée, exceptée pour des programmes nécessitant des timings rigoureux. Ces programmes sont en général conçus par des développeurs expérimentés. Par conséquent, amis débutants, nous vous prenez pas trop la tête avec delayMicroseconds() ;)

Obtenir le temps depuis le démarrage du programme

La fonction delay() permet de faire une temporisation bloquante de N millisecondes et la fonction delayMicroseconds() permet de faire une temporisation bloquante de N microsecondes.

Maintenant, comment faire une temporisation non bloquante ? Ou plus simplement, comment obtenir un temps relatif au lancement du programme ?

Le framework Arduino fournit deux fonctions pour obtenir le temps depuis le démarrage du programme : millis() et micros().

Obtenir le temps en millisecondes depuis le démarrage du programme

La fonction millis() permet d'obtenir le nombre de millisecondes depuis le démarrage du programme.

1
unsigned long millis();

La fonction millis() n'accepte aucun paramètre et retourne un entier sur 32 bits (unsigned long) contenant le nombre de millisecondes depuis le démarrage du programme.

Attention à la milliseconde qui fait déborder le vase

La fonction millis() retourne un nombre entier de taille fixe (32 bits), cela signifie qu'après un certain temps la valeur retournée pas millis() va déborder et revenir à zéro.

Le débordement de millis() se produit environ 50 jours après le démarrage du programme. Pour être précis, le débordement se produit après 4 294 967 296 millisecondes, soit 49 jours, 17 heures, 2 minutes, 47 secondes et 296 millisecondes.

Si votre code est susceptible de rester en activité plus de 49 jours et que vous utilisez millis(), je vous conseille vivement de lire le chapitre bonus ;)

PS Le débordement d'une valeur s'appelle un "rollover" en informatique. Si jamais vous avez besoin de faire des recherches sur internet, vous connaissez maintenant le terme technique ;)

Obtenir le temps en microsecondes depuis le démarrage du programme

À l'instar de delay() et delayMicrosecondes(), le framework Arduino fournit une variante de millis() retournant le temps en microsecondes. Cette fonction se nomme sobrement micros().

La fonction micros() permet d'obtenir le nombre de microsecondes depuis le démarrage du programme.

1
unsigned long micros();

La fonction micros() n'accepte aucun paramètre et retourne un entier sur 32 bits (unsigned long) contenant le nombre de microsecondes depuis le démarrage du programme.

N.B. La fonction micros() a une granularité de 4 microsecondes. Cela signifie que micros() retournera une mesure de temps identique dans un intervalle inférieur à 4 microsecondes.

Attention à la microseconde qui fait déborder le vase, le retour

Comme millis(), la fonction micros() est sujette au débordement. Cela signifie qu'après un certain temps la valeur retournée pas micros() va déborder et revenir à zéro.

De la même façon qu'un verre va se remplir beaucoup plus vite avec une lance d'incendie qu'avec un compte goutte, la fonction micros() déborde beaucoup plus rapidement que la fonction millis().

La fonction micros() déborde après seulement 71 minutes, 34 secondes, 967 millisecondes et 296 microsecondes (on aime être précis sur Carnet du Maker ;) ).

Si votre code est susceptible de rester en activité plus de 71 minutes et que vous utilisez micros(), je vous conseille vivement de lire le chapitre bonus ;)

N.B. Je n'ai jamais vu le moindre code utiliser micros() pour autre chose que du débug. Donc comme pour delayMicroseconds(), amis débutants, pas besoin de vous prendre la tête avec cette fonction ;)

Bonus : gérer le débordement de millis() et micros()

Dans ce chapitre bonus, nous allons voir comment gérer le débordement (retour à zéro) des fonctions millis() et micros().

Pour ce faire, nous allons créer deux fonctions nommées superMillis() et superMicros() qui auront la particularité de gérer en interne les débordements sans que le code appelant ait à lever le petit doigt.

Sans plus attendre, voici le code des deux fonctions :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/** 
 * Retourne le nombre de millisecondes depuis le démarrage du programme.
 *
 * @return Le nombre de millisecondes depuis le démarrage du programme sous la forme d'un
 * nombre entier sur 64 bits (unsigned long long).
 */
unsigned long long superMillis() {
  static unsigned long nbRollover = 0;
  static unsigned long previousMillis = 0;
  unsigned long currentMillis = millis();
  
  if (currentMillis < previousMillis) {
     nbRollover++;
  }
  previousMillis = currentMillis;

  unsigned long long finalMillis = nbRollover;
  finalMillis <<= 32;
  finalMillis +=  currentMillis;
  return finalMillis;
}

/** 
 * Retourne le nombre de microsecondes depuis le démarrage du programme.
 *
 * @return Le nombre de microsecondes depuis le démarrage du programme sous la forme d'un
 * nombre entier sur 64 bits (unsigned long long).
 */
unsigned long long superMicros() {
  static unsigned long nbRollover = 0;
  static unsigned long previousMicros = 0;
  unsigned long currentMicros = micros();
  
  if (currentMicros < previousMicros) {
     nbRollover++;
  }
  previousMicros = currentMicros;

  unsigned long long finalMicros = nbRollover;
  finalMicros <<= 32;
  finalMicros +=  currentMicros;
  return finalMicros;
}

L'extrait de code ci-dessus est disponible en téléchargement sur cette page (le lien de téléchargement en .zip contient le projet Arduino prêt à l'emploi avec un exemple d'affichage sur le port série de la carte Arduino).

Voici comment le code fonctionne :

1
2
static unsigned long nbRollover = 0;
static unsigned long previousMillis = 0;

Tout d'abord, le code déclare deux variables statiques dont la valeur sera conservée entre deux appels de la fonction.

La variable nbRollover contient le nombre de débordements que la fonction a détecté et géré. La variable previousMillis contient la valeur de retour du précédent appel à millis() fait par la fonction.

1
unsigned long currentMillis = millis();

Le code déclare ensuite une variable temporaire nommée currentMillis qui contient la valeur de retour actuelle de millis().

1
2
3
4
if (currentMillis < previousMillis) {
   nbRollover++;
}
previousMillis = currentMillis;

Le code teste ensuite si la valeur actuelle de millis() est inférieure à la valeur précédente. La fonction millis() retourne normalement une valeur strictement incrémentale, donc si le test échoue, c'est qu'un débordement a eu lieu.

Si un débordement est détecté, la valeur contenue dans la variable nbRollover est incrémentée de 1.

L'ancienne valeur de millis() est ensuite mise à jour avec la valeur actuelle de millis().

1
2
3
4
unsigned long long finalMicros = nbRollover;
finalMicros <<= 32;
finalMicros +=  currentMicros;
return finalMicros;

Les dernières lignes de code permettent de transformer deux nombres entiers sur 32 bits en un nombre entier sur 64 bits. Cela ressemble un peu à de la magie noire, mais il s'agit simplement d'opérations mathématiques en base 2. Je ne vais pas rentrer dans les détails du calcul, ce type d'opération fera l'objet d'un article dédié par la suite ;)

Le principe de fonctionnement de superMicros() est strictement identique, mais avec micros() au lieu de millis().

Au final, ces deux "super" fonctions permettent d'étendre la plage de valeur de millis() et micros() de 32 bits à 64 bits.

Cela signifie que superMillis() peut fonctionner pendant 584 942 417 années, 129 jours, 14 heures, 25 minutes, 51 secondes et 616 millisecondes. La fin du monde aura eu lieu bien avant que superMillis() ne déborde pour la première fois ;)

Pour superMicros(), le premier débordement aura lieu après 584 942 années, 152 jours, 8 heures, 1 minute, 49 secondes, 551 millisecondes et 616 microsecondes. Je pense qu'on peut dire sans se tromper qu'il y a de la marge.

N.B. Si vous utilisez ces fonctions, il faudra utiliser des unsigned long long au lieu de unsigned long comme type de variable pour stocker la valeur de retour.

Pourquoi ce n'est pas comme ça de base ?

On pourrait se demander, à juste titre, pourquoi les implémentations de base de millis() et micros() ne sont pas sur 64 bits.

En réalité, le support des nombres entiers sur 64 bits (unsigned long long) n'existe pas depuis très longtemps dans le logiciel Arduino. Du coup, beaucoup de choses dans le framework Arduino utilisent des nombres entiers sur 32 bits, dont les fonctions millis() et micros().

Par soucis de rétrocompatibilité, cela n'a pas évolué, pour le moment du moins.

Conclusion

Ce tutoriel est désormais terminé.

Si ce tutoriel vous a plu, n'hésitez pas à le commenter sur le forum, à le diffuser sur les réseaux sociaux et à soutenir le site si cela vous fait plaisir.