Asservissement et pilotage de robot autonome

FIXME Les liens des archives dans cet article sont cassés.

Cet article est une introduction intuitive aux principaux problèmes que l'on peut rencontrer lorsque l'on souhaite contrôler le déplacement d'un robot. Comme chaque problème a sa solution, les principales seront décrites, en utilisant le moins de théorie possible. Le code source associé est sous license GPL, il est librement téléchargeable à cette adresse .

Sommaire

  • Introduction
  • Un mot sur le matériel (mécanique et électronique)
  • Asservissement d'un moteur à courant continu avec un régulateur PID
  • Génération de trajectoire moteur
  • Odométrie et positionnement
  • Primitives de déplacement robot
  • Pilotage du robot

Avant propos

J'ai essayé dans cette série d'article de partager du mieux que je pouvais mon travail sur l'asservissement et le pilotage de robot autonome. Cependant vous ne devez pas perdre de vue que je suis loin d'être un expert dans le domaine, je n'ai même pas suivi de cours d'automatique (je me suis trompé de département de spécialité ;) ). Je vais donc faire ici une simple introduction intuitive aux principaux problèmes que l'on peut rencontrer lorsque l'on souhaite contrôler le déplacement d'un robot et une description sommaire des principales solutions que l'on peut y apporter, tout cela en utilisant le moins de théorie possible. Je me suis ici concentré sur la partie informatique, même si quand c'est nécessaire je donne quelques informations sur la mécanique et l'électronique employées.

Tout le code source correspondant à ces articles est téléchargeable ici, il est entièrement sous licence GPL, ce qui signifie que vous pouvez librement l'utiliser, l'étudier, le modifier et le redistribuer, du moment que vous respectez les conditions de cette même licence. Je vous conseille de lire d'abord les headers (*.h) puisque c'est là que se trouve la majorité des commentaires. Le code est compilé pour un M32C85 de Renesas et il se base sur l'OS temps réel µC/OS-II. Malheureusement toutes les dépendances pour construire le binaire ne sont pas fournies, nous utilisons notamment une version commerciale de µC/OS-II qui nous a été donné par son concepteur et que nous ne pouvons bien entendu pas redistribuer librement.

Si vous trouvez des erreurs, ou si vous avez des remarques, des suggestions ou tout autre commentaire, n'hésitez pas à envoyer un mail depuis cette page : contact ClubElek, en précisant bien dans l'objet que vous parlez de l'article sur l'asservissement.

Introduction

Le document est divisé en 7 parties, correspondant aux différents modules nécessaires au déplacement autonome d'un robot :

Découpage en bloc [Découpage en Bloc]

Il faut noter que cet article est destiné aux robots de type “différentiel”, c'est à dire aux robots possédants une roue motrice de chaque coté, les roues étant contrôlées indépendamment l'une de l'autre par deux moteurs. Dans notre cas, il s'agira de moteurs à courant continu. Ce type de configuration autorise un large champ de mouvement, dont la rotation sur place. Le robot n'est cependant pas “holonome”, puisqu'il ne peut pas se déplacer selon n'importe quelle direction sans effectuer de rotation. Cependant, même si ce document cible cette configuration de robot particulière, la majeure partie des informations s'appliquent à beaucoup d'autres situations.

Schéma Robot Différentiel [Schéma Robot Différentiel]

Un mot sur le matériel

Technologie de moteur

Il existe trois principaux types de moteurs :

  • Les moteurs à courant continu (abrégé en CC) : ils sont très simple à faire tourner car il suffit de leur appliquer une tension et, en théorie leur vitesse de rotation varie linéairement avec la tension qu'on leur applique, l'intensité traversant le moteur étant quant à elle lié au couple que doit imposer le moteur. Cependant la vitesse que va prendre le moteur n'est pas fixe, et va dépendre de beaucoup de facteurs. Il faut donc, pour obtenir un positionnement précis, coupler ces moteurs avec un système d'encodeurs. La plupart des moteurs industriels sont livrés avec un encodeur optique monté directement sur l'arbre du moteur. Ces encodeurs permettent de déterminer la position du moteur avec une grande précision (de 1/100e de tour à plus de 1/5000e pour les plus précis).
  • Les moteurs pas à pas (abrégé en PaP) : Légèrement plus complexe à mettre en mouvement, ils sont composés de plusieurs phases qu'il faut alimenter successivement pour le faire tourner. L'avantage est qu'il n'y a théoriquement pas besoin de les asservir, par contre ils possèdent généralement un rendement moins bon, et si on leur demande de fournir un effort trop important peuvent “sauter des pas”, du coup leur positionnement n'est pas garanti.
  • Les moteurs sans balais (ou moteur brushless) : beaucoup plus complexes à mettre en mouvement, ce sont des moteurs un peu hybride entre les pas a pas et les moteurs à courant continu, généralement ils possèdent trois phases, qu'il faut alimenter en léger décalage par rapport à la vitesse de rotation du moteur. Ces moteurs ont un bon rendement et un excellent rapport puissance/encombrement. Par contre du fait de leur caractéristique, ils sont assez complexes à mettre en mouvement.

Généralement, à la coupe Eurobot, les équipes débutantes préfèrent employer des moteurs pas à pas, car beaucoup plus facile à mettre en oeuvre du point de vue informatique. Le moteur CC reste, quant à lui, le meilleur compromis entre simplicité de mise en oeuvre et efficacité, c'est donc la solution utilisé sur la majorité des robots.

Capteur d'odométrie

Pour pouvoir contrôler le déplacement du robot, nous avons bien entendu besoin de capteurs qui nous informeront sur la manière dont le robot se déplace. Il existe encore une fois plusieurs techniques :

  • L'encodeur en queue de moteur : nous avons vu qu'il était possible d'équiper les moteurs de bonne qualité avec des encodeurs optiques en queue de moteur. Cependant cette technique ne permet pas toujours de suivre avec précision la position du robot. En effet, elle permet uniquement de lire le déplacement des roues de propulsion, qui peuvent glisser, avoir un diamètre variable et, pire encore, avoir une largeur non négligeable.
  • Encodeur sur 'roue folle' : la solution au problème des encodeurs sur les roues de propulsion peut être résolu simplement en équipant le robot d'une deuxième paire de roues très fine, qui entraîne directement un encodeur de bonne précision. Cette méthode n'est pas très simple à mettre en oeuvre d'un point de vue mécanique car elle demande une place importante sur l'axe des roues au niveau du sol. De plus, il reste le problème du robot qui se fait pousser en travers, les roues folles n'étant ici d'aucune utilité, et pire encore elles peuvent être endommagées à cause de leur finesse.
  • Souris optique : une autre possibilité, encore assez peu éprouvée, est de placer sous le robot des souris optiques de bonne qualité, cette technique est assez économique (le prix d'une souris est bien inférieur à celui d'un encodeur optique industriel), quoique beaucoup plus complexe à mettre en oeuvre. L'avantage de ce système, est qu'il ne possède aucun contact avec le sol, donc il ne s'abîme pas, et on peut le décaler par rapport à l'axe des roues du robot.

Châssis

L'expérience montre que la qualité du châssis est primordiale pour un asservissement du robot de qualité. Nous avons déjà testé un même système d'asservissement (moteur+encodeur+électronique de commande+programme) sur deux châssis différents, et nous avons pu constater que la réaction du robot n'était pas du tout la même dans les deux cas. Plus le châssis est de mauvaise qualité, notamment au niveau des appuis au sol, et de la transmission de la puissance, plus l'asservissement nécessite d'être bien réglé et doit être mis en oeuvre à une fréquence plus élevé. Un châssis de bonne qualité doit être très stable (pas de balancement du robot), assez léger. Le système de propulsion doit avoir le moins de jeu possible.

Calculateur

Il existe des composants tout intégrés qui gèrent l'asservissement d'un moteur CC muni d'un codeur optique. Les plus connus sont le LM629 et le HCTL1100, ils ont l'avantage d'être extrêmement performants, mais pèchent un peu sur la flexibilité, le prix et la simplicité du protocole de communication.

L'autre solution est donc de coder soi-même son asservissement, mais il faut pour cela utiliser un microcontrôleur relativement puissant et il faut aussi être capable de décoder les quadratures des encodeurs optiques. Certains µC possèdent des entrées spécialisées. Il existe des composants intégrés qui ne font que ça (HCTL2020). On peut aussi réaliser le schéma avec des portes logiques, ou mieux encore sur un FPGA ou un CPLD. Faire le décodage des quadratures en soft est assez délicat, car sur un moteur rapide muni d'un encodeur de bonne résolution, les fronts des deux signaux peuvent atteindre plus de 100kHz. A priori de nombreuses combinaisons sont possibles pour arriver à un asservissement correct, cependant il ne faut pas perdre de vue deux points qui sont déterminant pour la qualité de l'asservissement : il faut introduire le moins possible de retard dans la boucle de contrôle (lecture de l'état du système, décision et commande). On privilégiera donc les systèmes sur une même carte, et il faut faire en sorte que la fréquence de cette boucle soit la plus rapide et la plus stable possible. Il faut donc exclure les OS non temps réel, et choisir un processeur suffisamment puissant. Dans notre cas nous utilisons un Renesas M32C85 32bits @ 32Mips couplé à un FPGA, ce qui est très largement suffisant pour asservir rapidement deux moteurs CC.

Électronique de Puissance

Ce n'est plus tout à fait mon domaine ici. Vous trouverez plusieurs schémas sur internet : il suffit de chercher “Pont en H” ou “H bridge” sur Google. Le principe est de transformer une PWM (signal logique de fréquence fixe, dont le rapport cyclique varie), et un signal indiquant le sens de rotation du moteur en une tension hachée sur notre moteur qui peut avoir besoin de plusieurs ampères pour tourner. Là encore, ils existent des composants tout intégrés qui font ça très bien, comme le LMD18200 par exemple.

Asservissement d'un moteur à courant continu avec un régulateur PID

Objectif

Le but de l'asservissement est d'être capable de contrôler avec précision un moteur, dont la vitesse de rotation est naturellement imprécise et instable. Par exemple dans le cas d'un robot différentiel, si l'on veut faire en sorte que le robot puisse aller droit, il faut pouvoir commander les deux roues exactement à la même vitesse. Or comme un moteur à courant continu a une vitesse qui varie facilement, il faut d'une part être capable de la mesurer et, d'autre part, pouvoir faire varier la tension du moteur pour qu'il tourne correctement. Pour cela il existe plusieurs algorithmes. Le plus répandu s'appelle boucle de régulation PID. C'est celui que nous allons voir plus en détail ici.

Approche intuitive de la régulation PID

Pour comprendre comment fonctionne un contrôleur PID, le plus simple est de se mettre à sa place : imaginez que vous ayez à contrôler un système à l'aide d'un joystick. Pour être original on va prendre le cas d'un ascenseur sur un rail vertical, en poussant la manette vers le haut, le moteur qui déplace l'ascenseur se met à tourner et entraîne la cabine vers le haut, idem vers le bas. Lorsque le joystick est au neutre, le moteur est en roue libre, et l'ascenseur retombe ! Notre objectif est donc d'amener la cabine à une position précise, en mettant le moins de temps possible.

Normalement, c'est une tâche qu'un humain sait faire assez instinctivement, par contre il est plus difficile de savoir comment il raisonne pour atteindre l'objectif. Lorsqu'on y réfléchit, on trouve une première solution assez simple : “Plus la cabine est basse par rapport à son objectif, plus on pousse la manette vers le haut”. Si on appliquait rigoureusement cette consigne, on se rendrait compte que non seulement la cabine dépasse systématiquement son objectif, en oscillant avec de moins en moins d'amplitude autour de celui ci, mais qu'en plus après avoir atteint un état stable, la cabine est en dessous de son objectif.

En effet, le point d'équilibre entre la force du moteur et le poids de la cabine n'est atteint que pour un écart à la consigne significatif : rappelez vous, notre seule consigne était “Plus la cabine est basse par rapport à son objectif, plus on pousse la manette vers le haut”, donc lorsque la cabine est au niveau de son objectif, la règle nous indique de laisser la manette au neutre, ce qui fait redescendre la cabine, entraînée par son propre poids). Commençons donc par résoudre le second problème qui semble évident, ajoutons une règle qui dit : “Plus la cabine reste longtemps basse par rapport à son objectif, plus on pousse la manette vers le haut”, lorsque l'on teste cette nouvelle méthode, on constate que le problème d'oscillation autour de l'objectif est toujours important, mais que cette fois la cabine se stabilise au bout d'un certain temps à une position très proche du point à atteindre.

Notre problème d'oscillation vient quant à lui du fait que nous n'anticipons pas le franchissement de l'objectif, et donc que la cabine, entraînée par son inertie, est emporté dans son mouvement, et dépasse considérablement le point visé au premier passage. Pour éviter le problème, on se fixe une troisième et dernière règle : “Plus on s'approche de l'objectif plus on ralenti” ce qui se traduit aussi à l'inverse par : “Plus on s'éloigne de l'objectif, plus on accélère dans le sens inverse”. A ce stade, si on applique chacune des règles précédentes dans les bonnes proportions, on devrait être capable de contrôler notre système avec précision et rapidité.

 Courbe de réponse indicielle typique d'un régulateur PID

Un contrôleur PID fonctionne exactement de cette manière, à l'aide des trois règles intuitives décrites précédemment correspondant aux trois lettres P-I-D.

P pour proportionnelle, correspond à la première règle, on cherche donc à appliquer une consigne proportionnelle à l'erreur :

Sortie = kP*Erreur

kP étant le coefficient de proportionnalité de la composante proportionnelle. I comme intégrale, vient s'ajouter à la première composante en évitant cette fois l'accumulation de l'erreur au cours du temps. Un correcteur PI donnera la sortie en suivant cette formule

Sortie = kP*Erreur + kI*SommeDesErreurs.

kI étant le coefficient de proportionnalité de la composante intégrale.

D pour dérivée permet d'anticiper et d'amortir le mouvement, en prenant en compte la variation de l'erreur au cours du temps :

Sortie = kP*Erreur + kI*SommeDesErreurs + kD*VariationDeLErreur

kD étant le coefficient de proportionnalité de la composante dérivée.

Composantes du PID

[Les trois composantes du PID] La résultante (en violet), est égale à la somme des trois composantes : Proportionnelle (en vert), Intégrale (positive en jaune, négative en rouge), Dérivée (en bleu)

Implémentation en C

Le codage d'une boucle de régulation en PID est relativement aisé. L'idée de base est de créer une fonction à laquelle on passe l'erreur courante, cette fonction retient la somme des erreurs depuis le début du programme, ainsi que l'erreur à l'itération précédente. La sortie de la fonction est la somme des trois composantes coefficientées : la proportionnelle, l'intégrale et la dérivée de l'erreur. La première étant tout simplement calculée à partir de l'erreur courante, la seconde à partir de la somme des erreurs, et la troisième à partir de la différence entre l'erreur courante et l'erreur précédente (on calcule donc la dérivée sur une itération), on applique ensuite à chacune de ces valeurs un coefficient multiplicatif afin d'obtenir la consigne. Ces coefficients sont les réglages de notre régulateur. Voici un exemple simple de calcul de PID :

//Nos trois coefficients
static int kP=100;
static int kI=10;
static int kD=50;
int ComputePID(int error)
{
 
    static int lastError=0;
    static int errSum=0;
 
    int P,I,D;
 
    errSum += error;                 //Somme les erreurs depuis le début 
 
    errDif = error - lastError;      //Calcule la variation de l'erreur
    lastError = error;
 
    P = error * kP;                  //Proportionnelle
    I = errSum * kI;                 //Intégrale
    D = errDif * kD;                 //Dérivée
 
    return P + I + D;                //Le résultat est la somme des trois
                                     //composantes calculées précédemment
 
}

Vous trouverez les sources complètes dans les fichiers motor_PID(.h/.c) présent dans cette archive.

Suivant le cas il peut aussi être nécessaire de borner la valeur en sortie, par exemple si la sortie est une PWM généré par un timer 8 bits, on limitera la valeur à plus ou moins 255.

Le bout de code ci dessus doit être appelé le plus fréquemment possible. La méthode de régulation en elle même n'étant pas optimum, c'est la fréquence à laquelle on exécutera le code qui va déterminer la qualité de l'asservissement. Pour donner un ordre de grandeur, un bon asservissement pour un moteur à courant continu tourne entre 200 Hz et 5 kHz. Il est aussi très important que la fréquence d'asservissement soit stable. Typiquement, on la créera à l'aide d'un timer matériel.

Réglages des coefficients

Les trois coefficients kP, kI et kD peuvent être déterminés de plusieurs manières, il existe des méthodes théoriques et d'autres plus basées sur l'expérimentation.

Deux méthodes sont particulièrement répandues pour le réglage d'une boucle de contrôle en PID, la première est entièrement théorique, il s'agit de la méthode de “Naslin”. La seconde se base sur l'expérience et est donc beaucoup moins calculatoire, il s'agit de la méthode de “Ziegler et Nichols”. Ces deux méthodes ne seront pas détaillés ici, cherchez un cours d'automatique pour plus d'infos. Une fois qu'une solution approximative à pu être déterminée, on peut l'améliorer arbitrairement si l'on comprend l'influence de chacun des coefficients :

kP et kI améliorent le temps de réponse mais rendent le système moins stable kI permet en plus d'éliminer l'erreur statique, mais en contrepartie crée beaucoup d'oscillation kD ralentit la réponse, mais permet d'atténuer les oscillations et donc rend le système plus stable Pour avoir un bon système, il faut donc trouver un bon compromis entre la rapidité, la stabilité et l'erreur statique. Si vous voulez gagner du temps, il vaut mieux être capable de pouvoir régler les trois coefficients de l'asservissement à la volée, sans avoir à recompiler le programme ou reprogrammer le µC.

Pour caractériser la qualité d'un asservissement, on observe généralement sa réponse indicielle, c'est à dire qu'à T0, on passe la commande à une valeur donnée, et on mesure les caractéristiques suivantes : Le dépassement (overshoot) : c'est la distance de laquelle le système dépasse sa consigne. Le temps de montée (rise time) : cela représente le temps nécessaire au système pour passer de 10% à 90% de la consigne, il est donc significatif de la vitesse à laquelle le système réagit. Le temps de réponse (settling time) : c'est le temps nécessaire au système pour se stabiliser à la consigne avec une marge acceptable : typiquement 2%.

Bien entendu l'objectif est de diminuer au maximum ces trois valeurs. Dans certain cas, il peut ne pas être acceptable d'avoir un dépassement important, on le réduit donc au détriment du temps de montée. Il faut noter que les coefficients ne sont valables que pour une fréquence d'asservissement donnée. Si l'on double la fréquence d'asservissement, et que l'on souhaite conserver les réglages, il faut diviser par deux le coefficient kI, et doubler le coefficient kD.

Les sources complètes du régulateur PID seront bientôt accessibles ici. Bien que son principe reste identique, l'algorithme utilisé est légèrement plus complexe que celui présenté ici. J'y ai ajouté la possibilité de choisir le nombre de périodes sur lesquelles la dérivation est calculée, le nombre de périodes sur lesquelles l'intégration se fait, la valeur de l'intégrale peut être bornée et éventuellement une alarme peut être déclenchée lorsque l'intégrale dépasse une certaine valeur. Enfin, il est possible d'instancier plusieurs contrôleurs PID, avec des réglages différents, ce qui nous sera très utile par la suite.

Génération de trajectoire moteur

Objectif

Grâce à notre boucle de régulation en PID, nous sommes désormais capables de donner un ordre en position à notre moteur et, celui-ci atteint son objectif de manière satisfaisante (rapidement et avec précision). Cependant, il reste un problème à résoudre : si nous reprenons l'exemple précédent de l'ascenseur, en admettant que le moteur permettant de déplacer la cabine soit très puissant. Si l'on décide de monter d'un étage, et que pour cela on fixe la consigne du PID à l'étage supérieur, le moteur va partir très violemment, et s'arrêter avec une bonne oscillation autour du point d'arrivée : les passagers vont être sacrément secoués pendant le déplacement. Ce que nous voulons ici, c'est donc contrôler l'intégralité du déplacement de la cabine, et non pas seulement un point d'arrivée. Pour cela, il suffit de rajouter un niveau de commande. Etant donné que le moteur est capable d'atteindre une consigne le plus rapidement possible, il suffit de le guider en faisant varier progressivement cette consigne afin de créer un mouvement plus tranquille. Notre régulateur PID verrouille donc avec une certaine fermeté la position de la cabine tout en effectuant une série de micro-déplacements, un peu comme un moteur pas à pas. Lorsque les micro-déplacements sont enchaînés rapidement, le mouvement devient fluide et précis.

Commande de trajectoire en position

Notre première trajectoire moteur, va nous permettre de résoudre le problème de l'ascenseur, il s'agit d'une commande qui permet de déplacer le moteur d'une position à une autre sans à-coup.

Une méthode simple est de générer une trajectoire avec une vitesse en trapèze sur notre moteur, la première phase est une accélération constante, jusqu'à atteindre la vitesse maximale que l'on aura spécifié (notre vitesse de croisière), puis, avant d'arriver à l'objectif le moteur décélère de manière constante, jusqu'à son arrêt total sur l'objectif. Si l'on souhaite être plus précis et stable sur la trajectoire imposée au moteur, il faut veiller à calculer les commandes en position de manière théorique, c'est à dire que si l'on veut faire accélérer le moteur, il ne faut pas calculer l'ordre au temps t en se basant sur celui à t-1, mais bien sur la position à laquelle aurait du se trouver le moteur, soit en se basant sur sa position à t0.

De cette manière, si le moteur n'arrive pas à suivre temporairement la commande imposée, il la rattrapera au coup suivant puisqu'on lui demandera d'aller un peu plus loin que nécessaire pour assurer l'accélération. Ceci peut d'ailleurs créer des phénomènes un peu étranges, comme par exemple quand on oublie de brancher la puissance des moteurs (arrêt d'urgence), L'asservissement prend un retard considérable sur sa consigne et part très brutalement lorsque la puissance est rétablie. De la même manière, si l'on demande au moteur de tourner à une vitesse plus importante que ce qu'il est capable de faire, l'asservissement va prendre du retard sur la consigne qu'on lui aura imposé, et l'arrêt sera violent. D'où l'intérêt dans ces deux cas de vérifier que l'erreur n'augmente pas trop brutalement.

3 phases trajectoire trapézoidale [Les trois phases d'une trajectoire en trapèze]

Ici nous pouvons maîtriser l'accélération de notre moteur comme, par exemple ceux utilisés dans les trains, asservissent de manière à limiter la dérivée de l'accélération qui caractérise la 'secousse' (jerk en anglais) ressentie par les passagers lors de la mise en mouvement d'un wagon. Afin de gagner en finesse de réglage, on peut effectuer un changement d'unité, pour passer des ticks des encodeurs à une unité dont la granularité serait plus fine. Cela permet de garder nos calculs sur des entiers, ce qui est appréciable sur les petits microcontrôleurs. Implémentation en C

Le code est divisé en deux parties. D'abord, on calcule la forme du trapèze, notamment le nombre d'itération nécessaire pour atteindre les points caractéristiques de la trajectoire. Ensuite, un deuxième code permet d'incrémenter la consigne moteur. Il est appelé avant chaque appel à la régulation en PID. Ces fonctions ne traitent qu'avec des unités propres à l'asservissement, typiquement des ticks moteurs, et des périodes d'asservissement. Il faut donc implémenter une couche d'abstraction pour pouvoir directement commander le moteur en lui passant des mètres, m/s et m/s². La subtilité de la première partie réside dans la difficulté d'obtenir un résultat précis avec des entiers, il faut bien faire attention ici aux problèmes d'arrondis, en faisant en sorte de tourner les calculs dans le sens le plus avantageux. Il faut aussi faire attention à bien traiter le cas ou la vitesse maximale ne peut pas être atteinte sur la distance demandée (accélération trop faible), dans ce cas notre vitesse n'est plus un trapèze mais un triangle accélération - décélération.

Vous trouverez les sources dans les fichiers motor_positionCommand(.h/.c) présent dans cette archive.

Commande de trajectoire en vitesse

De la même façon, il peut être utile de pouvoir commander notre moteur en vitesse pure. Le calcul est plus simple puisqu'il suffit ici d'effectuer uniquement les deux premières phases du mouvement précédent : accélération et vitesse constante. Par contre, contrairement à la commande en position, la commande en vitesse n'a d'utilité que si l'on peut l'exécuter avec une vitesse initiale non nulle. On peut de cette manière enchaîner les commandes de vitesse, et créer des trajectoires complexes.

Implémentation en C

Le code ressemble énormément au précédent. J'ai gardé l'idée de la distance à parcourir, sauf qu'elle peut correspondre ici soit à la fin de la trajectoire, le robot étant toujours à sa vitesse cible à ce moment, soit à une sorte de distance de sécurité par exemple dans le cas d'un téléguidage au joystick, le robot s'arrête automatiquement si il ne reçoit plus d'ordre et qu'il a parcouru la distance.

Vous trouverez les sources dans les fichiers motor_speedCommand(.h/.c) présent dans cette archive.

Asservissement du différentiel (aussi appelé asservissement polaire ou en alpha-delta)

Jusqu'à présent nous sommes capable d'asservir le déplacement de chacune des roues du robot indépendamment l'une de l'autre. Mais intuitivement, on comprend bien que ce type d'asservissement n'est ni optimum, ni très pratique à utiliser.

Il n'est pas optimum car lorsque l'on souhaite faire exécuter au robot une ligne droite, si au cours du mouvement, la roue gauche prend du retard par rapport à la roue droite, son retard ne sera pas rattrapé immédiatement, et le robot va dévier de sa trajectoire. La solution pour éviter cela est de coupler l'asservissement de la roue gauche à celui de la roue droite de telle façon que si jamais la roue gauche prend du retard, la roue droite va ralentir elle aussi pour compenser et éviter ainsi la déviation angulaire.

La manière la plus simple de réaliser ce type d'asservissement et de considérer la somme des moteurs gauche et droit comme un moteur virtuel traduisant l'avancement du robot (noté delta), et la différence de ces mêmes moteurs comme un second moteur virtuel traduisant cette fois l'orientation du robot (noté alpha). De cette façon tout le code créé jusqu'à présent est directement réutilisable.

Cette technique est directement tirée de celle employée par l'équipe de l'IUT Ville d'Avray et l'équipe “Microb Technology” sur leurs robots participants à la coupe Eurobot 2006.

Odométrie

Principe

L'odométrie (dead reckoning en anglais) est une technique qui consiste à calculer la position du robot, en cumulant tous les déplacements effectués depuis la dernière position connue. Il s'agit d'une technique de positionnement relative qui est donc peu précise puisque les erreurs de mesure s'accumulent au cours du temps. Par sa nature, on l'associe généralement à l'asservissement du robot, en effet, comme l'asservissement, l'odométrie nécessite de lire les encodeurs moteurs le plus régulièrement possible. On effectue donc les calculs au même moment.

Principe du calcul d'odométrie

 Schéma explicitant les variables pour les calculs d'odométrie

Pour plus de clarté, le schéma n'est pas à l'échelle, dL et dR doivent être très largement inférieur à l'entraxe du robot.

Calibration

Pour utiliser pleinement un système d'odométrie à base de roue folle, ou d'encodeur sur arbre moteur, il faut calibrer les paramètres suivants avec soin :

  • Périmètre des roues
  • Distance entre les deux roues (entraxe)

Le périmètre des roues influe sur la précision de tous les déplacements du robot (ligne droite et rotation), il faut donc le calibrer en premier. Pour cela rien de plus simple : il suffit de faire une série de lignes droites, et de calculer l'écart entre la valeur théorique et la valeur mesurée. La distance entre les deux roues n'influe, elle, que sur les rotations. Il faut donc, de la même façon, effectuer un certain nombre de rotations sur place, et calculer l'écart entre la théorie et la pratique. Il existe des méthodes un peu plus complètes, notamment la méthode UMBmark (University of Michigan Benchmark test) qui consiste à effectuer un certain nombre de trajectoires en carré avec le robot, dans un sens puis dans l'autre, puis de déduire les corrections sur l'odométrie des positions d'arrivée successives du robot.

Détection du patinage

En couplant deux types de capteur d'odométrie différent, le premier directement relié aux roues de propulsion, et le second traduisant directement l'avancement du robot, il est possible de déterminer si le robot patine. Cela peut être très utile pour détecter les contacts avec un éventuel obstacle.

Mise en oeuvre en C

Le calcul d'odométrie est relativement simple : en faisant l'approximation des petits angles (le calcul doit donc toujours s'effectuer sur des micro-déplacements). On peut déterminer les variations de l'angle et de la position du robot comme ceci :

    dAlpha = (dRight-dLeft)/2;   //variation de l'angle
    dDelta = (dRight+dLeft)/2;   //variation de l'avancement
 
    //conversion en radian
    alpha += dAlpha / entraxeEnTick;
 
    //calcul des décalages selon X et Y
    dX = cosf(alpha) * dDelta;
    dY = sinf(alpha) * dDelta;
 
    //conversion de la position en mètre
    X += dX / tickParMetre;
    Y += dY / tickParMetre;

Vous trouverez les sources complètes dans les fichiers robot_odometry(.h/.c) présent dans cette archive.

Une optimisation simple consiste à ne pas recalculer le cosinus et le sinus à chaque coup car il s'agit d'une opération très lourde pour un microcontrôleur. On ne recalcule donc le vrai cosinus et sinus qu'une fois sur cent par exemple, le reste du temps on calcule le nouveau sinus par une approximation tiré d'une décomposition de Taylor au second ordre :

cos(x) = cos(a)-sin(a)*(x-a)+O(x-a)^2 (pour x~=a)

sin(x) = sin(a)-cos(a)*(x-a)+O(x-a)^2 (pour x~=a)

Génération de trajectoire robot

Maintenant que nous sommes capables de piloter les deux moteurs du robot avec précision, nous pouvons facilement le faire se déplacer. Pour faciliter tout de même un peu la programmation des stratégies plus évolués, nous allons créer une petite couche d'abstraction, qui nous permettra d'effectuer des trajectoires simples, avec des unités plus intuitives.

Primitives d'asservissement en position

Ligne droite

L'objectif ici est de faire avancer le robot en ligne droite d'une certaine distance avec une vitesse, une accélération et une décélération précise. Pour cela, il suffit d'asservir une trajectoire sur nos moteurs virtuels représentant le différentiel du robot : on verrouille l'angle à une vitesse nulle, et on exécute une trajectoire sous forme de rampe trapézoïdale (avec les trois phases d'accélération, vitesse constante et décélération) sur l'avancement du robot. On peut tout aussi bien faire ce mouvement en utilisant l'asservissement sur les deux roues gauche et droite : il suffit pour cela de leur appliquer à toutes les deux une même trajectoire trapézoïdale.

Rotation sur place

Le but de cette manœuvre est d'effectuer une rotation sur place, d'un angle précis, avec une vitesse, une accélération et une décélération fixés sur chacune des roues. De la même manière que précédemment, on asservit l'angle avec une rampe trapézoïdale, et on verrouille l'avancement à une vitesse nulle. Pour exécuter le mouvement avec l'asservissement en roues indépendantes, on applique une trajectoire trapézoïdale opposée sur chacune des roues.

Rotation selon un arc de cercle

Il suffit, ici, d'asservir chaque roue, en lui ordonnant un déplacement, à nouveau avec l'asservissement de position. Le déplacement sur la roue gauche doit être égal à : dL = angle*(rayon-entraxe/2.0f); où l'angle représente la 'distance' à parcourir sur l'arc de cercle, rayon étant le rayon de ce même cercle et entraxe, la distance entre les deux roues du robot. On obtient la distance à parcourir sur la roue droite de la même façon en changeant simplement le signe :

dR = angle*(rayon+entraxe/2.0f);

Il faut ensuite calculer la vitesse maximale à atteindre sur chaque roue. Pour cela il suffit de calculer le rapport des distances à parcourir sur chaque roue. La vitesse de la roue la plus lente sera donc égale à la vitesse de la roue la plus rapide multiplié par ce ratio. On considère ici que la vitesse maximale, n'est pas la vitesse du robot, mais la vitesse maximale que peut atteindre une roue. Cela traduit directement le fait que le moteur contrôlant la roue possède lui aussi une vitesse maximale.

Pour que notre trajectoire soit complète, il reste encore une petite subtilité : le robot partant à l'arrêt, il ne faut pas oublier la phase d'accélération pour que, pendant cette phase, le robot se déplace bien selon l'arc de cercle, il faut que l'accélération et la décélération sur chaque roue soit proportionnelle à la vitesse maximale de la roue. Si ce n'était pas le cas et que, par exemple, on choisissait la même accélération sur chacune des roues, le robot commencerai à partir en ligne droite, poursuivrai sur une sorte d'arc de clothoïde, et finirait a nouveau en ligne droite, le tout bien entendu sans avoir parcouru l'angle désiré.

if(fabs(dL) > fabs(dR))
{
    ratio = dR/dL;
    vMaxL = vMax; accL = accMax; decL = decMax;
    vMaxR = vMax*ratio; accR = accMax*ratio; decR = decMax*ratio;
}
else
{
    ratio = dR/dL;
    vMaxL = vMax*ratio; accL = accMax*ratio; decL = decMax*ratio;
    vMaxR = vMax; accR = accMax; decR = decMax;
}

Vous trouverez les sources complètes dans les fichiers robot_trajectory(.h/.c) présent dans cette archive.

Pilotage du robot (trajectoire robot)

Maintenant que notre robot sait faire différents morceaux de trajectoire, il faut enchaîner tout ça pour qu'il soit capable d'atteindre ses objectifs. Imaginons que le robot soit à une position (X,Y,Alpha), comment faire pour qu'il atteigne la position (X', Y') ?

Rotation - Ligne droite

Ici, l'idée la plus simple est de chercher un objectif pour le robot, lorsque l'objectif est trouvé, on calcule sa position par rapport à la position courante du robot, il suffit alors d'enchaîner les deux déplacements élémentaires : rotation sur place puis ligne droite pour l'atteindre rapidement. L'angle est calculé à l'aide de la fonction arc tangente (plus simplement “atan2f” en C). Le principal avantage de la méthode est qu'elle est simple et qu'elle permet de connaître à l'avance la trajectoire emprunté par le robot, ce qui est très utile dans le cas où l'on veut faire de l'évitement d'obstacle. Par contre le déplacement généré n'est pas forcément optimal dans le sens ou le robot doit être arrêté avant le début du mouvement, après la rotation, et en fin de mouvement.

Vous trouverez les sources complètes dans les fichiers robot_traj_wrappers(.h/.c) présent dans cette archive.

Clothoïdes

Les clothoïdes (ou spirale de Cornu) sont des courbes dont le degré de courbure varie de manière linéaire (la dérivée seconde de l'angle du robot est constante). Ce type de courbe traduit parfaitement la trajectoire que peut emprunter un robot (ou plus généralement une voiture). A première vue, on pourrait croire qu'une trajectoire composée de ligne droite et d'arc de cercle est réalisable, mais lorsqu'on y réfléchit plus attentivement, on s'aperçoit que ce type de mouvement implique que les deux roues subissent pendant un bref instant une accélération infinie, ce qui est impossible. Une trajectoire qui n'assure pas la continuité des accélérations (pour la rotation et l'avancement) du robot est donc théoriquement irréalisable. Cela signifie donc que l'on ne peut déterminer à l'avance et de manière précise le mouvement d'un robot que si ce mouvement est réalisable et donc, que s'il est du type clothoïde ou anticlothoïde, dont la ligne droite et la rotation sont des cas particuliers. Le calcul de clothoïdes permet donc de générer des trajectoires optimales, tout en connaissant à l'avance le chemin emprunté par le robot (ce qui peut être très utile pour l'évitement d'obstacle). Le seul inconvénient des clothoïdes est qu'elles sont beaucoup plus complexes à calculer, notamment lorsque l'on cherche à faire des trajectoires complexes en évitant les obstacles.

Comparaison Cercle Clothoide [Graphe différence entre clothoïde et ligne-arc-ligne] :

En rouge : une trajectoire en arc de cercle non réalisable par un robot non-holonome sans arrêt aux points A et B.

En bleue : une trajectoire en arc de clothoïde réalisable par un robot non-holonome sans marquer d'arrêt.

Utilisation d'un contrôleur flou

Une solution intermédiaire est d'employer un contrôleur flou jouant le rôle du “pilote du robot”…

[ en construction … ]

Article écrit par ROUVIERE Julien

5 avril 2007