Nous allons passer sur un nouveau robot de la marque MakeBlock : le modèle Ultimate qui permet de faire ceci par exemple :
Commençons par présenter les trois nouveaux composants de ce modèle.
Première différence : les moteurs à courant continu sont remplacés par des encodeurs.
Vu de l'extérieur, on ne voit pas grosse différence.
Les moteurs à courant continu (DC Motor) du modèle précédent sont simples à contrôler : plus on met de tension électrique (la grandeur qui se mesure en volt), plus le moteur tourne vite. Cette commande de tension se fait par l'utilisation d'un nombre compris entre 0 et 255.C'est simple, mais nous n'avons pas de possibilité de contrôler exactement le nombre de tours qu'a fait le moteur. On met de la tension et on espère qu'il tourne comme il devrait... Or, une commande de 2500 ms en montée ou en descente ne donnera la même distance parcourue. C'est génant.
Comme on peut le voir ce type de moteur ne possède que deux bornes : il suffit de lui appliquer une tension.
Heureusement, il existe un autre type de dispositif : l'encodeur. Un moteur de ce type va pouvoir fournir une indication précise sur le nombre de tours qu'il a fait. Ainsi, on peut le commander pour faire 100 tours en descente et le commander pour qu'il refasse 100 tours précisement en montée. Il reviendra donc exactement à sa position initiale.
On peut voir sur l'image qu'il y a plus que deux bornes : il faut commander le moteur et on peut lire de combien de degrés le moteur à tourner.
En réalité, le robot Ultimata en version tank avec pince possède deux types de moteur de ce type :
Il existe également une grosse différence au niveau de la carte.
Le robot précédent utilisait une carte Orion basée sur la UNO-328. La carte Orion fournit de base 8 prises RJ-45 et 2 prises permettant de contrôler directement des moteurs à courant continu.
Le robot Ultimate est lui fourni avec une carte MegaPi basée sur la carte Arduino MEGA2560.
Sur la photo, on peut voir en haut à gauche, les 4 interfaces qui permettent de contrôler des moteurs à courant continu, des moteurs pas à pas (stepper en anglais) ou des codeurs.
Pour les codeurs, on a placé, sur ces 4 interfaces, 4 Drivers capables de gérer chacun deux moteurs à courant continu ou un Codeur.
La maquette possède également un Shield permettant de gérer 4 entrées/sorties avec les RJ45 typiques des robots Makeblock :
Au final, la carte ressemble alors à ceci (avec le Shield Bluetooth en plus en haut à droite):
On placera les 3 codeurs sur les premieres bornes blanches et le moteur pas à pas de la pince sur deux des bornes vertes du dernier module de commande des moteurs.
Il s'agit d'un modèle simple à utiliser, alimenté par un moteur à courant continu mais muni d'un dispositif de protection pour ne pas pouvoir l'endommager ni à l'ouverture complète ni à la fermeture complète.
En gros, dans un sens on la ferme et dans l'autre sens de rotation, on l'ouvre.
Voyons d'ailleurs maintenant comment commander les 4 moteurs de la façon la plus facile possible : comme des moteurs à courant continu.
Dans un premier temps, nous allons vérifier que les moteurs fonctionnent bien en les commandant comme de simples moteurs à courant continu. Cela nous permettra de vérifier la connectique.
En effet, on peut contrôler le codeur pour qu'il se comporte comme un simple moteur à courant continu. Les vrais moteurs à courant continu n'ont besoin que de deux cables qu'on peut placer sur les bornes vertes. Ainsi sur l'interface n°1, on nommera ces bornes PORT1A
et PORT1B
puisqu'on a 4 fils. Ici, nous avons donc cabler 3 codeurs sur les ports 1, 2 et 3. A l'interne, on n'utilisera par contre que deux des entrées : on pourra les commander comme si on avait branché un moteur à courant continu sur les bornes vertes.
01° Ouvrir Arduino. Aller dans le menu OUTILS - TYPE DE CARTE et choisir Arduino/Genuino Mega or Mega2560.
02° Ouvrir Arduino. Aller dans le menu OUTILS - TYPE DE CARTE et choisir Arduino/Genuino Mega or Mega2560.
Pour trouver un programme compatible pour la gestion des moteurs à courant continu, le mieux est encore d'aller chercher dans les exemples : FICHIER - EXEMPLES - MAKEBLOCK DRIVE. A partir de là, il faudra chercher les fichiers MeMegaPi. Ici, je fournis un dérivé du MeMegaPiDMotor.
03° Utiliser le programme d'exemple fourni ci-dessous. Attention, je considère que vous avez placé votre pince sur les deux premières broches de l'interface n°4 :.
#include "MeMegaPi.h"
#include <SoftwareSerial.h>
MeMegaPiDCMotor motorPince(PORT4B);
uint8_t motorSpeed = 100;
void ouverture_totale()
{
motorPince.run(-100);
delay(6000);
motorPince.stop();
}
void setup()
{
ouverture_totale();
}
void loop()
{
motorPince.run(motorSpeed);
delay(1000);
motorPince.run(-motorSpeed);
delay(1000);
motorPince.stop();
delay(5000);
}
Normalement, vous devriez constater que la pince tente initialement de s'ouvrir à fond. Ensuite, elle se sert et se remet en place pendant 2s puis reste immobile pendant 5s.
Attention :Les parties suivantes vous demande de commander des pièces qui sont moins protégées que la pince. Pensez à utiliser l'interrupteur si vous constatez que l'échelle peut forcer sur la carte ou monter trop haut.
04° Faire des tests de façon à trouver l'initialisation à utiliser pour commander la plate-forme (parmi les choix-ci dessous). Voyez également si on la fait monter avec une vitesse positive ou négative.
MeMegaPiDCMotor motorEchelle(PORT3B);
MeMegaPiDCMotor motorEchelle(PORT3A);
05° Faire des tests de façon à trouver l'initialisation à utiliser pour commander le moteur droit puis le moteur gauche.
MeMegaPiDCMotor motorDroit(PORT1B);
MeMegaPiDCMotor motorDroit(PORT1A);
MeMegaPiDCMotor motorDroit(PORT2B);
MeMegaPiDCMotor motorDroit(PORT2A);
Passons aux codeurs. D'abord quelle est la différence entre un moteur DC et un codeur DC ?
Un moteur DC tourne lorsqu'on lui impose une tension. Simple. Il tourne. De combien de tours ? Là, c'est impossible de le savoir précisément.
Un codeur DC tourne lorsqu'on lui impose une tension. Mais il possède des bornes de sortie qui renvoient des signaux. Par exemple, le codeur pourrait envoyer 100 signaux lorsque son axe de rotation fait un tour complet. Ils permettent donc de savoir de combien de degrés le moteur à tourner.
On veut tourner de 1/4 de tours (90°) en sachant qu'on possède un codeur qui envoie 100 signaux par tour ? C'est simple : il vous suffit de le faire fonctionner jusqu'à avoir obtenu 25 signaux de sortie (un quart de 100). Simple et efficace.
Observez le signal rouge parvenant à traverser le disque dans le système de codage ci-dessous :
Si vous comptez, vous pourrez voir qu'il y a 16 trous qui permettent le passage du signal rouge (nous verrons plus tard à quoi servent les autres). Du coup, 16 détections d'apparition du lumière rouge veut dire qu'on a fait un tour complet, 360°.
Sur les codeurs Makeblock, le dispositif est composée d'un émetteur lumineux, d'un capuchon comportant 8 encoches et d'un récepteur de lumière :
Si on met de côté les vibrations et autres oscillations, cela veut dire qu'à chaque fois qu'on a reçu 8 impulsions de capteur de lumière, c'est que le moteur a fait un tour.
Pour voir comment contrôler ces fameux codeurs, le mieux est encore d'aller voir le fichier test : il se nomme MegaPiOnBoardStepperTest.
06° Placer le programme suivant dans Arduino. Il est basé sur l'un des exemples : EXEMPLES - MAKEBLOCKDRIVE - MeMegaPiProMotor -- MegaPiOnBoardStepperTest. Il est néanmoins modifié ici pour respecter quelques règles comme le fait :
#include "MeMegaPi.h"
#include <SoftwareSerial.h>
MeEncoderOnBoard encoder_1(SLOT1);
MeEncoderOnBoard encoder_2(SLOT2);
uint8_t rpmMoteurRoue = 60;// A configurer sans dépasser l'inndication sur le moteur
uint8_t reductionMoteurRoue = 46;// A configurer en regardant votre moteur
uint8_t nbrEncoches = 8;// A configurer en regardant votre moteur
void gestion_impulsions_encoder1()
{
if (digitalRead(encoder_1.getPortB()) == 0) encoder_1.pulsePosMinus();
else encoder_1.pulsePosPlus();
}
void gestion_impulsions_encoder2()
{
if (digitalRead(encoder_2.getPortB()) == 0) encoder_2.pulsePosMinus();
else encoder_2.pulsePosPlus();
}
void setup()
{
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
Serial.begin(115200);
//set pwm 1khz
// gestion du port 1 (le plus à droite)
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
// gestion du port 2
TCCR2A = _BV(WGM20);//PIN8
TCCR2B = _BV(CS21) | _BV(CS20) | _BV(WGM22);
// gestion du port 3 : en commentaire car inutile pour l'instant
// TCCR3A = _BV(WGM30);//PIN9
// TCCR3B = _BV(CS31) | _BV(CS30) | _BV(WGM32);
// gestion du port 4 : en commentaire car inutile pour l'instant
// TCCR4A = _BV(WGM40);//PIN5
// TCCR4B = _BV(CS41) | _BV(CS40) | _BV(WGM42);
encoder_1.setPulse(nbrEncoches);
encoder_1.setRatio(reductionMoteurRoue);
encoder_1.setPosPid(1.8,0,0.5);
encoder_1.setSpeedPid(0.18,0,0);
encoder_2.setPulse(nbrEncoches);
encoder_2.setRatio(reductionMoteurRoue);
encoder_2.setPosPid(1.8,0,0.5);
encoder_2.setSpeedPid(0.18,0,0);
}
void loop()
{
if (Serial.available())
{
char a = Serial.read();
switch(a)
{
case '0': // Retour position de base
encoder_1.moveTo(0, rpmMoteurRoue);
encoder_2.moveTo(0, rpmMoteurRoue);
break;
case '1': // +1 tour moteur / position initiale
encoder_1.moveTo(-360, rpmMoteurRoue);
encoder_2.moveTo(360, rpmMoteurRoue);
break;
case '2': // -1 tour moteur / position initiale
encoder_1.moveTo(360, rpmMoteurRoue);
encoder_2.moveTo(-360, rpmMoteurRoue);
break;
case '3': // +2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(-360*2, rpmMoteurRoue);
encoder_2.moveTo(360*2, rpmMoteurRoue);
break;
case '4': // -2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(360*2, rpmMoteurRoue);
encoder_2.moveTo(-360*2, rpmMoteurRoue);
break;
case '5': // arrêt
encoder_1.moveTo(encoder_1.getCurPos(), rpmMoteurRoue);
encoder_2.moveTo(encoder_2.getCurPos(), rpmMoteurRoue);
break;
case '6': // rotation -1/4 tour encodeur 1 / position initiale, petite vitesse
encoder_1.moveTo(-360/4, rpmMoteurRoue);
break;
default:
break;
}
}
encoder_1.loop();
encoder_2.loop();
Serial.print("Spd 1 : ");
Serial.print(encoder_1.getCurrentSpeed());
Serial.print(" - Spd 2 : ");
Serial.print(encoder_2.getCurrentSpeed());
Serial.print(" --- angle 1 : ");
Serial.print(encoder_1.getCurPos());
Serial.print(" - angle 2 : ");
Serial.print(encoder_2.getCurPos());
Serial.print(" --- Imp. 1 : ");
Serial.print(encoder_1.getPulsePos());
Serial.print(" - Imp. 2 : ");
Serial.println(encoder_2.getPulsePos());
}
07° Lancer le programme et la console série. Placer le robot pour qu'il ne puisse pas tomber en avancant ou en reculant.. Attention : pensez à modifier la vitesse de communication de votre console pour que cela corresponde à votre programme : regardez la méthode begin de la liaison série dans la fonction setup. Si le robot fait du bruit à l'"arrêt", c'est qu'il n'est pas vraiment à l'arrêt. Utilisez alors la commande '5' pour obtenir un arrêt réel.
08° Taper les commandes de 0 à 4 via la liaison série pour voir ce qu'elles provoquent.
Comme vous devriez le constater, le robot avance en tapant 1. Par contre, si vous retapez 1 tout de suite, il ne réavance pas ! Pourquoi ?
Simplement parce que le robot se déplace vers une position précise qu'il parvient à estimer à l'aide des impulsions qu'il détecte sur son moteur.
09° Sachant que votre codeur possède 8 encoches du côté moteur et que son réducteur est un réducteur 1/46 (en théorie), combien de tours devra faire le moteur pour que la roue tourne de 360° ? Cela correspond à combien d'impulsions moteur ?
...CORRECTION...
Puisqu'on réduit par 46 la rotation dans le sens moteur vers roue à l'aide du réducteur 1/46, il faut que le moteur fasse 46 tours pour que la roue tourne d'un tour, soit de 360°.
Dans la mesure où le moteur envoie 8 impulsions lorsqu'il fait un tour, on va devoir compter 8*46 = 368 impulsions.
La méthode getPulsePos vous donne le nombre d'impulsions stockées pour ce codeur.
10° Combien d'impulsions gagne-t-on approximativement si on tape 1 ? Combien d'impulsions perd-t-on si on revient à la positon 0 ?
...CORRECTION...
Derrière Imp, le print affiche justement la réponse de getPulsePos sur les deux encodeurs.
On n'obtient pas exactement les bonnes valeurs (368) mais on s'en approche.
Tentons maintenant de comprendre comment fonctionne ce programme... Cela nous permettra de tenter de corriger le fait que la roue ne tourne pas exactement de 1 tour.
#include "MeMegaPi.h"
#include <SoftwareSerial.h>
MeEncoderOnBoard encoder_1(SLOT1);
MeEncoderOnBoard encoder_2(SLOT2);
uint8_t rpmMoteurRoue = 60;// A configurer sans dépasser l'inndication sur le moteur
uint8_t reductionMoteurRoue = 46;// A configurer en regardant votre moteur
uint8_t nbrEncoches = 8;// A configurer en regardant votre moteur
Ici, rien de bien nouveau : on permet l'accès aux codes contenus dans les bibliothèques MeMegaPi (gestion de la carte MegaPi) et SoftwareSerial (gestion de la liaison Série).
On voit par contre l'apparition d'un nouveau constructeur : MeEncoderOnBoard. Il permet la création d'un objet permettant de faire l'interface entre le programme et le codeur. On pourra notamment y stocker le nombre d'impulsiosn détectées.
Ainsi, maintenant, nous avons ceci pour contrôler les moteurs en tant que codeurs :
MeEncoderOnBoard encoder_1(SLOT1);
Alors qu'avant nous les contrôlions comme de simples moteurs à courant continu à l'aide d'un autre constructeur :
MeMegaPiDCMotor motorDroit(PORT1B);
Il est temps d'aller voir la fonction setup puisque c'est la première à s'activer :
void setup()
{
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
Serial.begin(115200);
//set pwm 1khz
// gestion du port 1 (le plus à droite)
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
// gestion du port 2
TCCR2A = _BV(WGM20);//PIN8
TCCR2B = _BV(CS21) | _BV(CS20) | _BV(WGM22);
// gestion du port 3 : en commentaire car inutile pour l'instant
// TCCR3A = _BV(WGM30);//PIN9
// TCCR3B = _BV(CS31) | _BV(CS30) | _BV(WGM32);
// gestion du port 4 : en commentaire car inutile pour l'instant
// TCCR4A = _BV(WGM40);//PIN5
// TCCR4B = _BV(CS41) | _BV(CS40) | _BV(WGM42);
encoder_1.setPulse(nbrEncoches);
encoder_1.setRatio(reductionMoteurRoue);
encoder_1.setPosPid(1.8,0,0.5);
encoder_1.setSpeedPid(0.18,0,0);
encoder_2.setPulse(nbrEncoches);
encoder_2.setRatio(reductionMoteurRoue);
encoder_2.setPosPid(1.8,0,0.5);
encoder_2.setSpeedPid(0.18,0,0);
}
Et là, c'est le drame : pas une ligne qui contienne quelque chose que nous ayons déjà vu.
Nous allons ici nous focaliser sur les deux premières lignes :
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
11° Réflexe à avoir quand on ne sait pas : Faire une recherche Internet du type "Arduino + attachInterrupt". Vous devriez réussir à trouver plein de documentations. Tenter d'en comprendre des bouts. C'est surement en anglais, surtout si vous allez à la source de la documentation : DOCUMENTATION SUR ARDUINO.CC
Résumons : la fonction attachInterrupt est une fonction intégrée directement dans le langage. Comme print par exemple. Du coup, dans les codes ici présents, je ne les indique pas en rouge. Juste en noir.
Cette fonction attachInterrupt est un gestionnaire d'événements spécialisé dans la surveillance des broches de votre Arduino. En gros, cette fonction va provoquer la surveillance d'une des broches et va déclencher une fonction lorsque les données recues sur cette broche sont modifiées.
La fonction attend les arguments suivants dans l'ordre :
12° Que fait la fonction suivante ? (posez-vous la question sans lancer le code)
attachInterrupt( digitalPinToInterrupt(3), blink, CHANGE );
...CORRECTION...
On surveille l'événement suivant : la broche 3 change d'état.
En cas de détection, on lance la fonction blink.
13° Que fait le code suivant ?(posez-vous la question sans lancer le code)
const byte pinCapteur = 2;
attachInterrupt( digitalPinToInterrupt(pinCapteur), blink, RISING );
...CORRECTION...
On surveille l'événement suivant : la broche 2 passe de l'état LOW à l'état HIGH.
En cas de détection, on lance la fonction blink.
Retournons à notre Robot Makeblock :
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
Alors, c'est quoi ces deux lignes ? En réalité, c'est simple : nous ne savons absolument pas sur quelle broche de la MEGA PI est reliée le signal de notre codeur 1 par exemple. Heureusement, les concepteurs de Makeblock ont créé une méthode getIntNum qu'on peut appliquer aux objets fabriqués à partir du constructeur MeEncoderOnBoard. Par exemple, quand on le déclare, on tape simplement :
MeEncoderOnBoard encoder_1(SLOT1);
On sait donc que la variable encoder_1 mène à une interface qui permet de gérer un codeur qui doit être connecté au SLOT1 de notre carte MEGAPI. Mais aucun noméro de broche. Pour la connaitre, il faut aller lire les schémas OpenSource de la MegaPi ou simplement utiliser encoder_1.getIntNum
. Vous préférez quoi ?
Si vraiment vous êtes curieux, vous pouvez utiliser la liaison série pour afficher la valeur de la broche. Mais ca ne restera valable que sur votre configuration actuelle :
Serial.println( encoder_2.getIntNum() );
14° Alors, que fait le code suivant ?
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
...CORRECTION...
On surveille les fronts montants (RISING) de la broche de sortie du codeur correspondant à encoder_1. On lancera alors à chaque fois la fonction gestion_impulsions_encoder1.
On surveille les fronts montants (RISING) de la broche de sortie du codeur correspondant à encoder_2. On lancera alors à chaque fois la fonction gestion_impulsions_encoder2.
Nous progressons : lorsque les codeurs envoient un signal indiquant qu'ils ont tourné, on active les fonctions gestion_impulsions_encoder1 et gestion_impulsions_encoder2.
Allons voir ces fonctions :
void gestion_impulsions_encoder1(void)
{
if (digitalRead(encoder_1.getPortB()) == 0)
encoder_1.pulsePosMinus();
else
encoder_1.pulsePosPlus();
}
void gestion_impulsions_encoder2(void)
{
if (digitalRead(encoder_2.getPortB()) == 0)
encoder_2.pulsePosMinus();
else
encoder_2.pulsePosPlus();
}
Si nous étions partis voir le code permettant de créer les objets MeEncoderOnBoard, nous aurions pu voir que la méthode getIntNum surveille ce que se passe sur la voie A du codeur optique. Pour savoir si le moteur tourne dans un sens ou dans l'autre, il suffit donc de voir si la voie B est déjà active lorsqu'on détecte la voie A. Revoici l'animation où on voit les succesions entre voie A et voie B lorsque le disque tourne dans un sens. S'il tourne dans l'autre sens, la succession sera donc inversée.
Vous pouvez avec un clic droit ralentir l'animation : on voit que dans ce sens de lecture, la voie A (en rouge) s'active avant la voie B dans le sens de rotation présentée.
15° Le code est plus ou moins explicite : dans quel cas augmenter le nombre d'imupulsions mémorisées ? Dans quel cas diminuer le nombre d'impulsions mémorisées ?
...CORRECTION...
Lorsque la voie B n'est pas activé lorsqu'on détecte un front montant sur la voie A, on fait appel à la méthode pulsePosMinus. Si le créateur de code a été explicite dans la dénomination de ces méthodes, on doit certainement diminuer le nombre d'impulsions de 1. Dans l'autre cas, on utilise pulsePosPlus : cette fois, on va augmenter de un le nombre d'impulsions mémorisées.
D'ailleurs, on trouve ceci dans la documentation du code de MeEncoderOnBoard :
void MeEncoderOnBoard::pulsePosPlus(void)
{
encode_structure.pulsePos++;
}
On pourrait croire qu'on dit ce qu'on veut et qu'il s'agit d'interpréter le code. Et réalité, pas de tout : on peut aller lire le code et comprendre ce que fait telle ou telle méthode. Avec du matériel Arduino, c'est facile puisque les codes sont librement accessibles. Soit directement en allant fouiller dans Arduino, soit en allant chercher dans la documentation en ligne. Ici, c'est sur github :
Avec la fonction recherche du navigateur web, on peut trouver facilement les fonctions qu'on désire.
Si on résume : on crée des objets de classe MeEncoderOnBoard permettant d'interagir avec les encodeurs . Lorsque le codeur tourne dasn un sens, on augmente le nombre d'impulsions qu'on stocke directement dasn l'objet. Dans l'autre sens, on diminue le nombre d'impulsions de un. On augmente ou diminue de 8 impulsions à chaque fois que le moteur fait un tour complet.
Pour accéder au nombre d'impulsions stockées, on utilise la méthode pulsePulsePos sur l'objet-encodeur voulu. Par exemple : encoder_1.getPulsePos())
.
Reprenons le cours de notre lecture de la fonction setup :
void setup()
{
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
Serial.begin(115200);
//set pwm 1khz
// gestion du port 1 (le plus à droite)
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
// gestion du port 2
TCCR2A = _BV(WGM20);//PIN8
TCCR2B = _BV(CS21) | _BV(CS20) | _BV(WGM22);
// gestion du port 3 : en commentaire car inutile pour l'instant
// TCCR3A = _BV(WGM30);//PIN9
// TCCR3B = _BV(CS31) | _BV(CS30) | _BV(WGM32);
// gestion du port 4 : en commentaire car inutile pour l'instant
// TCCR4A = _BV(WGM40);//PIN5
// TCCR4B = _BV(CS41) | _BV(CS40) | _BV(WGM42);
encoder_1.setPulse(nbrEncoches);
encoder_1.setRatio(reductionMoteurRoue);
encoder_1.setPosPid(1.8,0,0.5);
encoder_1.setSpeedPid(0.18,0,0);
encoder_2.setPulse(nbrEncoches);
encoder_2.setRatio(reductionMoteurRoue);
encoder_2.setPosPid(1.8,0,0.5);
encoder_2.setSpeedPid(0.18,0,0);
}
Passons sur la partie bizarre avec les TCCR. Si vraiment vous voulez savoir ce qu'on y fait, allez voir la FAQ. Globalement, on configure la carte pour qu'elle gère correctement nos deux encodeurs. Si vous avez besoin d'utiliser des encodeurs sur les ports 3 et 4, il faudra enlever les commentaires sur les lignes correspondantes.
Nous arrivons alors à ceci :
encoder_1.setPulse(nbrEncoches);
encoder_1.setRatio(reductionMoteurRoue);
encoder_1.setPosPid(1.8,0,0.5);
encoder_1.setSpeedPid(0.18,0,0);
encoder_2.setPulse(nbrEncoches);
encoder_2.setRatio(reductionMoteurRoue);
encoder_2.setPosPid(1.8,0,0.5);
encoder_2.setSpeedPid(0.18,0,0);
16° Sans rentrer dans les détails, voir dans la documentation ce que font ces différentes méthodes.
La fonction loop est un peu plus simple à comprendre.
void loop()
{
if (Serial.available())
{
char a = Serial.read();
switch(a)
{
case '0': // Retour position de base
encoder_1.moveTo(0, rpmMoteurRoue);
encoder_2.moveTo(0, rpmMoteurRoue);
break;
case '1': // +1 tour moteur / position initiale
encoder_1.moveTo(-360, rpmMoteurRoue);
encoder_2.moveTo(360, rpmMoteurRoue);
break;
case '2': // -1 tour moteur / position initiale
encoder_1.moveTo(360, rpmMoteurRoue);
encoder_2.moveTo(-360, rpmMoteurRoue);
break;
case '3': // +2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(-360*2, rpmMoteurRoue);
encoder_2.moveTo(360*2, rpmMoteurRoue);
break;
case '4': // -2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(360*2, rpmMoteurRoue);
encoder_2.moveTo(-360*2, rpmMoteurRoue);
break;
case '5': // arrêt
encoder_1.moveTo(encoder_1.getCurPos(), rpmMoteurRoue);
encoder_2.moveTo(encoder_2.getCurPos(), rpmMoteurRoue);
break;
case '6': // rotation -1/4 tour encodeur 1 / position initiale, petite vitesse
encoder_1.moveTo(-360/4, rpmMoteurRoue);
break;
default:
break;
}
}
encoder_1.loop();
encoder_2.loop();
Serial.print("Spd 1 : ");
Serial.print(encoder_1.getCurrentSpeed());
Serial.print(" - Spd 2 : ");
Serial.print(encoder_2.getCurrentSpeed());
Serial.print(" --- angle 1 : ");
Serial.print(encoder_1.getCurPos());
Serial.print(" - angle 2 : ");
Serial.print(encoder_2.getCurPos());
Serial.print(" --- Imp. 1 : ");
Serial.print(encoder_1.getPulsePos());
Serial.print(" - Imp. 2 : ");
Serial.println(encoder_2.getPulsePos());
}
La métode moveTo permet de fournir l'angle de rotation en degrés qu'on veut imposer à la ROUE. Un tour correspond donc à 360° et deux tours à 720°. Le deuxième argument à fournir est la vitesse voulue en vitesse de croisière. Ne dépasser pas la vitesse notée sur votre moteur.
17° Rajouter un cas '8' pour tenter d'effectuer 10 tours. Compter les tours réels de roues. Modifier alors la valeur de la réduction pour tenter d'obtenir réellement 10 tours. Lors de mes essais, j'ai dû mettre un rapport de réduction de 1/47 alors que le moto-rédecteur indique 1/46.
Il existe une seconde méthode de mise en mouvement : la méthode move. Cette fois, il s'agit de rajouter un angle par rapport à la positon actuelle, pas d'pbtenir un angle particulier.
18° Rajouter un cas 'A' pour tenter de gagner 360°. Utiliser plusieurs fois le cas : le robot devrait avancer d'un tour à chaque fois.
...CORRECTION...
case 'A': // on avance d'un tour
encoder_1.move(-360, rpmMoteurRoue);
encoder_2.move(360, rpmMoteurRoue);
break;
19° Rajouter un cas 'M' et 'D' pour faire monter et descendre de quelques degrés l'échelle. Attention, il faudra bien penser à regarder les spécifications sur le moteur ! Si vous tentez de le faire fonctionner trop vite, il peut chauffer fortement !
...CORRECTION...
#include "MeMegaPi.h"
#include <SoftwareSerial.h>
MeEncoderOnBoard encoder_1(SLOT1);
MeEncoderOnBoard encoder_2(SLOT2);
MeEncoderOnBoard encoder_3(SLOT3);
uint8_t rpmMoteurRoue = 60;// A configurer sans dépasser l'inndication sur le moteur
uint8_t reductionMoteurRoue = 47;// A configurer en regardant votre moteur
uint8_t nbrEncoches = 8;// A configurer en regardant votre moteur
uint8_t rpmMoteurEchelle = 40;// A configurer sans dépasser l'inndication sur le moteur
uint8_t reductionMoteurEchelle = 75;// A configurer en regardant votre moteur
void gestion_impulsions_encoder1()
{
if (digitalRead(encoder_1.getPortB()) == 0) encoder_1.pulsePosMinus();
else encoder_1.pulsePosPlus();
}
void gestion_impulsions_encoder2()
{
if (digitalRead(encoder_2.getPortB()) == 0) encoder_2.pulsePosMinus();
else encoder_2.pulsePosPlus();
}
void gestion_impulsions_encoder3()
{
if (digitalRead(encoder_3.getPortB()) == 0) encoder_3.pulsePosMinus();
else encoder_3.pulsePosPlus();
}
void setup()
{
attachInterrupt(encoder_1.getIntNum(), gestion_impulsions_encoder1, RISING);
attachInterrupt(encoder_2.getIntNum(), gestion_impulsions_encoder2, RISING);
attachInterrupt(encoder_3.getIntNum(), gestion_impulsions_encoder3, RISING);
Serial.begin(115200);
//set pwm 1khz
// gestion du port 1 (le plus à droite)
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
// gestion du port 2
TCCR2A = _BV(WGM20);//PIN8
TCCR2B = _BV(CS21) | _BV(CS20) | _BV(WGM22);
// gestion du port 3 : en commentaire car inutile pour l'instant
TCCR3A = _BV(WGM30);//PIN9
TCCR3B = _BV(CS31) | _BV(CS30) | _BV(WGM32);
// gestion du port 4 : en commentaire car inutile pour l'instant
// TCCR4A = _BV(WGM40);//PIN5
// TCCR4B = _BV(CS41) | _BV(CS40) | _BV(WGM42);
encoder_1.setPulse(nbrEncoches);
encoder_1.setRatio(reductionMoteurRoue);
encoder_1.setPosPid(1.8,0,0.5);
encoder_1.setSpeedPid(0.18,0,0);
encoder_2.setPulse(nbrEncoches);
encoder_2.setRatio(reductionMoteurRoue);
encoder_2.setPosPid(1.8,0,0.5);
encoder_2.setSpeedPid(0.18,0,0);
encoder_3.setPulse(nbrEncoches);
encoder_3.setRatio(reductionMoteurEchelle);
encoder_3.setPosPid(1.8,0,0.5);
encoder_3.setSpeedPid(0.18,0,0);
}
void loop()
{
if (Serial.available())
{
char a = Serial.read();
switch(a)
{
case '0': // Retour position de base
encoder_1.moveTo(0, rpmMoteurRoue);
encoder_2.moveTo(0, rpmMoteurRoue);
break;
case '1': // +1 tour moteur / position initiale
encoder_1.moveTo(-360, rpmMoteurRoue);
encoder_2.moveTo(360, rpmMoteurRoue);
break;
case '2': // -1 tour moteur / position initiale
encoder_1.moveTo(360, rpmMoteurRoue);
encoder_2.moveTo(-360, rpmMoteurRoue);
break;
case '3': // +2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(-360*2, rpmMoteurRoue);
encoder_2.moveTo(360*2, rpmMoteurRoue);
break;
case '4': // -2 tours moteur / position initiale, petite vitesse
encoder_1.moveTo(360*2, rpmMoteurRoue);
encoder_2.moveTo(-360*2, rpmMoteurRoue);
break;
case '5': // arrêt
encoder_1.moveTo(encoder_1.getCurPos(), rpmMoteurRoue);
encoder_2.moveTo(encoder_2.getCurPos(), rpmMoteurRoue);
encoder_3.moveTo(encoder_3.getCurPos(), rpmMoteurRoue);
break;
case '6': // rotation -1/4 tour encodeur 1 / position initiale, petite vitesse
encoder_1.moveTo(-360/4, rpmMoteurRoue);
break;
case 'M': // on monte d'un 1/4 de tour
encoder_3.move(360*0.25, rpmMoteurEchelle);
break;
case 'D': // on monte d'un 1/4 de tour
encoder_3.move(-360*0.25, rpmMoteurEchelle);
break;
default:
break;
}
}
encoder_1.loop();
encoder_2.loop();
encoder_3.loop();
}
20° Il ne reste plus qu'à gérer la pince. Attention : il faudra la commander comme un moteur à courant continu et pas comme un encodeur. Bon courage.
Si vous avez utilisé la bibliothèque MeMegaPiPro.h, il va être temps de passer à MeMegaPi.h si vous voulez commander à la fois des encodeurs et des moteurs.
Question : où trouver de la documentation ?
Question : c'est quoi le code bizarre qu'on a totalement occulté dans l'explication ?
//set pwm 1khz
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
TCCR2A = _BV(WGM21) | _BV(WGM20);//PIN8
TCCR2B = _BV(CS22);
TCCR3A = _BV(WGM30);//PIN9
TCCR3B = _BV(CS31) | _BV(CS30) | _BV(WGM32);
TCCR4A = _BV(WGM40);//PIN5
TCCR4B = _BV(CS41) | _BV(CS40) | _BV(WGM42);
Je n'en ai pas parlé car c'est une partie un peu complexe de la programmation d'un micro-contrôleur : on agit directement sur les valeurs stockées dans les registres, une zone mémoire très rapide d'accés, intégrée dans votre micro-contrôleur.
Un arduino possède plusieurs registres gérant différentes choses. J'en présente certains plus bas. Les tableaux sont tirés de la documentation du micro-contrôleur, disponible en téléchargement gratuitement bien entendu.
Il y a donc des registres DDRA, DDRB, DDRC ... jusqu'à DDRL pour les micro-contrôleurs MegaPi2650.
Exemple : DDRA = b00001111;
veut dire que les quatres premières broches (liées aux bits 0-1-2-3) gérées par le registre DDRA seront des SORTIES (1) et que les quatre dernières seront des entrées (0). Allons voir la documentation du micro-contrôleur MEGA2560 :
Broches du port A
En regardant la documentation ci-dessous, on voit donc qu'on déclare en SORTIE les broches PA0, PA1, PA2 et PA3.
Il suffit alors d'aller voir la partie 1 de la documentation, pin configurations, pour connaître les numéros des broches correspondantes : il s'agit donc respectivement des broches physiques 78, 77, 76 et 75.
Numéros des broches
Exemple 2 : Imaginons qu'on veuille travailler avec le port 72. En regardant les deux extraits de documentation précédents, on voit qu'il correspond à PA6 du port A, le 6e bit du registre. DDA6
est un raccourci pour accéder rapidement au numéro du bit 6 du registre DDRA. Taper DDA6
dans un programme Arduino renvoie donc ... 6. Ici, ce n'est pas très utile puisque le numéro du bit est donné dans le nom. Comment puis-je le savoir ? Je suis parti voir dans la documentation du micro-contrôleur :
Exemple 3 : on peut par contre utiliser _BV
(pour BIT VALUE) pour obtenir le poids du bit : _BV(DDA6)
permet d'accéder à la valeur qu'encode le bit 6 du registre DDRA. On obtient donc 64 pour le 6e bit : les bits 0-1-2-3-4-5-6 encodent respectivement 1-2-4-8-16-32-64.
Une fois que la borne/pin a été configurée en ENTREE ou SORTIE, on peut la lire ou imposer son état de sortie avec deux nouveaux registres.
Imaginons que PORTA
renvoie la valeur 9 = 8+1. En binaire, cela correspond à b0001001 :
Registre DDRA | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Type de borne : | entrée | entrée | entrée | entrée | sortie | sortie | sortie | sortie |
Registre PORTA | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
On impose donc | HAUT | BAS | BAS | HAUT | ||||
sur la broche | 75 | 76 | 77 | 78 |
Imaginons que PINA
renvoie la valeur 96 = 64+32. En binaire, cela correspond à b0110000 :
Registre DDRA | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Type de borne : | entrée | entrée | entrée | entrée | sortie | sortie | sortie | sortie |
Registre PINA | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
On lit donc | BAS | HAUT | HAUT | BAS | ||||
sur la broche | 71 | 72 | 73 | 74 |
Nous voici enfin dans les registres qui nous intéressent. Tout d'abord, bravo ! Si vous êtes encore là, c'est que vous en voulez.
Première chose : les registres de ce type fonctionnent ici par deux car il s'agit en réalité d'un registre de 16 bits. Il en faut donc deux de 8 bits pour stocker toutes les informations voulues.
//set pwm 1khz
TCCR1A = _BV(WGM10);//PIN12
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
...
Deuxième chose : ces registres servent à générer une tension qui varie de façon particulière en fonction du temps. A l'aide d'une tension ne pouvant valoir que 0V ou 9V, on pourra par exemple donner l'impression à un moteur qu'on lui applique 4,5V ! Comment ?
En imposant par exemple 0V pendant 50% du temps et 9V pendant 50% du temps. Le tout, c'est de le faire rapidement pour que le moteur n'est pas le temps de réagir aux variations brutales.
Voici les intitulés des bits dans la documentation (sur lesquels j'ai rajouté les bits mis à 1 par le programme) :
L'ensemble des deux registres, nous permet de contôler les formes de tensions engendrées sur l'une des broches.
Contenu des registres : le premier registre contient donc 1
puisqu'on active son premier bit uniquement.
Régistre TCCR1A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Voici donc des codes équivalents :
TCCR1A = _BV(WGM10);
TCCR1A = 1;
Pour le second, c'est plus compliqué : le caractère |
désigne un OU binaire : on mettre le bit à 1 si l'un des 3 autres possèdent un bit à 1 à cet endroit. En regardant ci-dessous, on voit que le registre contient donc 11
= 8+2+1
Registre TCCR1B | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
CS10 (bit 0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
CS11 (bit 1) | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
WGM12 (bit 3) | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
Voici donc des codes équivalents :
TCCR1B = _BV(CS11) | _BV(CS10) | _BV(WGM12);
TCCR1B = 2 | 1 | 8;
TCCR1B = 11;
En réalité, on ne se soucie que des valeurs des bits pour savoir quelle type de tension va être générée.
Pour rappel, voici les contenus des deux registres. Les bits à 1 sont notifiés sur l'image.
Et voici le tableau qui permet de connaitre le mode de fonctionnement utilisé sur le port sélectionné : sur notre commande du port physique 1, seul WGM10 et WGM12 sont placés à 1.
Pour information, WGM veut dire Waveform Generation mode. La connaissance de ces bits va donc permettre de connaitre le type de tension générée.
On génére donc de la Fast PWM... Que signifie PWM ? Pulse Width Modulation.
En français, c'est MLI : Modulation de largeur d'impulsion.
Source : wikipedia - Auteur : Eighthave, modified by Teslaton - Domaine public
Comme vous pouvez le voir, plus on laisse la tension modulée à 0, plus le moteur aura l'impression que la tension est basse. Au contraire, plus on placera la tension modulée sur la valeur HAUTE, plus le moteur aura l'impression qu'on lui place une tension importante aux bornes. Le pourcentage affiché correspond au pourcentage de temps passé à l'état HAUT par rapport au temps total d'un motif, qu'on nommera PERIODE T.
Comme vous pouvez le voir, la période T est fixe : chaque motif dure le même temps total même si on passe plus ou moins de temps à l'état HAUT. La fréquence f est donc fixe également puisque f = 1 / T.
Comment régler la fréquence par contre ? Et bien, le réglage intervient en partie via le registre TCCR1B :
Les bits CS12, CS11 et CS10 sont les bits de Clock Set (CS) : leurs valeurs influencent la fréquence de la modulation.
On voit donc qu'il existe une fréquence de base qu'on va pouvoir diviser par 1, 8, 64, 256 ou 1024 par exemple.
Comme la réponse à la FAQ commence à être longue et que ce n'est pas le but de cette activité, je botte en touche. Le code vous affiche qu'on obtient un signal de 1kHz. Comme de toutes manières vous ne devriez pas voir l'intêret pour l'instant de la rêgler de façon différente, nous allons conclure ici.
En conclusion, on se souviendra qu'on peut accéder et modifer les valeurs stockées dans les registres mais qu'il faut pour cela aller lire de façon assez précise la documentation de votre micro-contrôleur.
Dernière remarque : le code vous fournit également des indications sur la broche subissant le PWM sur le port 4 (le plus à gauche) jusqu'au port 1 (le plus à droite). Mais attention, il ne s'agit pas ici des ports du micro-controleur mais des ports permettant de gérer les codeurs. Comme vous le voyez, rentrer dans les détails du fonctionnement interne d'un ordinateur mèle informatique et physique. C'est complexe, oui, mais c'est ce qui en fait une discipline nécessitant rigueur et méthodologie.
IMAGE DISPONIBLE SUR LE SITE DE MAKEBLOCK : https://www.makeblock.com/project/megapi