Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Programmation > Apprendre à coder > Les micro-contrôleurs > RTOS sur AVR > AvrX > Tutorial 1 : les inévitables LEDs clignotantes

Tutorial 1 : les inévitables LEDs clignotantes

dimanche 10 décembre 2006, par Eric P.

Et bien, comme d’hab on va faire nos premiers pas en faisant clignoter des LEDs.

Ce tutorial est basé sur l’exemple Timers.c inclus dans la distribution d’AvrX, mais dépouillé de ce qui ne nous intéresse pas pour l’instant.

Y sont illustrées deux fonctionnalités de base :
 les tâches
 les timers

L’application

L’ambition de cette application est assez rudimentaire : faire clignoter deux LEDs avec des périodes différentes et également des rapports cycliques personnalisés.

Le source suppose que les deux LEDs sont connectées entre Vcc et les pins 0 et 1 du port D, au travers d’une résistance de limitation de courant. Se reporter aux premiers articles de la rubrique Les µC sans ta mère pour les détails d’un tel montage.

Analyse du source

Voici le source principal de l’application :

tutorial1.c

Commençons la visite.

On commence par trouver les includes habituels de WinAVR (io.h et interrupt.h) auxquels s’ajoutent :
 avrx.h qui définit l’API de la librairie AvrX (celle que nous avons compilée pendant la phase de mise en place de l’environnement)
 hardware.h qui sert juste à configurer certains aspects hardware, et notamment le port sur lequel se trouvent les LEDs. On en reparlera plus tard, mais il est de toute manière assez trivial à comprendre.

La gestion du temps

Viennent ensuite la définition des deux timers qui vont nous servir à cadencer les LEDs. La macro AVRX_TIMER déclare un bloc de contrôle pour un timer. Il s’agit d’une simple structure, qui est utilisée par toutes les fonctions relatives aux timers. Pour plus de détail, se reporter au fichier avrx.h.

On trouve ensuite l’interrupt handler pour l’interruption Timer0 overflow. Cette déclaration utilise la macro AVRX_SIGINT. Il est nécessaire d’utiliser cette macro et non SIGNAL ou INTERRUPT pour être compatible avec la gestion de contexte d’AvrX.

On va y trouver principalement l’appel au système de gestion des timers d’AvrX, via la fonction AvrXTimerHandler(). Cet appel est compris entre les appels à IntProlog() et Epilog(), et après avoir remis le compteur du timer à sa position de départ pour qu’il continue sont oeuvre.

Le IntProlog()/Epilog() permet la commutation entre les contextes du kernel et celui de l’application (des tâches). Il existe à ce titre deux versions de Prolog() :
 IntProlog() qu’on voit ici qui est prévue pour s’exécuter au sein d’un interrupt handler
 Prolog() qui fait la même chose mais en dehors de ce cas particulier
La différence réside dans les sauvegardes de registres et de contexte d’exécution qui sont faites. Tout cela est expliqué sur le site d’AvrX, et se retrouve dans les sources d’AvrX (pour autant qu’on sache lire l’assembleur AVR, mais tôt ou tard il faut y passer).

Comme précisé dans la documentation, depuis la version 2.6, IntProlog() ne démasque pas les interruptions. Si on veut être en mesure de les traiter, il faut ajouter un appel à la fonction EndCritical() (et non pas EndCriticalSection() comme indiqué sur le site de l’auteur) pour le faire.

L’interrupt handler peut également contenir tout code spécifique à l’application. Dans un de ses articles, Larry Barello présente la structure de handler qu’il utilise couramment et qui consiste à :
 faire tourner le timer a grande vitesse
 y gérer des aspect critiques tels que PWM, lecture d’encodeurs ou autres
 toutes les n passages (en fonction de la résolution souhaitée), passer dans la séquence vue ci-dessus pour faire fonctionner normalement la gestion des timers AvrX.

Les tâches

C’est là qu’on va faire notre boulot.

Une tâche est définie par la macro <code<AVRX_GCC_TASKDEF. Sous cette forme, non seulement elle déclare la fonction qui contient le code, mais aussi le bloc de contrôle de la tâche utilisé par le moniteur d’AvrX. C’est une version condensée de :
 AVRX_GCC_TASK qui déclare simplement le bloc, et permet de déporter la fonction plus loin
 CTASKFUNC qui déclare la fonction elle-même

Il existe aussi la macro AVRX_EXTERNTASK qui permet de référencer une tâche définie dans un autre fichier source.

Cette macro prend 3 paramètres :
 le nom de la tâche, utilisé en interne pour générer les identificateurs des structures de données et de la fonction associées
 la taille du stack (en byte)
 la priorité de la taille

La taille du stack est la partie délicate de l’opération car il faut l’évaluer correctement pour d’une part ne pas allouer plus de mémoire que nécessaire, et d’autre part ne pas se trouver à cours de stack avec à la clé un magnifique plantage tout à fait imprévisible et particulièrement difficile à pister. A noter que la définition des macros intègre automatiquement le stack nécessaire à la gestion interne de la tâche. La quantité indiquée par le paramètre est donc ce qui est propre à l’application. Pour l’évaluer, il faut tenir compte des variables locales (on aura donc tout intérêt à utiliser des variables en registres si possible) et des appels faits à d’autres fonctions. Reportez-vous au site de l’auteur, car il y indique des méthodes d’évaluation applicables.

Le contenu de la fonction qui s’exécute dans la tâche est une boucle sans fin dans laquelle on ne se préoccupe que du boulot à faire, et sans se soucier des autres actions de notre application. C’est là toute la simplification apportée par un noyau de ce type par rapport à une approche à base de FSM. 

Le code est assez clair à comprendre :
 on commence par démarrer un de nos timers, pour une durée de 800 ms via l’appel à AvrXStartTimer()
 on attend ensuite son expiration via l’appel à AvrXWaitTimer() qui est bloquant
 on fait ce qu’on a à faire avec la LED
 on effectue la deuxième partie du cycle de clignotement, qui ressemble comme deux gouttes d’eau à la première

C’est tout.

La deuxième tâche est sur le même modèle, avec les différences suivantes :
 on a utilisé AvrXDelay() qui encapsule la séquence AvrXStartTimer()/AvrXWaitTimer(), ce qui est très pratique si on juste besoin d’une pause
 il n’y a pas de deuxième partie de cycle, car on veut un clignotement symétrique (rapport de cycle = 50%)

Le programme principal

Il consiste à initialiser le contexte d’AvrX, ainsi que les périphériques du micro comme à l’accoutumée. A noter qu’il démarre dans le contexte du kernel.

Une fois cela fait, on schedule les tâches pour leur exécution en appelant AvrXRunTask() pour chacune d’entre elles.

Puis on lance la machine en quittant le contexte kernel via Epilog() précédemment rencontré. A partir de là, toute notre installation est en branle et le main n’a plus rien à faire.

La définition de la configuration

Elle est dans le fichier hardware.h ci-après.

hardware.h

Les seules choses qui nous sont spécifiques sont les définitions de LED et LEDDDR. Tout le reste est de la tambouille interne, notamment pour définir le décompte du timer0 en fonction de la clock, du prescaler et de la résolution souhaitée.

Il y a aussi des choses relatives à la configuration de la vitesse du port série, qu’on aurait pu supprimer car non utilisées. C’est en fait employé par le petit moniteur de debug qu’on peut ajouter et qui dialogue avec l’extérieur par liaison série.

Quelques précisions sur la résolution du timer

La base de temps de notre système est définie par la constante TICKRATE, qu’on peut configurer via le makefile (et qui prend la valeur de 1000 par défaut). Elle est exprimée en nombre de ticks par seconde (1000 ici). Autrement dit, l’unité de mesure du temps sera donc la milli-seconde, et c’est avec cette unité que seront exprimés les délais dans AvrXStartTimer et consorts.

Comment ça marche ?

Le compteur auquel est initialisé le timer 0 est calculé de manière à ce que l’overflow arrive environ toutes les milli-secondes. Ce calcul est effectué par la ligne :

contenue dans le source hardware.h A noter que le 256 correspond à la valeur du prescaler utilisé, et qui est défini en dur par AvrX. C’est cependant assez simple à changer :

 tout d’abord, il faut modifier la définition de TMC8_CK256 dans hardware.h de manière à l’initialiser avec la configuration des bits CS00 à CS02 du registre TCCR0 (se reporter au datasheet de l’AVR pour tous les détails) correspondant au prescaler souhaité. Au passage, le choix de l’identificateur TMC8_CK256 n’est pas judicieux, car il présume qu’on utilise uniquement la valeur de 256.

 ensuite, on modifier la définition de TCNT0_INIT pour y replacer 256 par le prescaler retenu. Il aurait été mieux que cette valeur soit un define également.

Mon intime conviction est qu’il serait d’ailleurs préférable d’utiliser un prescaler de 64, car le calcul de TCNT0_INIT tombe alors juste, et qu’il n’y a de ce fait plus l’erreur d’arrondi. L’étendue de comptage pour obtenir 1ms est alors de 125, et on peut augmenter le tick rate jusqu’à 5000. En théorie, on devrait pouvoir aller jusqu’à 25000, mais au-delà de 6000 il doit y avoir des effets de bord (overflow dans les calculs ?) qui font que ça ne fonctionne plus.

En pratique, tout cela veut dire que si on veut exprimer les temps en autre chose que la milli-seconde, il suffit de modifier TICKRATE et en jouant sur le prescaler, mais tout en faisant attention à ce que TCNT0_INIT reste dans l’étendue 0-255.

Tout cela est cependant théorique, car dans la pratique, il faut savoir que si on augment le tickrate, on va donc augmenter le nombre de changements de contexte par unité de temps. Or chaque changement consiste à sauvegarder tous les registres dans le descripteur de contexte de la tâche en cours, puis à restaurer ceux de la tâche à qui on va donner la main, plus quelques autres opérations de gestion interne. Tout cela est loin d’être gratuit en temps processeur, et donc si on augmente la fréquence à laquelle on le fait, le micro va passer le plus clair de son temps à commuter les tâches, et s’il a le temps, à les exécuter.

Comment s’en sortir ?

Pour illustrer cela, prenons par exemple le cas de la génération des pulses pour des servos. Au besoin replongez-vous dans l’article Bouge ton servo. Nous avons besoin de générer des pulses de largeur comprise entre 1ms et 2ms, soit une excursion de 1ms. Une résolution du 10ème semble être le minimum, soit donc un découpage du temps à 0.1ms. On voit tout de suite qu’on ne pourra donc pas utiliser les services d’AvrX pour cela, puisque les fonctions de gestion des timers ont pour résolution le tickrate global. Quant à multiplier celui-ci par 10, il vaut mieux ne pas y penser, pour les raisons exposées juste avant.

La solution est globalement de :
 calibrer le prescaler et le compteur du timer 0 pour que l’interrupt handler du timer overflow soit exécuté 8 (ou 16) fois par tick
 gérer les pulses des servos dans la première partie de l’interrupt handler
 une fois tous les 8 (ou 16) passages, exécuter la séquence relative à AvrX telle que contenue dans le code de l’exemple

Remarque : pourquoi utiliser un facteur 8 ou 16 ci-dessus alors qu’on parlait de diviser par 10 tout à l’heure ? Tout simplement par que dans un contexte où on ne dispose pas d’unité de calcul flottant comme c’est le cas ici, il faut raisonner en binaire et non pas en décimal pour tout ce qui est multiplication et division. Oubliez donc les puissances de 10 et ne raisonnez qu’en puissances de 2. D’où la suggestion de travailler avec une résolution de 1/8ème de tick, ou de 1/16ème si besoin de plus de précision

Le makefile

Le voici :

makefile

En fait, ce n’est pas celui fourni par AvrX, mais une version maison résultat de la fusion entre le makefile type de WinAVR et les ajouts d’AvrX. En effet, celui utilisé par Larry Barello semble être basé sur une version plus ancienne de WinAVR. De plus, bien que la variable d’environnement AVRX soit mentionnée dans la procédure d’installation de l’environnement, elle n’est absolument pas utilisée. Ce petit défaut a été corrigé dans la version proposée.

Tout y est suffisamment commenté pour ne pas s’étendre plus sur le sujet ici. De toute manière, les targets sont les mêmes que pour tout projet WinAVR.

Les différentes commandes sont :

Conclusion

Voilà pour cette première prise en main d’AvrX. Avouez que c’est assez sympa...

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.