É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
|
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é.
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);
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.
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.
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.
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
}
|
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.
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;
}
|
Le fichier source complet est ici, il contient les quatre variantes du programme :
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 :
- 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.
- 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.
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 :
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.
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.
|