Les périphériques des microcontrôleurs

Un potentiomètre est un autre périphérique d'entrée utilisable par un MCU. Il se présente sous diverses formes, tailles et types. En voici quelques exemples :

potars_1.jpg

C'est une résistance variable avec trois bornes, les deux bornes extérieures de la résistance sont reliées à une source de tension, la borne centrale est relié à un curseur, permettant de délivrer une tension variable en fonction de la position du bouton de commande.

Le type qui nous intéresse pour nos applications est le type linéaire, qui délivre sur le curseur une tension proportionnelle à la position angulaire. Nous utiliserons un modèle ayant une résistance comprise entre 5 et 50 kOhms.

La tension aux bornes peut ne pas être strictement proportionnelle, il s'agit, là encore, d'un dispositif électro-mécanique et il peut aussi y avoir, en plus, des variations d'un modèle à l'autre dans une même série.

Pour lire la tension présente sur le curseur, nous allons utiliser un convertisseur analogique vers numérique (ADC), présent dans la plupart des MCUs.

Nous allons brancher les bornes extérieures du potentiomètre entre le 0 V et le + V et le curseur sur une entrée analogique du MCU.

Le principe étant le même pour la plupart des MCUs, je vais utiliser le PIC 12F675 pour les explications. La précision maximum du convertisseur de ce circuit est de 10 bits, soit une valeur variant de 0 à 1023. Il dispose de quatre entrées analogiques, mais d'un seul circuit de conversion, ce qui oblige à lire les entrées les unes après les autres.

Dans l'exemple, je vais n'utiliser qu'une seule entrée, appelée AN0 qui se trouve sur la broche GP0.

Pour commencer, il faut initialiser le convertisseur :

Code:
   TRISIO = 0b000001;                           // GP0 as input, all other as outputs
    ANSELbits.ANS = 0b0001;                     // use AN0 (GP0) as analog input
    ANSELbits.ADCS = 0b101;                     // duration of analog sampling time set to Fosc / 16 (4 usec)
    VCFG = 0;                                   // voltage reference for analog input is VDD
    ADFM = 1;                                   // conversion result right justified
  • TRISIO est le registre qui indique les ports (analogiques ou numériques) utilisés en entrée ou en sortie.
  • ANSELbits.ANS permet de définir de 1 à 4 entrées analogiques utilisées.
  • ANSELbits.ADCS définit le temps que va durer la conversion, plus ce temps sera élevé, plus la conversion sera précise.
  • VCFG indique la tension de référence à utiliser. J'utilise ici VDD (+ V). Quand cette tension sera présente sur la broche d'entrée, la valeur de la conversion sera au maximum (1023), quand il y aura 0 V sur la borne d'entrée, la valeur sera 0.
  • ADFM indique comment le résultat est rangé en mémoire. La valeur convertie est rangée dans deux octets, si je veux utiliser les dix bits, je dois ranger le résultat à droite (deux bits dans le premier octet, les huit bits suivants dans le deuxième octet), par contre, si un résultat sur huit bits (de 0 à 255) est suffisant, je range le résultat à gauche et je n'utilise que le premier octet.
Pour lire la valeur d'une entrée analogique, le sous-programme suivant est utilisé :

Code:
unsigned int get_ADvalue (unsigned char ADch)
{
    unsigned int value;

    ADCON0bits.CHS = ADch;                      // set analog channel
    ADON = 1;                                   // AD converter on
    GO_nDONE = 1;                               // start of conversion
    while (GO_nDONE);                           // wait for end of conversion
    value = ADRESH << 8 | ADRESL;               // get value on 10 bits
    ADON = 0;                                   // AD converter off
    return value;
}
  • ADCON0bits.CHS indique la broche à utiliser comme entrée.
  • ADON met en route ou arrête le convertisseur.
  • GO_nDONE démarre la lecture de la broche. Il repasse automatiquement à zéro lorsqu'elle est terminée.
    Plutôt qu'attendre que la conversion soit terminée en utilisant une boucle, comme dans cet exemple, il est possible d'activer une interruption (ADIE) qui positionnera le bit ADIF lorsque GO_nDONE passera à zéro et activera le sous-programme d'interruption.
  • ADRESH et ADRESL sont les deux octets dans lesquels le résultat est rangé. Dans ce MCU, ils ne sont pas consécutifs en mémoire, ce qui oblige à utiliser cette expression barbare. Avec d'autres MCUs, on peut lire directement le résultat dans le registre ADRES.
Comme dit précédemment, du fait des imprécisions du système mécanique, le résultat ne sera pas une valeur angulaire précise au 1/1024ème. Le programme devra tenir compte de cela.
 
Un encodeur rotatif est un périphérique d'entrée qui envoie une information en fonction de la position angulaire de son arbre. Contrairement au potentiomètre, qui est limité dans sa possibilité de rotation, l'encodeur rotatif n'a pas de butée et son arbre peut effectuer un nombre infini de tours. Un de ses usages courants est la roulette de la souris d'un ordinateur ou le bouton de recherche de stations sur un autoradio. Il est parfois couplé à un ou plusieurs boutons poussoirs.

Il existe deux types d'encodeurs :

  • L'encodeur incrémental, qui envoie des signaux en quadrature (c'est à dire décalés de 90°) sur deux broches à chaque changement de position de l'arbre, à charge pour le programmme de détecter le sens de rotation (droite ou gauche) et entretenir un compteur. Ce type d'encodeur ne se souvient pas de sa position, il est juste capable d'indiquer dans quel sens son arbre est déplacé.
  • L'encodeur absolu, qui indique directement la valeur angulaire de l'arbre, entre 0 et sa valeur maximum sur un tour complet. Il en existe qui comptent le nombre de tours effecués et conservent cette information dans une mémoire non volatile, mais, en général, cela est pris en charge par le programme.
Voici une photo de deux codeurs de type incrémental, celui de droite dispose de 5 boutons poussoirs :

rotary_encoder-1.jpg

Fonctionnement de l'encodeur incrémental

Il y a un certain nombre de positions pour un tour complet de l'arbre, dépendant des modèles. À chaque changement de position, les valeurs sur les broches de sortie changent suivant un ordre bien précis. La valeur de ces broches est vu par le MCU comme un nombre sur deux bits, pouvant donc prendre les valeurs "00", "01", "10" et "11".

Voyons comment cela se passe avec le dessin ci-dessous :

rotary_encoder-3.jpg

Les lignes rouges indiquent les changements de position, les nombres dans les rectangles verts sont le résulat de la valeur précédente multipliée par 4 et additionnée à la valeur courante. Nous voyons que pour chaque sens de rotation, il n'y a que 4 valeurs possibles :

  • "1101", "0100", "0010" ou "1011" (13, 4, 2 ou 11 en décimal) pour une rotation à droite.
  • "1110", "0111", "0001" ou "1000" (14, 7, 1 ou 8 en décimal) pour une rotation à gauche.
Toutes les autres valeurs, en particulier celles générées par les rebonds, sont invalides. Lorsque l'encodeur est utilisé à la main, pour faire varier une valeur, les rebonds ne sont pas gênants. Par contre, si l'encodeur est entraîné mécaniquement, par exemple par un moteur pas-à-pas, il faut les éliminer, soit par un condensateur extérieur, soit par programme.
Voici un exemple de sous-programme, il peut être appelé par une interruption déclenchée par un changement d'état sur les broches d'entrée ou par un timer :

Code:
signed int turncount = 0;

void read_quadrature (void) {
    static unsigned char val = 3;
                           /*    0  1 2 3 4 5 6  7  8 9 10 11 12 13 14 15    */
    static const char state[] = {0,-1,1,0,1,0,0,-1,-1,0,0, 1, 0, 1,-1, 0,};

    val = ((val & 0b11) << 2) | (PORTA & 0b11);    // get current value and add it to the previous value
    turncount += state[val];
}
Les broches de l'encodeur sont connectées sur les broches RA0 et RA1 d'un MCU disposant d'un port A. Le tableau "state[ ]" contient les indications de direction, rangées dans l'ordre des valeurs définies précédemment. Il n'y a pas de détection des rebonds dans ce bout de code. Une solution pourrait être de vérifier que les entrées restent dans le même état pendant un certain temps avant de les utiliser, ce temps étant à déterminer en fonction de l'utilisation.

Fonctionnement de l'encodeur absolu

Nous entrons là dans le domaine professionnel et il y a une multitude d'offres, la plupart à des tarifs laissant rêveur !

Il existe pourtant un composant accessible aux amateurs, appelé EAW ACE-128 et fabriqué par Bourns. Il est vendu environ 8.50 €.

Comme son nom l'indique, il y a 128 positions pour un tour complet de l'arbre et la valeur absolue de la position est disponible sur 8 broches, 2 autres broches sont utilisées pour donner la référence 0 V. C'est un composant électro-mécanique, avec toutes ses imperfections (rebonds) et il a une durée de vie donnée pour 50 000 tours minimum. Ce n'est pas spécifié, mais il est fort probable qu'une vitesse de rotation élevée doit l'abréger.

Puisqu'il y a huit broches pour lire la valeur, on aurait pu penser qu'on retrouverait directement un résultat variant séquentiellement de 0 à 127. Il n'en est rien et il faut utiliser une table de transcodage pour retrouver une valeur humainement logique. Le code nécessaire se trouve ci-dessous :

Code:
unsigned char read_absolute (void)
{
    static const unsigned char position[] = {
    0xff,0x38,0x28,0x37,0x18,0xff,0x27,0x34,0x08,0x39,0xff,0xff,0x17,0xff,0x24,0x0d,
    0x78,0xff,0x29,0x36,0xff,0xff,0xff,0x35,0x07,0xff,0xff,0xff,0x14,0x13,0x7d,0x12,
    0x68,0x69,0xff,0xff,0x19,0x6a,0x26,0xff,0xff,0x3a,0xff,0xff,0xff,0xff,0x25,0x0e,
    0x77,0x76,0xff,0xff,0xff,0x6b,0xff,0xff,0x04,0xff,0x03,0xff,0x6d,0x6c,0x02,0x01,
    0x58,0xff,0x59,0xff,0xff,0xff,0xff,0x33,0x09,0x0a,0x5a,0xff,0x16,0x0b,0xff,0x0c,
    0xff,0xff,0x2a,0x2b,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x15,0xff,0x7e,0x7f,
    0x67,0xff,0x66,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x5b,0xff,0xff,0xff,0xff,0xff,
    0x74,0x75,0xff,0xff,0x73,0xff,0xff,0xff,0x5d,0x5e,0x5c,0xff,0x72,0x5f,0x71,0x00,
    0x48,0x47,0xff,0x44,0x49,0xff,0xff,0x1d,0xff,0x46,0xff,0x45,0xff,0xff,0x23,0x22,
    0x79,0xff,0x7a,0xff,0x4a,0xff,0xff,0x1e,0x06,0xff,0x7b,0xff,0xff,0xff,0x7c,0x11,
    0xff,0xff,0xff,0x43,0x1a,0xff,0x1b,0x1c,0xff,0x3b,0xff,0xff,0xff,0xff,0xff,0x0f,
    0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x05,0xff,0xff,0xff,0x6e,0xff,0x6f,0x10,
    0x57,0x54,0xff,0x2d,0x56,0x55,0xff,0x32,0xff,0xff,0xff,0x2e,0xff,0xff,0xff,0x21,
    0xff,0x53,0xff,0x2c,0x4b,0xff,0xff,0x1f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x20,
    0x64,0x3d,0x65,0x42,0xff,0x3e,0xff,0x31,0x63,0x3c,0xff,0x2f,0xff,0xff,0xff,0x30,
    0x4d,0x52,0x4e,0x41,0x4c,0x3f,0xff,0x40,0x62,0x51,0x4f,0x50,0x61,0x60,0x70,0xff,
    };
    return position[PORTC];
}
Il existe aussi quelques encodeurs magnétiques sans contact qui offrent de meilleures performances mécaniques (durée de vie supérieure à cent millions de tours et un régime de rotation jusqu'à 10 000 tours par minute), avec une résolution pouvant aller jusqu'à 1024 positions par tour. Leurs prix (à partir de 35.00 € HT) sont relativement modérés, mais s'éloignent quand même d'un budget amateur.
 
Bonjour JP,

Quelques occupations depuis mon dernier passage sur le post...

Puisque tu nous parles d'encodeur magnétique je voudrais citer l'excellente société autrichienne AMS spécialisée dans les composants intelligents et dont le composant AS5045 équipe déjà la gamme de servo Hitec HS M7990TH
Depuis AMS a sorti le AS5048 version A ou B suivant le type de protocole à utiliser : 14 bits quand même.

Je suis bien incapable de lire les infos du 5045 mais je ne désespère pas avec un arduino et le protocole SPI, dès que j'aurais reçu le circuit.

Tu parles plus haut des projets évoqués à Viry... Je pense que l'avenir en actionneur est dans l'usage des brushless, les contrôleurs de brushless, comme les moteurs brushless sont nombreux sur le marché dans toutes les gammes de prix et de qualité. Mais ce qui manque c'est le contrôle en retour.

Un ATtiny85 ou un pic 16F675 sont tout à fait capables de gérer cela pour quels euros, un montage de type "pattes en l'air" noyé dans du rubson sera simple à réaliser pour un amateur.

Une entrée du signal issu du récepteur, une entrée point milieu d'un potentiomètre, voire d'un encodeur magnétique, une sortie qui produit un signal standard destiné au contrôleur de brushless et c'est tout. Reste juste à programmer pour gérer l'ensemble.

Le signal issu du récepteur indique la position du potentiomètre à atteindre, s'il n'est pas dans cette position, le programme envoie un créneau en avant toute (ou en arrière toute) vers le contrôleur de brushless.
Quand le potentiomètre se rapproche de la valeur de consigne, le signal commande un ralentissement du brushless. Quand la position de consigne est atteinte, le signal correspond à une commande d'arrêt du brushless.
Du simple comme programme, un peu moins s'il convient d'avoir un maintien en position sous effort comme c'est le cas des programmes implantés sur les servos standards, quand on "force" sur la timonerie on sent bien le pompage.
On a aussi à simuler le démarrage "gaz coupés" nécessaire pour activer le contrôleur au départ
puis la programmation des fins de courses.

Je vais essayer (en autres...) avec un ATtiny85 sous IDE Arduino, tu essaies de ton côté avec un pic ?

Amicalement

fr
 
froussel;2129017 à dit:
Puisque tu nous parles d'encodeur magnétique je voudrais citer l'excellente société autrichienne AMS spécialisée dans les composants intelligents et dont le composant AS5045 équipe déjà la gamme de servo Hitec HS M7990TH
Depuis AMS a sorti le AS5048 version A ou B suivant le type de protocole à utiliser : 14 bits quand même.
Il ne s'agit pas, stricto sensu, d'encodeurs, mais des composants électroniques servant à les réaliser. C'est exactement ce que cherchait Lagaffe pour faire une girouette électronique, ou un truc dans ce genre là !

Ceci dit, une résolution de 12 bits est déjà largement suffisante, alors 14 bits, c'est le grand luxe ! :)
 
L'usage habituel d'un servomoteur (servo) est d'être branché sur une prise d'un récepteur de radiocommande. Mais on peut également le brancher sur une broche de sortie d'un MCU et lui envoyer le signal qu'il attend.

Pour fonctionner, un servomoteur a besoin de trois fils.

Deux d'entre eux servent à l'alimentation du moteur et de l'électronique. Le troisième sert à véhiculer le signal de commande de position.

Le câblage de la prise est le même pour toutes les marques :

  • Le premier fil, noir ou marron, est branché au 0 V, il est d'un côté de la prise.
  • Le deuxième fil, rouge, est branché au + V, il est au centre de la prise.
  • Le troisième fil, jaune ou blanc, véhicule le signal, il est du côté opposé au fil 0 V sur la prise.
Ce signal est une impulsion de + V, de largeur variable (PWM = Pulse Width Modulation), variant entre 900 et 2100 µs, répétée à intervalle régulier, généralement de l'ordre de 20 000 µs (50 hz). Certains servos de type numérique peuvent accepter une fréquence de répétition plus rapide, mais il vaut mieux expérimenter auparavant.

Le neutre est indiqué par une impulsion de 1500 µs.

Les variateurs pour moteurs à balais et les contrôleurs pour moteurs sans balai se commandent aussi avec ce genre de signal.

L'impulsion de commande est générée en mettant à l'état 1 la broche durant le temps voulu et en la remettant à l'état 0 durant le temps restant pour l'impulsion de répétition. Ceci implique donc l'utilisation d'un timer.

Je donne deux exemples de code. Dans les deux, le timer 1 est réglé pour compter directement en microsecondes. Le premier est un sous-programme appelé régulièrement à chaque intervalle de répétition, généré par ailleurs par un moyen que je ne détaillerai pas :
Code:
#define NEUTRAL 1500

static volatile bit clock;
unsigned int pulse = NEUTRAL;

void generate_frame (unsigned int pulse)
{
    TMR1ON = 0;                                 // stop timer 1
    clock = 0;                                  // reset time indicator
    TMR1 = 10 - pulse;                          // set timer 1
    GP5 = 1;                                    // set port
    TMR1ON = 1;                                 // start timer 1
    while (!clock);                             // wait until time has elapsed
    GP5 = 0;                                    // clear port
}

static void interrupt routine (void)
{
    if (TMR1IE && TMR1IF) {                     // timer 1 interrupt
        TMR1IF = 0;                             // clear interrupt flag
        clock = 1;                              // set time elapsed indicator
    }
}
Dans l'exemple ci-dessus, le sous-programme d'interruption sert juste à positionner un indicateur lorsque le temps est écoulé.

Le deuxième exemple génère l'impulsion et l'intervalle de répétition directement dans le sous-programme d'interruption :
Code:
#define REPEAT 20000
#define NEUTRAL 1500

unsigned int pulse = NEUTRAL;

static void interrupt routine (void)
{
    static bit pulsenum = 0;
    static unsigned int p = NEUTRAL;

    if (TMR1IE && TMR1IF) {                     // timer 1 interrupt
        TMR1IF = 0;                             // clear interrupt flag
        TMR1ON = 0;                             // stop timer 1
        pulsenum = ~pulsenum;                   // invert pulse number
        GP5 = pulsenum;                         // set GP5 according to pulse number
        if (pulsenum) {                         // if first pulse
            p = pulse;                          // get length of pulse
            TMR1 = 10 - p;                      // set timer to length of pulse
        }
        else {
            TMR1 = (10 - REPEAT) + p;           // set timer to length of long pulse
        }
        TMR1ON = 1;                             // start timer 1
    }
}
Cet exemple permet de ne pas bloquer pas le reste du programme, comme le faisait le premier avec la boucle d'attente de l'indicateur de temps écoulé.
 
Haut