Aller au contenu

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.

Alt text


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:

enum Jour {DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI};

Ce code va déclarer un type Jour qui peut prendre les valeurs suivantes: DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI.

enum Jour {DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI};

Jour jour = MERCREDI;

void setup() {
  Serial.begin(115200);
  Serial.println(jour);
}

void loop() {}

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:

enum Etat {LAMPE_ETEINTE, LAMPE_ALLUMEE};

Etat etatCourant = LAMPE_ETEINTE;

//...

  switch (etatCourant) {
    case LAMPE_ETEINTE:
      // faire quelque chose
      // etatEteint();

      break;

    case LAMPE_ALLUMEE:
      // faire quelque chose d'autre
      // etatAllume();

      break;
  }

// ...

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:

1
2
3
#include <OneButton.h> // on inclut la librairie
const byte buttonPin = 4; // on définit un nom pour la pin associée au bouton
OneButton button(buttonPin, true); // true pour dire qu'on est en INPUT_PULLUP, donc actif LOW, connecté à GND

On déclare ensuite une fonction callback:

1
2
3
4
5
void simpleclick()
{
  // le code à exécuter quand on fait un clic sur le bouton
  // ...
}
Et dans le setup(), on attache cette fonction au bouton:

1
2
3
4
5
void setup() {
  // ...
  button.attachClick(simpleclick); // on attache la fonction simpleclick 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é.

1
2
3
void loop() {
  button.tick();  // On vérifie l'état des boutons, ce qui déclenche les appels aux fonctions
}

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é:

// La librairie de gestion des boutons
#include <OneButton.h>
const byte buttonPin = 4; // notre bouton est sur la pin 4
OneButton button(buttonPin, true); // true pour le mettre en INPUT_PULLUP

// les pins utilisées pour les LEDs
const byte pinLedRouge = 8;
const byte pinLedOrange = 9;
const byte pinLedJaune = 10;
const byte pinLedVerte = 11;

// la liste des états possible de notre système
// ainsi qu'une variable etatCourant prenant une de ces valeurs
enum Etat {REPOS, ETAT_V, ETAT_VJ, ETAT_VJO, ETAT_VJOR};

Etat etatCourant;

// ------------------------------------------------------
// Cette fonction installe l'état initial
// ------------------------------------------------------
void mettreAuRepos()
{
  digitalWrite(pinLedVerte,  LOW);
  digitalWrite(pinLedJaune,  LOW);
  digitalWrite(pinLedOrange, LOW);
  digitalWrite(pinLedRouge,  LOW);
  etatCourant = REPOS;
}

// ------------------------------------------------------
// La fonction callback, appelée automatiquement quand on clique
// ------------------------------------------------------
void simpleclick()
{
  switch (etatCourant) {
    case REPOS: // on était au repos et on a un appui, on allume la verte
      digitalWrite(pinLedVerte, HIGH); // LED verte alimentée
      etatCourant = ETAT_V; // on note le nouvel état de notre système
      break;

    case ETAT_V: // on était led verte allumée et on a un appui, on allume la jaune
      digitalWrite(pinLedJaune, HIGH); // LED jaune alimentée
      etatCourant = ETAT_VJ;// on note le nouvel état de notre système
      break;

    case ETAT_VJ: // vert et jaune allumées, on a un appui, on allume la orange
      digitalWrite(pinLedOrange, HIGH); // LED orange alimentée
      etatCourant = ETAT_VJO;// on note le nouvel état de notre système
      break;

    case ETAT_VJO:// vert, orange et jaune allumées, on a un appui, on allume la rouge
      digitalWrite(pinLedRouge, HIGH); // LED rouge alimentée
      etatCourant = ETAT_VJOR;// on note le nouvel état de notre système
      break;

    case ETAT_VJOR: // tout était allumé, on a un appui, on retourne au repos
      mettreAuRepos(); // on retourne à l'état initial
      break;
  }
}

// ------------------------------------------------------
// On initialise notre système dans le setup
// ------------------------------------------------------
void setup() {
  pinMode (pinLedRouge, OUTPUT);
  pinMode (pinLedOrange, OUTPUT);
  pinMode (pinLedJaune, OUTPUT);
  pinMode (pinLedVerte, OUTPUT);

  //conditions Initiales
  mettreAuRepos();

  // On attache la fonction simpleClick() comme callback
  button.attachClick(simpleclick);
}

void loop() {
  // On vérifie l'état des boutons, ce qui déclenche l'appel de la fonction callback si nécessaire
  button.tick();

  // ici on peut faire autre chose du moment que ça ne prend pas trop longtemps

}

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:

1
2
3
4
void doubleclick()
{
  // notre code ici en cas de double-clic
}

Et dans le setup(), on va attacher cette fonction comme callback de double-clic:

button.attachDoubleClick(doubleclick);

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():

void doubleclick()
{
  switch (etatCourant) {
    case REPOS:    // dans tous les états
    case ETAT_V:   // sauf celui où tout était allumé
    case ETAT_VJ:  // en cas de double-clic on veut
    case ETAT_VJO: // allumer toutes les LEDs
      digitalWrite(pinLedVerte, HIGH);
      digitalWrite(pinLedJaune, HIGH);
      digitalWrite(pinLedOrange, HIGH);
      digitalWrite(pinLedRouge, HIGH);
      etatCourant = ETAT_VJOR;  // et on déclare notre nouvel état courant 
      break;

    case ETAT_VJOR: // on a tout qui est allumé et on reçoit le double-clic
      mettreAuRepos(); // donc on retourne à l'état de repos
      break;
  }
}

Remarque: Dans un case, lorsqu'il n'y a pas de break, cela signifie que l'on veut passer à l'exécution du case suivant. C'est ce que l'on fait ici pour les cas REPOS, ETAT_V, ETAT_VJ et ETAT_VJO qui 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.

1
2
3
// On introduit le temps comme évènement supplémentaire
unsigned long chrono; // attention, type unsigned long comme millis()
const unsigned long TimeOut = 15000ul; // 15 secondes (le ul à la fin pour unsigned long, une bonne habitude à prendre)

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:

1
2
3
4
void timeOut()
{
  if (etatCourant != REPOS) mettreAuRepos(); // si on n'est pas au repos, passer au repos
}

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:

chrono = millis(); // on vient d'avoir une action donc on ré-arme notre chronomètre

Voici le code final de l'exercice #3:

Cliquez pour voir
// La librairie de gestion des boutons
#include <OneButton.h>
const byte buttonPin = 4; // notre bouton est sur la pin 4
OneButton button(buttonPin, true); // true pour le mettre en INPUT_PULLUP

// les pins utilisées pour les LEDs
const byte pinLedRouge = 8;
const byte pinLedOrange = 9;
const byte pinLedJaune = 10;
const byte pinLedVerte = 11;

unsigned long currentTime;

// On introduit le temps comme évènement supplémentaire
unsigned long chrono; // attention, type unsigned long comme millis()
const unsigned long TimeOut = 15000ul; // 15 secondes (le ul à la fin pour unsigned long)

// la liste des états possible de notre système
// ainsi qu'une variable etatCourant prenant une de ces valeurs
enum {REPOS, ETAT_V, ETAT_VJ, ETAT_VJO, ETAT_VJOR} etatCourant;

// ------------------------------------------------------
// Cette fonction installe l'état initial
// ------------------------------------------------------
void mettreAuRepos()
{
  digitalWrite(pinLedVerte,  LOW);
  digitalWrite(pinLedJaune,  LOW);
  digitalWrite(pinLedOrange, LOW);
  digitalWrite(pinLedRouge,  LOW);
  etatCourant = REPOS;
}

// ------------------------------------------------------
// La fonction callback, appelée automatiquement quand on clique
// ------------------------------------------------------
void simpleclick()
{
  switch (etatCourant) {
    case REPOS: // on était au repos et on a un appui, on allume la verte
      digitalWrite(pinLedVerte, HIGH); // LED verte alimentée
      etatCourant = ETAT_V; // on note le nouvel état de notre système
      break;

    case ETAT_V: // on était led verte allumée et on a un appui, on allume la jaune
      digitalWrite(pinLedJaune, HIGH); // LED jaune alimentée
      etatCourant = ETAT_VJ;// on note le nouvel état de notre système
      break;

    case ETAT_VJ: // vert et jaune allumées, on a un appui, on allume la orange
      digitalWrite(pinLedOrange, HIGH); // LED orange alimentée
      etatCourant = ETAT_VJO;// on note le nouvel état de notre système
      break;

    case ETAT_VJO:// vert, orange et jaune allumées, on a un appui, on allume la rouge
      digitalWrite(pinLedRouge, HIGH); // LED rouge alimentée
      etatCourant = ETAT_VJOR;// on note le nouvel état de notre système
      break;

    case ETAT_VJOR: // tout était allumé, on a un appui, on retourne au repos
      mettreAuRepos(); // on retourne à l'état initial
      break;
  }
  chrono = currentTime; // on vient d'avoir une action donc on ré-arme notre chronomètre
}

// ------------------------------------------------------
// La fonction callback, appelée automatiquement quand on double-clique
// ------------------------------------------------------
void doubleclick()
{
  switch (etatCourant) {
    case REPOS:    // dans tous les états
    case ETAT_V:   // sauf celui où tout était allumé
    case ETAT_VJ:  // en cas de double click on veut
    case ETAT_VJO: // allumer toutes les LEDs
      digitalWrite(pinLedVerte, HIGH);
      digitalWrite(pinLedJaune, HIGH);
      digitalWrite(pinLedOrange, HIGH);
      digitalWrite(pinLedRouge, HIGH);
      etatCourant = ETAT_VJOR;  // et on déclare notre nouvel état courant
      break;

    case ETAT_VJOR: // on a tout qui est allumé et on reçoit le double click
      mettreAuRepos(); // donc on retourne à l'état de repos
      break;
  }
  chrono = currentTime; // on vient d'avoir une action donc on ré-arme notre chronomètre
}

// ------------------------------------------------------
// La fonction appelée en cas de dépassement du délai
// (on pourrait aussi faire un if (etatCourant != REPOS) mettreAuRepos(); )
// ------------------------------------------------------
void timeOut()
{
  /* // version longue si on veut le switch case
    switch (etatCourant) {
    case ETAT_V:    // pour tous les états
    case ETAT_VO:   // sauf celui au repos
    case ETAT_VOJ:  // on doit tout éteindre
    case ETAT_VOJR: // et revenir à l'état initial
      mettreAuRepos(); // ce que fait cette fonction
      break;
    }
  */
  // version courte, si on n'est pas au repos, alors passer au repos
  if (etatCourant != REPOS) mettreAuRepos();

  // à noter que le timeOut continuera de se déclencher toutes les 15 secondes
  // mais ne fera rien puisqu'on sera au repos
  // ça peut être utile pour continuer à faire autre chose
  // sinon il faut tester avant de l’appeler qu’on n’est pas au repos
}

// ------------------------------------------------------
// On initialise notre système dans le setup
// ------------------------------------------------------
void setup() {
  pinMode (pinLedRouge,  OUTPUT);
  pinMode (pinLedOrange, OUTPUT);
  pinMode (pinLedJaune,  OUTPUT);
  pinMode (pinLedVerte,  OUTPUT);

  //conditions Initiales
  mettreAuRepos();

  // On attache la fonction simpleClick() comme callback en cas de simple clic
  button.attachClick(simpleclick);

  // On attache la fonction doubleClick() comme callback en cas de double-clic
  button.attachDoubleClick(doubleclick);
}

void loop() {
  currentTime = millis();

  // On vérifie l'état des boutons, ce qui déclenche l'appel d'une des fonctions callBack si nécessaire
  button.tick();

  // On vérifie le timer et on déclenche l'événement si nécessaire
  // rajouter dans la condition “&& (etatCourant != REPOS)” si vous ne souhaitez pas
  // appeler la fonction au repos
  if (currentTime - chrono >= TimeOut) {
    timeOut();
    chrono = currentTime; // on ré-arme notre chronomètre
  }

  // ici on peut faire autre chose du moment que ça ne prend pas trop longtemps

}
Ce code correspond à l'ensemble du diagramme d'états ![](fsm/fsm_diagram_04.png) Qui bien que "compliqué" car il y a beaucoup de choses à gérer, n'est pas compliqué à programmer, même si on vous rajoute des transitions et des contraintes, car vous avez créé une structure de code saine et évolutive.

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

    Utilisation de fonctions pour gérer chaque état
    enum State { RED, GREEN, YELLOW };
    
    State currentState = RED;
    
    unsigned long previousTime = 0;
    unsigned long currentTime = 0;
    
    void loop() {
      currentTime = millis();
    
      switch (currentState) {
        case RED:
          redState();
          break;
        case YELLOW:
          yellowState();
          break;
        case GREEN:
          greenState();
          break;
      }
    }
    
    void redState() {
      digitalWrite(ledR, HIGH);
      digitalWrite(ledY, LOW);
      digitalWrite(ledG, LOW);
      if (timeElapsed(5000)) {
        currentState = GREEN;
      }
    }
    
    void greenState(){
      digitalWrite(ledR, LOW);
      digitalWrite(ledY, LOW);
      digitalWrite(ledG, HIGH);
      if (timeElapsed(5000)) {
        currentState = YELLOW;
      }
    }
    
    void yellowState(){
      digitalWrite(ledR, LOW);
      digitalWrite(ledY, HIGH);
      digitalWrite(ledG, LOW);
      if (timeElapsed(2000)) {
        currentState = RED;
      }
    }
    
    bool timeElapsed(unsigned long duration) {
      static unsigned long previousTime = millis();
      if (millis() - previousTime >= duration) {
        previousTime = millis();
        return true;
      } else {
        return false;
      }
    }
    

  • Résultat
    Alt text


Exercices

  1. Utilisez le concept de machine à états pour votre laboratoire en cours.

Références