Aller au contenu principal

millis() et les machines à états

Ce que ce chapitre apporte 5 points
  • Mesurer un intervalle avec millis() sans suspendre le programme.
  • Mener plusieurs activités à des rythmes différents dans la même loop().
  • Écrire le test d'intervalle sous la forme qui reste juste quand millis() revient à zéro.
  • Décrire un comportement par des états et des transitions.
  • Traduire une machine à états en programme avec switch.

Un programme construit avec delay fait une chose à la fois, et s'aveugle pendant chaque pause. Dès qu'il faut clignoter tout en surveillant un bouton, ou mener une séquence qui réagit à tout instant, cette façon d'écrire ne suffit plus. Ce chapitre en présente une autre : au lieu de dormir, le programme regarde l'heure à chaque tour et décide s'il est temps d'agir. Associée à la notion d'état, elle permet d'écrire des comportements complets sans jamais bloquer.

Regarder l'heure au lieu de dormir

millis() rend le nombre de millisecondes écoulées depuis le démarrage de la carte. Le programme ne s'arrête plus : à chaque passage dans loop(), il compare l'heure actuelle à celle de sa dernière action, et n'agit que si l'intervalle voulu est écoulé.

Arduino
const int LED = 13;
const unsigned long INTERVALLE = 500;
unsigned long derniereAction = 0; // l'instant du dernier basculement
int etat = LOW;
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
unsigned long maintenant = millis();
if (maintenant - derniereAction >= INTERVALLE) { // assez de temps écoulé ?
derniereAction = maintenant;
etat = !etat; // basculer
digitalWrite(LED, etat);
}
// ici, le programme reste libre de faire autre chose
}

La LED clignote exactement comme avec delay, mais loop() tourne des milliers de fois par seconde au lieu de dormir. Tout ce qui est écrit après le if s'exécute à chaque tour.

Vérification rapideon peut se reprendre

Que rend millis() ?

Plusieurs choses à la fois

La conséquence est immédiate : chaque activité garde son propre horodatage, et elles avancent ensemble sans se gêner.

Arduino
const int LED_LENTE = 12, LED_RAPIDE = 11, LED_BOUTON = 13, BOUTON = 2;
unsigned long dernierLent = 0, dernierRapide = 0;
int etatLent = LOW, etatRapide = LOW;
void setup() {
pinMode(LED_LENTE, OUTPUT);
pinMode(LED_RAPIDE, OUTPUT);
pinMode(LED_BOUTON, OUTPUT);
pinMode(BOUTON, INPUT_PULLUP);
}
void loop() {
unsigned long maintenant = millis();
if (maintenant - dernierLent >= 500) { // toutes les 500 ms
dernierLent = maintenant;
etatLent = !etatLent;
digitalWrite(LED_LENTE, etatLent);
}
if (maintenant - dernierRapide >= 350) { // toutes les 350 ms
dernierRapide = maintenant;
etatRapide = !etatRapide;
digitalWrite(LED_RAPIDE, etatRapide);
}
digitalWrite(LED_BOUTON, digitalRead(BOUTON) == LOW); // réagit à chaque tour
}
50 ms par bit20 bauds40 bits, 2 s en tout
LED lente0000000000111111111100000000001111111111LED rapide00000001111111000000011111110000000111110400 ms800 ms1,2 s1,6 s2 s
Les deux premières secondes, par tranches de 50 ms. La LED lente bascule toutes les 500 ms, la rapide toutes les 350 ms : aucune des deux n'attend l'autre, et le bouton est lu entre-temps des milliers de fois par seconde.

Simulé avec un appui de 56 ms, ce programme allume la LED du bouton à l'instant même de l'appui et l'éteint à l'instant où il cesse. La même lecture, placée après un delay(500), l'aurait manqué près de neuf fois sur dix.

Le prix : écrire l'horodatage soi-même
delay gardait le temps pour le programmeur. Avec millis(), chaque activité doit mémoriser l'instant de sa dernière action, dans une variable unsigned long déclarée hors de loop(), ou dans une variable static de loop(), qui garde sa valeur d'un tour au suivant comme au chapitre 4. Déclarée à l'intérieur comme une variable locale ordinaire, elle serait recréée et remise à zéro à chaque tour, et l'intervalle ne serait jamais atteint ou toujours dépassé.
Vérification rapideon peut se reprendre

Pourquoi l'horodatage ne peut-il pas être une variable locale ordinaire de loop() ?

Le retour à zéro

millis() est un unsigned long : après environ 49,7 jours de fonctionnement, il dépasse sa capacité et repart de 0. Un thermostat ou une alarme restent allumés bien plus longtemps, et le test d'intervalle doit y survivre.

La forme par addition
if (millis() >= derniereAction + INTERVALLE) paraît équivalent. Mais si derniereAction vaut 4 294 967 000, peu avant le retour à zéro, l'addition dépasse la capacité et vaut 204. Or millis() vaut encore plus de quatre milliards, bien au-delà de 204 : le test est vrai dès le tour suivant, et l'action se déclenche à chaque tour de loop() jusqu'au retour à zéro, au lieu d'une fois toutes les 500 ms.
La forme par soustraction, millis() - derniereAction >= INTERVALLE, calcule directement la durée écoulée. En arithmétique non signée, cette différence reste juste de part et d'autre du retour à zéro : elle vaut 100 dans le même cas, et l'action attend.
Vérification rapideon peut se reprendre

1.Quel test d'intervalle reste juste quand millis() revient à zéro ?

2.Après combien de temps millis() revient-il à zéro ?

Penser en états

Un feu, une lampe à plusieurs modes, une porte automatique ne font pas « une suite d'instructions » : ils sont dans un état, et certains événements les font passer dans un autre.

Définition

Une machine à états décrit un comportement par un ensemble fini d'états, et par des transitions qui font passer de l'un à l'autre quand un événement survient : un appui, une durée écoulée, une mesure qui franchit un seuil.

Cet objet a un parcours entier pour lui. Le chapitre sur les états et les événements le pose formellement, Implémenter un automate compare le switch employé ici à une table de transitions, et Les limites des automates finis dit précisément à partir de quand un nombre fini d'états ne suffit plus.

Exemple : une lampe à trois modes, commandée par un seul bouton. Chaque appui la fait passer au mode suivant, et le mode clignotant doit clignoter sans cesser d'écouter le bouton.

Graphe orienté et pondéré3 sommets, 3 arcs
appuiappuiappuiETEINTEFIXECLIGNOTANTE
Les trois états de la lampe et leurs transitions. Le même événement, un appui, mène à un état différent selon l'état de départ : c'est ce qu'une simple condition sur le bouton ne sait pas exprimer.

La traduction en programme suit toujours le même schéma : une variable retient l'état courant, un switch exécute le comportement de cet état et teste ses transitions.

Le programme emploie une syntaxe nouvelle : enum crée un type dont les valeurs sont des noms, ici ETEINTE, FIXE et CLIGNOTANTE, que le compilateur numérote 0, 1, 2. Le switch du chapitre 2 les compare alors comme des entiers, mais le programme les écrit par leur nom.

Arduino
const int BOUTON = 2, LAMPE = 9;
const unsigned long STABLE_MS = 30, DEMI_PERIODE = 250;
enum Mode { ETEINTE, FIXE, CLIGNOTANTE };
Mode mode = ETEINTE;
int lectureBrute = HIGH, etatStable = HIGH;
unsigned long dernierChangement = 0, dernierBasculement = 0;
int allumee = LOW;
bool appuiDetecte() { // anti-rebond du chapitre 5, sans delay
int lecture = digitalRead(BOUTON);
if (lecture != lectureBrute) {
lectureBrute = lecture;
dernierChangement = millis();
}
if (millis() - dernierChangement >= STABLE_MS && lectureBrute != etatStable) {
etatStable = lectureBrute;
return etatStable == LOW;
}
return false;
}
void setup() {
pinMode(BOUTON, INPUT_PULLUP);
pinMode(LAMPE, OUTPUT);
}
void loop() {
bool appui = appuiDetecte();
switch (mode) {
case ETEINTE:
digitalWrite(LAMPE, LOW);
if (appui) mode = FIXE;
break;
case FIXE:
digitalWrite(LAMPE, HIGH);
if (appui) {
mode = CLIGNOTANTE;
allumee = HIGH; // la lampe part allumée
dernierBasculement = millis();
}
break;
case CLIGNOTANTE:
if (millis() - dernierBasculement >= DEMI_PERIODE) {
dernierBasculement = millis();
allumee = !allumee;
digitalWrite(LAMPE, allumee);
}
if (appui) mode = ETEINTE;
break;
}
}

enum Mode donne un nom à chaque état : le programme se lit comme le diagramme. Le mode clignotant emploie millis(), si bien que l'appui qui le termine est pris en compte à n'importe quel instant, et non à la fin d'une pause. Le bouton est lu par la fonction d'anti-rebond du chapitre 5, qui ne bloque pas non plus.

Dessiner d'abord
Le diagramme d'états se dessine avant le code, sur papier. Chaque flèche devient un if dans le cas correspondant du switch, et un événement qu'aucune flèche ne prévoit signale un comportement oublié : un appui long, deux secondes, doit-il éteindre la lampe directement, quel que soit le mode ? Le diagramme ci-dessus n'en dit rien, et le programme le traite comme un appui bref.
Vérification rapideon peut se reprendre

Dans une machine à états, qu'est-ce qui fait passer d'un état à un autre ?

Mettre en pratique

Un programme clignote avec delay(500) et doit aussi allumer une LED quand on appuie sur un bouton. Pourquoi l'appui est-il souvent ignoré, et comment corriger ?

Pendant chaque delay(500), le programme est suspendu : le bouton n'est lu qu'une fois par demi-seconde. Un appui bref tombe le plus souvent pendant une pause.

La correction remplace la pause par un test d'intervalle : if (millis() - derniereAction >= 500). Le clignotement garde le même rythme, et loop() tourne sans arrêt, ce qui fait lire le bouton à chaque tour.

derniereAction vaut 4 294 967 000 et INTERVALLE vaut 500. Cent millisecondes plus tard, que valent millis() - derniereAction et le test par addition ?

millis() vaut 4 294 967 100. La soustraction donne 100 : l'intervalle n'est pas écoulé, rien ne se passe, ce qui est juste.

L'addition derniereAction + 500 dépasse la capacité d'un unsigned long et vaut 204. Le test millis() >= 204 est vrai, et il l'était déjà une milliseconde après l'action : par addition, l'action se déclenche à chaque tour de loop() jusqu'au retour à zéro. Seule la forme par soustraction est correcte.

La variable derniereAction a été déclarée dans loop(). La LED ne clignote plus. Pourquoi ?

À chaque tour, derniereAction est recréée et remise à 0. Le test millis() - 0 >= 500 est vrai dès que la carte tourne depuis plus d'une demi-seconde : la LED bascule à chaque tour, des milliers de fois par seconde, et paraît allumée à moitié.

L'horodatage doit survivre d'un tour à l'autre : il se déclare hors de loop(), ou dans loop() avec le mot static, qui le fait initialiser une seule fois.

Dans la lampe à trois modes, que se passerait-il si le mode CLIGNOTANTE utilisait delay(250) pour clignoter ?

Pendant chaque pause, appuiDetecte() ne serait pas appelée. Un appui bref serait perdu, et même un appui long ne serait pris en compte qu'après la fin de la pause en cours. La lampe paraîtrait ne pas obéir.

La machine à états ne suffit donc pas seule : c'est l'association d'états et d'un temps mesuré avec millis() qui permet de réagir à tout instant.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices