C'est une petite réflexion sur le multitâche dans arduino. C'est mon quart d'heure pensée profonde de l'été. C'est de l'apologie assumée du non-bloquant. Je mettais mes idées en ordre avant de me remettre sur Emile.
Le concept selon wikipedia :
"La simultanéité apparente est le résultat de l’alternance rapide d’exécution des processus présents en mémoire. Le passage de l’exécution d’un processus à un autre est appelé commutation de contexte. Ces commutations peuvent être initiées par les programmes eux-mêmes (multitâche coopératif) ou par le système d’exploitation lors d’événements externes (multitâche préemptif)."
Cool !! Dans la procédure loop(), quand on appelle différentes librairies les unes après les autres, on fait du multitâches peut-être sans le savoir. Le mot "apparente" dans la définition est importante. Notre arduino favori ne fera qu'une seule chose à la fois. Mais tellement vite que cela semble être fait simultanément.
Piloter le driver de moteurs, compter les ticks des roues codeuses, afficher un message, commander et lire un capteur, envoyer et recevoir sur un port série, exécuter la logique de décision du robot ... On ne met pas un arduino pour chacune de ces fonctions mais on peut vouloir dédier un arduino pour la lecture des ticks des roues codeuses. Car on le fait avec des interruptions. A chacune des interruptions, Arduino sauvegarde l'état des registres du programme principale, exécute la procédure de l'interruption, rétabli les registres et reprend le programme principale. On voit bien qu'il va être difficile pour l'arduino de capter un 2e ticks qui arriverait pendant une interruption. C'est pour cela que les interruptions doivent être très courtes. C'est une manière de faire du multitâches avec arduino en mode préemptif. C'est très pratique mais on voit apparaître une limite. A partir d'une certaine fréquence d'interruption, l'arduino ne pourra plus les exécuter toutes. Avant d'atteindre sa limite, il ne fera plus que traiter ces interruptions. On peut choisir d'ignorer les interruptions pendant un temps dans le programme principale mais pas plus. C'est pour cela qu'on dédie un arduino, pour maximiser le nombre de ces lectures. Malgrés la limite, c'est le meilleur moyen de lire des évènements déclenchés par un composant du robot extérieur à l'arduino.
De là à dire qu'il faut 2 arduino dans un robot, j'en suis pas loin. C'est un moyen radicale de faire du multitâche.
L'autre manière de faire du multitâches, c'est le mode coopératif. Il ne règle pas le problème qu'on vient de voir. On est toujours dans les limites de fonctionnement d'un micro contrôleur. Il permet de faire du multi tâches en le gérant soit même dans le programme. On le fait quand on est pas dépendant d'évènements extérieur et quand on a pas de contraintes de mesure de temps. Il consiste à faire des sous-programmes avec des fonctions ou des classes. À réserver des variables à chaque sous programmes. Et à appeler ces sous programmes dans la procédure loop(). C'est le même concept que l'interruption quand arduino sauvegarde les registres et les restaure après l'interruption. L'arduino a besoin de remplacer ses variables (ou registres) pour laisser la place aux données de l'interruption. Quand on le fait soit-même dans un programme, on ne va pas remplacer, on va avoir plusieurs variables, autant qu'il en faut pour chacun des sous programmes.
Pour illustrer, je vais expliquer pourquoi il faut se passer de la procédure delay(). Et comment ? En faisant du multitâche coopératif. Admettons qu'on veuille piloter la vitesse de rotation d'un servo moteur. Sans pilotage particulier, le servo ira à la position demandée aussi vite qu'il peut. Là, on veut qu'il aille à 10° par secondes. On est dans loop(). On a une variable qui contient la position courante. On commence à 0°, on place le servo à 0°, on attend 1 seconde avec delay(), on avance de 10° et ainsi de suite. Le résultat est un peu saccadé. On se dit qu'on va faire mieux en avançant de 1° à la fois avec des attentes de 100 ms. On est bien à la vitesse voulu et c'est pas saccadé. Génial !! On a réussi. Maintenant, on va placer un 2e servo. À la suite du programme, dans loop(), je vais piloter mon 2e moteur avec le même principe. Je déplace le moteur 1 de 1°, j'attend 100ms, je déplace le moteur 2 de 1°, j'attend 100ms et ainsi de suite. Ça fonctionne mais la vitesse est divisée par 2. Le temps d'attente est maintenant de 200ms. Je réfléchi 5 min et je décide de diviser par 2 les valeurs dans le delay(). Tout fonctionne bien.
Pour mon humanoïde avec 13 dof, je vais diviser par 13 pour conserver ma vitesse ... J'arrête là ce raisonnement. Tous les moteurs ne fonctionnent pas en même temps et pas tous à la même vitesse. Je n'ai pas envie de calculer chaque valeur de delay() pour mes 13 servo. La solution consiste à ne pas attendre, a ne pas utiliser de fonction bloquante et laisser la boucle loop() s'exécuter aussi vite qu'elle peut. Indépendamment de la période de calcul des servo. Pour utiliser le temps d'attente du delay() et pour laisser le temps aux autres calculs. Il n'y a pas que la vitesse des moteurs à calculer. Alors comment on fait ? Le principe : je connais la position courante et la vitesse que doit avoir chacun des moteurs. A chaque passage dans loop(), je regarde l'heure qu'il est (toujours en ms). Et pour chaque moteur, je calcul la position où il doit être pour cette heure de passage dans loop(). On ne maîtrise pas le délai entre 2 passages dans loop() mais on le mesure et on en déduit la position.
Pour y arriver, on va dire que le calcul de la position d'un servo est un sous programme. Chaque servo doit avoir ses propres variables : vitesse, heure de départ, position de départ et position d'arrivée. Faire une classe Servo est tout indiqué. On va utiliser la fonction map().
La durée du parcours est donnée par la formule : vitesse = abs((position d'arrivée - position de départ)) / durée du parcours.
L'heure d'arrivée est donnée par l'heure de départ + durée du parcours.
Et chaque moteur est piloté par la fonction suivante.
long position courante = map(heure qu'il est, heure de départ, heure d'arrivée, position de départ, position d'arrivée);
Pour boucler la boucle, il y a le cas du capteur ultrason HC-SR04. Pour avoir une lecture de la distance, la fonction pulseIn va mesurer le temps qu'un pin du module ultrason reste à l'état haut. L'enjeu consiste à détecter avec précision les changements d'état. PulseIn est une instruction bloquante. Le temps d'attente est dépendant de la distance. Pendant ce temps, on voudrait que l'arduino fasse autre chose. Dans ce but, on pourrait avoir envie de mesurer le temps entre le front montant et le front descendant avec un code multitâches coopératif. Dans la boucle, je lis l'état du capteur. s'il passe à haut, je note l'heure (en ms). Je continue de lire à chaque passage de la boucle. S'il repasse à bas, je note aussi l'heure. La différence de temps entre ces 2 horaires maximise la largeur de l'impulsion. C'est possible mais on perd en précision. La précision serait dépendante de la longueur de la boucle. C'est à dire la quantité d'instruction qu'on a placé dans le procédure loop(). Même si la fonction pulseIn() ressemble à la procédure delay() par on caractère bloquant, on peut être tenté de l'utiliser. Parce que c'est simple à faire. Et si on veut en optimiser le résultat, on peut même ignorer les interruptions pendant qu'elle attend ... Cela fonctionne très bien, mais là on coupe toute possibilité de multitâches !! Et si on a des roues codeuses à intercepter, soit on arrête le robot pendant qu'on utilise le capteur, soit on prend un 2e arduino. La bonne solution est de passer par les interruptions pour intercepter les changements d'état du capteur. Le seul inconvénient est qu'il faudra un arduino avec plus de 2 pin d'interruption si on souhaite les codeuses en même temps.