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

De Wiki Robotronik
Révision datée du 23 février 2020 à 22:24 par Nornort (discussion | contributions) (Oups)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)

Réalisation d'un asservissement en position d'un bras à hélice 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 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 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) 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) 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

  1. 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

  1. 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 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

  1. 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

  1. 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 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…

Source

SAVOYAT Marc-Antoine, Réalisation d'un asservissement en position d'un bras à hélice, Wiki ClubElek, 4 mars 2010, CC-BY-NC-SA, https://wiki.clubelek.fr/articles:realisation_d_un_asservissement_en_position_d_un_bras_a_helice