Moteur CC + Codeur - Contrôle en position

Bonjour à tous,

J'essaye de contrôler via Arduino Uno un moteur CC en position. J'ai en plus un codeur magnétique couplé au moto-réducteur.

Voici les références :
Moto-réducteur : Pololu - 298:1 Micro Metal Gearmotor MP 6V with Extended Motor Shaft
Codeur : Pololu - Magnetic Encoder Pair Kit for Micro Metal Gearmotors, 12 CPR, 2.7-18V
Driver : Pololu DRV8835 Dual Motor Driver Shield for Arduino

J'alimente le moteur en 6 VDC et le codeur en 3 VDC.

J'ai tapé le code pour l'asservissement en postion en m'inspirant grandement de ce site :

La librairie DRV8835MotorShield.h est spécifique au driver que j'utilise.

Mon code me permet d'entrer le nombre de tour que je veux effectuer, et permet de calculer le nombre de ticks en entrée à envoyer pour aller à la position voulue via un PID.
En théorie, on a : 297.92 (rapport de réduction) * 12 (nombre de ticks par tour du codeur) = 3575 ticks/tour.

Voici le code :

// Ce code permet d'asservir en position angulaire un moteur à courant continu.

#include <SimpleTimer.h> // bibliothèque
#include <DRV8835MotorShield.h> // bibliothèque

SimpleTimer timer;                 // Timer pour échantillonnage

//definition des entrées
int encoderPinA = 2;//compteur 1 Moteur 1
int encoderPinB = 3;//compteur 2 Moteur 1

#define LED_PIN 13

DRV8835MotorShield motors;

//int LED=12;
//init echantillonage
unsigned int time = 0;
const int frequence_echantillonnage = 200; // 20 Hz - mesure toutes les 50ms

//init compteur :
int encoder0Pos = 0; //position de départ=0
int lastReportedPos = 0; 
boolean A_set = false;
boolean B_set = false;

//consigne
double target_tour = 1; //nombre de tour désiré

double target_deg = 360*target_tour; //nombre de degrès pour atteindre target_tour
int target_ticks; //plus simple d'asservir en ticks car ce sera toujours un nombre entier

// init calculs asservissement PID
int erreur = 0; //erreur
float erreurPrecedente = 0;
float somme_erreur = 0;

//Definition des constantes du correcteur PID
float kp = 0.80;              // Coefficient proportionnel   // choisis par tatonnement sur le moniteur. Ce sont les valeurs qui donnaient les meilleures performances
float ki = 0; //5.5;          // Coefficient intégrateur
float kd = 0;//100;           // Coefficient dérivateur

// Routine d'initialisation //
void setup() {

  pinMode(LED_PIN, OUTPUT); 
  
  // uncomment one or both of the following lines if your motors' directions need to be flipped
  //motors.flipM1(true);
  //motors.flipM2(true);

  target_ticks = target_tour*297.92*12.0; // rapport de réduction = 297.92 et nombre de ticks par tour = 12 // nombre de ticks en entrée pour avoir target_tour en sortie

 
  Serial.begin(9600);         // Initialisation port COM
  //pinMode(LED, OUTPUT);
 
  pinMode(encoderPinA, INPUT);  //sorties encodeurs
  pinMode(encoderPinB, INPUT); 
  digitalWrite(encoderPinA, HIGH);  // Resistance interne arduino ON
  digitalWrite(encoderPinB, HIGH);  // Resistance interne arduino ON
 
  // Interruption de l'encodeur A en sortie 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);
  // Interruption de l'encodeur A en sortie 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);
  

  digitalWrite(LED_PIN, 0);  // Initialisation sortie moteur à 0 
  delay(300);                // Pause de 0,3 sec pour laisser le temps au moteur de s'arréter si celui-ci est en marche

  timer.setInterval(1000/frequence_echantillonnage, asservissement);  // Echantillonage pour calcul du PID et asservissement; toutes les 100ms, on recommence la routine
}

void loop(){

  timer.run();  //on fait tourner l'horloge
  //  digitalWrite(LED,HIGH); // met la broche au niveau haut (5V) – allume la LED

  //delay(500); // pause de 500 millisecondes (ms)

  //digitalWrite(LED,LOW); // met la broche au niveau bas (0V) – éteint la LED

  //delay(500); // pause de 500ms
 
}

// Interruption appelée à tous les changements d'état de A
void doEncoderA(){
  A_set = digitalRead(encoderPinA) == HIGH;

  encoder0Pos += (A_set != B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
}

// Interruption appelée à tous les changements d'état de B
void doEncoderB(){
  B_set = digitalRead(encoderPinB) == HIGH;

  encoder0Pos += (A_set == B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
}

// Cette fonction est appelée toutes les 20ms pour calcul du correcteur PID
void asservissement()
{
  time += 20; // pratique pour graphes excel après affichage sur le moniteur

  erreur = target_ticks - encoder0Pos;
  somme_erreur += erreur;

  // Calcul de la vitesse courante du moteur
  int vitMot = kp * erreur + kd * (erreur - erreurPrecedente) + ki * (somme_erreur);

  erreurPrecedente = erreur; // Ecrase l'erreur précedente par la nouvelle erreur

    // Normalisation et contrôle du moteur
  if(vitMot > 25) vitMot = 25;  // sachant que l'on est branché sur un DRV8835, vitesse limite du moteur
  else if (vitMot < -25) vitMot = -25;

    if (vitMot > 0) {
    digitalWrite(LED_PIN, HIGH);
     } 
  else {
     digitalWrite(LED_PIN, LOW);
       }
     motors.setM1Speed(vitMot);  

float angle_deg = encoder0Pos/297.92/12.0*360.0; //Position angulaire de sortie, pratique pour comparer avec la consigne d'entrée
float tour = encoder0Pos/297.92/12.0;

  Serial.print(erreur);  // affiche sur le moniteur les données voulues
  Serial.print(" ");
  Serial.print(encoder0Pos);
  Serial.print(" ");
  Serial.print(angle_deg);
  Serial.print(" ");
  Serial.print(tour);
  Serial.print(" ");
  Serial.println(vitMot);
}

Mon problème est le suivant :

Le code effectue parfaitement bien la fonction, mais en réalité si je demande au moteur de tourner d'un tour dans le sens positif (sens trigo), il ne fait qu'un demi-tour, alors que si je demande un tour dans le sens négatif (sens horaire), il effectue un tour et demi.

Avez-vous des idées sur la cause du problème ? Merci.

j'ai pas tout lu mais je déclarerai encoder0Pos, A_set et B_set comme volatile vu l'usage dans les Interruptions

et dans cette formule de PID

  int vitMot = kp * erreur + kd * (erreur - erreurPrecedente) + ki * (somme_erreur);

(j'ai pas vérifié) mais je m'assurerai que le calcul est entièrement effectué en flottant avant d'être converti en int

Bonjour,

A mon avis de la façon dont tu gères ton codeur tu as une incrémentation tout les 1/2 pas codeur.
Ce qui expliquerait que tu ne tournes que d'un 1/2 tour.
Maintenant pourquoi tu tournes d'un tour 1/2 dans l'autre sens, je n'ai pas d'explication.

Bonjour,

Que disent tes sorties sur le moniteur série ? Ca devrait pas mal aider à trouver la cause du problème

Tu devrais valider ta routine de gestion de l'encodeur en coupant l'alimentation du moteur et en tournant l'arbre moteur à la main afin de vérifier si le comptage est correcte.

J-M-L:
j'ai pas tout lu mais je déclarerai encoder0Pos, A_set et B_set comme volatile vu l'usage dans les Interruptions

et dans cette formule de PID

  int vitMot = kp * erreur + kd * (erreur - erreurPrecedente) + ki * (somme_erreur);

(j'ai pas vérifié) mais je m'assurerai que le calcul est entièrement effectué en flottant avant d'être converti en int

J'ai déclaré encoder0Pos, A_set et B_set comme volatile mais je n'ai pas observé de changement.
Comment peux-t-on faire cette vérification ?

kamill:
Bonjour,

A mon avis de la façon dont tu gères ton codeur tu as une incrémentation tout les 1/2 pas codeur.
Ce qui expliquerait que tu ne tournes que d'un 1/2 tour.
Maintenant pourquoi tu tournes d'un tour 1/2 dans l'autre sens, je n'ai pas d'explication.

J'ai eu la même réflexion, je n'explique pas cette différence de comptage d'un sens à l'autre...

3Sigma:
Bonjour,

Que disent tes sorties sur le moniteur série ? Ca devrait pas mal aider à trouver la cause du problème

Les valeurs sur les sorties du moniteur série sont toutes cohérentes, les calculs sont donc probablement corrects, c'est la réalité qui est éloignée des valeurs affichées.
Ci-joint une capture d’écran du moniteur, avec respectivement de gauche à droite :
erreur, encoder0Pos, angle_deg, tour, vitMot

On voit donc que le moteur tourne jusqu'à atteindre une erreur d'environ 0 (erreur = target_ticks - encoder0Pos), le moniteur affiche aussi que le moteur a effectué 360° soit un tour. La vitesse est limité à 25 jusqu'à ce que erreur*0.8 < 25 (PID).

fdufnews:
Tu devrais valider ta routine de gestion de l'encodeur en coupant l'alimentation du moteur et en tournant l'arbre moteur à la main afin de vérifier si le comptage est correcte.

Les résultats sont totalement différents entre le comptage à la main et celui automatique.

De plus, j'ai remarqué que le comptage est différent selon ma tension d'alimentation du capteur, j'ignore pourquoi, les résultats ne sont pas non plus proportionnelles à la tension d'entrée.

J'ai déclaré encoder0Pos, A_set et B_set comme volatile mais je n'ai pas observé de changement.
Comment peux-t-on faire cette vérification ?

ce n'était peut-être pas le pb mais si vous ne déclarez pas les variables comme volatiles, le compilateur peut décider d'optimiser le code et conserver la valeur de la variable en mémoire dans un registre pendant l'exécution de la boucle par exemple. Ce qui fait que une interruption peut survenir, modifier la valeur en mémoire et ensuite l'exécution revient à la boucle mais comme le code ne relis pas la mémoire et se contente d'accéder le register, il n'aura pas la dernière info.

J-M-L:
ce n'était peut-être pas le pb mais si vous ne déclarez pas les variables comme volatile, le compilateur peut décider d'optimiser du code et conserver la valeur de la variable en mémoire dans un registre pendant l'exécution de la boucle par exemple. ce qui fait que une interruption peut survenir, modifier la valeur en mémoire et ensure revenir à la boucle mais comme le code ne relis pas la mémoire et se contente d'accéder le register, il n'aura pas la dernière info.

Très bien, merci pour l'explication, mon code n'en sera qu'améliorer :slight_smile:

EDIT - tout ce qui est ci dessous est à oublier :slight_smile:

Oui et c'est aussi le cas entre 2 interruptions
en fait dans le code aujourd'hui vous avez un risque d'erreur lié à l'usage de encoder0Pos dans les 2 interruptions
```
~~// Interruption appelée à tous les changements d'état de A
void doEncoderA(){
  A_set = digitalRead(encoderPinA) == HIGH;

encoder0Pos += (A_set != B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
}

// Interruption appelée à tous les changements d'état de B
void doEncoderB(){
  B_set = digitalRead(encoderPinB) == HIGH;

encoder0Pos += (A_set == B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
}~~
~~ ~~si vous n'avez pas de chance la valeur de `encoder0Pos` peut changer pendant l'exécution de l'interruption sur la pin 3 car cette interruption peut être elle même interrompue par une interruption sur la pin 2 qui est plus prioritaire. si vous n'avez vraiment pas de bol, ça se passe en plein pendant l'addition de `doEncoderB` et vous écrasez la valeur qui a été modifiée par `doEncoderA`~~ ~~je n'ai pas regardé comment fonctionne votre moteur et si c'est possible que `doEncoderB` soit interrompu par `doEncoderA` si le moteur tourne dans un sens ou dans l'autre mais ça pourrait expliquer aussi votre pb.~~ ~~le meilleur moyen de tester cela sera de traiter l'addition sous forme de code critique atomique et donc d'interrompre les interruptions avant l'addition et les réactiver ensuite avec un appel à `noInterrupts`() et `interrupts`()~~ ~~
~~// Interruption appelée à tous les changements d'état de A
void doEncoderA(){
  A_set = digitalRead(encoderPinA) == HIGH;
  noInterrupts(); // section critique
  encoder0Pos += (A_set != B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
  interrupts();
}

// Interruption appelée à tous les changements d'état de B
void doEncoderB(){
  B_set = digitalRead(encoderPinB) == HIGH;
  noInterrupts(); // section critique
  encoder0Pos += (A_set == B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
  interrupts();
}~~
```

Serial est initialisé à 9600 bauds.
Il y a une bonne quantité d'information à envoyer.
Serial, lorsque le buffer d'émission sature, est une fonction bloquante.
Il serait peut-être judicieux d'utiliser un baudrate plus élevé afin de vider rapidement le buffer d'émission et ne pas risquer de blocage.

Concernant la sortie de l'encodeur, je pense qu'il serait bon d'utiliser des résistances externes de 10K, comme préconisé par le vendeur de l'encodeur, plutôt que les pullups du processeur afin de garantir des fronts de monté un peu plus raides.

J-M-L:
si vous n'avez pas de chance la valeur de encoder0Pos peut changer pendant l'exécution de l'interruption sur la pin 3 car cette interruption peut être elle même interrompue par une interruption sur la pin 2 qui est plus prioritaire. si vous n'avez vraiment pas de bol, ça se passe en plein pendant l'addition de doEncoderB et vous écrasez la valeur qui a été modifiée par doEncoderA

Bonjour,

Les interruptions sont masquées lorsqu'on entre en interruption. Dans le cas ou on voudrait traiter une interruption pendant le traitement de l'interruption, il faut les démasquer explicitement

J-M-L:
le meilleur moyen de tester cela sera de traiter l'addition sous forme de code critique atomique et donc d'interrompre les interruptions avant l'addition et les réactiver ensuite avec un appel à noInterrupts() et interrupts()

// Interruption appelée à tous les changements d'état de A

void doEncoderA(){
  A_set = digitalRead(encoderPinA) == HIGH;
  noInterrupts(); // section critique
  encoder0Pos += (A_set != B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
  interrupts();
}

// Interruption appelée à tous les changements d'état de B
void doEncoderB(){
  B_set = digitalRead(encoderPinB) == HIGH;
  noInterrupts(); // section critique
  encoder0Pos += (A_set == B_set) ? -1 : +1; //modifie le compteur selon les deux états des encodeurs
  interrupts();
}

Merci pour l'aide. J'ai changé mon code pour inclure les fonctions noInterrupts() et Interrupts(), je n'ai observé aucune différence..

fdufnews:
Serial est initialisé à 9600 bauds.
Il y a une bonne quantité d'information à envoyer.
Serial, lorsque le buffer d'émission sature, est une fonction bloquante.
Il serait peut-être judicieux d'utiliser un baudrate plus élevé afin de vider rapidement le buffer d'émission et ne pas risquer de blocage.

Concernant la sortie de l'encodeur, je pense qu'il serait bon d'utiliser des résistances externes de 10K, comme préconisé par le vendeur de l'encodeur, plutôt que les pullups du processeur afin de garantir des fronts de monté un peu plus raides.

Pensant que l'erreur vient du codeur, j'ai changé mon code pour lire mon signal.

Voici le nouveau :

// Ce code permet d'asservir en position angulaire un moteur à courant continu.

#include <SimpleTimer.h> // bibliothèque
#include <DRV8835MotorShield.h> // bibliothèque

SimpleTimer timer;                 // Timer pour échantillonnage

int val; 
int encoder0PinA = 2;
int encoder0PinB = 3;
int encoder0Pos = 0;
int encoder0PinALast = LOW;
int n = LOW;

#define LED_PIN 13

DRV8835MotorShield motors;

//int LED=12;
//init echantillonage
unsigned int time = 0;
const int frequence_echantillonnage = 20; // 20 Hz - mesure toutes les 50ms

//consigne
double target_tour = -1;//nombre de tour désiré

double target_deg = 360*target_tour; //nombre de degrès pour atteindre target_tour
int target_ticks; //plus simple d'asservir en ticks car ce sera toujours un nombre entier

// init calculs asservissement PID
float erreur = 0; //erreur
float erreurPrecedente = 0;
float somme_erreur = 0;

//Definition des constantes du correcteur PID
float kp = 0.80;              // Coefficient proportionnel   // choisis par tatonnement sur le moniteur. Ce sont les valeurs qui donnaient les meilleures performances
float ki = 0; //5.5;          // Coefficient intégrateur
float kd = 0;//100;           // Coefficient dérivateur


void setup() { 
   pinMode (encoder0PinA,INPUT);
   pinMode (encoder0PinB,INPUT);
   Serial.begin (115200);

   pinMode(LED_PIN, OUTPUT); 
  
  // uncomment one or both of the following lines if your motors' directions need to be flipped
  //motors.flipM1(true);
  //motors.flipM2(true);

  target_ticks = target_tour*297.92*12; // rapport de réduction = 297.92 et nombre de ticks par tour = 12 // nombre de ticks en entrée pour avoir target_tour en sortie

  digitalWrite(LED_PIN, 0);  // Initialisation sortie moteur à 0 
  delay(300);                // Pause de 0,3 sec pour laisser le temps au moteur de s'arréter si celui-ci est en marche

  timer.setInterval(1000/frequence_echantillonnage, asservissement);  // Echantillonage pour calcul du PID et asservissement; toutes les 100ms, on recommence la routine
}
 

void loop() { 

 timer.run();  //on fait tourner l'horloge
  //  digitalWrite(LED,HIGH); // met la broche au niveau haut (5V) – allume la LED

  //delay(500); // pause de 500 millisecondes (ms)

  //digitalWrite(LED,LOW); // met la broche au niveau bas (0V) – éteint la LED

  //delay(500); // pause de 500ms
  
   n = digitalRead(encoder0PinA);
   if ((encoder0PinALast == LOW) && (n == HIGH)) {
     if (digitalRead(encoder0PinB) == LOW) {
       encoder0Pos--;
     } else {
       encoder0Pos++;
     }
    
   } 
   encoder0PinALast = n;
} 


void asservissement()
{
  time += 20; // pratique pour graphes excel après affichage sur le moniteur

  erreur = target_ticks + encoder0Pos;
  somme_erreur += erreur;

  // Calcul de la vitesse courante du moteur
  int vitMot = kp * erreur + kd * (erreur - erreurPrecedente) + ki * (somme_erreur);

  erreurPrecedente = erreur; // Ecrase l'erreur précedente par la nouvelle erreur

    // Normalisation et contrôle du moteur
  if(vitMot > 25) vitMot = 25;  // sachant que l'on est branché sur un DRV8835, vitesse limite du moteur
  else if (vitMot < -25) vitMot = -25;

    if (vitMot > 0) {
    digitalWrite(LED_PIN, HIGH);
     } 
  else {
    digitalWrite(LED_PIN, LOW);
       }
     motors.setM1Speed(vitMot);  

float angle_deg = (encoder0Pos/(297.92*12.0))*360.0; //Position angulaire de sortie, pratique pour comparer avec la consigne d'entrée
float tour = encoder0Pos/(297.92*12.0); // nombre de tour effectués

  Serial.print(erreur);  // affiche sur le moniteur les données voulues
  Serial.print(" ");
  Serial.print(encoder0Pos);
  Serial.print(" ");
  Serial.print(angle_deg);
  Serial.print(" ");
  Serial.print(tour);
  Serial.print(" ");
  Serial.println(vitMot);
}

Le comportement est déjà bien meilleur :

  • Pour effectuer 1 tour de l'arbre, peu importe la tension d'alimentation du codeur, il me faut 900 ticks
  • La mesure est la même pour les deux sens :sweat_smile:

Je pense donc que mon codeur rate des ticks et n'en voit que 900 au lieu de 3575 en théorie.

ThomasCarlier:
Merci pour l'aide. J'ai changé mon code pour inclure les fonctions noInterrupts() et Interrupts(), je n'ai observé aucune différence..

C'est totalement inutile voire nuisible puisque tu réautorises les interruptions avant la fin réelle de l'interruption.

kamill:
C'est totalement inutile voire nuisible puisque tu réautorises les interruptions avant la fin réelle de l'interruption.

effectivement!

j'ai dit n'importe quoi pour l'arduino - j'avais une autre architecture en tête dans laquelle le processeur et le compilateur ne désactivent pas les interruptions dans le code des interruptions...

@Thomas - virez le code que vous avez rajouté, ce n'est pas une bonne idée

J-M-L:
@Thomas - virez le code que vous avez rajouté, ce n'est pas une bonne idée

C'est fait, pas de problème on aura essayé !

ThomasCarlier:
Le comportement est déjà bien meilleur :

  • Pour effectuer 1 tour de l'arbre, peu importe la tension d'alimentation du codeur, il me faut 900 ticks
  • La mesure est la même pour les deux sens :sweat_smile:

Je pense donc que mon codeur rate des ticks et n'en voit que 900 au lieu de 3575 en théorie.

Je continue mes essais, chose très étonnante, le nombre de ticks pour faire un tour est le même (900) à vitesse de rotation mini et maxi !
Dès lors, je pensais qu'à vitesse maxi je perdrais plus de ticks et que mon total serait bien inférieur à 900 par tour.

Je me demande si je perd réellement des ticks ou si l'erreur est ailleurs ? :roll_eyes:

ThomasCarlier:
Je continue mes essais, chose très étonnante, le nombre de ticks pour faire un tour est le même (900) à vitesse de rotation mini et maxi !
Dès lors, je pensais qu'à vitesse maxi je perdrais plus de ticks et que mon total serait bien inférieur à 900 par tour.

Je me demande si je perd réellement des ticks ou si l'erreur est ailleurs ? :roll_eyes:

Autre idée, en théorie j'ai 297.92*12 = 3575 ticks/tour

Or les 12 ticks/tour du codeur sont des CPR (counts per revolution).
Il y a donc un facteur 4 avec ma valeur réelle (environ 900 ticks/tour) et la valeur théorique, est-ce que mon encoder0Pos ne s'incrémenterai pas qu'à chaque 4 CPR ?

Tu es sûr de ce que tu fais dans doEncoderA et doEncoderB?
Je suis un peu étonné que sur la transition de A tu ne testes pas l'état des B et inversement.

Dans la specif du codeur que tu cites au début il est écrit

The encoder board senses the rotation of the magnetic disc and provides a resolution of 12 counts per revolution of the motor shaft when counting both edges of both channels. To compute the counts per revolution of the gearbox output shaft, multiply the gear ratio by 12.

Dans ta dernière version de programme tu ne traites qu'un front sur un seul canal, donc il semblerait qu'il faille diviser par 4

fdufnews:
Tu es sûr de ce que tu fais dans doEncoderA et doEncoderB?
Je suis un peu étonné que sur la transition de A tu ne testes pas l'état des B et inversement.

Tu verras post #11 que j'ai modifié mon code, je n'utilise plus doEncoderA et doEncoderB pour acquérir le signal du codeur.

J'utilise le code suivant :

int val; 
int encoder0PinA = 2;
int encoder0PinB = 3;
volatile long encoder0Pos = 0;
int encoder0PinALast = LOW;
int n = LOW;


void loop() { 

   n = digitalRead(encoder0PinA);
   if ((encoder0PinALast == LOW) && (n == HIGH)) {
     if (digitalRead(encoder0PinB) == LOW) {
       encoder0Pos--;
     } else {
       encoder0Pos++;
     }
    
   } 
   encoder0PinALast = n;
}

kamill:
Dans la specif du codeur que tu cites au début il est écritDans ta dernière version de programme tu ne traites qu'un front sur un seul canal, donc il semblerait qu'il faille diviser par 4

Si c'est bien le cas ça résoudrait mon problème ! :slight_smile:

Comment est-ce que je peux modifier mon code pour traiter les 2 fronts des deux canaux, pour m'assurer que c'est bien cela ?