Piloter un servo-moteur par un seul timer
Dans le cours précédent nous avons vu une méthode (non optimale) de pilotage d'un servo avec deux timers. Nous avons par ailleurs évoqué une manière d'optimiser celà en n'utilisant qu'un seul timer.
Pour rappel, voici le timeline théorique du pilotage d'un servo moteur :

Au lieu d'utiliser deux timers, un pour le temps inter-pulse, et l'autre pour la durée de l'impulsion, nous allons n'utiliser qu'un seul timer :
- Mettre le pin du servo à 1
- Ecrire la consigne d'impulsion dans le timer (65535 - (temps en µS * 10000) dans le cas d'un pic à 40Mhz)
- Lorsque l'interruption se déclenche, remettre le pin du servo à 0
- Ecrire par exemple 0 dans le timer (on va attendre 6,5535ms, ce qui est inférieur à 20ms, donc pas de problème)
- Lorsque l'interruption se déclenche, recommencer
Nous n'avons pas besoin d'attendre 20ms entre chaque pulse. En effet, 20ms est le temps maximum entre chaque pulse, mais nous pouvons très bien ne pas avoir de temps inter-pulse du tout comme on peut le voir dans le schéma ci-dessous (il est tout de même conseillé d'attendre quelques millisecondes entre chaque pulse, pour laisser le pin du servo assez longtemps à l'état bas et que le servo le détecte) :

Voici le morceau de code d'interruption que l'on peut utiliser pour piloter un servo avec un temps inter-pulse de 6,5535ms (durée max d'un timer 16bits sans préscaler)
/* timer qui se charge de l'impulsion ET du délai inter-pulse*/ if (PIR1bits.TMR1IF) { // on réarme le timer PIR1bits.TMR1IF = 0; /* le servo était en inter-pulse : on lui fourni un pulse */ // on a pas besoin d'une variable de statut, il suffit de // lire la valeur du registre LATCH du port (CF datasheet) if (LATAbits.LATA0 == PULSE_OFF) { // on met le pin du servo à l'état haut (PULSE_ON = 1) IO_SERVO = PULSE_ON; // le timer va se déclencher à la fin de l'impulsion WriteTimer1(65535 - consigne); } else /* on était en impulsion : on attends un peu */ { // on remet le pin à 0, le pulse est terminé IO_SERVO = PULSE_OFF; // on va attendre 6,5535ms avant de relancer un pulse WriteTimer1(0); } }
Rien de bien neuf ici mis-à part l'utilisation du registre LATCH : dans le datasheet il est indiqué que le registre latch (LAT) contient la valeur présente sur le pin (présente, lorsque le pin est en lecture, ou écrite par le programme, lorsque le pin est en écriture). Celà nous permet de lire directement la valeur présente sur le pin qui nous interesse, au lieu de devoir la stocker dans une variable.
Voici le fichier source ainsi que le fichier hex.
Passons maintenant à des choses plus sérieuses ;)
Plusieurs servos avec un seul timer
Appliquons notre "optimisation" pour le pilotage de plusieurs servos. Voici comment serait le timeline :

Nous avons une consigne d'impulsion de 2ms maximum pour chaque servo, et il nous faut un délai inter-pulse de moins de 20ms. Si l'ont veut n'utiliser qu'un seul timer, et donc déclencher le pulse des servos l'un après l'autre, on ne pourra piloter que 10 servos maximum. En effet, 2ms * 10 = 20ms. Si tous les servos doivent être à fond "à droite" (consigne de 2ms, à fond dans le sens des aiguilles d'une montre), le temps inter-pulse sera limite. Selon les servos, un temps inter-pulse un peu plus long ne gêne pas forcément, mais mieux vaut rester prudent.
- Mettre le pin du servo1 à 1
- Ecrire la consigne d'impulsion dans le timer (65535 - (temps en µS * 10000) dans le cas d'un pic à 40Mhz)
- Lorsque l'interruption se déclenche, remettre le pin du servo1 à 0
- Mettre le pin du servo2 à 1
- Ecrire la consigne d'impulsion...
- Lorsque l'interruption se déclenche, remettre le pin du servo2 à 0
- Mettre le pin du servo3 à 1
- Ecrire la consigne d'impulsion...
- Lorsque l'interruption se déclenche, remettre le pin du servo3 à 0
- ...
- continuer avec tous les servos
- ...
- Une fois que tous les servos ont eu leur impulsion, recommencer avec le servo1
Afin de nous y retrouver dans tous nos servos, et de ne pas s'emmêler les pinceaux avec autant de variables qu'il
y a de servos, nous allons utiliser un tableau, et une variable qui retiendra le servo actif. Appelons tab notre
tableau de 2 colonnes (état du pulse du servo et consigne) et N lignes (N étant le nombre de servos à piloter).
Appelons s_actif la variable contenant le numéro du servo actif. Cette variable pourra prendre les valeurs de 0 à
N-1 (N étant le nombre de servos).
Prenons l'exemple de trois servos : s_actif prendra les valeurs de 0 à 2 (ce qui fait bien 3 servos).
(1) Lancer le pulse du premier servo, écrire la consigne dans le timer puis lorsque le timer se déclencher :
- (2) mettre tab[ s_actif ][ 0 ] à 0 (dans la première colonne on a l'état du pulse)
- (3) mettre le servo actif à jour (s_actif + 1, sauf si c'est le dernier servo qui est actif, auquel cas on active à nouveau le premier)
- (4) mettre tab[ s_actif ][ 0 ] à 1
- (5) écrire 65535 - tab[ s_actif ][ 1 ] dans le timer (dans la deuxième colonne se trouve la consigne)
- (6) refléter l'état de "tab" sur les servos (*)
- (7) attendre que l'interruption se déclenche et recommencer en (2)
(*) : nous allons utiliser une fonction qui va lire chaque ligne du tableau, et pour chaque numéro de servo (chaque ligne correspondant à un servo), mettra un 1 ou un 0 sur le pin du port correspondant.
Afin que le code reste modifiable facilement, nous nous servirons de #define pour chaque servo. Ainsi, si il faut changer un servo de port/pin, celà pourra être fait en ne modifiant qu'une ligne dans le programme, celle du #define, autrement il aurait fallu modifier toutes les lignes dans le programme utilisant ce port/pin. Le but pour un programmeur est d'avoir un programme clair (facilement compréhensible, afin qu'il s'y retrouve rapidemment si il doit le modifier même après plusieurs mois sans y avoir touché) et facilement modifiable. Prenons comme exemple concret notre programme qui doit piloter 10 servos : si tout à coup il était nécéssaire de faire une acquisition analogique (disponible uniquement sur le port A), il faudrait déplacer tous les servos sur un autre port et donc modifier chacune des occurences de PORTAbits.RAx (x étant le numéro des pins utilisés sur le port A pour les servos).
Imaginons qu'il y en ai 20 à différents endroits dans le code source : 10 pour activer le pulse dans l'interrupt et
10 pour le désactiver, toujours dans l'interrupt. Il pourrait y en avoir 10 de plus pour initialiser les servos à 0
(inutile dans notre cas), 10 de plus si on voulait pour une raison ou une autre forcer la valeur à 1 (ou à 0) des
servos, etc...
Tout ceci peut être simplifié en utilisant des defines : 10 lignes au début du programme définissant un nom pour
chaque servo associé aux pins : #define servo0 PORTAbits.RA0, #define servo1 PORTAbits.RA1, ... il n'y aura donc
que ces quelques lignes, toutes regroupées au début du programme (ou dans un autre fichier!) qu'il faudra modifier.
Clarté, simplicité et modules
Je fais ici une petite parenthèse sur l'utilisation d'autres fichiers : un programme modulaire (découpé en plusieurs parties, ayant chacune sa fonction) est une manière de faire un programme plus clair et facilement modifiable. Reprenons notre exemple précédent, nous pourrions placer tous les #define dans un fichier d'en-tête (header file). Ce fichier aura pour extension ".h", et sera inclu dans notre fichier ".c" par la commande #include "nomfichier.h". Un fichier d'en-tête contient généralement les définitions, variables globales etc... et sert d'interface pour les autres modules, ou tout simplement d'interface avec un utilisateur voulant utiliser votre module.
Pour éviter à une personne voulant utiliser votre programme de devoir modifier directement le fichier ".c" (qui peut-être très long et très complexe), vous pouvez lui mettre à disposition un fichier ".h" avec les quelques lignes à modifier pour son utilisation (dans notre cas, les #define avec les ports/pins à utiliser pour chaque servo). En allant un peu plus loin, il est même possible (au prix d'un développement un peu plus complexe) de définir un nombre de servos dans le fichier d'en-tête. Votre code sera ainsi modifiable facilement, et donc utilisable par le plus grand nombre! Nous verrons des exemples de ce style de programmation dans de prochains cours, avec la création d'une "librairie" de gestion d'un LCD par exemple.
Code du programme
Voici le code final de l'interruption (se reporter au lien en bas du cours pour voir le détail du code source dans son intégralité) :
if (PIR1bits.TMR1IF) { // on réarme le timer PIR1bits.TMR1IF = 0; /* on met le pin du servo actif * à l'état bas (PULSE_OFF = 0) */ tab[s_actif][0] = PULSE_OFF; /* on change le servo actif */ if (s_actif < (nb_servos - 1)) s_actif++; else s_actif = 0; /* on met le pin du servo suivant * à l'état haut (PULSE_ON = 1) */ tab[s_actif][0] = PULSE_ON; /* le timer va se déclencher à la fin du pulse */ WriteTimer1(65535 - tab[s_actif][1]); /* on apelle la fonction maj_servos *pour refléter les changements */ maj_servos(); }
Dans la fonction "main" de notre programme il nous faut initialiser le tableau avec les consignes de chaque servo, puis démarrer le pulse du servo0. Ensuite le reste se passera tout seul dans l'interruption. Voici le code de la fonction maj_servos :
/*********************************************************** * fonction qui va refléter les changements de "tab" sur * * les pins de chaque servo * ***********************************************************/ void maj_servos(void) { /* va mettre chaque servo à l'état haut ou bas * selon l'état stocké dans le tableau */ servo0 = tab[0][0]; servo1 = tab[2][0]; servo2 = tab[2][0]; servo3 = tab[3][0]; servo4 = tab[4][0]; servo5 = tab[5][0]; servo6 = tab[6][0]; servo7 = tab[7][0]; servo8 = tab[8][0]; servo9 = tab[9][0]; }
Cool non? Mais il est possible de faire encore plus optimisé, pour piloter (en théorie)jusqu'à 20 servos avec un seul timer! Comment? je vous laisse-y réfléchir jusqu'au prochain cours!
Comme d'habitude voici le code source ainsi que le fichier .hex compilé.
Le cours qui suit montre une autre méthode, un peu plus "optimisée", qui permet de piloter (au moins en théorie) jusqu'à 20 servos, et une autre qui permettrait d'un piloter un nombre quasi infini, mais nous verrons les limites de ces méthodes.
Schéma de montage avec servo-moteur
Pour rappel, voici le schéma de montage avec un servo-moteur. Si l'ont veut piloter n servos, il suffit de les connecter aux pins libres de votre choix, puis de refléter ce choix dans les #define au début du programme. Voilà le schéma eagle.
