Ne plus bloquer : millis() et machines à états
Ce que ce chapitre apporte
- 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.
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(). Déclarée à l'intérieur, elle serait remise à zéro à chaque tour, et l'intervalle ne serait jamais atteint ou toujours dépassé.
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. Cent millisecondes plus tard, millis() vaut 4 294 967 100, qui est bien supérieur à 204 : l'action se déclenche après 100 ms au lieu de 500.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.
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.
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.
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 état sans flèche sortante signale un comportement oublié : que doit faire la lampe si l'on appuie pendant qu'elle clignote ?
Exercices type
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 : l'action se déclenche à tort, 400 ms trop tôt. 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().
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.
Vérification
1.Que rend millis() ?
2.Pourquoi l'horodatage de la dernière action se déclare-t-il hors de loop() ?
3.Quel test d'intervalle reste juste quand millis() revient à zéro ?
4.Après combien de temps millis() revient-il à zéro ?
5.Dans une machine à états, qu'est-ce qui fait passer d'un état à un autre ?
La méthode
- Lister les activités qui doivent avancer ensemble, et donner à chacune son intervalle et son horodatage.
- Écrire chaque test sous la forme
maintenant - derniere >= INTERVALLE, jamais par addition. - Déclarer les horodatages en
unsigned long, hors deloop(). - Dessiner le diagramme d'états avant le code : les états, les événements, une flèche par transition.
- Traduire avec une variable d'état, un
enumpour nommer les états, unswitchpour le comportement, unifpar flèche. - Vérifier qu'aucun état ne bloque : pas de
delaydans un état qui doit rester attentif à un événement.
Synthèse
millis()rend le temps écoulé depuis le démarrage ; il permet d'agir quand un intervalle est écoulé, sans suspendre le programme.- Chaque activité garde son horodatage dans un
unsigned longglobal ; plusieurs activités avancent alors ensemble. - Le test s'écrit par soustraction :
millis() - derniere >= INTERVALLEreste juste quandmillis()revient à zéro, après environ 49,7 jours. - Une machine à états décrit un comportement par des états et des transitions déclenchées par des événements.
- En programme : une variable d'état, un
enum, unswitch, unifpar transition. - États et
millis()ensemble permettent de réagir à tout instant, ce qu'aucundelayne permet.