Piloter un servo-moteur par timers
Dans le cours précédent nous avons appris l'utilité des interruptions, et nous avons vu une application des timers pour faire clignoter une LED. Dans ce cours, nous allons réutiliser nos acquis pour une autre application : le pilotage d'un servo-moteur.
Si vous ne connaissez pas encore la base du pilotage d'un servo-moteur, reportez-vous au site des Fribottes, et plus particulièrement à l'article en rapport dans leur base de données techniques : Pic et Servo
En résumé, imaginons que nous voulons faire tourner un servo-moteur complètement à gauche (sens inverse des aiguilles d'une montre), nous allons lui fournir une impulsion de 1ms, et ce toutes les 20ms. Afin de privilégier l'aspect pédagogique, au détriment de l'optimisation, nous allons utiliser deux timers, un qui se déclenchera toutes les 1ms, et un autre qui se déclenchera toutes les 20ms.
Deux timers pour un servo-moteur
Nous allons donc utiliser deux timers (le timer1 et le timer3 par exemple), un qui se déclenchera toutes les millisecondes (donc tous les 10000 cycles, vu que un cycle prends 0,1 nanoseconde), et un autre qui se déclenchera toutes les 20ms (donc tous les 200000 cycles). Le "problème" est que les timers 16bits ne peuvent "compter" que 65536 cycles (2^16 = 65536)... comment faire? utiliser un prescaler!
Nous avons abordé le thème du prescaler très rapidemment dans le cours précédent : c'est un mécanisme qui permet de configurer le timer afin qu'il ne s'incrémente que toutes les 2, 4, 8... fois. Selon les timers, il y a plus ou moins de possibilités de prescaling, dans notre cas il nous faut un prescaler de 4, en sachant qu'on ne fera compter au timer que 50000 cycles (50000 * 4 = 200000 cycles = 20ms).
Dans le cours précédent, nous avions "arrondi" le timer afin de nous libérer de la tache d'écriture dans un timer. Ce coup-ci, nous allons nous forcer un peu, et découvrir la fonction WriteTimerX(nombre), avec "X" le numéro du timer, et "nombre" le nombre de cycles que l'ont veut écrire dans le timer. Nous utiliserons le timer1 pour gérer l'impulsion, et le timer3 pour gérer le temps inter-pulse. Comme d'habitude, se reporter au fichier PDF décrivant les fonctions de la libc de C18.
Le timer compte par défaut de 0 à 65535, donc si on veut le faire se déclencher dans 10000 cycles, il faudra écrire 55535 afin qu'il ne lui reste que 10000 cycles à compter! De la même manière, pour qu'il se déclenche au bout de 50000 cycles, il faudra écrire 15535.
Le programme
Voici le programme :
/* PIC 18F252 quartz 10Mhz + PLL = 40Mhz * pilote un servo avec deux timers : la consigne est de 1ms * (au max dans le sens inverse des aiguilles d'une montre */ #include <p18f252.h> /* déclarations pour le PIC18F252 */ #include <timers.h> /* fonctions pour les timers */ #define IO_SERVO PORTAbits.RA0 #define IO_SERVO_TRIS TRISAbits.TRISA0 #define PULSE_ON 1 #define PULSE_OFF 0 /* fonction interruption */ void MyInterrupt(void); #pragma code highVector=0x008 void atInterrupthigh(void) { _asm GOTO MyInterrupt _endasm } #pragma code // retour à la zone de code // ************************ // **** Interruptions **** // ************************ #pragma interrupt MyInterrupt void MyInterrupt(void) { unsigned char sauv1; unsigned char sauv2; sauv1 = PRODL; sauv2 = PRODH; /* timer qui se charge de l'impulsion */ if (PIR1bits.TMR1IF) { // on ne remet pas ce bit à 0, il sera // réarmé par le timer3 //PIR1bits.TMR1IF = 0; // on remet le pin à 0, le pulse est terminé IO_SERVO = PULSE_OFF; } /* timer qui se charge du temps inter-pulse */ if (PIR2bits.TMR3IF) { // on réarme le timer PIR2bits.TMR3IF = 0; // déclenchement du timer dans 20ms WriteTimer3(15535); /* on commence l'impulsion de 1ms */ // on met le pin à 1 IO_SERVO = PULSE_ON; // on écrit 55535 dans le timer1 pour // qu'il se déclenche dans 1ms WriteTimer1(55535); // et on réarme le timer1 PIR1bits.TMR1IF = 0; } PRODL = sauv1; PRODH = sauv2; } /* fonction principale */ void main (void) { // on crée le timer1 OpenTimer1(TIMER_INT_ON & T1_8BIT_RW & T1_SOURCE_INT & T1_PS_1_1 & T1_OSC1EN_OFF & T1_SYNC_EXT_OFF); // puis le timer3 avec un prescaler de 4 OpenTimer3(TIMER_INT_ON & T3_8BIT_RW & T3_SOURCE_INT & T3_PS_1_4 & T3_SYNC_EXT_ON & T1_SOURCE_CCP); // on va compter 4 * 50000 cycles = 20ms // avant de lever l'interruption WriteTimer3(15535); // On active toutes les interruptions INTCONbits.GIE = 1; INTCONbits.PEIE = 1; // on configure le pin 0 du port A en sortie IO_SERVO_TRIS = 0; // et on démarre la boucle infinie while (1) { } } /* configuration du PIC */ #pragma romdata CONFIG _CONFIG_DECL ( _CONFIG1H_DEFAULT & _OSC_HSPLL_1H , _CONFIG2L_DEFAULT & _PWRT_ON_2L , _CONFIG2H_DEFAULT & _WDT_OFF_2H, _CONFIG3H_DEFAULT, _CONFIG4L_DEFAULT & _STVR_OFF_4L & _LVP_OFF_4L & _DEBUG_OFF_4L , _CONFIG5L_DEFAULT & _CP0_OFF_5L & _CP1_OFF_5L & _CP2_OFF_5L & _CP3_OFF_5L, _CONFIG5H_DEFAULT & _CPB_OFF_5H & _CPD_OFF_5H, _CONFIG6L_DEFAULT & _WRT0_OFF_6L & _WRT1_OFF_6L & _WRT2_OFF_6L & _WRT3_OFF_6L, _CONFIG6H_DEFAULT & _WPC_OFF_6H & _WPB_OFF_6H & _WPD_OFF_6H, _CONFIG7L_DEFAULT & _EBTR0_OFF_7L & _EBTR1_OFF_7L & _EBTR2_OFF_7L & _EBTR3_OFF_7L, _CONFIG7H_DEFAULT & _EBTRB_OFF_7H ); #pragma romdata
Mais mais mais mais MAIS C'EST IMMONDE ! Bon n'abusons pas non plus, mais utiliser deux timers dans ce cas était superflu, vraiment superflu. Néanmoins, pédagogiquement c'était mieux hein, n'est-ce pas que c'était mieux :P En se basant sur le programme précédent (utilisation d'une variable compteur), il aurait été possible de n'utiliser qu'un seul timer, comme vous pouvez le découvrir dans le cours suivant.
Je pense qu'il n'est pas utile d'expliquer tout le programme pas-à-pas comme les cours précédents, après tout vous commencez à vous y retrouver plus facilement maintenant, et puis il n'y a rien de neuf. Détaillons par contre la logique du morceau de programme exécuté lors d'une interruption :
void MyInterrupt(void) { unsigned char sauv1; unsigned char sauv2; sauv1 = PRODL; sauv2 = PRODH; /* timer qui se charge de l'impulsion */ if (PIR1bits.TMR1IF) { // on ne remet pas ce bit à 0, il sera // réarmé par le timer3 //PIR1bits.TMR1IF = 0; // on remet le pin à 0, le pulse est terminé IO_SERVO = PULSE_OFF; } /* timer qui se charge du temps inter-pulse */ if (PIR2bits.TMR3IF) { // on réarme le timer PIR2bits.TMR3IF = 0; // déclenchement du timer dans 20ms WriteTimer3(15535); /* on commence l'impulsion de 1ms */ // on met le pin à 1 IO_SERVO = PULSE_ON; // on écrit 55535 dans le timer1 pour qu'il se // déclenche dans 1ms WriteTimer1(55535); // et on réarme le timer1 PIR1bits.TMR1IF = 0; } PRODL = sauv1; PRODH = sauv2; }
On reconnait la sauvegarde des registres, puis il y a deux gros blocs de code, chacuns conditionnés par un "if". Ce "if" (qui se traduit par "si" en français) va tester la valeur d'un bit dans les registres PIR1bits et PIR2bits. Si le bit correspondant est à 1, alors le morceau de code qui suit (englobé par { et }) sera exécuté, autrement le programme passera à la suite directement. Ces bits sont en fait mis à 1 lors d'un overflow du timer correspondant : le bit TMR1IF correspond au timer1, et le bit TMR3IF correspond au timer3. Lorsqu'un des deux timers fait un overflow (il ajoute 1 à son compteur qui est déjà à 65535, le maximum, ce qui remet le compteur à 0), le bit associé est mis à 1, ce qui génère l'interruption. Le timer ne pourra alors plus déclencher d'interruption avant que ce bit ne soit remis à 0.
Donc dans notre cas voilà le déroulement : au tout début du programme, on écrit 15535 dans le timer3, pour lui faire déclencher une interruption au bout de 20ms (4 * 50000 cycles). Pendant ces 20ms, le timer1 se sera déclenché une fois, mais n'aura pas été réarmé. Donc au bout des 20ms, on entre dans la procédure d'interruption. Le bit TMR3IF sera à 1 (vu que le timer3 sera la source de l'interruption), donc nous allons exécuter le morceau de code englobé par le "if" :
- remise à 0 de TMR3IF (pour réarmer le timer3)
- on écrit 15535 dans le timer3 pour qu'il se déclenche au bout de 20ms
- on met le pin 0 du port A à 1 (début de l'impulsion)
- on écrit 55535 dans le timer1 pour qu'il se déclenche au bout de 1ms
- on réarme le timer1
Le prochain à déclencher sera donc le timer1 (au bout de 1ms). Le bit TMR1IF sera à 1, donc le code englobé par le "if" sera éxecuté : dans notre cas, une seule chose, la remise à 0 du pin 0 du port A, ce qui met fin à l'impulsion.
Voilà voilà, on y est! Dernière petite précision : c'est l'option "T3_PS_1_4" qui définit le prescaler de 4.
Voilà le fichier source ainsi que le fichier .hex compilé. Faites comme pour les cours précédents pour rapatrier ces fichiers sur votre ordinateur !
Dans le cours suivant, une méthode simple pour piloter un servo avec un seul timer, puis une autre pour piloter jusqu'à 10 servos avec un seul timer.
Schéma de montage avec servo-moteur
Le schéma de montage avec un servo-moteur ne diffère que très peu du montage avec la LED : bien faire attention de ne pas se tromper lors du branchement du servo-moteur : le fil noir va sur la masse, le fil rouge sur le +5V, et le troisième fil (généralement blanc ou jaune) sur le pin 0 du port A. Voilà le schéma eagle.
