La machine à états finis - Première version¶
Note 1: Cet article est une réinterprétation de l'excellent post de J-M-L sur le forum Arduino.
Note 2: Il s'agit de la première version des machines à états finis. Dans un autre cours, nous allons voir comment faire cela de manière plus élégante et plus facile à maintenir en utilisant des fonctions pour gérer les différents états.
Introduction¶
Les machines à états finis sont un outil puissant pour contrôler le comportement des programmes et, ce, surtout sur les appareils électroniques. Elles permettent de décrire l'état du système et comment il évolue en réponse aux entrées et sorties. Les machines à états finis sont utiles pour définir des comportements complexes en utilisant des algorithmes simples, et elles peuvent être implémentées efficacement sur un microcontrôleur comme l'Arduino. En utilisant des machines à états finis, les programmeurs peuvent décrire clairement et de manière organisée les différents états possibles du système, les transitions entre ces états et les actions à effectuer à chaque étape. Cela peut être particulièrement utile pour des projets impliquant des systèmes autonomes, tels que des robots ou des dispositifs IoT, où le comportement doit être précis et cohérent.
Pourquoi utiliser une machine à états finis?¶
En utilisant des machines à états finis, les programmeurs peuvent concevoir des systèmes qui ont des comportements complexes, tout en conservant un code facile à lire, à maintenir et à déboguer.
Il s'agit d'une méthode de développement complémentaire à la programmation par tâche. Ils ne sont pas exclusifs l'un de l'autre, mais peuvent être utilisés ensemble pour créer des systèmes plus robustes et plus flexibles.
Détails¶
- Une machine peut avoir un ou plusieurs états.
- Pour passer d’un état à l’autre, il y a une transition.
- L’idée générale est d’écrire un programme pilotant un système qui doit réagir à des événements en déclenchant des actions qui modifient ce système. La réaction peut dépendre de l’état actuel du système.
- Souvent, on utilise le sigle FSM dans la documentation
- Finite State Machine
Cas d'étude: l'ampoule¶
- Pensez à une ampoule qui peut avoir 2 états: allumée ou éteinte.
- Pour passer d’un état à l’autre, on appuie sur un bouton.
- Il n’y a qu’un seul événement possible.

- Souvent on retrouve des interrupteurs avec minuterie
- On peut ainsi ajouter un nouveau type d’événement lié au temps passé dans un état.
- Dans ce cas, si l’ampoule est allumée et que le délai est dépassé, celle-ci s’éteindra.
- On retrouve ainsi:
- Un événement: délai expiré
- Une action: éteindre la lumière
- Une transition d’état: passage de allumée à éteinte

Implémentation¶
Pour faciliter l’implémentation d’une FSM, nous allons utiliser deux principes de programmation: - L’énumération - Le switch case
Rappel: énumération¶
L’énumération est un type de donnée qui consiste en un ensemble de valeurs nommées
Exemple:
Ce code va déclarer un type Jour qui peut prendre les valeurs suivantes: DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI.
Nous allons voir la valeur 3 s'afficher dans le moniteur série.
Question: Pour quelle raison ce sera cette valeur?
Pourquoi parlons-nous de cela? Parce qu'un enum, c'est bien pratique pour lister les états de notre système, afin que le programmeur s'y retrouve facilement.
Dans mon exemple ci-dessus de minuterie, on a vu qu'on avait deux états et donc on pourrait déclarer:
enum Etat {LAMPE_ETEINTE, LAMPE_ALLUMEE};
On définit ainsi un type Etat qui peut prendre les valeurs LAMPE_ETEINTE ou LAMPE_ALLUMEE.
Rappel: switch/case¶
On vous laisse le soin de lire la doc de programmation sur le switch/case. Son intérêt réside dans le fait que, bien souvent dans nos machines à états, on aura besoin de dire: «si l'état courant est celui-ci, alors faire cela; sinon, si l'état courant est celui-là, alors faire autre chose, etc.». Si vous avez de nombreux états possibles, tous ces tests imbriqués rendent le code difficile à lire et le switch/case simplifie tout cela. En combinant cela habilement avec notre enum, on pourra par exemple écrire:
Mise en pratique: Arduino¶
Construisons un cas un peu similaire à celui de la minuterie, mais un peu plus complexe pour avoir de nombreux états à gérer.
Étape 1: monter sa platine d'essai et connecter l'Arduino
Dans nos cours, nous utilisons des Arduino Mega 2560. Le montage ci-dessous utilise les broches 4, 8, 9, 10 et 11 sur la Mega.
Il vous faudra:
- 4 LEDs de couleur: dans la démonstration, je vais utiliser rouge, orange, jaune et vert.
- 4 résistances de 200Ω à 400Ω (en fonction de vos LEDs)
- 1 bouton momentané
- Des fils pour connecter tout cela
Voici le montage:


On relie les GND de l'Arduino avec le rail GND de la platine d'essai (j'ai relié les 2 rails opposés GND de la platine ensemble pour avoir GND des 2 côtés).
On connecte:
- Pin 4 --> bouton --> GND (en câblant «croisé», on est sûr d'avoir les bonnes broches)
- Pin 8 --> LED rouge --> 220 Ω --> GND
- Pin 9 --> LED orange --> 220 Ω --> GND
- Pin 10 --> LED jaune --> 220 Ω --> GND
- Pin 11 --> LED verte --> 220 Ω --> GND
Voilà à partir de là on va effectuer 3 exercices pour comprendre comment fonctionne une FSM.
Exercice 1: allumer une LED¶
Dans cet exercice, nous souhaitons démarrer avec tout éteint et utiliser le bouton pour allumer les LEDs les unes à la suite des autres pour éclairer de plus en plus fort (ou, ici, faire des couleurs):
- Premier appui la LED verte s'allume
- Deuxième appui la LED verte reste allumée et on allume la jaune
- Troisième appui la LED orange s'allume en plus
- Quatrième appui la LED rouge s'allume en plus
- Cinquième appui tout s'éteint.
Cela ressemble fortement à une machine à états que l'on pourrait décrire ainsi:
Plusieurs états:
- tout éteint (REPOS)
- Led Verte allumée (V)
- Led Verte et Jaune allumées (VJ)
- Led Verte, Jaune et Orange allumées (VJO)
- Led Verte, Jaune et Orange et Rouge allumées (VJOR)
état initial = repos
action possible = clic sur le bouton
et voici le diagramme des transitions possibles

Comment coder tout cela?
Pour se concentrer sur l'essentiel, nous allons utiliser la librairie OneButton.
Vous déclarez un objet bouton en précisant sur quelle broche il est connecté et s'il est actif à l'état HIGH ou LOW (c'est-à-dire si son pinMode() est en INPUT_PULLUP ou pas). Ensuite, vous attachez une fonction à appeler (on dit que c'est un callback en anglais) quand une action est détectée sur le bouton.
Dans le code, ça ressemble à ceci:
On déclare ensuite une fonction callback:
Et dans lesetup(), on attache cette fonction au bouton:
Enfin dans la loop(), la librairie doit être appelée de manière répétitive pour voir si un bouton est appuyé.
Voilà. L'utilisation d'un bouton est relativement simple d'emploi et ça permet de nous concentrer sur notre machine à états (si vous êtes curieux, allez voir les sources de la librairie et vous verrez que c'est aussi une machine à états).
Revenons au code
Il va falloir déclarer toutes les broches utilisées pour les LEDs, instancier le bouton, et coder la machine à états en utilisant une énumération pour les différents états. On va aussi déclarer une fonction callback qui est appelée quand on appuie sur le bouton, dans laquelle on aura un beau switch/case, comme mentionné plus haut.
Voici le code commenté:
Toute l'intelligence de la machine est donc dans la fonction de rappel simpleclick(), qui est très simple à lire grâce au switch/case et à l'usage de codes d'état lisibles tels que déclarés dans l'enum.
Pour faire simple, grâce au switch/case, on regarde quel est notre état courant et, comme on sait que cette fonction n'est appelée que lorsqu'on a reçu un clic, on sait qu'il faut passer à l'état suivant. En regardant le diagramme, on sait quelle action il faut faire et quel est l'état suivant: il suffit donc de coder cela. C'est tout simple.
Exercice 2: le double-clic¶
Dans cet exercice nous souhaitons compliquer un peu le fonctionnement de notre machine précédente.
C'est bien gentil de pouvoir augmenter la luminosité petit à petit, mais il y a des gens pressés sur terre, et on nous demande maintenant de modifier notre machine pour qu'un double-clic sur le bouton allume toutes les LEDs si elles n'étaient pas déjà toutes allumées, et les éteigne toutes si elles étaient toutes allumées.
Notre machine se complique donc un petit peu. On a un nouvel événement à prendre en compte, le double-clic, qui va générer de nouvelles transitions: une transition qui va de tous les états sauf "tout allumé" vers l'état "tout allumé", et une transition de "tout allumé" vers l'état de repos en cas de double-clic.
Sur un diagramme, les nouvelles transitions ressemblent donc à cela:

Ces nouvelles transitions s'ajoutent aux anciennes.
Comment va-t-on gérer cela?
Le concepteur de la librairie OneButton, dans sa grande sagesse, a prévu cela et cela fonctionne de la même manière que précédemment: vous déclarez une fonction callback qui sera appelée quand un double-clic est détecté. On va donc créer une fonction:
Et dans le setup(), on va attacher cette fonction comme callback de double-clic:
Le code qui se trouve dans le callback est simple: on peut soit faire un if sur l'état pour voir si toutes les LEDs sont allumées et faire ce qu'il faut, ou conserver notre structure avec le switch/case, ce que je vais faire ici puisque c'est plus lisible.
Cela nous donne donc ceci, toute la magie est dans la fonction doubleclick():
Remarque: Dans un
case, lorsqu'il n'y a pas debreak, cela signifie que l'on veut passer à l'exécution ducasesuivant. C'est ce que l'on fait ici pour les casREPOS,ETAT_V,ETAT_VJetETAT_VJOqui sont tous identiques.
Exercice 3: le chronomètre¶
Dans cet exercice, on nous demande de nous montrer économes... Il ne faut pas laisser la lumière allumée trop longtemps et donc on nous demande d'ajouter une minuterie. Le cahier des charges stipule: «Si la lumière est allumée plus de 15 secondes sans action de la part de l'utilisateur, alors tout éteindre.»
Maintenant nous sommes rodés. On voit tout de suite qu'il s'agit d'un nouveau type d'événement qu'il va falloir prendre en compte dans notre machine à états: le temps qui passe.
Notre machine se complique donc un petit peu. On a un nouvel événement à prendre en compte, le "délai expiré", qui va générer de nouvelles transitions: une transition qui va de tous les états sauf "tout éteint" vers l'état "tout éteint".
Sur un diagramme, les nouvelles transitions ressemblent donc à cela:

Ces nouvelles transitions s'ajoutent aux anciennes.
Comment va-t-on gérer cela?
On ne peut bien sûr pas mettre de delay(15000) dans notre code sinon les boutons ne seraient plus opérationnels. On ne doit pas bloquer le code! On ne va pas réinventer la roue pour cela, on va utiliser une technique classique.
Vous avez tous lu le tutoriel (sinon il faut le lire) blink without delay, qui est un des exemples standards de la gestion du temps. Pour ceux dont la langue de Shakespeare est un défi, il y a l'excellent tutoriel d'Eskimon sur la gestion du temps et la fonction millis() (voir à la fin de l'article).
Une fois que vous maîtrisez ce concept, on va l'appliquer.
Il va donc nous falloir une variable chrono qui va mémoriser l'heure de la dernière action de l'utilisateur.
L'évènement "délai expiré" est un événement comme un autre, il se gère au même niveau que là où on regarde si les boutons sont appuyés, donc dans la loop().
Après avoir vérifié les boutons, on va regarder si le délai depuis la dernière action est expiré et, si c'est le cas, on va déclencher un appel à la fonction timeOut().
Cette fonction doit regarder dans quel état on est et si au moins une des LEDs est allumée alors tout éteindre et revenir au repos. On pourrait faire un switch/case pour traiter chaque cas indépendamment et bien mettre du code pour chaque transition du diagramme comme dans l'exercice #2 où on avait conservé le switch/case pour la lisibilité, mais maintenant vous êtes rodés et des pros, donc on va juste tester le cas qui nous intéresse plutôt que de regarder tous les cas. En effet un simple test sur l'état courant pour voir si on n'est pas au repos suffit et dans ce cas revenir à l'état repos.
La fonction fera donc tout simplement:
Bien sûr, il faut réarmer notre compteur à chaque fois que l'utilisateur appuie sur un bouton puisque le cahier des charges dit 15 secondes après la dernière action. On va donc rajouter dans nos fonctions callback simpleclick() et doubleclick() une ligne de code qui maintient notre «top chrono» en faisant simplement:
Voici le code final de l'exercice #3:
Cliquez pour voir
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
Cette technique s'applique à de nombreux cas, donc il est bon de la maîtriser
Bon codage à toutes et tous!
Annexe¶
Autre exemple de diagramme d'état¶
Voici un exemple simple qui gère le temps d'allumage d'un système de feux de circulation.
-
Code
-
Résultat

Exercices¶
- Utilisez le concept de machine à états pour votre laboratoire en cours.