Eric nous avait présenté il y a quelque temps une introduction aux OS temps-réel sur les microcontrôleurs. Je vous invite à relire ses articles et ses expérimentations avec AVRX :
Je vais faire quasiment de même en utilisant des instructions particulières du langage Dynamic C et tester sur processeur Rabbit 5000.
La programmation d’un robot se résume la plupart du temps à une machine à état : une variable stocke l’état courant et une boucle infinie teste les entrées/sorties du robot pour faire changer l’état en fonction des événements analysés.
On a donc généralement un code très simple :
// déclaration
int etat_robot ;
main()
// initialisation
etat_robot = ETAT_INITIAL ;
// boucle infinie
while (1)
// tests E/S et périphériques et communications ...
if (test_bit(0))
etat_robot = ETAT_DEMARRE ;
if (test_reception_octet(’C’))
etat_robot = ETAT_TRAITEMENT_C ;
// le traitement de la machine à état
switch (etat_robot)
case ETAT_INITIAL :
// prépare ceci, interroge cela, etc.
etat_robot = ETAT_ATTEND_DEPART ;
// on peut gérer la machine à état aussi ici
break ;
case ETAT_ATTEND_DEPART :
// pas grand chose
... etc ...
Cela s’implémente sans problème sur un microcontrôleur et on peut même terminer 4ème de la Coupe de France avec (si vous êtiez pas déjà au courant..)
Mais cela nécessite de gérer avec précision le temps qu’on passe dans nos états pour ne pas gêner l’acquisition des signaux. De plus, on n’a pas toujours un déroulement séquentiel et il va falloir des astuces d’écriture pour faire dans 2 ou 3 états ce qui s’exécute finalement en parallèle en mêlant le code spécifique à l’état courant du code commun à plusieurs.
Ce sont les deux raisons du passage à une programmation multi-tâche : ne pas avoir à compter les millisecondes passées dans tel ou tel endroit du code et pouvoir simplifier l’écriture du programme en séparant les parties du programme dédiées à des tâches s’exécutant en parallèle mais ne gagnant pas à être écrites de manière enchevêtrée.
La solution : le multitâche coopératif
Avant d’étudier un OS temps réel, avec exclusion mutuelle, sémaphores et tout le toutim, on va faire un peu de multitâche simple. C’est celui qu’on trouvait sur les premiers systèmes d’exploitation et qui est décrit dans l’article Wikipedia que j’ai référencé.
L’expérimentation étant faite sur le module RCM5700, on va bénéficier du langage Dynamic C qui gère la création de tâches parallèles.
L’approche est simple, on l’avait déjà utilisée dans l’article sur les premiers pas avec le processeur Rabbit. On va déclarer chacun des états de notre machine initiale dans un "costate" distinct.
Voici ce que ça donne : il s’agit ici d’attendre un interrupteur - jack en langage de Coupe Eurobot - qui va déclencher le démarrage du match. On va alors compter les secondes pour s’arrêter à 90 s.
// déclarations de variables
int seconds ;
main()
// initialisations des variables
seconds = -1 ; // le match n’est pas encore démarré
// initialisation des entrées/sorties
BitWrPortI(PDDR, &PDDRShadow, 0, 1) ;
BitWrPortI(PDDDR, &PDDDRShadow, 0, 1) ;
BitWrPortI(PDFR, &PDFRShadow, 0, 1) ;
while (1)
costate
waitfor(!BitRdPortI(PDDR,1)) ;
printf("Jack out !\r\n") ;
seconds = 0 ;
waitfor(DelayMs(200)) ; // anti rebond
costate
waitfor(DelaySec(1)) ;
printf("seconds : %d\r\n",seconds++) ;
Le résultat dans la console de l’environnement de développement :
Bon, ça fonctionne mais on a perdu l’enchainement : on ne veut pas commencer à compter les secondes tant qu’on n’a pas démarré.
Pour cela, on va nommer chacun de nos états. On les déclare (cela s’appelle des "CoData" :
// déclarations de variables
int seconds ;
CoData cd1,cd2 ;
Et on ajoute ce nom de variable derrière le mot-clé "costate" :
costate cd1
Observons ce qui se passe :
Plus rien, que ce soit dans l’affichage du chrono ou dans l’attente du jack de départ : normal car maintenant qu’ils sont nommés, ces CoData ne sont pas lancés au démarrage. Il faut utiliser pour cela une fonction, "CoBegin()" prenant en paramètre le CoData concerné.
Par contre, cela signifie que le CoData doit être "réarmé" à chaque fois. C’est très utile pour le jack (on en a besoin "sur commande") mais pas pour le chrono, qui fonctionne seul une fois lancé. Pour cela, il y a le mot clé "always_on" après la déclaration du CoData et les commandes "CoPause()" et "CoResume()" pour gérer le premier appel au chrono.
Le premier CoData va être activé au démarrage (il attend le jack de départ), tandis que le second CoData (le chrono) sera lancé depuis le premier, lorsque le jack est sorti.
Voici le code définitif :
// déclarations de variables
int seconds ;
CoData cd1,cd2 ;
main()
// initialisations des variables
seconds = -1 ; // le match n’est pas encore démarré
// initialisation des entrées/sorties
BitWrPortI(PDDR, &PDDRShadow, 0, 1) ;
BitWrPortI(PDDDR, &PDDDRShadow, 0, 1) ;
BitWrPortI(PDFR, &PDFRShadow, 0, 1) ;
// attendre le jack
CoBegin(&cd1) ;
CoPause(&cd2) ;
while (1)
costate cd1
waitfor(!BitRdPortI(PDDR,1)) ;
printf("Jack out !\r\n") ;
seconds = 0 ;
CoResume(&cd2) ;
waitfor(DelayMs(200)) ; // anti rebond
costate cd2 always_on
waitfor(DelaySec(1)) ;
printf("seconds : %d\r\n",++seconds) ;
if (seconds >= 10)
printf("Fin du match !\r\n") ;
CoPause(&cd1) ;
CoPause(&cd2) ;
// etc.
// le match est fini, on éteint tous les threads
Et le résultat : j’appuie plusieurs fois sur le bouton figurant le jack de départ mais le code d’écoute n’étant plus exécuté, rien ne se passe et le chrono égrenne les secondes. A la fin du match on arrête tous les CoData (10s mais je jure que ça fonctionne avec 90s).
On va alors pouvoir gérer de cette manière une machine à état tout à fait classique. On pourra rajouter ensuite autant de tâches parallèle, pour gérer la communication série, pour gérer les entrées-sorties, etc..