TER3M4M

Accueil > 4M

📚 Notes : plusieurs tâches en parallèle (la multi-tâche) - synchroniser avec millis()

Introduction

Plusieurs programmes que nous avons vus jusqu’à présent utilisent la fonction delay() pour gérer le temps entre les différentes actions du robot. Cependànt, cette fonction bloque le programme pendant le temps spécifié, ce qui peut être problématique si le robot doit effectuer plusieurs tâches en même temps - comme lire des capteurs, contrôler des moteurs, clignoter des DEL. Dans ce cas, il est préférable d’utiliser une autre approche pour gérer le temps, notamment en utilisant la fonction millis(). Par contre, cette fonction vient avec une nouvelle logique de gestion des délais qui est important de comprendre.

En bref

Définitions
bloquer
arrêter l’exécution du programme pour un certain temps, notamment avec delay()
multi-tâche
gérer plusieurs tâches en même temps. Avec certain matériel, c’est possible de lancer plusieurs fils d’exécution en parallèle (comme Scratch), mais il y a un seul fil d’exécution possible avec Arduino. Pour faire de la multi-tâche avec Arduino, on doit gérer les tâches en séquence tout en vérifier si c’est le moment de lancer une instruction spécifique.
millis()
fonction qui retourne le nombre de millisecondes écoulées depuis le démarrage du programme. On peut l’utiliser pour gérer les intervalles de temps sans bloquer le programme.
variable locale
variable déclarée à l’intérieur d’une fonction et qui n’est accessible que dans cette fonction. Elle est détruite à la fin de la fonction sauf si elle est déclarée static.

🛠️ Pratique - mise en place

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

  1. Créer un nouveau projet PlatformIO nommé multi-tasking.
  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.

Exemple - robot qui dance en tournant avec moustaches comme signal d’arrêt

Imagine un robot qui fait 3 tours à gauche et ensuite 3 tours à droite infiniment, sauf si une de ses moustaches est enfoncée. À ce moment, il devrait arrêter de bouger.

Voir le tutoriel sur le capteur de moustache pour ajouter les moustaches à votre robot.

Présumant qu’on implémente le code pour ce comportement comme une machine à états finis, le diagramme d’états est plus simple que celui pour le code précédent :

diagramme d'états multi-tâche

Ici :

Solution avec delay()

Avec ce que nous avons vus jusqu’à présent, incluant la bibliothèque personnelle RobotDrive, un exemple de code pour le robot de l’exemple 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <Arduino.h>
#include <RobotDrive.h>

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

const int rightWhisker = 7;
const int pressed = LOW; // ou 0

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

enum class States {
  SETUP,
  DANCE,
  STOP
};

States currentState = States::SETUP;

/*
DÉCLARATIONS AVANCÉES DES FONCTIONS
*/

void dance();

/*
DÉFINITION DES FONCTIONS DU PROGRAMME
*/

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

// dans une MEF (FSM), sert à vérifier en perpétuité l'état de la machine
void loop() {
  switch (currentState) {
  case States::SETUP:
    currentState = States::DANCE; // transition faite directement
    break;
  case States::DANCE:
    dance(); // appel la fonction pour le code de cet état
    break;
  case States::STOP:
    stop();
    break; // état final : aucune transition
  }
}

// Le code pour l'état States::DANCE
void dance() {
  static const int millisForOneTurn = 2100; // à calibrer; avec turnLeft() et turnRight()

  turnLeft();
  delay(3 * millisForOneTurn);   // attendre 3 tours à gauche
  turnRight();
  delay(3 * millisForOneTurn);  // attendre 3 tours à droite

  if (digitalRead(rightWhisker) == pressed) {
    currentState = States::STOP; // transition faite dans la fonction de l'état
  }
}

Quelques notes sur ce code

Dans ce code, on voit la définition d’une fonction pour définir les instructions pour l’état DANCE. Voici quelques éléments à noter en lien avec cette décision :

Vous pouvez également noter que les transitions de la machine à états finis sont déclarées à différents endroits dans le code :

🛠️ Pratique - analyser la solution avec delay()

  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. Est-ce que vous pouvez fiablement arrêter le robot en enfonçant une moustache?
    • À quel moment est-ce que le robot semble réagir à l’enfoncement de la moustache?
    • Est-ce que le robot s’arrête avant de finir les 3 rotations à gauche ou à droite?
    • Si la moustache n’est pas enfoncée au moment de la transition entre les directions, est-ce que le contact influence le programme?
  4. Si vous bouger la condition qui vérifie l’état de la moustache avant les instructions pour les mouvements, est-ce que le robot réagit différemment?

Introduction à millis()

Avec la solution précédente, le problème est que le programme est bloqué durant chaque delay(). Notamment, la boucle loop() n’est pas en train de se répéter alors on n’arrive pas à l’instruction pour lire les capteurs. Si la moustache est enfoncée pendant un delay(), le robot n’est pas en mesure de le détecter.

La solution est de remplacer le code qui bloque le programme (delay()) par un code qui lui permet d’itérer en continu tout en respectant les délais voulus. Heureusement, il y a la fonction millis() qui nous donnne des lectures de chronomètre en millisecondes depuis le lancemement du programme.

La logique de base pour remplacer delay() par millis() est la suivante :

1
2
3
Est-ce la différence entre maintenant et le temps de référence est plus grande que le délai voulu?
    Si oui, faire la chose voulue et mettre à jour le temps de référence.
    Si non, fait rien.

Ça ne semble peut-être pas majeur comme changement, mais ce l’est! Le temps nécessaire pour passer à travers cette sélection est minime et le code suivant s’exécute immédiatement. Quand ce code se trouve dans une boucle, tout le code de la boucle a le temps de se répéter plusieurs fois avant que le délai soit atteint et la tâche voulue est exécutée.

On atteint alors la possibilité d’une vraie multi-tâche, où plusieurs tâches peuvent être gérées en même temps.

Implémentation de base

Avec la syntaxe Arduino, l’algorithme général ci-dessus donne le code de base suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Arduino.h>

const int waitTime = 3000; // 3 secondes, par exemple
unsigned long referenceTime = millis();

void setup() {}

void loop() {
  if (millis() - referenceTime >= waitTime) {
    // code pour la tâche voulue
    referenceTime = millis(); // mettre à jour le temps de référence
  }
}

Implémentation plus robuste

Parce que le code dans loop() peut devenir assez complexe avec plusieurs tâches, on tend à écrire une fonction spécifique pour chaque tâche. Cela nous donne la possibilité de gérer les variables liées au délai entièrement à l’intérieur de la fonction, ce qui rend le code plus lisible et plus facile à maintenir.

Voici l’exemple précédent réécrit avec une fonction pour la tâche :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Arduino.h>

void doTask(); // déclaration avancée de la fonction

void setup() {}

void loop() {
  doTask();
}

void doTask() {
  static const int waitTime = 3000; // 3 secondes, par exemple
  static unsigned long referenceTime = millis();

  if (millis() - referenceTime >= waitTime) {
    // code pour la tâche voulue
    referenceTime = millis(); // mettre à jour le temps de référence
  }
}

🛠️ Pratique - tester rapidement l’implémentation avec millis()

  1. Créez un nouveau projet PlatformIO nommé millis-test.
  2. Copiez le code ci-dessous dans le fichier /src/main.cpp et le transférez vers une carte Arduino (comme celle dans votre robot).
  3. Vérifier que le DEL intégré clignote toutes les 0.5 secondes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <Arduino.h>

void blink();

void setup() {
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);
}

void loop() {
  blink();
}

void blink() {
  static const int waitTime = 500; 
  static unsigned long referenceTime = millis();

  if (millis() - referenceTime >= waitTime) {
    digitalWrite(13, !digitalRead(13));
    referenceTime = millis(); // mettre à jour le temps de référence
  }
}

Notez l’astuce pour la ligne digitalWrite(13, !digitalRead(13)); : ! est l’opérateur de négation logique qui inverse la valeur d’une variable booléenne. Alors chaque fois qu’on passe sur cette instruction, on donne au DEL (avec digitalWrite) l’état inverse de ce qu’il avait (obtenu avec digitalRead).

Solution avec millis()

Revenant à l’exemple du robot qui dance en tournant avec moustaches comme signal d’arrêt, voici une solution qui utilise millis() pour gérer les délais :

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

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

const int rightWhisker = 7;
const int pressed = LOW; // ou 0

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

enum class States {
  SETUP,
  DANCE,
  STOP
};

States currentState = States::SETUP;

/*
DÉCLARATIONS AVANCÉES DES FONCTIONS
*/

void dance();

/*
DÉFINITION DES FONCTIONS DU PROGRAMME
*/

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

// dans une MEF (FSM), sert à vérifier en perpétuité l'état de la machine
void loop() {
  switch (currentState) {
  case States::SETUP:
    currentState = States::DANCE; // transition faite directement
    break;
  case States::DANCE:
    dance(); // appel la fonction pour le code de cet état
    break;
  case States::STOP:
    stop();
    break; // état final : aucune transition
  }
}

// Le code pour l'état States::DANCE
void dance() {
  static const int millisForOneTurn = 2100; // à calibrer; avec turnLeft() et turnRight()
  static byte toLeft = 1; // direction de rotation

  static unsigned long referenceTime = millis(); // temps de référence
  
  // vérifie s'il faut changer la direction
  if (millis() - referenceTime >= 3 * millisForOneTurn) {
    if (toLeft) {
      turnLeft();
    } else {
      turnRight();
    }
    toLeft = !toLeft; // change la direction
    referenceTime = millis(); // mettre à jour le temps de référence
  }

  // vérifie s'il faut changer l'état
  if (digitalRead(rightWhisker) == pressed) {
    currentState = States::STOP;
  }
}

Quelques notes sur ce code

🛠️ Pratique - analyser la solution avec millis()

  1. Remplacer le code dans /src/main.cpp du projet multi-tasking que nous avons initialisé avec une solution utilisant delay() par le code ci-dessus.
  2. Compilez le code pour vérifier qu’il n’y a pas d’erreurs de transcription.
  3. Téléversez le code vers votre base robotique à entraînement différentiel et observez le comportement du robot.
  4. Est-ce que vous pouvez maintenant fiablement arrêter le robot en enfonçant une moustache?
    • À quel moment est-ce que le robot semble réagir à l’enfoncement de la moustache?
    • Est-ce que le robot s’arrête avant de finir les 3 rotations à gauche ou à droite?
    • Si la moustache n’est pas enfoncée au moment de la transition entre les directions, est-ce que le contact influence le programme?