TER3M4M

Accueil > 4M

📚 Notes : plusieurs tâches en séquence - la machine à états finis

Introduction

Dans le domaine de la robotique, le robot autonome peut avoir à choisir quelle tâche accomplir en fonction de son environnement. Par exemple, un robot aspirateur peut avoir à décider s’il doit nettoyer une pièce ou retourner à sa base de recharge. Pour gérer ces situations, on utilise souvent une machine à états finis (FSM - Finite State Machine en anglais) pour décrire les différents états possibles du robot et les transitions entre ces états.

Dans cette leçon, nous allons voir comment représenter une machine à états finis avec un diagramme d’états et comment le programmer pour un microcontrôleur Arduino (ou tout autre microcontrôleur compatible avec le langage C++).

La machine à états finis est utile pour choisir une tâche ou une action parmis plusieurs tâches et se concentrer uniquement sur la tâche choisie. Pour accomplir plusieurs tâches ou sous-tâches simultanément, on utilise une autre approche appelée la multi-tâche. C’est le sujet de la prochaine leçon. Les deux approches sont souvent combinées pour gérer le fonctionnement d’un robot autonome.

En bref

Définitions
Machine à états finis (FSM)
un système qui, à un moment donné, peut être dans un seul état parmi un nombre défini d’états. Chaque état est associé à un ensemble d’actions et de transitions vers d’autres états.
Diagramme d’états
une représentation graphique d’une machine à états finis. Chaque état est représenté par un ovale et chaque transition par une flèche. On peut annoter les flèches avec la condition de transition. Ainsi, le diagramme d’états remplace à très haut niveau le pseudocode pour décrire le comportement du robot.

Énumération (enum) : un type de données qui permet de définir un ensemble de constantes numériques. C’est utile pour définir les différents états de la machine à états finis parce qu’on peut les nommer de façon descriptive au lieu de se rappeler du chiffre associé.

Switch-case
une structure de contrôle qui permet de comparer une variable à une liste de valeurs possibles. C’est une façon plus lisible qu’une structure if-else if pour gérer ce type de cas. Dans les FSM, la valeur à comparer est l’état et chaque valeur possible est un état défini dans l’énumération.

Exemple - robot qui pivote à gauche et à droite puis s’arrête

Imagine un robot qui fait 3 tours à gauche et ensuite 3 tours à droite et finalement s’arrête.

C’est un exemple très simple qu’on peut programmer (avec raison) sans une machine à états finis. Mais sa simplicité nous permet de nous concentrer sur les nouveaux éléments de planification et de code qu’on voudra inclure pour gérer des cas plus complexes qui en auraient de besoin.

Diagramme d’états

Voici le diagramme d’états pour ce robot :

diagramme d'états pour l'exemple simple

Dans d’autres contextes, c’est possible qu’il y ait des boucles dans le diagramme d’états. Par exemple :

Il pourrait aussi y avoir des embranchements dans le diagramme d’états. Par exemple :

L’extension Draw.io Integration de Henning Dieterichs

Si vous ajoutez l’extension Draw.io Integration à VS Code, vous pouvez produire des diagrammes comme celui ci-dessus directement dans VS Code. Simplement créer un nouveau fichier avec l’extension de fichier .drawio.png et l’ouvrir en choisissant Draw.io comme éditeur. Vous aurez accès à la même interface que sur le site web app.diagrams.net mais sans avoir à quitter votre environnement de travail ni à télécharger le fichier pour l’inclure dans votre projet.

🛠️ Pratique - mise en place

Préparer votre projet maintenant pour le reste des exercices qui suivent à la fin des notes.

  1. Créez un nouveau projet PlatformIO nommé FSM.
  2. Configurez votre projet en lui ajoutant les bibliothèques nécessaires :
    1. Ajoutez la ligne suivante à son fichier platformio.ini : lib_deps = arduino-libraries/Servo@^1.2.1 afin d’ajouter la bibliothèque externe Servo à votre projet.
    2. Copier le dossier RobotDrive de vos bibliothèques personnelles dans le dossier lib du projet.
  3. Copiez le diagramme d’états plus haut dans le dossier /src de votre projet en faisant un clic-droit et en choisissant Enregistrez l'image sous... pour le télécharger. C’est un fichier de type .drawio.png que vous pouvez ouvrir et modifier avec l’extension Draw.io Integration de VS Code.

Énumération des états

Le code pour déclarer nos états en C++ ressemblerait, en version la plus simple à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
// ... directives #include
// ... déclarations d'autres variables globales

enum class States {
  SETUP,
  TURN_LEFT,
  TURN_RIGHT,
  STOP
};

States currentState = States::SETUP;

// ... fonctions setup(), loop() et autres

Internellement, C++ assigne une valeur entière à chaque état dans l’énumération. Par défaut, la première valeur est 0 et chaque valeur suivante est incrémentée de 1. Ainsi, SETUP est 0, TURN_LEFT est 1, TURN_RIGHT est 2 et STOP est 3. Par défaut, chaque état reçoit une valeur unique. On peut définir nos propres valeurs si on veut, mais c’est rarement nécessaire pour une FSM où c’est simplement l’identifiant de l’état qui importe.

Alternative sans enum

Sans un enum on peut déclarer nos états comme suit mais là on passe du temps à inventer des valeurs pour chaque état et on risque de faire des erreurs :

1
2
3
4
5
6
7
// alternative sans enum - déconseillée
const int SETUP = 0;
const int TURN_LEFT = 1;
const int TURN_RIGHT = 2;
const int STOP = 3;

int currentState = SETUP; // même type que les états (int)

Structure de contrôle switch-case

Avec une machine à états finis (MEF ou FSM en anglais), la fonction loop() d’un sketch Arduino sert uniquement à vérifier en continue une cascade conditionnelle pour savoir quel code exécuter en fonction de l’état courant du robot.

Utiliser if-else if

Avec if-else if, cela ressemblerait à ceci :

1
2
3
4
5
6
7
8
9
10
11
void loop() {
  if (currentState == States::SETUP) {
    currentState = States::TURN_LEFT; // transition immédiate
  } else if (currentState == States::TURN_LEFT) {
    // code pour l'état TURN_LEFT
  } else if (currentState == States::TURN_RIGHT) {
    // code pour l'état TURN_RIGHT
  } else if (currentState == States::STOP) {
    // code pour l'état STOP
  }
}

On peut voir qu’à chaque fois que la boucle se répète, on vérifie l’état actuel du robot afin de choisir le code approprié à exécuter. Si l’état ne change pas, on exécute le même code à chaque itération de la boucle. Il faut alors inclure un mécanisme pour activer la condition de transition vers le prochain état dans le code pour chaque état.

La première transition, de l’état SETUP à l’état TURN_LEFT, est immédiate car la condition est simplement la fin de la fonction setup(). Ainsi, la première fois que la fonction loop() est appelée, on passe directement à l’état TURN_LEFT.

Cette structure if-else if est tout à fait acceptable, mais on se répète beaucoup : la condition est toujours currentState == ÉTAT. En plus, ces conditions sont un peu masquées par la structure du code, soit derrière } else if () et on peut avoir de la difficulté à trouver un cas spécifique.

Il y a une meilleure structure pour ce type de comparaison : switch-case.

Utiliser switch-case

Avec switch-case, la même logique ressemblerait à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void loop() {
  switch (currentState) {
    case States::SETUP:
      currentState = States::TURN_LEFT; // transition immédiate
      break;
    case States::TURN_LEFT:
      // code pour l'état TURN_LEFT
      break;
    case States::TURN_RIGHT:
      // code pour l'état TURN_RIGHT
      break;
    case States::STOP:
      // code pour l'état STOP
      break;
  }
}

C’est immédiatement beaucoup plus lisible, mais il y a quelques détails à noter. Voici la structure générale d’un switch-case :

1
2
3
4
5
6
7
8
switch (variable) {
  case VALEUR1:
    // code pour VALEUR1
    break;
  // autres cas
  default:
    // code pour toutes les autres valeurs
}

Tout ensemble

Intégrant les nouveautées pour la FSM et le code que nous avons utilisé pour gérer les déplacements du robot (la bibliothèque personnelle RobotDrive), le code pour la FSM pourrait ressembler à ceci :

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
#include <Arduino.h>
#include <RobotDrive.h>

/*
DÉFINIR LES CONNEXIONS MATÉRIELLES
*/

const int millisForOneTurn = 2100; // à calibrer; avec turnLeft() et turnRight()

/*
DÉFINIR LES ÉTATS DU ROBOT
*/

enum class States {
  SETUP,
  TURN_LEFT,
  TURN_RIGHT,
  STOP
};

States currentState = States::SETUP;

// initialiser le matériel et les connexions
void setup() {
  setRobotDrivePins(10, 11);
}

// dans une MEF (FSM), sert à vérifier en perpétuité l'état de la machine
void loop() {
  switch (currentState) {
  case States::SETUP:
    currentState = States::TURN_LEFT;
  case States::TURN_LEFT:
    turnLeft();
    delay(3 * millisForOneTurn);
    currentState = States::TURN_RIGHT;
    break;
  case States::TURN_RIGHT:
    turnRight();
    delay(3 * millisForOneTurn);
    currentState = States::STOP;
    break;
  case States::STOP:
    stop();
    break;
  }
}

Vous voyez sans doute que la FSM n’était pas nécessaire ici : on aurait pu simplement décrire les trois tours d’un côté et les trois tours de l’autre dans la fonction setup() et laisser la fonction loop() vide. Notamment, les conditions de transition ici sont simplement la fin de l’état précédent.

En général, la condition de transition est plus complexe que cela. Et certains états doivent gérer plusieurs actions en simultané. C’est là que la FSM devient très utile pour organiser le code. On verra un exemple dans la leçon sur la multi-tâche.

🛠️ Pratique - suite

  1. Copiez le code ci-dessus dans le fichier /src/main.cpp et compilez-le pour vérifier qu’il n’y a pas d’erreurs de transcription.
  2. Téléversez le code vers votre base robotique à entraînement différentiel et observez le comportement du robot.
  3. Calibrez la constante millisForOneTurn pour que le robot fasse exactement 3 tours à gauche et 3 tours à droite.
  4. Définissez un nouvel état de votre choix.
    1. Ajoutez cet état dans l’énumération.
    2. Modifiez le diagramme d’états pour inclure votre nouvel état. Vous devrez avoir :
      • une transition vers cet état
      • une transition de cet état vers un autre état
    3. Ajoutez un cas pour cet état dans la structure switch-case.
    4. Modifiez le code pour le faire correspondre au diagramme d’états modifié :
      • changez la transition de l’état précédent vers cet état
      • ajoutez le code actif pour cet état
      • ajoutez une transition de cet état vers le prochain état