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.
delay()
static
.Préparer votre projet maintenant pour le reste des exercices qui suivent à la fin des notes.
multi-tasking
.platformio.ini
: lib_deps = arduino-libraries/Servo@^1.2.1
afin d’ajouter la bibliothèque externe Servo
à votre projet.RobotDrive
de vos bibliothèques personnelles dans le dossier lib
du projet.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 :
Ici :
DANCE
s’occupe d’alterner la direction de pivotement entre gauche et droite à chaque trois secondes, comme si on clignote une DEL.DANCE
à STOP
est déclenchée par l’enfoncement d’une moustache (une lecture de capteur).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
}
}
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 :
switch-case
de loop()
le rend plus facile à lire.millisForOneTurn
est seulement utilisée dans la fonction dance()
, on l’a bougé de sa déclaration globale au début du fichier à une déclaration locale dans la fonction dance()
. Cela rend la fonction plus facile à gérer et plus portable parce que la constante qu’elle utilise est déclarée à l’intérieur de son bloc de code.
On y ajoute le mot-clé
static
pour que la variable soit initialisée une seule fois et conservée entre les appels de la fonction.
Vous pouvez également noter que les transitions de la machine à états finis sont déclarées à différents endroits dans le code :
SETUP
à l’état DANCE
est faite directement dans la structure switch-case
de loop()
parce que c’est la seule instruction pour cet état.DANCE
à l’état STOP
est faite dans la fonction dance()
parce que c’est là que la condition pour la transition est vérifiée.STOP
n’a pas de transition parce que c’est l’état final du robot.delay()
/src/main.cpp
et compilez-le pour vérifier qu’il n’y a pas d’erreurs de transcription.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.
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
}
}
unsigned long
pour pouvoir contenir les valeurs de 0 à 4,294,967,295 (en millisecondes, c’est environ 50 jours). Un unsigned int
ne suffirait pas pour des délais de plus de 65 secondes!millis()
nous donne le temps actuel en millisecondes depuis le lancement du programme. On l’utilise chaque fois qu’on a besoin du temps (comme pour initialiser referenceTime
, pour calculer la différence entre maintenant et referenceTime
et pour réinitialiser referenceTime
).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
}
}
loop()
, on doit déclarer la fonction avant loop()
pour que le compilateur sache qu’elle existe. C’est ce que fait la ligne void doTask(); // déclaration avancée de la fonction
.waitTime
et referenceTime
sont maintenant des variables locales à la fonction doTask()
. Elles sont static
pour être :
doTask()
sans même changer les noms des varaibles internes à la fonction, soit les variables locales. Parce que ces variables sont seulement accessibles dans la fonction où elles sont déclarées, des variables avec le même nom dans différentes fonctions ne seront pas confondues.millis()
millis-test
./src/main.cpp
et le transférez vers une carte Arduino (comme celle dans votre robot).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 (avecdigitalWrite
) l’état inverse de ce qu’il avait (obtenu avecdigitalRead
).
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;
}
}
loop()
est appelée en continu, donc la fonction dance()
est appelée en continu aussi. C’est pourquoi on peut utiliser millis()
pour gérer les délais : on passe une fois à travers le code et quelques instants plus tard, on passe à nouveau à travers le code, et ce, infiniment.dance()
attend avant de donner une nouvelle instruction de mouvement, il faut donner une première instruction de mouvement dans setup()
afin qu’il se mette en mouvement immédiatement. Sinon il faudrait attendre le premier délai de 3 * millisForOneTurn
avant de voir le robot bouger.if
et non dans une séquence d’instructions incluant delay()
, il faut un autre mécanisme pour alterner la direction de rotation : on a ajouté une variable toLeft
qui est static
pour être conservée entre les appels de la fonction dance()
. Sa valeur s’inverse (entre 1 et 0) à chaque fois que le délai est atteint.millis()
/src/main.cpp
du projet multi-tasking
que nous avons initialisé avec une solution utilisant delay()
par le code ci-dessus.