Le monde des micro contrôleurs

Schémas

Périphériques des micro contrôleurs

Éléments de programmation du 12F509

Éléments de programmation du 12F675

Éléments de programmation du 16F1825

Création d'un projet sous MPLAB IDE

Éléments de programmation du 12F675

Nous avons vu, avec le PIC 12F509, qu'on pouvait faire des réalisations intéressantes avec un composant de grande diffusion et d'un prix modéré.

Toutefois, il y a quand même des limitations et je vais donc vous parler maintenant d'un composant un peu plus puissant : le 12F675.

Il se présente sous la forme d'un boîtier huit broches et coûte à peine plus cher que son petit frère. Il reprend l'organisation interne et les caractéristiques du 12F509, mais il permet en plus :

  • d'utiliser un deuxième timer d'une précision de seize bits,
  • de gérer des interruptions,
  • de lire des entrées analogiques qu'il peut comparer à une tension de référence ou convertir en valeurs numériques,
  • de lire et écrire dans 128 octets de mémoire non volatile (EEPROM), permettant de sauver et retrouver des valeurs, même après l'arrêt de l'alimentation.
Cela permet de réaliser des montages plus élaborés, mais pas forcément plus complexes en composants externes, puisque la complexité de la réalisation est prise en compte par le programme du MCU.

On le trouve couramment en France à 1.96 EUR pièce (1.23 EUR pièce, acheté par paquet de 10).

Sa fiche descriptive est ici.


Présentation des fonctionnalités supplémentaires


Les timers


Un timer est un registre qui compte automatiquement, en parallèle du programme, le nombre d'impulsions qu'il reçoit d'une horloge. Lorsqu'il atteint la valeur maximum, il recommence à partir de zéro. Un timer sur 8 bits va compter de 0 à 255 et un timer sur 16 bits de 0 à 65535.

L'horloge peut être interne ou externe, fournie sur une broche du MCU.

Pour rendre l'utilisation plus souple, il est possible d'utiliser un diviseur (prescaler) qui permet d'augmenter de 1 la valeur du registre seulement lorsqu'un certain nombre d'impulsions ont été reçues de l'horloge.

Les valeurs de divisions sont généralement de 1:1, 1:2, 1:4, 1:8, 1:16, 1:32, 1:64, 1:128 et 1:256.

Il est possible de ranger une valeur initiale dans le registre pour déclencher un événement lorsque la valeur repasse par zéro. Par exemple, avec un timer sur 8 bits, pour compter 100 impulsions, je rangerai la valeur 155 (255 - 100) et j'attendrai que la valeur repasse par zéro.

Le timer 16 bits du 12F675 (TMR1) peut être interrompu et relancé.


Les interruptions


Nous venons de voir, avec les timers, qu'il peut être nécessaire d'attendre qu'un événement se produise pour dérouler une séquence de code. On peut tester l'arrivée de l'événement dans une boucle, ou demander au MCU qu'il déclenche une séquence de code particulière (appelée séquence d'interruption) lorsque cet événement arrive.

Lorsque l'événement arrive, le MCU suspend l'exécution des instructions en cours, sauvegarde le contexte, positionne un indicateur et se débranche vers la séquence d'interruption. Lorsque la séquence est terminée, le MCU restaure le contexte et reprend l'exécution normale du progamme.

Dans l'architecture PIC, toutes les interruptions sont traitées par un seul sous programme, ayant n'importe quel nom, mais comportant le mot clé "interrupt" dans sa déclaration. Lorsqu'il est activé, le sous programme teste alors tous les indicateurs qu'il attend pour trouver celui qui a déclenché l'interruption

Il faut noter que ce sous programme ne doit jamais être appelé explicitement.

Par exemple, pour déclencher une interruption sur le passage à zéro du timer 0, nous rangeons la valeur initiale dans le timer, nous positionnons un bit dans un registre pour indiquer que nous voulons une interruption sur cet événement précis puis nous autorisons les interruptions :

    TMR0 = 255 - 100 // Wait for 100 ticks
    T0IF = 0;        // clear interrupt flag
    T0IE = 1;        // TMR0 Overflow Interrupt Enable bit
    PEIE = 1;        // Peripheral Interrupt Enable bit
    ei();            // Enable general interrupts
    . . .
    . . .

Lorsque le timer va repasser par zéro, l'indicateur "T0IF" va être positionné et le sous programme d'interruption va être appelé, à charge pour lui de trouver ce qui a bien pu se passer :

static void interrupt routine (void)
{
    if (T0IE && T0IF) {  // timer 0 interrupt
        T0IF = 0;        // clear interrupt flag
// Do timer processing
        return;
    }
}


Les conversions analogiques / décimales


Le PIC12F675 est capable de lire une tension sur une broche, de la comparer à une tension de référence (interne ou externe) et de la convertir en une valeur décimale.

On ne peut pas dire qu'il fasse des merveilles par rapport à d'autres MCU puisque la valeur convertie est rangée dans un nombre de 10 bits, soit une plage de valeurs de 0 à 1023. Mais ça peut quand même rendre service.

Quatre broches peuvent être utilisées comme entrées analogiques : GP0, GP1, GP2 et GP4. Toutefois, si une référence de tension externe est utilisée, elle sera branchée sur la broche GP1.

Pour utiliser cette fonctionnalité, il faut positionner quelques bits dans différents registres :

  • ANSELbits.ADCS : temps d'acquisition de la valeur,
  • ANSELbits.ANS : broches à utiliser, c'est un masque, indiqué par des bits comme dans TRISIO,
  • ADCON0bits.CHS : canal analogique à utiliser, un seul à la fois, dépend de la broche, c'est une valeur numérique (0 à 3) :
    • 0 pour GP0,
    • 1 pour GP1,
    • 2 pour GP2,
    • 3 pour GP4,
  • ADFM : indicateur de justification du résultat dans le registre (droite ou gauche),
  • VCFG : source de la tension de référence (interne ou externe),
  • ADON : mise en service du convertisseur,
  • CMCON : mise hors service du comparateur,
  • ADIE : pour générer un interruption en fin de conversion,
  • TRISIO : pour indiquer que la broche est utilisée en entrée.

Une fois que tout cela est fait, le reste est simple : on positionne le bit "GO_DONE" du registre ADCON0 pour lancer la conversion, on attend qu'elle soit terminée, en testant que le bit repasse à zéro ou par une interruption et on récupère le résultat dans les registres "ADRESH" et "ADRESL" :

    GO_DONE = 1;
    while (GO_DONE);
    potval = ADRESH << 8 | ADRESL;

Simple et de bon goût, n'est ce pas ?


La mémoire non volatile (EEPROM)


Pour accéder à un octet de l'EEPROM en cours d'exécution du programme, il existe deux fonctions dans la biblothèque du compilateur :

  • eeprom_read() à laquelle on passe l'adresse à lire et qui renvoie la valeur,
  • eeprom_write() à laquelle on passe l'adresse et la valeur à écrire.

Exemple :

void eetest(void) {
    unsigned char value = 1;
    unsigned char address = 0;

    eeprom_write(address, value);    // write value to EEPROM address
    value = eeprom_read(address);    // read from EEPROM at address
}

Il est aussi possible de précharger des valeurs constantes dans l'EEPROM, au moment de la compilation du programme, à l'aide de la macro EEPROM_DATA.

Cette macro accepte huit paramètres. On peut appeler la macro autant de fois que nécessaire pour charger plus de huit octets, les octets non utilisés, à la fin, doivent être à zéro. L'appel de la macro ne doit pas se trouver dans du code exécutable.

Exemple :

__EEPROM_DATA('C','o','p','y','r','i','g', 'h');
__EEPROM_DATA('t',' ','2','0','1','2',0,0);


Montage n° 1


C'est le montage le plus simple, à brancher sur une voie proportionnelle :

  • manche au centre, tout est éteint,
  • manche en haut, un port est allumé,
  • manche en bas, un autre port est allumé.

Lorsque le manche est au centre (neutre), le signal dure 1500 microsecondes (notées usec par la suite), dans une direction, il dure 2000 usec, dans la direction opposée, il dure 1000 usec.

Le signal est répété toutes les 20000 ou 22500 usec, suivant l'émetteur.

Pour faire cela, nous avons besoin de mesurer le temps du signal de la voie en sortie du récepteur.

  • Nous allons utiliser le timer 1, ce qui permet d'obtenir un résultat directement en usec.
  • Le début et la fin du signal seront détectés par une interruption.
  • Le port utilisé en entrée sera GP3, c'est le seul qui ne peut pas être utilisé en sortie, ce qui laisse libre l'utilisation des 5 autres ports.
  • Les ports GP0 et GP1 seront utilisés pour sortir la valeur.

Voici le début du programme :

#include <htc.h>
#include <pic.h>
#include <pic12f65.h>

// set fuse bits
__CONFIG(FOSC_INTRCIO & WDTE_OFF & BOREN_OFF & CP_OFF & CPD_OFF & MCLRE_OFF);

__EEPROM_DATA('T','u','t','o','-','1',0,0);

volatile bit flagcnt = 0;
volatile unsigned int cnt = 0;

main (void)
{

La configuration n'appelle pas de commentaire particulier, il y a juste à noter que j'utilise l'oscillateur interne à 4 Mhz (FOSC_INTRCIO), ce qui évite l'utilisation d'un quartz externe.

Je range dans l'EEPROM le nom du programme, pour voir immédiatement ce qu'il y a comme programme dans le MCU lorsque je trouve un circuit qui traîne sur le bureau.

La ligne "volatile bit flagcnt = 0;" déclare une variable sur un bit qui servira d'indicateur lorsque l'impulsion est en cours de réception.

La ligne "volatile unsigned int cnt = 0;" déclare une variable numérique non signée d'une longueur de seize bits qui contiendra le temps de l'impulsion reçue.

À noter l'attribut "volatile" dans la déclaration des variables. Ces variables sont positionnées dans la séquence d'interruption, qui peut intervenir n'importe quand lors de l'exécution du programme. L'attribut avertit le compilateur de ce fait et il chargera les valeurs des variables à chaque fois qu'elles sont référencées, sans chercher à optimiser le code généré.

La ligne "main (void)" et l'accolade sur la ligne qui suit marquent le début du code exécutable.

Voici les initialisations propres au programme :

    OPTION_REG = 0b10011000;           // set OPTION register
    INTCON = 0;                        // disable all interrupts and clear all flags
    T1CON = 0;                         // TMR1 CONTROL register
    ADCON0 = ANSEL = 0;                // turn off analog conversion
    CMCON = 0x07;                      // turn off analog comparator
    TRIS = 0b001000;                   // GP3 as input, all others as output
    GPIO = 0;                          // all ports off
    TMR1ON = 1;                        // TMR1 is now counting each usec elapsed.
    GPIF = 0;                          // clear interrupt flag
    GPIE = 1;                          // Port Change Interrupt Enable bit
    IOC = 0b001000;                    // enable interrupts on GP3 pin change
    ei();                              // enable general interrupts

J'ai configuré tous les ports en sortie dans le registre TRIS, ça pourra toujours servir quand je rajouterai du code plus tard.
Le timer 1 est activé pour compter chaque usec écoulée.
Le bit "GPIF" sert à indiquer qu'une interruption a eu lieu sur une des entrées de GPIO, il doit être à zéro avant d'activer les interruptions.
L'instruction "GPIE = 1;" indique que je vais utiliser le port GPIO comme source d'interruption lorsque la valeur de GP3 changera ("IOC = 0b001000;").
L'appel à la fonction de bibliothèque "ei();" met en route le système d'interruptions.

Le programme principal se présente comme suit :

    for (;;) {                                  // loop for ever
        if (!flagcnt) {                         // pulse received
            if (cnt >= 900 && cnt <= 2100) {    // verify value
                if (cnt < 1200)                 // low time pulse
                    GPIO = 0b000001;            // set GP0
                else if (cnt > 1800)            // high time pulse
                    GPIO = 0b000010;            // set GP1
                else                            // medium time pulse
                    GPIO = 0;                   // clear all outputs
            }
        }
    }
}

L'instruction "for (;;)" de la première ligne est le début d'une boucle infinie, qui s'exécutera tant que le MCU sera alimenté.

L'instruction "if" sert à tester une condition, si la condition est vraie, l'instruction qui suit est exécutée.

L'instruction suivant le mot "else" est exécutée si la condition du "if" précédent est fausse.

L'instruction "if (cnt >= 900 && cnt <= 2100)" vérifie que l'impulsion reçue est dans les bornes requises.

Les ports sont positionnés par l'instruction "GPIO = valeur;".

Et voici enfin la séquence traitant les interruptions :

static void interrupt routine (void)
{
    if (GPIE && GPIF) {                // GPIO port change
        GPIF = 0;                      // clear interrupt flag
        if (GP3 && !flagcnt) {         // start of pulse
            TMR1 = 0xff82U;            // reset TMR1 to number of cycles elapsed since entering the routine
            flagcnt = 1;               // count pulse duration in progress
        }
        else if (!GP3 && flagcnt) {    // end of pulse
            cnt = TMR1;                // get TMR1 value
            flagcnt = 0;               // count pulse duration disabled
        }
    }
}

L'instruction "if (GPIE && GPIF) {" vérifie si l'interruption est bien celle qu'on attend.

L'instruction "GPIF = 0;" efface l'indicateur d'interruption, ce n'est pas fait automatiquement. Quand un programme prend en compte une interruption, il doit toujours effacer l'indicateur correspondant.

L'instruction "if (GP3 && !flagcnt) {" vérifie que GP3 est passé à 1 et que nous ne sommes pas déjà en train de compter.

L'instruction "TMR1 = 0xff82U;" initialise le timer. Il n'est pas simplement remis à zéro, mais à une valeur qui tient compte du nombre d'impulsions d'horloge entre le moment où l'interruption a été détectée et le moment où on positionne le timer, valeur à laquelle on soustrait le nombre d'impulsions qui auront lieu entre la détection de l'interruption suivante et la lecture du timer. Cela permet d'avoir un nombre de microsecondes à peu près exact. Nous ne pouvons pas avoir une valeur absolument exacte car nous utilisons l'horloge interne qui a une précision de seulement 1%, ce qui donne une marge d'erreur de 15 usec sur la durée de l'impulsion de neutre.

L'instruction "flagcnt = 1;" positionne l'indicateur de comptage.

L'instruction "else if (!GP3 && flagcnt) {" vérifie que GP3 est passé à zéro et que nous sommes en train de compter.

L'instruction "cnt = TMR1;" lit la valeur du timer.

L'instruction "flagcnt = 0;" efface l'indicateur de comptage.

Le fichier source complet est ici.


Montage n° 2


Ce montage n'a pas grand chose à voir avec le modélisme nautique, puisqu'il s'agit d'un testeur de servos. Il peut fonctionner en mode automatique ou en mode manuel.

En plus du matériel mentionné dans la description du PIC12F509, il faut un potentiomètre avec son bouton et un interrupteur simple.

La valeur du potentiomètre doit être comprise entre 5K et 50K, il sert à positionner le servo en mode manuel ou à déterminer la vitesse du déplacement en mode automatique. Il doit avoir une courbe de réponse linéaire et non logarithmique.

Les broches exérieures du potentiomètre sont à brancher au plus et au moins, sur sa broche centrale nous allons donc trouver, suivant la position, une tension variant de 0 V à +V. Cette broche est reliée au port GP4 du MCU, qui va servir d'entrée analogique pour mesurer cette tension.

Les broches de l'interrupteur sont branchées entre le 0 V et la broche GP2. Quand il est ouvert, le testeur est en mode automatique, quand il est fermé, le testeur est en mode manuel.

Sur la broche GP5, avec le timer 1, nous générons un signal PWM (Pulse Width Modulation, Impulsion modulée en largeur) dont la durée varie de 900usec à 2100 usec. Ce signal est répété toutes les 22500 usec, pour respecter le standard de fait.

Le schéma électronique est le suivant :

Voici le début du programme :

#include <htc.h>
#include <pic.h>
#include <pic12f675.h>

#define FLEN 22500
#define NEUTRAL 1500
#define MAX 2100
#define MIN 900
// if MIN == 900 -> LOOPAD = 5 else LOOPAD = 4
#define LOOPAD 5

__CONFIG(FOSC_INTRCIO & WDTE_OFF & BOREN_OFF & CP_OFF & CPD_OFF & MCLRE_OFF);

__EEPROM_DATA('T','u','t','o','-','2',0,0);

unsigned char mode = 0;
signed int direction = 1;
unsigned int next = NEUTRAL;

unsigned int get_ADvalue (unsigned char ADch);
static void interrupt routine (void);

main (void)
{
    signed char direction = 1;
    unsigned int ADval, i, x;

J'ai défini ("#define") des constantes pour modifier les caractéristiques de l'impulsion si besoin est.

La constante "LOOPAD" est utilisée dans le programme de conversion analogique : lorsqu'elle est égale à 5, la valeur convertie varie de 0 à 1278, lorsqu'elle est égale à 4, la valeur convertie varie de 0 à 1023. La constante "MIN" est ajoutée à cette valeur pour donner la valeur correspondant à la largeur de l'impulsion, donc un résultat entre 900 et 2178 usec. Si on veut limiter la variation de la largeur entre 1000 et 2000usec, il faut changer les constantes "MAX", "MIN" et "LOOPAD".

La configuration n'appelle pas de commentaire particulier, elle est identique à celle du montage n° 1.

La ligne "main (void)" et l'accolade sur la ligne qui suit marquent le début du code exécutable.

Voici les initialisations propres au programme :

    OPTION_REG = 0b00011000;                    // OPTION register, GPIO pull-ups are enabled
    INTCON = 0;                                 // disable all interrupts and clear all flags
    T1CON = 0;                                  // TMR1 control register
    CMCON = 0x07;                               // turn off analog comparator
    ANSELbits.ADCS = 0b101;                     // duration of analog sampling time set to Fosc / 16 (4 usec)
    ANSELbits.ANS = 0b1000;                     // use GP4 as analog input
    ADFM = 1;                                   // conversion result right justified
    VCFG = 0;                                   // voltage reference is VDD
    TRISIO = 0b011100;                          // GP2, GP3 and GP4 as input, all other as outputs
    WPU = 0b000100;                             // enable pull-up on GP2
    GPIO = 0;                                   // all output ports off
    TMR1 = 1 - FLEN;                            // load TMR1 with initial value
    TMR1ON = 1;                                 // start timer 1
    TMR1IE = 1;                                 // enable timer 1 interrupt
    PEIE = 1;                                   // enable peripheral interrupts

Dans le registre "OPTION", j'ai indiqué que j'allais activer les "pull-ups", ce sont des résistances internes forçant les entrées à +V lorsque rien n'est branché dessus. Il faut préciser ensuite, dans le registre "WPU" quelles sont les entrées concernées.

L'instruction "T1CON = 0;" configure le timer 1 pour compter chaque usec écoulée.

L'instruction "ANSELbits.ADCS = 0b101;" indique le temps d'échantillonage à utiliser dans la conversion analogique.

L'instruction "ANSELbits.ANS = 0b1000;" indique que je n'utilise que le canal analogique 3, qui se trouve sur la broche GP4.

L'instruction "WPU = 0b000100;" indique que j'active la résistance "pull-up" de la broche GP2, qui vaudra donc 1 lorsque l'interrupteur est ouvert et 0 lorsque l'interrupteur est fermé.

L'instruction "TMR1 = 1 - FLEN;" configure le timer 1 pour déclencher une interruption au bout de 22500 usec. Comme le registre "TMR1" est défini comme un entier non signé, il ne peut pas prendre de valeur négative. Cete écriture est donc strictement équivalente à "0xffff - FLEN", elle est juste plus simple à écrire.

Le programme principal se présente comme suit :

    for (;;) {                                  // loop for ever
        ADval = get_ADvalue(3);                 // get analog value on channel 3 (GP4)
        if (GP2) {                              // automatic mode when GP2 is on
            x = next + direction;               // set pulse length for next frame
            if (next == MIN)                    // min value reached
                direction = +1;                 // change direction
            else if (next == MAX)               // max value reached
                direction = -1;                 // change direction
            ADval >>= 2;                        // divide analog value by 4
            for (i = 0; i < ADval; i++);        // loop for delaying travel speed
        }
        else {                                  // manual mode
            x = MIN + ADval;                    // set pulse length for next frame
        }
        di();                                   // disable general interrupts
        next = x;                               // store pulse length
        ei();                                   // enable general interrupts
    }
}

L'instruction "for (;;)" de la première ligne est le début d'une boucle infinie, qui s'exécutera tant que le MCU sera alimenté.

L'instruction "ADval = get_ADvalue(3);" appelle une fonction interne renvoyant la valeur du registre analogique 3.

L'instruction "if (GP2) {" teste la position de l'interrupteur.

L'instruction "x = next + direction;" change la valeur de l'impulsion en mode automatique.

Les quatre instructions suivantes changent le signe de l'incrément de valeur d'impulsion lorsque les bornes sont atteintes.

Pour introduire un délai entre les impulsions dépendant de la position du potentiomètre, on exécute une boucle "for" vide, mais comme les valeurs analogiques sont trop élévées, on les divise préalablement par quatre : "ADval >>= 2;"

Les instructions suivant le "else" correspondent au mode manuel et positionnent le temps d'impulsion en fonction de la position du potentiomètre.

Les trois dernières instructions rangent la valeur de l'impulsion. Pour cela, on commence par suspendre les interruptions pour éviter un conflit d'accès en mémoire avec le sous programme d'interruption, on range la valeur et on restaure les interruptions.

Voici la fonction lisant le port analogique :

unsigned int get_ADvalue (unsigned char ADch)
{
    unsigned char i;
    unsigned int value = 0;

    ADCON0bits.CHS = ADch;                      // set analog channel
    ADON = 1;                                   // AD converter on
    for (i = 0; i < LOOPAD; i++) {              // loop 4 or 5 times, depending on MIN
        GO_DONE = 1;                            // start of conversion
        while (GO_DONE);                        // wait for end of conversion
        value += ADRESH << 8 | ADRESL;          // get value on 10 bits
    }
    value >>= 2;                                // divide by 4, the value is now between 0 and 1023 or 1278
    ADON = 0;                                   // AD converter off
    return value;
}

L'instruction "ADCON0bits.CHS = ADch;" indique le canal analogique à utiliser.

L'instruction "ADON = 1;" met le convertisseur dans l'état prêt, mais ne démarre pas la conversion.

La lecture du port va se faire 4 ou 5 fois, pour lisser le résultat.

L'instruction "GO_DONE = 1;" démarre la conversion.

L'instruction "while (GO_DONE);" attend que le bit repasse à zéro, ce qui indique que la conversion est terminée.

L'instruction "value += ADRESH << 8 | ADRESL;" accumule les valeurs successives.

L'instruction "value >>= 2;" divise la valeur trouvée par 4. Suivant la valeur de la constante "LOOPAD", nous aurons donc soit les 4/4, soit les 5/4 de la valeur moyenne.

L'instruction "ADON = 0;" met le convertisseur dans l'état arrêté.

L'instruction "return value;" renvoie la valeur au programme appelant.

Et voici enfin la séquence traitant les interruptions :

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

    if (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
            pulse = next;                       // get length of pulse
            TMR1 = 120 - pulse;                 // set timer to length of pulse
        }
        else {
            TMR1 = (80 - FLEN) + pulse;         // set timer to length of long pulse
        }
        TMR1ON = 1;                             // start timer 1
    }
}

Le bit "pulsenum" indique si on envoie une impulsion ou si on envoie le palier de synchronisation.

L'instruction "if (TMRIF) {" vérifie si l'interruption est bien celle qu'on attend.

L'instruction "TMR1IF = 0;" efface l'indicateur d'interruption.

L'instruction "TMR1ON = 0;" arrête le timer.

L'instruction "pulsenum = ~pulsenum;" inverse l'indicateur d'impulsion.

L'instruction "GP5 = pulsenum;" positionne GP5 en fonction de l'indicateur.

Si l'indicateur est à 1, on initialise le timer à la valeur de l'impulsion. S'il est à zéro, on initialise le timer à la longueur de l'impulsion de synchronisation, diminuée de la longueur de l'impulsion précédente.

L'instruction "TMR1ON = 1;" redémarre le timer.

Le fichier source complet est ici.


Montage n° 3


Ce montage est presque le même que le montage n° 2, mais il est maintenant possible de lui apprendre les positions neutre et extrêmes en le branchant sur une sortie servo d'un récepteur. Nous allons aussi modifier le schéma pour ajouter deux LEDs sur les ports GP0 et GP1, ces LEDS indiquent le mode de fonctionnement. Il vaut mieux utiliser des LEDs de couleurs différentes, mais ce n'est pas obligatoire.

Un appui long sur le bouton poussoir fait passer le circuit en mode apprentissage, un autre appui long le fait repasser en mode testeur.

En mode testeur, les LEDS sont allumées en fixe. Le passage d'un mode de fonctionnement à l'autre se fait par un appui bref sur le bouton.

  • Lorsque les deux LEDs sont allumées, le signal de sortie correspond au neutre.
  • Lorsque c'est la LED sur GP0 qui est allumée, le testeur est en mode automatique.
  • Lorsque c'est l'autre LED, le testeur est en mode manuel.

En mode apprentissage, les LEDS clignotent pour indiquer le type de signal reçu.

  • Lorsqu'elles clignotent en même temps, on peut enregistrer la position position courante du manche comme étant le neutre avec un appui bref sur le bouton, le clignotement s'arrête brièvement pour indiquer que la manoeuvre est prise en compte.
  • Lorsqu'une seule clignote, on peut enregistrer la position extrême correspondante.

Le schéma électronique est le suivant :

Le programme est découpé en deux sous-programmes, l'un pour le mode testeur, l'autre pour le mode apprentissage.

Nous avons déjà vu le mode testeur dans le montage n° 2. Le mode apprentissage consiste à mesurer la durée de l'impulsion sur GP3, ce que nous avons déjà vu dans le montage n° 1, puis à écrire le résultat dans l'EEPROM quand le bouton est pressé.

Le seul code vraiment nouveau est la détection de l'appui sur le bouton. Pour celà, nous maintenons le port GP2 à l'état 1 en utilisant la résistance de "pull-up" interne :

    OPTION_REG = 0b00011000;                    // OPTION register, GPIO pull-ups are enabled
    WPU = 0b000100;                             // enable pull-up on GP2

En branchant le bouton poussoir entre le 0 V et le port GP2, un appui fera passer GP2 à zéro. Le problème est que le contact étant mécanique, il ne s'établit pas instantanément, mais par rebond. Pour être sûr que le contact soit bien établi, il faut attendre quelques millisecondes. C'est le but du sous-programme "bounce()" :

unsigned char bounce (unsigned char port)
{
    if (GPIO & port)                            // if port is on
        return 1;                               // return state of port
    __delay_ms (50);                            // wait 50 msec
    return (GPIO & port);                       // return state of port
}

Pour détecter un appui long, il faut vérifier que le port ne change pas d'état pendant un certain temps, puis que le bouton est relâché. C'est le but du sous-programme "long_bounce()" :

unsigned char long_bounce (unsigned char port)
{
    unsigned int i;

    if (GPIO & port)                            // if port is active
        return 1;                               // return state of port
    for (i = 0; i < 10; i++) {                  // do 10 times
        __delay_ms (50);                        // wait 50 msec
        if (GPIO & port)                        // if port is active
            return 1;                           // return state of port
    }
    do {} while ((GPIO & port) == 0);           // wait until port returns to active
    return 0;                                   // return state of port
}

À noter que ces deux sous-programmes sont toujours utilisés ensemble, comme dans cet exemple :

        if (!bounce (0b000100)) {               // if GP2 is off
            if (!long_bounce (0b000100))        // if GP2 is off for a long time
                return;                         // enter learning mode
            if (++state > STATE_AUTOMATIC)      // change state mode
                state = STATE_NEUTRAL;
            else
                next = neutral;
        }

Le fichier source complet est ici.


Montage n° 4


Dans ce montage, nous n'allons plus allumer ou éteindre des ports mais commander directement quatre servos proportionnels sur le même manche. Il permet, par exemple, de remplacer des électro-aimants par des servos normaux, ou de commander des moteurs à balais en utilisant des micro-swiches actionnés par les palonniers des servos.

Au départ, tous les servos sont dans une position initiale.

Pour commander un servo particulier, on positionne le manche dans l'une des quatre positions suivantes :

  • Manche tout en haut, le servo sur le port GP1 est actionné.
  • Manche tout en bas, le servo sur le port GP2 est actionné.
  • Manche à mi-course en haut, le servo sur le port GP4 est actionné.
  • Manche à mi-course en bas, le servo sur le port GP5 est actionné.

Il y a deux modes de fonctionnement, choisis par le port GP0, qui doit être positionné avant la mise sous tension :

    Quand GP0 n'est pas connecté, les servos sont en mode fugitif :
    • Quand on actionne le manche dans une certaine position, le servo correspondant va dans la position extrême opposée et y reste tant que le manche reste dans cette position.
    • Quand on relâche le manche (retour au centre), le servo revient à la position initiale.
  • Quand GP0 est connecté au 0 V, les servos sont en mode bascule :
    • Une première action positionne le servo dans la position extrême.
    • Le servo reste dans cette position au retour au centre du manche.
    • Une deuxième action le fait revenir à la position initiale.

Le schéma électronique est le suivant :

Le programme est similaire dans le principe à celui du montage n° 3 du 12F509, mais génère des trames PWM au lieu de simplement allumer ou éteindre des ports. La longueur des trames est rangée dans un tableau appelé "chval[]". Pour chaque trame, nous allons allumer le port correspondant pour le temps correspondant à la valeur, en utilisant le timer 1 :

        TMR1IF = 0;                                // clear interrupt flag
        TMR1IE = 1;                                // enable timer 1 interrupt
        PEIE = 1;                                  // enable peripheral interrupts
        for (i = 0; i < 4; i++) {                  // write pulses on successive ports
/*                                           GP1      GP2      GP4      GP5        */
            const unsigned char kport[] = {0b000010,0b000100,0b010000,0b100000};
            di();                                  // disable interrupts
            clock = 0;                             // reset time indicator
            TMR1 = 10 - chval[i];                  // set timer 1
            GPIO = kport[i];                       // set port
            ei();                                  // enable interrupts
            while (!clock);                        // wait until time has elapsed
            GPIO = 0;                              // clear all ports
        }
        TMR1IE = 0;                                // disable timer 1 interrupt
        PEIE = 0;                                  // disable peripheral interrupts

Le sous-programme d'interruption pour le timer est très simple, il positionne juste l'indicateur "clock" lorsque le temps est écoulé :

    if (TMR1IE && TMR1IF) {                        // timer 1 interrupt
        TMR1IF = 0;                                // clear interrupt flag
        clock = 1;                                 // set time elapsed indicator
    }
  • Il existe une variante de ce programme, où le positionnement des servos se fait en mode séquentiel :
    • Les servos sont au neutre en position initiale.
    • Quand on actionne le manche dans une certaine position, le servo correspondant va dans une position extrême.
    • Quand on relâche le manche (retour au centre), le servo revient au neutre.
    • Quand on actionne à nouveau le manche, le servo va dans la position extrême opposée.

      En clair, à chaque action sur le manche, le servo ira alternativement à droite, puis au neutre, puis à gauche, puis au neutre et le cycle recommence.

    Dans cette variante, le port GP0 sert également à configurer le mode, fugitif ou bascule.

Le fichier source complet est ici, il contient les deux variantes du programme :

  • Le fichier "675_tuto4_1.c" pour la variante en mode tout-ou-rien.
  • Le fichier "675_tuto4_2.c" pour la variante en mode séquentiel.

Montage n° 5


Ce montage et ses variantes permettent d'actionner de un à huit servos proportionnels en utilisant un bouton poussoir fugitif ou un manche avec retour au centre.

Il est semblable au montage n° 10 pour le 12F509.

Pour positionner un servo donné, nous allons actionner et relâcher autant de fois que nécessaire le bouton ou le manche, le dernier appui devant être plus long que les autres.

Il y a deux modes de fonctionnement, choisis par le port GP0, qui doit être positionné avant la mise sous tension :

    Quand GP0 n'est pas connecté, les servos sont en mode fugitif :
    • Quand on actionne le bouton un certain nombre de fois, le servo correspondant va dans la position extrême opposée et y reste tant que le bouton reste dans cette position.
    • Quand on relâche le bouton (retour au centre por le manche), le servo revient à la position initiale.
  • Quand GP0 est connecté au 0 V, les servos sont en mode bascule :
    • Une première action positionne le servo dans la position extrême.
    • Le servo reste dans cette position au retour au centre du manche.
    • Une deuxième action le fait revenir à la position initiale.

Le schéma électronique est le suivant :

Le programme est sensiblement identique à celui du montage n° 4, seule la séquence de décodage est différente :
void count_sequence (uint16_t idle, uint8_t start, uint8_t stop)
{
    static uint8_t seq = 0, prev = 2;
    static uint8_t tempo = TEMPO;
    uint8_t pval = pulse > PULSE;

    if (prev <= 1) {                            // not at initial value
        if (prev ^= pval) {                     // each time the pulse value is changed,
            seq += pval;                        // the sequence number is incremented
            if (GP0 && !seq)                    // return to idle state
                chval[0]=chval[1]=chval[2]=chval[3] = idle;
            tempo = TEMPO;
        }
        else {                                  // when the value doesn't change, 
            if (tempo)
                tempo--;                        // wait some milliseconds before accepting the command
            else {
                if (seq>=start &&
                    seq<=stop && pval)          // sequence inside bounds and positive value
                    change_state (seq - start); // set ports according to the sequence number
                seq = 0;
            }
        }
    }
    prev = pval;
}

  • En plus de la variante pour traiter les impulsions de 5 à 8, il existe deux autres variantes de ce programme, où le positionnement des servos se fait en mode séquentiel : à chaque action sur le bouton, le servo ira alternativement à droite, puis au neutre, puis à gauche, puis au neutre et le cycle recommence.

    Dans ces variantes, le port GP0 sert également à configurer le mode, fugitif ou bascule.

Le fichier source complet est ici, il contient les quatre variantes du programme :

  • Le fichier "675_tuto5_1.c" pour la variante en mode tout-ou-rien pour les impulsions de 1 à 4.
  • Le fichier "675_tuto5_2.c" pour la variante en mode tout-ou-rien pour les impulsions de 5 à 8.
  • Le fichier "675_tuto5_3.c" pour la variante en mode séquentiel pour les impulsions de 1 à 4.
  • Le fichier "675_tuto5_4.c" pour la variante en mode séquentiel pour les impulsions de 5 à 8.

  • Le fichier "675_tuto5.c" qui contient les sous-programmes communs aux quatre variantes.
  • Le fichier "675_tuto5.h" qui contient les déclarations des sous-programmes et les variables communes.

    Ces deux derniers fichiers doivent être ajoutés à chacun des quatre projets :


Montage n° 6


Ce montage et ses variantes permettent de rediriger un signal proportionnel vers un port parmi quatre ou huit.

Le port GP3 reçoit le signal à rediriger et le port GP0 reçoit le signal de commande. Les ports qui ne reçoivent pas le signal redirigé sont positionnés au neutre.

Il y a deux modes de commande :

  1. Suivant la position d'un manche ou d'un potentiomètre, comme dans le montage n° 4. Dans ce mode de commande, lorsque le manche est au centre (1500 µs), il n'y a pas de redirection et tous les servos sont positionnés au neutre.

    Au lieu d'un manche ou d'un potentiomètre, quand on dispose d'un émetteur programmable, on peut utiliser une combinaison d'interrupteurs pour générer la longueur de trame nécessaire. Les longueurs de trame sont les suivantes :

    • Inférieure à 1100 µs : le signal est redirigé vers le port GP1.
    • Entre 1100 µs et 1350 µs : le signal est redirigé vers le port GP4.
    • Entre 1650 µs et 1900 µs : le signal est redirigé vers le port GP5.
    • Supérieure à 1900 µs : le signal est redirigé vers le port GP2.

  2. En envoyant des impulsions, de 1 à 4 ou de 5 à 8, comme dans le montage n° 5. Dans ces variantes, au départ le signal est redirigé vers le port GP1.

    • La première ou cinquième impulsion redirige le signal vers le port GP1.
    • La deuxième ou sixième impulsion redirige le signal vers le port GP2.
    • La troisième ou septième impulsion redirige le signal vers le port GP3.
    • La quatrième ou huitième impulsion redirige le signal vers le port GP4.

      La redirection est maintenue jusqu'à la réception d'une nouvelle série d'impulsions.

Le schéma électronique est le suivant :

Le programme est un mélange de ceux des montages n° 4 et 5 et n'appelle pas de commentaire particulier, seul le programme d'interruptions est différent car il doit lire le temps des deux signaux d'entrée :

static void interrupt routine (void)
{
    if (GPIF && GPIE) {                         // GPIO port change
        GPIF = 0;                               // clear interrupt flag
        TMR1ON = 0;                             // stop timer
        if (!GP3 && getGP3 == 1) {              // end of pulse
            valGP3 = (TMR1 - iniGP3) + 90;      // get TMR1 value
            getGP3 = 0;                         // count pulse disabled
        }
        if (!GP0 && getGP0 == 1) {              // end of pulse
            valGP0 = (TMR1 - iniGP0) + 90;      // get TMR1 value
            getGP0 = 0;                         // count pulse disabled
        }
        if (GP3 && getGP3 == 2) {               // start of pulse
            iniGP3 = TMR1;                      // reset TMR1
            getGP3 = 1;                         // count pulse in progress
        }
        if (GP0 && getGP0 == 2) {               // start of pulse
            iniGP0 = TMR1;                      // reset TMR1
            getGP0 = 1;                         // count pulse in progress
        }
        TMR1ON = 1;                             // restart timer
    }
    if (TMR1IE && TMR1IF) {                     // timer 1 interrupt
        TMR1IF = 0;                             // clear interrupt flag
        clock = 1;                              // set time elapsed indicator
    }
}

Comme il n'est pas possible de rediriger plus d'un port à la fois, si le besoin s'en fait sentir, il faut alors utiliser plusieurs montages connectés en parallèle, comme dans l'exemple ci-dessous :

La voie 1 est redirigée sur les 4 servos de gauche, la voie 3 est redirigée sur les 4 servos de droite, le tout dépendant de la valeur de la voie 2.

Le fichier source complet est ici, il contient les trois variantes du programme :

  • Le fichier "675_tuto6_1.c" pour la variante en mode positionnel.
  • Le fichier "675_tuto6_2.c" pour la variante traitant les impulsions de 1 à 4.
  • Le fichier "675_tuto6_3.c" pour la variante traitant les impulsions de 5 à 8.

  • Le fichier "675_tuto6.c" qui contient les sous-programmes communs aux trois variantes.
  • Le fichier "675_tuto6.h" qui contient les déclarations des sous-programmes et les variables communes.

    Ces deux derniers fichiers doivent être ajoutés à chacun des trois projets :


Montage n° 7


Ce montage permet de commander un moteur pas-à-pas en rotation continue.

Suivant la position d'un manche proportionnel, le moteur tourne dans un sens ou dans l'autre, à une vitesse variable.

Pour plus de détails sur ce genre de moteur, vous pouvez consulter le chapitre qui leur est consacré, dans la rubrique "Périphériques".

N'importe quel moteur pas-à-pas peut être utilisé avec ce montage, mais, pour les essais, j'ai utilisé le moteur 28BYJ-48 qui est vendu, avec son driver, moins de $3.00 (port compris) un peu partout. Ce moteur est lent (environ 12 tours par minute à pleine vitesse) et il n'a pas beaucoup de couple, malgré son réducteur intégré. Par contre, il a une très bonne précision en position (1/2048ème de tour) et sa lenteur peut devenir un avantage pour certaines animations.

Schéma

Programme

Le programme est divisé en un fichier de déclarations et trois fichiers source.

  • Le fichier de déclarations, "675_tuto7.h", permet de choisir le mode de fonctionnement : pas-entier, demi-pas ou double-phase :

#define NEUTRAL 1500
#define MAX 2000
#define MIN 1000
#define DEADBAND 50

// motor modes
#define FULL-STEP
// #define HALF_STEP
// #define TWO_PHASE

/* pulse.c */
extern void init_pulse (void);
extern void set_position (void);

/* step.c */
extern void count_delay (void);
extern void step_position (uint16_t pulse);

  • Le premier fichier, "675_tuto7.c", contient le programme principal qui fait juste l'appel au sous-programme d'initialisation et la boucle infinie.

main (void)
{
    init_pulse ();             // initializations
    for (;;) {                 // loop for ever
        set_position ();       // set motor position depending on the pulse length
    }
}

  • Le deuxième fichier, "675_tuto7_pulse.c", contient les initialisations, l'appel aux sous-programmes de gestion du moteur et le sous-programme d'interruptions.

#define INIT_TMR0 (256 - 100)

static volatile uint8_t getGP3;
static volatile uint16_t valGP3, iniGP3;

static void interrupt routine (void);

void init_pulse (void)
{
    OPTION_REG = 0b00001000;                // pull-ups enabled, no TMR0 prescaler
    INTCON = 0;                             // disable all interrupts and clear all flags
    T1CON = 0;                              // TMR1 control register
    ADCON0 = ANSEL = 0;                     // turn off analog conversion
    CMCON = 0x07;                           // turn off analog comparator
    TRISIO = 0b001100;                      // GP2 and GP3 as input, all other as outputs
    GPIO = 0;                               // all output ports off
    TMR0 = INIT_TMR0;                       // reset timer 0
    TMR1 = 0;                               // reset timer 1
    TMR1ON = 1;                             // TMR1 is now counting
    TMR0IF = 0;                             // clear interrupt flag on TMR0
    TMR0IE = 1;                             // enable timer 0 interrupt
    IOC = 0b001000;                         // enable interrupts on GP3 pin change
    GPIF = 0;                               // clear interrupt flag on GP3
    GPIE = 1;                               // Port Change Interrupt enabled
    ei();                                   // enable general interrupts
}

Les deux timers sont initialisés à 1µs, TMR0 est utilisé pour le délai entre chaque pas et TMR1 pour mesurer le temps de l'impulsion reçue du récepteur.

Le sous-programme "set_position()" vérifie qu'une impulsion valide a été reçue et appelle le sous-programme de positionnement du moteur :

void set_position (void)
{
    if (!getGP3) {
        if (valGP3 >= MIN && valGP3 <= MAX)
            step_position (valGP3);
        getGP3 = 2;                                    // count pulse enabled
    }
}

Le sous-programme d'interruptions mesure le temps de la trame reçue sur GP3 et appelle le sous-programme de délai toutes les 100 µs.

static void interrupt routine (void)
{
    if (GPIF) {                                   // GPIO port change
        TMR1ON = 0;                               // stop timer
        if (!GP3 && getGP3 == 1) {                // end of pulse
            valGP3 = (TMR1 - iniGP3) + 20;        // get TMR1 value
            getGP3 = 0;                           // count pulse disabled
            TMR1 = 0;                             // reset timer
        }
        if (GP3 && getGP3 == 2) {                 // start of pulse
            iniGP3 = TMR1;                        // get timer value
            getGP3 = 1;                           // count pulse in progress
        }
        GPIF = 0;                                 // clear interrupt flag
        TMR1ON = 1;                               // restart timer
    }
    if (TMR0IF) {                                 // timer 1 interrupt
        TMR0IF = 0;                               // clear interrupt flag
        TMR0 = INIT_TMR0;                         // reset timer 0
        count_delay();                            // count delay between steps
    }
}

  • Le troisième fichier, "675_tuto7_step.c", contient les sous-programmes de gestion du moteur.

Suivant les constantes définies dans le fichier de déclarations, la table des pas est définie sous forme de valeurs à ranger dans GPIO, les ports utilisés sont GP0 pour l'enroulement "A", GP1 pour le "B", GP4 pour le "C" et GP5 pour le "D".

#ifdef FULL-STEP
    static const uint8_t tabstep[4] = {0b000001, 0b000010, 0b010000, 0b100000,};
    static const uint8_t maskstep = 0x03;
#elif defined HALF_STEP
    static const uint8_t tabstep[8] = {0b000001, 0b000011, 0b000010, 0b010010,
                                       0b010000, 0b110000, 0b100000, 0b100001,};
    static const uint8_t maskstep = 0x07;
#elif defined(TWO_PHASE)
    static const uint8_t tabstep[4] = {0b000011, 0b010010, 0b110000, 0b100001,};
    static const uint8_t maskstep = 0x03;
#endif

#define MAX_DELAY 250
#define MIN_DELAY 16
#define TEMPO_STOP 1500;

Le sous programme "count_delay()" compte le délai entre pas. Une fois ce délai écoulé, si le moteur est en cours d'arrêt (variable "direction" égale à 0), la tension sur les enroulements est maintenue pendant quelques millisecondes, sinon, la nouvelle position est calculée et les enroulements sont alimentés suivant la valeur trouvée dans la table des pas.

void count_delay (void)
{
    if (!delaystep) {
        if (!direction) {
            if (tempo_stop)
                tempo_stop--;
            else
                GPIO = 0;
        }
        else {
            position = (position + direction) & 0x07;
            GPIO = tabstep[position & maskstep];
            delaystep = initdelay;
        }
    }
    else
        delaystep--;
}

Le sous-programme "step_position()" est appelé chaque fois qu'une impulsion est reçue. En fonction de la longueur de cette impulsion, il positionne la variable "direction" et calcule le délai entre pas dans la variable "initdelay".

void step_position (uint16_t pulse)
{
    if (pulse > NEUTRAL - DEADBAND &&
        pulse < NEUTRAL + DEADBAND) {
        if (direction) {
            direction = 0;
            initdelay = MAX_DELAY;
            tempo_stop = TEMPO_STOP;
        }
    }
    else {
        if (pulse > NEUTRAL) {
            direction = 1;
            initdelay = MAX - (pulse - DEADBAND);
        }
        else {
            direction = -1;
            initdelay = (pulse + DEADBAND) - MIN;
        }
        initdelay = (2 << (initdelay >> 6)) + MIN_DELAY;
    }
}

Le fichier source complet est ici.