Réalisation d'un asservissement en position d'un bras à hélice

FIXME Images non disponibles…

L’asservissement est une notion fondamentale de l’automatisme et de l’électronique. Ce projet est l'occasion de faire un petit système très simple et assez complet qui met en œuvre un asservissement en temps discret sur un pic 18F2680 et quelques fonctionnalités de ce micro-contrôleur. Bien qu'il peut paraitre sans intérêt, il faut imaginer ce bras reproduit à l'identique dans le cadre d'un quadrirotor, à ce moment là l'asservissement aurait pour but de stabiliser l'assiette de l'engin ( le codeur serai alors remplacé par des accéléromètres ou une centrale inertielle).

Mécanique

img_1219.jpgimg_1216.jpg

La mécanique est assez simple. Un bras fait avec une pivot à roulement à bille au-bout duquel on met une hélice sur un moteur (ici un moteur brushless avec son variateur sensorless). La liaison entre le bâti et le bras a un axe assez volumineux sur lequel le codeur en quadrature vient s'engrener. Pour que le contact soit assurer en permanence le codeur est monté sur un genre de chariot qui est collé contre l'axe du bras par un système d'élastique Le codeur en quadrature est en fait une molette de souris. Le bâti est fixé à la table avec un serre joint pour éviter que la maquette se balade un peut partout…

Électronique

img_1218.jpg

Dans la partie électronique il n'y a rien de surhumain, juste deux points assez important de souligner:

  • pour l'alimentation du pic il est INDISPENSABLE de mettre une capa au plus près de la pin d'alimentation du pic, sous peine de reboot intempestif (j'ai eu la malheureuse expérience de chercher des bugs logiciel pendant des heures avant de me rendre compte que la capa d'alimentation du pic était trop loin!!). Enfin il faut bien séparer les alimentations du pic et du variateur (alimentation de commande et de puissance)

capteur quadrature.jpg

  • pour ce qui est du codeur en quadrature, ce dernier est très largement sujet au rebonds (mise à un ou à zéro non franche du signal). Pour éviter que ce phénomène soit un problème on met des condensateurs en parallèle des résistances qui mène les signaux de sortie à la masse.

Informatique

La partie informatique est uniquement de la programmation de microcontrôleur PIC. Je détaille ici les points importants du projet, à savoir : la génération de la PWM pour la commande du variateur (ou d'un servomoteur), la prise en charge du codeur en quadrature avec un astuce pour s'immuniser de façon logiciel des rebonds et enfin quelques idées sur la méthode de réalisation d'un asservissement en temps discret en langage C sur un microcontrôleur.

Réalisation de la PWM

Pour la réalisation d'une PWM j'utilise ici l'interruption du Timer1. Ce dernier a l'avantage d'être de 16bits. Je définit au début du programme une constante #define T1MS qui correspond au nombre de cycles (Fosc/4) nécessaires pour faire 1ms soit 8000 pour Fosc=32Mhz (8Mhz x4 PLL). Une période du signal PWM de commande du variateur est décomposable en 4 étapes :

  • 1ms constante à 1
  • le signal vraiment effectif : un temps à 1 compris entre 0 et 1ms
  • le complément jusqu'à 1ms du signal effectif à 0
  • le complément de 18ms à 0 pour obtenir un période globale de 20ms (cette étape pourra être divisé en plusieurs de durées plus courtes)

pwm.png

J'utilise deux variables globales (je sais que c'est mal mais c'est tellement pratique), la première unsigned int pwm ; qui contient un nombre entre 0 et T1MS image du signal effectif (on remarque que on a une assez bonne résolution) et la seconde unsigned char pwm_cas; qui nous renseignera sur l'étape en court de la PWM. On active les interruptions, on configure le Timer 1 et on se crée 3 fonctions pour démarrer, modifier et arrêter la PWM avec les lignes suivantes (je me suis définit #define OUTPWM PORTAbits.RA0 pour plus de faciliter dans le code, la PWM sera donc générée sur la pin RA0) :

INTCONbits.GIE = 1 ;    //activation générales des interruptions
INTCONbits.PEIE = 1 ;     //activation des interruption des périphériques (comme les timers)
PIE1bits.TMR1IE = 1;    //activation des interruption sur le timer 
T1CON = 0b0000000;    // Configuration du timer1 [ 16b ClockStatus Prescale1 Prescale0 OscilEnable Sync ClockSource ON ]
 
void pwmStart(void){
           T1CONbits.TMR1ON = 1;
}
 
void pwmStop(void){
           T1CONbits.TMR1ON = 0;
}
void pwmSet(int setpwm){
           if(setpwm > T1MS){
                      pwmSet(T1MS);
           }
           else if(setpwm < 0){
                      pwmSet(0);
           }
           else{
                      pwmSet(setpwm);
           }
}

Ensuite on traite les interruptions que lèvera le timer1 :

// Code qui s'exécutera lors de l'interruption
#pragma interrupt InterruptHandler   
void InterruptHandler (void){
           if(PIR1bits.TMR1IF){// Si l'interruption est provoquée par le timer1 (pwm)
                      PIR1bits.TMR1IF = 0 ; //on remet le flag du timer1 ‡ 0
                      if(pwm_cas==0){// Première ms de temps à 1
                                 OUTPWM = 1; 
                                 TMR1H=0xFF - T1MS/256;
                                 TMR1L=0xFF - T1MS%256 - 0x0B; //ajustement pour compenser le temps qu'il faut pour arriver dans l'interruption
                                 pwm_cas++;
                      }
                      else if(pwm_cas==1){// Cas du maintient supplémentaire qui donne réellement la commande
                                 TMR1H=0xFF - pwm/256;
                                 TMR1L=0xFF - pwm%256 - 0x0B; //ajustement pour compenser le temps qu'il faut pour arriver dans l'interruption
                                 pwm_cas++;
                      }
                      else if(pwm_cas==2){// complète le temps à 1 de la commande réelle avec le temps ‡ 0 jusqu'a 2ms
                                 OUTPWM = 0;
                                 TMR1H=0xFF - (T1MS - pwm)/256;
                                 TMR1L=0xFF - (T1MS - pwm)%256 - 0x0B; //ajustement pour compenser le temps qu'il faut pour arriver dans l'interruption
                                 pwm_cas++;
                      }
                      else if(pwm_cas<5){// complète le temps à 0 jusqu'a 20ms soit 18ms
                                 TMR1H=0xFF - 8*(T1MS/256);
                                 TMR1L=0xFF - 8*(T1MS%256) - 0x0B; //ajustement pour compenser le temps qu'il faut pour arriver dans l'interruption 
                                 pwm_cas++;
                      }
                      else{
                                 TMR1H=0xFF - 2*(T1MS/256);
                                 TMR1L=0xFF - 2*(T1MS%256) - 0x0B; //ajustement pour compenser le temps qu'il faut pour arriver dans l'interruption
                                 pwm_cas=0;
                      }           
           }
}
 
// Code qui permet de choper l'interruption
#pragma code InterruptVectorHigh = 0x08        //placer le code suivant ‡ l'adresse 0x08
void InterruptVectorHigh (void) {
           asm
                      goto InterruptHandler //jump to interrupt routine
           endasm
}

Pour utiliser cette pwm on peut faire par exemple dans le main (ou ailleurs) :

pwmSet(1000) ;
pwmStart() ;
...
pwmSet(2000) ;
...
pwmStop() ;

Codeur en quadrature

quadrature.jpg

Pour récupérer les information du codeur en quadrature et les transformer en une information sur la position sans être perturbé par les rebonds, j'ai utiliser les interruptions du portB et le timer2. Le principe est simple, lorsque un des 2 signaux du codeur change un interruption du portB est levé, lors de cette interruption je désactive les interruption du portB (pour éviter de prendre en compte un éventuel rebond) et j'active le timer2. Lorsque le timer2 lève son interruption, je considère que les rebonds sont passé, ainsi je réactive les interruptions du portB, je désactive le timer2 et je traite le signal du codeur en incrémentant ou décrémentant un variable globale : int pos ; j'utilise aussi unsigned char inB_old; pour pouvoir définir le sens de rotation. Pour coder tranquillement je me suis défini les variables suivantes : #define INA PORTBbits.RB4 #define INB PORTBbits.RB3. On active les interruptions nécessaires et on règle le timer2 avec les registres suivants :

INTCONbits.GIE = 1 ;    //activation générales des interruptions
INTCONbits.PEIE = 1 ;     //activation des interruption des périphériques (comme les timer)
INTCONbits.RBIE = 1 ; // activation des interruption du port B
PIE1bits.TMR2IE = 1;  //Activation interruption timer2
PR2 = 255; // "Temps" d'attente avant de réactiver les interruption du portB (max 255)

L'interruption du portB peut devenir un vrai casse tête car elle est assez mal documentée. Il faut obligatoirement lire le portB pour pouvoir mettre à 0 le flag de cette interruption, et elle ne fonctionne pas sur tout le portB, seulement sur les bits 4 à 7. Voici donc le code :

// Code qui s'exécutera lors de l'interruption
#pragma interrupt InterruptHandler   
void InterruptHandler (void){
            if(PIR1bits.TMR2IF){// Si l'interruption est levé par le timer2
                        // On réactive les interruption sur le portB
                        INTCONbits.RBIE = 1 ; 
                        if(INA == inB_old){ // On traite les informations du codeur
                                    pos++;
                        }
                        else{
                                    pos--;
                        }
                        inB_old = INB ; //on met à jour la variable inB_old
                        INTCONbits.RBIF = 0 ; //On met le flag des interruption sur portB ‡ 0 car les rebonds l'on mis ‡ 1
                        // On dÈsactive le timer2 et on traite le flag
                        TMR2 = 0; // On met le timer à zéro pour pouvoir le désactiver en toute sécurité
                        PIR1bits.TMR2IF = 0; // Remise à zéro du flag
                        T2CONbits.TMR2ON = 0; // désactivation du timer
            }
            if(INTCONbits.RBIF){ //Si l'interruption est provoqué par un changement d'état du port B entre RB4 et RB7
 
                        INB = INA; // il faut lire le portB pour pouvoir mettre le flag ‡ 0
                        INTCONbits.RBIF = 0 ; // on remet le flag d'interruption sur portB ‡ 0
                        INTCONbits.RBIE = 0 ; // on désactive les interruptions du portB pour éviter les rebonds, on les réactivera avec le timer
                        // On active le timer2 pour qu'il réactive les interruption portB après un petit temps 
                        T2CON = 0b0111110; // Configuration et activation du timer2 (postscale/ON/prÈscale) [Pos3 Pos2 Pos1 Pos0 ON Pre1 Pre0] 
            }
}
 
// Code qui permet de choper l'interruption
#pragma code InterruptVectorHigh = 0x08        //placer le code suivant ‡ l'adresse 0x08
void InterruptVectorHigh (void){
            asm
                        goto InterruptHandler //jump to interrupt routine
            endasm
}

Asservissement proprement dit

D'abord le fameux schémas bloc qui donne un aperçue visuel de la topologie de l'asservissement

:schemablock.jpg

Le principe d'un asservissement est d'appliquer aux actionneurs une consigne non pas lié à la consigne demandé mais lié à l'erreur entre la consigne demandé et la position actuelle du système, la boucle de retour (système en boucle fermée) est effectué avec un capteur. Ce concept s'oppose au systèmes en boucle ouverte ou l'on a aucun retour sur ce que l'on fait (on l'utilise pour les moteur pas à pas par exemple). Voici le code qui traduit se raisonnement en C :

//déclaration des variables
            int errold=0;
            int err, correct;
            int cmd=0;
            int Kp = 10; // coèf du proportionnel
            int Ki = 0; // coèf intégrateur
            int Kd = 0; // coèf dérivé
// On rentre dans la boucle d'asservissement
 
for(;;){
            err = cmd - pos; // voici l'erreur
            correct = Kp*err + Ki*(err + errold) + Kd*(err - errold); // on génère la nouvelle commande
            errold = err; // On met à jour l ‘ancienne erreur qui permet le calcul de l'intégrale et de la dérivée
            pwmSet(pwm+correct); // On applique la correction
            delay(5*( (err<0)? -err : err)); // on fini par un petit delay proportionnel à l'erreur pour pas que sa aille trop vite mais que autour de l'équilibre les correction (qui sont plus fines) soit plus fréquentes
            }

La fonction delay que j'ai utilisé :

void delay(int ms){
            long i = ms*T1MS/16; // On divise par 16 car chaque passage dans le while prend 16 instructions
            while(--i>0) continue;
}

Il faut ensuite trouver les meilleurs coefficients pour obtenir la meilleure réponse du système. Là il y a deux méthodes : Le calcul et le « feeling »…

Conclusion

Le système est assez stable mais la mise en position est assez lente sous peine de non stabilisation. Cette expérience m'a permis de mettre en évidence qu'un pic 18F2680 peut très bien réaliser un asservissement, j'ai même été obliger de mettre un delay pour le calmer ! En contre partie cette expérience m'a permis de mettre en évidence le doute que j'avais sur le mauvais temps de réponse du variateur de moteur brushless (modèle de modélisme). Ainsi pour la réalisation future d'un éventuel quadrirotor où ce temps de réponse est crucial je développerai mon propre contrôleur qui intègrera au passage le bus CAN…

Article écrit par SAVOYAT Marc-Antoine

4 mars 2010