Robotique – Internet des Objets – Cloud

Et autres Technologies du Futur

Tutoriel Arduino : asservissement en vitesse d’un moteur à courant continu

L’asservissement en vitesse d’un moteur à courant continu est la plupart du temps nécessaire pour les robots mobiles. On peut éventuellement se satisfaire d’un servomoteur à rotation continu dans le cas de petits robots mais dans un cas plus général (comme pour le robot gyropode Geeros), il sera préférable d’utiliser un moteur à courant continu avec réducteur, associé à un codeur incrémental (pour mesurer la vitesse de rotation). Le calcul de l’asservissement sera réalisé par un Arduino. Nous allons détailler tout ceci dans la suite de cet article.

Matériel utilisé

Ce tutoriel peut être mis en application facilement avec l’expérience « Commande de moteur électrique ».

La carte Romeo est intéressante car elle intègre de façon très compacte le micro-contrôleur (AVR Atmega328, le même cœur qu’un Arduino Uno) et un double pont en H permettant de contrôler deux moteurs à courant continu avec un courant max de 2 A en permanence (jusqu’à 3 A en pic). Ce composant est indispensable pour fournir suffisamment d’énergie au moteur et lui permettre de fonctionner à vitesse variable et dans les deux sens de rotation.

Si vous utilisez d’autres éléments pour ce tutoriel, faites un choix cohérent :

  • La tension de la batterie doit correspondre à celle du moteur (pour ne pas griller ce dernier avec une fausse manipulation)
  • Le courant max du moteur doit correspondre à ce que peut supporter le driver de puissance

Concernant ce dernier point, dans la pratique les risques sont très limités si vous ne bloquez pas le moteur. Le courant à vide est en général relativement faible ; le courant max est observé uniquement lorsque le moteur est bloqué alors qu’il est alimenté avec sa tension maximale.

Mesure de la vitesse de rotation avec un codeur incrémental

Le codeur incrémental fournit deux signaux carrés en quadrature, comme sur la capture ci-dessous:

Ces deux signaux permettent de mesurer à la fois la vitesse et le sens de rotation.

Calcul de la vitesse

La mesure de la vitesse se fait simplement en comptant le nombre d’impulsions pendant un temps fixe. Les données du problème sont les suivantes :

  • Le codeur est fixé à l’arbre moteur et non pas à l’arbre de sortie du réducteur (celui utilisé pour l’entrainement). Le rapport de réduction étant 34:1, l’arbre moteur fait 34 tours lorsque l’arbre « principal » en fait 1
  • Le codeur génère 48 impulsions à chaque fois qu’il fait un tour
  • La cadence d’échantillonnage utilisée pour l’asservissement sera de 0.01 s

Par conséquent, lorsque l’arbre principal fait un tour, le codeur génère :
34 * 48 = 1632 impulsions.

Si N est le nombre d’impulsions comptées en 0.01 s, la vitesse est (en rad/s, l’unité standard, sachant qu’un tour fait 2*π radians) :
2*π*N/(0.01*1632)

Un point très important concerne la résolution de la mesure, c’est-à-dire la plus petite valeur qu’il est possible de calculer. La formule est la suivante (en rad/s) :
2*π/(Ts*CPR*ratio)
avec :

  • Ts : cadence d’échantillonnage
  • CPR : nombre d’impulsions par tour du codeur
  • ratio : rapport de réduction du moteur

Le but étant d’avoir la plus faible résolution possible (pour avoir une bonne précision de mesure), il faut avoir Ts et/ou CPR et/ou ratio le plus grand possible. Cependant, ratio est fixé par le besoin en couple ou en vitesse maximale alors que Ts doit être plus petit que le temps de réponse souhaité pour l’asservissement du moteur (voir plus loin). Par conséquent, la seule réelle possibilité est de jouer sur CPR. Lors du choix d’un codeur incrémental, il est préférable de prendre celui qui permet d’obtenir le plus d’impulsions par tour (si le budget le permet).

Dans notre cas de figure, la résolution est la suivante
2*π/(0.01*1632) = 0.4 rad/s

Ce n’est pas exceptionnel, le seul moyen de faire mieux serait de réduire le temps de réponse de l’asservissement de vitesse du moteur (voir plus loin).

Comptage du nombre d’impulsions

Compter le nombre d’impulsions du codeur revient à compter le nombre de fronts montants et descendants des signaux jaune et bleu représentés sur l’image ci-dessus. Pour ce faire, la seule méthode viable consiste à brancher les deux signaux (les fils jaune et blanc sur le codeur utilisé) sur deux entrées « interruption » de la carte Arduino. Les deux autres fils (bleu et vert) seront respectivement branchés sur le 5 V et sur la masse de l’Arduino.

Sur une carte Romeo (comme sur un Arduino Uno d’ailleurs), il y a deux lignes d’interruption (numérotées 0 et 1), qui correspondent aux broches digitales 2 et 3. L’intérêt d’une ligne d’interruption est qu’elle permet, comme son nom l’indique, d’interrompre le déroulement des calculs sur le micro-contrôleur pour effectuer un traitement spécifique, en l’occurrence la mise à jour du compteur d’impulsions, avant de rendre la main à la boucle principale.

La seule « difficulté » est de savoir s’il faut incrémenter ou décrémenter le compteur dans le traitement de l’interruption. Il suffit pour cela d’observer les courbes ci-dessus, obtenues alors que le moteur tourne dans le sens positif. On constate que:

  • Lorsque la voie A (en jaune) passe au niveau haut, la voie B (en bleu) est au niveau bas
  • Lorsque la voie A passe au niveau bas, la voie B est au niveau haut

Quand le moteur tourne dans le sens positif, lors d’une interruption sur la voie A, les niveaux de A et B sont donc inversés. Le code correspondant sur l’Arduino sera le suivant :

void GestionInterruptionCodeurPinA()
{
  if (digitalReadFast2(codeurPinA) == digitalReadFast2(codeurPinB)) {
    ticksCodeur--;
  }
  else {
    ticksCodeur++;
  }
}

En ce qui concerne l’interruption liée à la voie B, c’est l’inverse :

  • Lorsque la voie B passe au niveau haut, la voie A est au niveau haut
  • Lorsque la voie B passe au niveau bas, la voie A est au niveau bas

Le code de traitement de l’interruption de la voie B sera donc le suivant :

void GestionInterruptionCodeurPinB()
{
  if (digitalReadFast2(codeurPinA) == digitalReadFast2(codeurPinB)) {
    ticksCodeur++;
  }
  else {
    ticksCodeur--;
  }
}

Vous aurez peut-être remarqué l’utilisation de la fonction digitalReadFast2() pour lire le niveau des entrées codeur, au lieu de la fonction standard digitalRead(). Cette fonction, issues de la librairie digitalWriteFast (téléchargeable à l’adresse http://www.3sigma.fr/telechargements/digitalWriteFast.zip) permet d’améliorer la rapidité de lecture et d’écriture des entrées digitales pour minimiser le temps passé dans la routine d’interruption (et donc rendre la main plus vite à la boucle de calcul principal). Pour utiliser cette bibliothèque dans votre code, vous devez faire deux choses simples

  • décompresser l’archive que vous aurez téléchargée dans le sous-répertoire « librairies » de l’installation de votre environnement de développement Arduino
  • ajouter au début de votre programme la ligne
    #include <digitalWriteFast.h>

Le code permettant de calculer la vitesse est le suivant :

// Nombre de ticks codeur depuis la dernière fois
codeurDeltaPos = ticksCodeur;
ticksCodeur = 0;

// Calcul de la vitesse de rotation
omega = ((2.*3.141592*((double)codeurDeltaPos))/1632.)/0.01;  // en rad/s

Pour que cela fonctionne correctement, ce code doit être exécuté à la cadence fixe de 0.01 s. L’utilisation de la fonction delay() pour faire ça est une mauvaise méthode. En effet, vous pourriez penser écrire le code suivant :

void loop() {

  // Traitements divers
  // Lecture d'entrées + calculs par exemple

  // Attente de 10 ms
  delay(10);

}

Le problème est que les « traitements divers » vont prendre un certain temps, inconnu (T) et potentiellement variable (dans le cas où ils incluent des instructions conditionnelles). Par conséquent, la boucle sera exécutée toutes les T+0.01 s et non pas toutes les 0.01 s, et la mesure de la vitesse de rotation sera fausse.

La bonne méthode consiste à utiliser un timer du micro-contrôleur pour générer une interruption toutes les 0.01 s. Vous pouvez utiliser pour cela la bibliothèque FlexiTimer2 (téléchargeable ici: http://www.3sigma.fr/telechargements/FlexiTimer2.zip). Après l’avoir décompressée, il faut la placer dans le sous-répertoire « librairies » de l’installation de l’environnement de développement Arduino et ajouter au début du programme la ligne :

#include <FlexiTimer2.h>

Enfin, il faut ajouter les deux lignes suivantes dans la fonction startup() :

FlexiTimer2::set(10, 1/1000., isrt); // résolution timer = 1 ms
FlexiTimer2::start();

La première ligne crée une interruption qui se produit toutes les 10 ms (avec une résolution de 1 ms), ce qui exécute alors la fonction isrt() (contenant le code permettant de calculer la vitesse de rotation et de réaliser l’asservissement de vitesse). La seconde ligne démarre ce timer.

 

Asservissement en vitesse du moteur

Pour asservir le moteur en vitesse, nous allons utiliser un régulateur de type PID (Proportionnel Intégral Dérivé). Ce régulateur combine les 3 actions suivantes:

  • Proportionnelle : action proportionnelle à l’écart entre la consigne de vitesse et la mesure
  • Intégrale: cette action est basée sur l’intégrale par rapport au temps de l’écart entre la consigne et la mesure. Elle permet de compenser une erreur permanente (appelée « erreur statique ») entre la consigne et la mesure
  • Dérivée: cette action est basée sur la dérivée par rapport au temps de l’écart entre la consigne et la mesure. Elle permet d’anticiper cet écart, ce qui est utile pour des systèmes naturellement instables, au prix d’une amplification des bruits de mesure

Il n’est pas utile d’associer systématiquement ces trois actions (la proportionnelle est cependant toujours présente). Dans notre cas de figure, un PI suffira, l’anticipation apportée par l’action dérivée n’étant pas utile.

Les gains proportionnel et l’intégral étant respectivement Kp=0.29 et Ki=8.93, le code du la régulation PID s’écrit:

  /******* Régulation PID ********/
  // Ecart entre la consigne et la mesure
  ecart = vref - omega;

  // Terme proportionnel
  P_x = Kp * ecart;

  // Calcul de la commande
  commande = P_x + I_x;

  // Terme intégral (sera utilisé lors du pas d'échantillonnage suivant)
  I_x = I_x + Ki * dt * ecart;
  /******* Fin régulation PID ********/

 

Code complet

Le code complet permettant de réaliser sur la carte Arduino Romeo cet asservissement de vitesse d’un moteur à courant continu avec codeur incrémental est le suivant :

#include <FlexiTimer2.h>
#include <digitalWriteFast.h> 

// Codeur incrémental
#define codeurInterruptionA 0
#define codeurInterruptionB 1
#define codeurPinA 2
#define codeurPinB 3
volatile long ticksCodeur = 0;

// Moteur CC
#define directionMoteur  4
#define pwmMoteur  5

// Cadence d'envoi des données en ms
#define TSDATA 100
unsigned long tempsDernierEnvoi = 0;
unsigned long tempsCourant = 0;

// Cadence d'échantillonnage en ms
#define CADENCE_MS 10
volatile double dt = CADENCE_MS/1000.;
volatile double temps = -CADENCE_MS/1000.;

volatile double omega;
volatile double commande = 0.;
volatile double vref = 3.14;

// PID
volatile double Kp = 0.29;
volatile double Ki = 8.93;
volatile double P_x = 0.;
volatile double I_x = 0.;
volatile double ecart = 0.;

// Initialisations
void setup(void) {

  // Codeur incrémental
  pinMode(codeurPinA, INPUT);      // entrée digitale pin A codeur
  pinMode(codeurPinB, INPUT);      // entrée digitale pin B codeur
  digitalWrite(codeurPinA, HIGH);  // activation de la résistance de pullup
  digitalWrite(codeurPinB, HIGH);  // activation de la résistance de pullup
  attachInterrupt(codeurInterruptionA, GestionInterruptionCodeurPinA, CHANGE);
  attachInterrupt(codeurInterruptionB, GestionInterruptionCodeurPinB, CHANGE);

  // Moteur CC
  pinMode(directionMoteur, OUTPUT);
  pinMode(pwmMoteur, OUTPUT);

  // Liaison série
  Serial.begin(9600);
  Serial.flush();

  // Compteur d'impulsions de l'encodeur
  ticksCodeur = 0;

  // La routine isrt est exécutée à cadence fixe
  FlexiTimer2::set(CADENCE_MS, 1/1000., isrt); // résolution timer = 1 ms
  FlexiTimer2::start();

}

// Boucle principale
void loop() {

  // Ecriture des données sur la liaison série
  ecritureData();

}

void isrt(){

  int codeurDeltaPos;
  double tensionBatterie;

  // Nombre de ticks codeur depuis la dernière fois
  codeurDeltaPos = ticksCodeur;
  ticksCodeur = 0;

  // Calcul de la vitesse de rotation
  omega = ((2.*3.141592*((double)codeurDeltaPos))/1632.)/dt;  // en rad/s

  /******* Régulation PID ********/
  // Ecart entre la consigne et la mesure
  ecart = vref - omega;

  // Terme proportionnel
  P_x = Kp * ecart;

  // Calcul de la commande
  commande = P_x + I_x;

  // Terme intégral (sera utilisé lors du pas d'échantillonnage suivant)
  I_x = I_x + Ki * dt * ecart;
  /******* Fin régulation PID ********/

  // Envoi de la commande au moteur
  tensionBatterie = 7.2;
  CommandeMoteur(commande, tensionBatterie);

  temps += dt;
}

void ecritureData(void) {

  // Ecriture des données en sortie tous les TSDATA millisecondes
  tempsCourant = millis();
  if (tempsCourant-tempsDernierEnvoi > TSDATA) {
    Serial.print(temps);

    Serial.print(",");
    Serial.print(omega);

    Serial.print("\r");
    Serial.print("\n");

    tempsDernierEnvoi = tempsCourant;
  }
}

void CommandeMoteur(double tension, double tensionBatterie)
{
	int tension_int;

	// Normalisation de la tension d'alimentation par
        // rapport à la tension batterie
	tension_int = (int)(255*(tension/tensionBatterie));

	// Saturation par sécurité
	if (tension_int>255) {
		tension_int = 255;
	}
	if (tension_int<-255) {
		tension_int = -255;
	}

        // Commande PWM
	if (tension_int>=0) {
		digitalWrite(directionMoteur, LOW);
		analogWrite(pwmMoteur, tension_int);
	}
	if (tension_int<0) {
		digitalWrite(directionMoteur, HIGH);
		analogWrite(pwmMoteur, -tension_int);
	}
}

// Routine de service d'interruption attachée à la voie A du codeur incrémental
void GestionInterruptionCodeurPinA()
{
  if (digitalReadFast2(codeurPinA) == digitalReadFast2(codeurPinB)) {
    ticksCodeur--;
  }
  else {
    ticksCodeur++;
  }
}

// Routine de service d'interruption attachée à la voie B du codeur incrémental
void GestionInterruptionCodeurPinB()
{
  if (digitalReadFast2(codeurPinA) == digitalReadFast2(codeurPinB)) {
    ticksCodeur++;
  }
  else {
    ticksCodeur--;
  }
}

Ce code intègre ce qui a été détaillé précédemment.

Catégorie : Arduino
  • Mat a dit :

    bravo pour ce tuto très clair et merci de partager ainsi vos connaissances
    :-)
    je file tester une partie de tout ça !

  • Sotheby a dit :

    Bonjour, je vais me lancer dans une régulation PID, comment as tu déterminé tes gains PI??? fondamentalement stp car mon appli est complétement différente.

    Merci d’avance.

  • Vincent a dit :

    Bonjour,

    Félicitation pour ton tutoriel qui est complet et synthétique !

    Tu m’as fait découvrir la bibliothèque DigitalWriteFast qui doit s’avérer très utile pour le gain de temps. Cependant j’aimerais savoir si tu as testé ton code parce que il y a une erreur dans la bibliothèque que j’ai chargée… En effet la fonction reste soulignée en rouge (dans mon code) alors que les arguments sont correctes. Peux-tu m’aider à ce sujet ?

    Cordialement

  • admin a dit :

    Bonjour,

    En effet, l’arborescence décompressée de la bibliothèque n’était pas utilisable telle quelle. J’ai modifié le lien de téléchargement (idem pour FlexiTimer2). Tu peux donc télécharger de nouveau, décompresser dans le dossier « libraries » de ton installation Arduino et ça va marcher.
    Si tu as toujours des soucis, n’hésites pas à me demander.

    Nicolas

  • Vincent a dit :

    Bonjour,

    Merci bien, je vais pouvoir regarder cela. Si un nouveau problème se pose je te tiens au courant.

    Cordialement

  • Vincent a dit :

    Bonjour,

    Je viens d’essayer la bibliothèque digitalWriteFast mais elle ne fonctionne toujours pas. Les arguements sont pourtant corrects mais elle reste toujours soulignée. Je ne vois pas trop d’où peut venir le problème d’après le header…
    Mais je vais utiliser une carte de comptage, car l’horloge de la MEGA 2560 n’est pas assez rapide pour compter toutes les impulsions d’après mes tests.
    Je vais essayer FlexiTimer dès que je l’a reçoit et s’il y a un problème je te tiendrais au courant.

    Cordialement

    • admin a dit :

      Bonjour,

      Où se trouve ton fichier digitalWriteFast.h ? Peux-tu me donner le chemin ? Normalement il doit être placé dans arduino-1.0/libraries/digitalWriteFast

      Tu as une erreur à la compilation ? Si oui, laquelle ?

      Merci d’avance pour les infos,

      Nicolas

      • Vincent a dit :

        Bonjour et désolé pour cette absence…

        Le chemin est « C:\Program Files\Arduino\libraries », c’est la où je place toutes mes librairies.

        L’erreur est la suivante :

        « #define digitalReadFast2(P)((int)_digitalReadFast2_((P)))
        Error : identificateur « _builtin_constant_p » non défini  »

        Donc n’ayant pas non eu beaucoup de temps pour rechercher l’erreur, j’ai continué avec digitalRead. Le gain de temps est si important que ça ?

        Bonne journée !!

  • admin a dit :

    Bonjour Vincent,

    Pour le gain de temps, tout dépend de ton besoin. Pour lire l’état d’un bouton poussoir, tu peux très largement t’en passer. Dans mon cas, c’est pour lire le niveau d’un signal codeur, il y a 1632 changements d’états par tour avec plusieurs tours par seconde, donc c’est intéressant d’aller le plus vite possible.
    Je te suggère de faire une installation « fraiche » de la dernière version de l’IDE Arduino, de télécharger cette bibliothèque et de tester. Je viens d’essayer, ça a fonctionné de mon côté.

    Bon courage,

    Nicolas

  • Argenty a dit :

    Bonjour,

    Dans un premier temps je souhaite te féliciter pour le tuto.
    Mais comme je débute dans le monde de l’arduino et l’électronique je voulais savoir si il était possible d’avoir le schéma de montage ?

    Ce tuto pourra m’aider car je souhaite fabriquer un follow focus HF.

    Merci d’avance :)

  • Luiggi a dit :

    Je veux faire un asservissement de vitesse sur une carte UNO plus un shield Motor officiel.
    Je commande la vitesse de rotation par un potentiomètre sans problème.
    Quand j’utilise la librairie FlexiTimer2 pour mesurer la vitesse, la commande de vitesse du moteur (pwm) ne fonctionne plus : je suis soit a l’arrêt soit à la vitesse max.
    Avez vous déjà rencontré ce type de problèmes?
    La librairie FlexiTimer2 agit elle sur les timer qui gérent le pwm?
    Cordialement et merci pour le travail réalisé

    Louis

    • admin a dit :

      Bonjour,

      Pouvez-vous poster votre code pour que je puisse me faire une idée plus précise ?

      Merci !

  • ZAID a dit :

    merci pour ce tuto il m’a beaucoup aidé ;)

  • carouch a dit :

    Bonjour,
    Merci pour ce superbe tuto!!
    Par contre, je n’arrive pas à savoir ce que je dois changer pour programmer par exemple que je veux 2tours à 20 tours par minute et 3 tours à 50tours par minute sans changer la valeur de la tensionbatterie.
    Je ne sais pas si j’ai bien tout compris : vref=3.14 = 3.14*9.549296= 30 tr/min. tu veux donc que ton moteur tourne à cette cadence?
    Merci pour ta réponse.
    Bonne journée

    • admin a dit :

      Bonjour,

      Oui, vref est la vitesse de rotation souhaitée en rad/s. 3.14 correspond à un demi-tour par seconde, donc en effet à 30 tr/min.

  • Franck a dit :

    Bonjour,

    Je débute sur une base d’EasyRobotics avec deux servo moteur modifier en moteur CC driver par une carte
    Ardumoto.

    Les tolérances de fabrication des moteur font que mon Robot n’avance pas droit. Donc la premier chose à faire
    je suppose et de faire un asservissement mais lequel?? En « vitesse » ou en « position »???

    Apres recherche je n’est pas bien saisi la différence entre les deux.

    Bonne journée.

    • admin a dit :

      Bonjour Franck,

      Lorsque le moteur est asservi en position, on lui donne une consigne d’angle (par exemple 50 degrés) et le moteur rejoint cette consigne. Si tu fais un asservissement de position sur ton robot, il ne va pas aller bien loin (le moteur va tourner de 50 degrés et s’arrêter).
      Tu dois donc faire un asservissement de vitesse et donner par exemple une consigne de 50 degrés/secondes. Dans ce cas, le moteur va tourner à cette vitesse jusqu’à ce que tu lui dises de s’arrêter (consigne de 0 degrés/seconde).

Votre adresse email ne sera pas publiée. Champs requis marqués avec *

*


cinq × 3 =