Bonjour à toutes et tous
En ce jour gris, et vu le nombre de questions qui se résument à bien programmer un automate fini (ou une machine à état) je me suis dit que j'allais faire un petit tutoriel pour expliquer comment on peut aborder ce genre de code de façon structurée. Bien sûr il existe des librairies faisant cela plus ou moins pour vous, mais autant comprendre comment ça fonctionne, on peut souvent se passer de librairies qui vont alourdir votre code.
L'idée générale c'est d'écrire un programme pilotant un "système" qui doit réagir en déclenchant des "actions" modifiant le système par exemple en fonction "d'événements" qui se produisent et la réaction peut dépendre de l'état courant de votre système.
Vite un exemple de la vraie vie pour mieux comprendre :
Prenez une ampoule connectée à un interrupteur à impulsion. Vous appuyez sur le bouton, que doit il se passer? c'est simple, si l'ampoule était éteinte, alors elle doit s'allumer (action = alimenter l'ampoule) et si elle était allumée, alors elle doit s'éteindre (action = couper l'alimentation de l'ampoule).
On voit donc ici qu'on introduit la notion d'état de notre système. On va avoir deux états ALLUME ou ETEINT et il n'y a qu'un seul évènement possible, c'est le click sur le bouton. On a définit des actions associées à ces transitions d'état.
On peut représenter donc notre système sous la forme suivante:
Bien souvent on trouvera ce genre de système dans les minuteries. On peut donc rajouter un nouveau type d'évènement lié au temps passé dans un état. Dans le cas de la minuterie, si on est dans l'état allumé et que le délai est passé, alors il faut éteindre la lumière. On retrouve bien un évenement (délai expiré), une action (éteindre la lumière) et une transition d'état (on passe de ALLUME a ETEINT).
On peut représenter donc notre système sous la forme suivante:
Bon l'objet de ce tuto n'étant pas de rentrer dans la théorie des automates finis, donc je vous laisse lire wikipedia pour en savoir plus et on va se concentrer sur des exemples que l'on rencontre assez fréquemment quand on joue avec des Arduinos.
Un outil pratique pour le programmeur de machine à états : les énumérations
En C++ une énumération est un type, nommé ou non, qui regroupe dans une liste des constantes entières connues dès la compilation.
La valeur de chacun des éléments de la liste sera distincte de celles des autres — la première vaudra 0, la deuxième vaudra 1, la troisième vaudra 2, etc.
Je peux par exemple écrire le code suivant
enum {lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche} jour;
qui va déclarer une variable jour
qui peut prendre comme valeur un des codes de la liste (lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche).
Donc si je fais le code suivant sur un Arduino
enum {lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche} jour;
void setup() {
Serial.begin(115200);
jour = jeudi;
Serial.println(jour);
}
void loop() {}
Je vais voir 3 dans la console Série (réglée à 115200 bauds)
Si vous voulez imposer une valeur à certains éléments de la liste c'est possible. La numérotation par défaut est de commencer à 0 et d'incrémenter de 1 à moins que le programme n'impose une valeur explicitement à des constantes énumérée (dans ce cas elle prend cette valeur et les suivantes vaudront un de plus que celle qui la précède dans l'ordre de déclaration)
Je peux par exemple écrire le code suivant
enum {lundi=1, mardi, mercredi, jeudi, vendredi, samedi, dimanche} jour;
qui va déclarer que lundi vaut 1, donc mardi vaudra 2 etc et si vous utilisez cette définition dans le code ci dessus, au lieu d'imprimer 3 dans la console, vous verrez donc 4. (on aurait pu dire
enum {lundi, mardi, mercredi, jeudi=12, vendredi, samedi, dimanche} jour;
et dans ce cas lundi serait 0, mardi 1, mercredi 2, jeudi 12, vendredi 13 (ça porte bonheur), samedi 14 etc...)
Pourquoi je parle de cela? parce qu'un enum
c'est bien pratique pour lister les états de notre système de façon à ce 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 {lampeEteinte, lampeAllumee} etatCourant;
Je définis ainsi une variable etatCourant
qui peut prendre la valeur lampeEteinte
ou lampeAllumee
à noter que depuis C++ 11 (dispo dans les dernières version de l'IDE) il est possible de définir le type sous jacent aux variables de l'énumération. Par défaut c'est un entier donc type int
qui va prendre deux octets de mémoire sur un UNO. si vous n'avez que quelques états (moins de 255) et que leur valeur ne vous importe pas (pas au dessus de 255) vous pouvez alors rajouter le type byte
dans la déclaration est ainsi économiser un octet. (vous pourriez aussi vouloir des unsigned long par exemple si vous avez besoin de grandes valeurs pré-définies genre lecture d'un code de télécommande IR)
Dans ce code la variable jour
sera un int
sur deux octets
enum {lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche} jour;
et dans ce code la variable jour
sera un byte
, donc sur un seul octet !
enum :byte {lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche} jour;
Un autre outil pratique pour le programmeur de machine à états : le switch/case
Je vous laisse lire la doc de programmation sur le switch/case. Son intérêt réside dans le fait que bien souvent dans nos machines à état 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
enum {lampeEteinte, lampeAllumee} etatCourant;
...
switch (etatCourant) {
case lampeEteinte:
// faire quelque chose
break;
case lampeAllumee:
// faire quelque chose d'autre
break;
}
Mise en pratique
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
il vous faudra
- 4 LEDs de couleur (rouge, orange, jaune, verte)
- 4 résistances de 200Ω à 300Ω (en fonction de vos LEDs)
- un bouton momentané
- un Arduino UNO ou similaire
- 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 pins)
Pin 8 --> Led rouge --> 200 Ω --> GND
Pin 9 --> Led orange --> 200 Ω --> GND
Pin 10 --> Led jaune --> 200 Ω --> GND
Pin 11 --> Led verte --> 200 Ω --> GND
Voilà à partir de là on va effectuer 3 exercices - cf les post suivants