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é.
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.
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.
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.
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é.
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.
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.
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.
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.
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.
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.
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.
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
Deux rythmes dans une même boucle, le retour à zéro du compteur, et les états d'une lampe.
- Deux rythmes dans la même boucleNiveau 3
- Ce qui se passe au bout de cinquante joursNiveau 4
- Où mène le cinquième appuiNiveau 2