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 12F509
|
Un microcontrôleur (MCU) a besoin d'être programmé pour fonctionner correctement. Il existe plusieurs langages de programmation,
je vais utiliser le langage C pour les différents montages car ce langage est (relativement) compréhensible et utilisable sur
pratiquement tous les MCU existants.
Je n'ai pas l'intention de donner un cours de programmation, il existe de très bons tutoriels pour ça sur le web. Je donnerai
plutôt des bouts de code, un peu comme des briques à assembler, avec quelques explications.
Mais avant d'entrer dans le sujet de la programmation immédiatement, il faut parler plus en détail du MCU.
Un MCU est un micro ordinateur, complet et autonome. Il est composé d'une unité de calcul (CPU), de différentes mémoires
(flash pour ranger le code du programme, SRAM pour ranger les données, quelque fois EEPROM pour conserver les données même
lorsque le MCU est mis hors tension), de ports d'entrées et de sorties et de registres internes permettant de configurer son fonctionnement.
Je vais décrire plus particuliérement l'utilisation du PIC12F509 de Microchip, qui est un composant très courant et
d'un prix abordable.
Il a une capacité de 1024 mots de programme, 41 octets de mémoire de données, un port en entrée seulement
et 5 ports pouvant être utilisés indépendamment en entrée ou en sortie.
Il possède un oscillateur interne de 4 Mhz d'une précision
de 1 % et ne nécessite donc pas de quartz pour fonctionner.
Tout ce dont il a besoin pour être opérationnel est une source
d'alimentation comprise entre 2 V et 5.5V.
Sa vitesse d'exécution est de 1 Mips (million d'instructions par secondes).
Sa fiche descriptive est ici.
L'essentiel de la programmation consiste à lire ou écrire les registres du MCU. Nous allons donc les examiner :
Il y a un mot de 12 bits de configuration, aussi appelé "fusibles", positionné à la compilation et neuf registres, mais nous n'utiliserons
que quatre d'entre eux, les autres étant gérés par le compilateur.
- Le registre OPTION est accessible en écriture. Les bits à positionner dans ce registre qui nous intéressent sont les suivants :
- bit 5 T0CS : source du signal pour TMR0,
- bit 4 T0SE : type de déclenchement pour TMR0,
- bit 3 PSA : assignation du diviseur pour TMR0,
- bit 2-0 PS<2:0> : valeur du diviseur, utilisé par TMR0. Les valeurs de division sont les suivantes :
0x0 = | 1:2 |
0x1 = | 1:4 |
0x2 = | 1:8 |
0x3 = | 1:16 |
0x4 = | 1:32 |
0x5 = | 1:64 |
0x6 = | 1:128 |
0x7 = | 1:256 |
Exemple :
OPTION = ~T0CS & ~PSA & (T0SE | 0x2);
|
- Le registre TMR0 est accessible en lecture et écriture. C'est un compteur perpétuel, lorsqu'il a atteint la valeur 255,
il repasse à zéro la fois suivante.
La fréquence de comptage dépend de la valeur du diviseur du registre OPTION, si le diviseur n'est pas utilisé, TMR0 va ajouter
1 toutes les microsecondes (noté usec dans le code),
si la valeur du diviseur est 0x0, TMR0 va ajouter 1 toutes les 2 µs, etc.
- Le registre TRIS est accessible en écriture. Les six derniers bits indiquent la direction des ports, un bit à 0 indique
que le port correspondant est en sortie,
un bit à 1 indique que le port correspondant est en entrée.
- Le registre GPIO est accessible en lecture et écriture. En fonction de la valeur du registre TRIS,
il permet de lire ou d'écrire les ports. Il peut être accédé d'une manière globale ou pour chaque port individuellement.
Les ports correspondent aux six derniers bits du registre, quand un bit est à 1, le port correspondant prend la valeur de la tension d'alimentation,
quand le bit est à 0, le port prend la valeur 0 V.
Il faut noter que le port GP3 est accessible uniquement en lecture et n'est donc pas concerné par les valeurs écrites dans ce registre.
Le nom des ports individuels est :
- GP0 (broche 7),
- GP1 (broche 6),
- GP2 (broche 5),
- GP3 (broche 4),
- GP4 (broche 3)
- GP5 (broche 2).
Exemple :
TRIS = 0b101000; // set GP3 and GP5 as input, all others as output
GPIO = 0; // set all output ports to 0 V
GPIO = 0b000011; // set GP0 and GP1 to 5 V, all other output ports to 0 V
GP1 = 1; // set GP1 to 5 V
|
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é.
Pour faire cela, nous avons besoin de mesurer le temps du signal de la voie en sortie du récepteur.
Lorsque le manche est au centre (neutre), le signal dure 1500 µs, en haut, il dure 2000 µs, en bas, il dure 1000 µs.
Le signal est répété toutes les 20000 ou 22500 µs, suivant l'émetteur.
Pour mesurer le temps, nous avons le registre TMR0. Dans ce montage, nous allons le diviser par 8 (8 µs par impulsion de comptage),
ce qui nous permet de mesurer un temps variant de 0 à 255 * 8 = 2040 µs.
Voici donc le début du programme :
#include <htc.h>
#include <pic.h>
#include <pic12f509.h>
// set fuse bits
__CONFIG(MCLRE_OFF & CP_OFF & WDT_OFF & OSC_IntRC);
unsigned char cnt;
main (void)
{
OPTION = ~T0CS & ~PSA & (nGPWU | nGPPU | T0SE | 0x2);
|
Je n'explique pas les valeurs dans les bits de configuration, ils sont à prendre tels quels.
La ligne "unsigned char cnt; " déclare une variable numérique non signée d'une longueur de huit bits et
non une variable de type caractère (ça n'existe pas en C, toutes les variables et constantes sont
numériques, écrire "x = 'a' + y; " est une instruction parfaitement valide).
La ligne "main (void) " et l'accolade sur la ligne qui suit marquent le début du code exécutable.
Les bits du registre OPTION sont aussi à prendre tels quels.
La valeur 0x2 à la fin de la ligne indique la division par 8 pour le registre TMR0.
Nous allons lire le signal sur le port GP3 et utiliser GP0 et GP1 pour sortir la valeur.
TRIS = 0b001000; // GP3 as input, all others as output
GPIO = 0; // all ports off
for (cnt = 0; cnt < 5; cnt++) { // wait for five pulses
do {} while (!GP3); // wait start of pulse
do {} while (GP3); // wait end of pulse
}
|
J'ai configuré tous les ports en sortie dans le registre TRIS, ça pourra toujours servir quand je rajouterai du code plus tard.
L'instruction "for (cnt = 0; cnt < 5; cnt++) { " de la troisième ligne est une boucle qui va s'exécuter 5 fois,
l'accolade ouvrante introduit un bloc d'instructions qui s'exécuteront dans le corps de la boucle.
L'instruction "do {} while (!GP3); " de la quatrième ligne est une boucle qui va s'exécuter tant que
le port GP3 sera égal à zéro.
L'instruction "do {} while (GP3); " de la cinquième ligne est une boucle qui va s'exécuter tant que
le port GP3 sera différent de zéro.
L'accolade de la sixième ligne indique la fin de la boucle "for (cnt = 0; cnt < 5; cnt++) ".
Arrivé à ce point, nous sommes donc sûr qu'il y a bien un signal répétitif qui arrive sur le port GP3.
Il n'y a plus qu'à mesurer le temps qu'il dure.
for (;;) { // loop for ever
do {} while (!GP3); // wait start of pulse
TMR0 = 255 - 100; // wait at least 100 * 8 = 800 usec
do {} while (TMR0 >= 2); // time elapsed, TMR0 has reached 0
do {} while (GP3); // wait end of pulse
cnt = TMR0; // TMR0 is now between 18 and 156, neutral is 88
|
L'instruction "for (;;) " de la première ligne est le début d'une boucle infinie, qui s'exécutera tant que
le MCU est alimenté.
L'instruction "do {} while (!GP3); " de la deuxième ligne attend que le port GP3 soit différent de zéro.
L'instruction "TMR0 = 255 - 100; " de la troisième ligne range une valeur dans le registre TMR0, qui va continuer
à compter à partir de cette valeur. Lorsqu'il repassera par la valeur zéro, 800 µs se seront écoulées. Pourquoi laisser passer ce temps avant
de faire la mesure ?
Le registre TMR0 peut prendre 256 valeurs, soit de 0 à 2040 µs. La durée du signal étant de 2000 µs, on pourrait penser
qu'en remettant le registre à zéro ce serait suffisant, mais ce temps de 2000 µs est donné avec le trim au neutre, si on met le trim en butée
le signal risque d'être plus long et de dépasser la capacité de comptage du registre. Avec ce décalage, je peux mesurer un temps variant de 800
à 2840 µs.
L'instruction "do {} while (TMR0 >= 2); " de la quatrième ligne attend que le registre TMR0 soit repassé par zéro.
L'instruction "do {} while (GP3); " de la cinquième ligne attend que le port GP3 revienne à zéro.
L'instruction "cnt = TMR0; " de la sixième ligne range la valeur du registre TMR0 dans une variable appelée
"cnt ".
Nous connaissons maintenant la durée du signal, qui est une valeur comprise entre 18 et 156, le neutre étant la valeur 88
(88 * 8 µs + 800 µs = 1504 µs).
Nous pouvons alors positionner les ports en fonction de cette valeur.
if (cnt < 78) { // 1400 usec
GP0 = 1; // GP0 on
}
else if (cnt > 98) { // 1600 usec
GP1 = 1; // GP1 on
}
else {
GPIO = 0; // all ports off
}
}
}
|
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 " s'exécute si la condition du "if " précédent
est fausse.
Les ports sont positionnés par l'opérateur "= ", soit individuellement,
dans le cas "GP1 = 1; ", soit globalement
dans le cas "GPIO = 0; ".
Les accolades des deux dernières lignes marquent respectivement la fin de la boucle "for "
et la fin du bloc de code exécutable.
Le fichier source complet est ici.
Dans le montage n° 1, le manche de la radio allume un port quand il est dans une position et l'éteint quand il revient au neutre.
Dans ce montage, le port sera utilisé comme une bascule : une première action dans un sens va l'allumer, une deuxième action dans
le même sens va l'éteindre.
Cela complique un peu les choses car il va falloir détecter que le manche est revenu au neutre avant de prendre en compte la deuxième action,
sinon le port va clignoter à la fréquence de 1 / 22500 µs !
Nous déclarons trois variables de type bit :
bit sGP0 = 0, sGP1 = 0, neutral = 0;
|
sGP0 contient l'état du port 0, sGP1 contient l'état du port 1, neutral indique que le manche est revenu au neutre
entre deux états (0 = le manche est au neutre, 1 = il n'y est pas).
Le code pour le décodage du temps de signal est modifié comme suit : on commence par tester si l'état précédent du manche
est le neutre, si c'est le cas on inverse (opérateur "~") l'état du port
GP0 et on positionne la variable "neutral" à 1 pour indiquer l'état du manche :
if (cnt < 78) { // 1400 usec
if (!neutral) { // previous state was neutral
sGP0 = ~sGP0; // invert sGP0
neutral = 1; // current state is not neutral
}
}
|
On fait la même chose pour GP1 :
else if (cnt > 98) { // 1600 usec
if (!neutral) { // previous state was neutral
sGP1 = ~sGP1; // invert sGP1
neutral = 1; // current state is not neutral
}
}
|
Lorsque le manche repasse par le neutre, on positionne l'état correspondant :
else { // 1500 usec, neutral
neutral = 0; // set state to neutral
}
|
Et, enfin, on positionne les ports en fonction de la valeur des variables :
GP0 = sGP0; // set port 0 according to toggle
GP1 = sGP1; // set port 1 according to toggle
|
Le fichier source complet est ici.
Dans ce montage, nous allons commander quatre ports sur le même manche proportionnel :
- manche au centre, tout est éteint,
- manche tout en haut, allumer le port GP0,
- manche à mi-course en haut, allumer le port GP1,
- manche à mi-course en bas, allumer le port GP2,
- manche tout en bas, allumer le port GP4.
Le port choisi reste allumé tant que le manche ne revient pas au centre.
Le problème est de ne pas allumer le port correspondant à la mi-course quand le manche se déplace vers une position en butée.
Nous allons pour cela introduire une temporisation, non pas en utilisant le registre TMR0, mais en comptant le nombre d'impulsions
reçues quand le manche est dans une position donnée. Le signal se répètant toutes les 22500 µs, si on compte 15 impulsions sur les positions
à mi-course, cela
fait un temps d'approximativement 3/10ème de seconde. Donc, si nous voulons mettre le manche en butée, il faut le faire rapidement.
Pour une position à mi-course, cela introduit un très léger retard pas vraiment gênant.
Nous déclarons une constante "TEMPO" de valeur 15 (qui pourra être modifiée selon le goût de chacun), une variable
"tempo" et une variable "neutral" :
#define TEMPO 15
unsigned char tempo = TEMPO;
bit neutral = 0;
|
Le code pour détecter la position du manche tout en haut est le suivant :
if (cnt < 24) { // 1000 usec
if (!neutral) {
GP0 = 1; // GP0 on
neutral = 1;
}
}
|
La variable "neutral" sert à indiquer la position précédente du manche, comme dans le montage n° 2.
Pour détecter la position du manche mi-course en haut, le code est le suivant :
else if (cnt > 26 && cnt < 80) { // 1250 usec
if (!neutral) {
if (tempo) // wait some milliseconds before accepting the command
tempo--;
else {
GP1 = 1; // GP1 on
neutral = 1;
}
}
}
|
Nous évaluons la variable "tempo" et ne faisons rien d'autre que la diminuer tant qu'elle est différente de zéro,
puis, quand elle est égale à zéro, nous allumons le port.
Pour détecter la position manche au centre, le code est le suivant :
else if (cnt > 82 && cnt < 94) { // 1500 usec, neutral
GPIO = 0; // all ports off
neutral = 0;
tempo = TEMPO;
}
|
Nous éteignons tous les ports, et remettons les variables "neutral" et "tempo" à leurs valeurs initiales.
Le traitement des autres positions se fait suivant le même principe :
else if (cnt > 96 && cnt < 138) { // 1750 usec
if (!neutral) {
if (tempo) // wait some milliseconds before accepting the command
tempo--;
else {
GP2 = 1; // GP2 on
neutral = 1;
}
}
}
else if (cnt > 140) { // 2000 usec
if (!neutral) {
GP4 = 1; // GP4 on
neutral = 1;
}
}
|
Le fichier source complet est ici.
Dans le montage n° 3, le manche de la radio allume un port quand il est dans une position et l'éteint quand il revient au neutre.
Dans ce montage, deux ports seront utilisés comme bascules : une première action dans une position va allumer le port, une deuxième action dans la
même position va l'éteindre.
Nous avons déjà vu cela dans le montage n° 2, le but de ce montage est de montrer une généralisation du principe.
Nous allons déclarer les variables nécessaires :
bit sGP0 = 0, sGP1 = 0, sGP2 = 0, sGP4 = 0;
|
J'ai déclaré quatre variables en vue d'éventuelles modifications.
Par rapport au code du montage n° 3, je remplace le positionnement direct du port ("GP0 = 1;") par une ligne qui inverse
la variable correspondante et positionne le port :
if (cnt < 24) { // 1000 usec
if (!neutral) {
GP0 = sGP0 = ~sGP0; // toggle sGP0 and set GP0 accordingly
neutral = 1; // set neutral state
}
}
else if (cnt > 26 && cnt < 80) { // 1250 usec
if (!neutral && !tempo--) { // wait some milliseconds before accepting the command
GP1 = sGP1 = ~sGP1; // toggle sGP1 and set GP1 accordingly
neutral = 1; // set neutral state
}
}
|
J'ai écrit le code de ce montage en utilisant une forme plus compacte de l'instruction d'affectation ("GP0 = sGP0 = ~sGP0;").
Dans une affectation multiple, l'ordre d'exécution est de la droite vers la gauche, quand j'écris "a = b = ~c;",
cela signifie qu'on prend la valeur inversée de "c", qu'on la range dans b, puis qu'on range la valeur de "b" dans "a" et non pas
qu'on range la valeur inversée de "c" dans "a" et "b", cet ordre d'évaluation peut avoir de l'importance si les variables sont
de longueurs différentes.
À noter aussi que j'ai regroupé deux instructions "if" et une instruction de diminution ("tempo--") en une seule
condition ("if (!neutral && !tempo--) {"), ce n'est pas par paresse ou pour embrouiller le code,
mais pour permettre au compilateur de générer moins de code exécutable.
Dans la séquence d'instructions correspondant au retour au neutre, je remplace "GPIO = 0;" par autant d'instructions qu'il faut
pour éteindre les ports qui n'ont pas de bascule :
else if (cnt > 82 && cnt < 94) { // 1500 usec, neutral
GP2 = 0; // GP2 and GP4 off
GP4 = 0; // GP2 and GP4 off
neutral = 0; // reset neutral state
tempo = TEMPO;
}
|
Je n'ai pas utilisé d'affectation multiple ici ("GP2 = GP4 = 0;"), car cela reviendrait à lire la valeur du port GP4 pour
l'écrire dans le port GP2.
La fin du code (traitement des ports sans bascule) est la suivante :
else if (cnt > 96 && cnt < 138) { // 1750 usec
if (!neutral && !tempo--) { // wait some milliseconds before accepting the command
GP2 = 1; // GP2 on
neutral = 1; // set neutral state
}
}
else if (cnt > 140) { // 2000 usec
if (!neutral) {
GP4 = 1; // GP4 on
neutral = 1; // set neutral state
}
}
|
Le fichier source complet est ici.
Dans ce montage, nous allons lire le port GP5 pour déterminer le fonctionnement du circuit : quand GP5 est branché sur 0 V,
la sortie GP1 est fugitive, quand le port GP5 est branché sur 5 V, la sortie GP1 est une bascule.
Nous commençons par positionner le registre TRIS pour utiler le port GP5 en lecture :
TRIS = 0b101000; // GP3 and GP5 as input, all other as output
|
Je remplace le code de traitement pour le port GP1 par les lignes suivantes :
else if (cnt > 26 && cnt < 80) { // 1250 usec
if (!neutral && !tempo--) { // wait some milliseconds before accepting the command
if (GP5) // state of GP5
GP1 = sGP1 = ~sGP1; // toggle sGP1 and set GP1 accordingly
else
GP1 = 1; // GP1 on
neutral = 1; // set neutral state
}
}
|
L'instruction "if (GP5)" lit le port et répond vrai quand GP5 est branché sur le 5 V (valeur 1).
Dans la séquence de traitement du neutre, j'ajoute les lignes de code suivantes :
if (!GP5) { // state of GP5
GP1 = 0; // GP1 off
}
|
L'instruction "if (!GP5)" lit le port et répond vrai quand GP5 est branché sur le 0 V (valeur 0).
Le fichier source complet est ici.
Le programme de ce montage a plus de lignes que les précédents, sans être réellement complexe.
Nous allons utiliser un interrupteur de l'émetteur pour générer des impulsions de longueur différentes (longues et courtes) et les ports
seront positionnés en fonction
du nombre de changement de longueur d'impulsions. Une impulsion se produit lorsque l'interrupteur change
de position, sa position initiale n'a pas d'importance.
Le port GP5 va aussi être à nouveau mis à contribution pour déterminer le fonctionnement du circuit : s'il est relié au 0 V,
le circuit traitera la série des quatre premiers changements de longueurs d'impulsions, s'il est relié au 5 V, le circuit traitera
la série des quatre changements suivants
(de 5 à 8).
Nous commençons par déclarer la constante et les variables nécessaires :
#define TEMPO 20
unsigned char cnt, tempo, sequence = 0, prev = 2;
bit sGP0 = 0, sGP1 = 0, sGP2 = 0, sGP4 = 0;
void decode_sequence (unsigned char last_value);
|
La variable "tempo" va servir pour introduire une temporisation entre chaque changement : lorsqu'au bout d'un certain temps
(4/10ème de seconde),
l'interrupteur n'a plus changé de position, on traite le nombre de changements, sinon on continue de compter.
La variable "sequence" sert à compter le nombre de changements d'impulsions reçus,
la variable "prev" contient le dernier état de l'interrupteur (0 ou 1), elle est initialisée à 2 pour indiquer qu'on ne connait
pas encore cet état au début du programme.
La dernière ligne déclare une fonction (du code exécutable qui peut être appelé plusieurs fois) qui accepte une variable en entrée.
Nous positionnons le registre TRIS pour utiliser le port GP5 en lecture :
TRIS = 0b101000; // GP3 and GP5 as input, all other ports as output
|
La suite du programme est identique au début des autres, jusqu'à l'instruction qui obtient la longueur de l'impulsion ("cnt = TMR0;).
Nous allons alors appeler la fonction de décodage en lui passant le résultat d'une condition et nous positionnons les ports
en fonction de ce décodage :
decode_sequence (cnt < 88); // count and decode number of switch toggles
GP0 = sGP0; // set port 0
GP1 = sGP1; // set port 1
GP2 = sGP2; // set port 2
GP4 = sGP4; // set port 4
}
}
|
L'instruction "decode_sequence (cnt < 88);" peut sembler étrange aux adeptes du BASIC ou autres langages exotiques,
mais il faut savoir qu'en langage C une condition est une expression numérique comme une autre, valant 0 lorsque la condition est fausse ou 1
lorsqu'elle est vraie. Dans ce programme, la seule chose qui nous intéresse est de savoir si l'impulsion reçue du récepteur est plus courte
que le neutre ou plus longue.
Le début de la fonction de décodage se présente comme suit :
void decode_sequence (unsigned char last_value)
{
if (prev != 2) { // initial value
if (prev ^= last_value) { // each time the value is changed,
sequence++; // the sequence number is incremented
tempo = TEMPO; // initialize temporisation
}
|
La condition "if (prev != 2) {" évite de compter quand on ne connait pas encore la position initiale de l'interrupteur.
La variable "prev" est positionnée à la fin de la fonction à la valeur de "last_value".
Lorsque la longueur de l'impulsion est différente de la longueur précédente, nous augmentons le compte et nous armons la temporisation.
Lorsque la longueur de l'impulsion n'a pas varié durant le temps de la temporisation, nous positionnons les variables
de valeurs des ports en fonction du nombre de changements reçus et de la valeur du port GP5 :
else if (!tempo-- && sequence) { // when the value doesn't change, wait before accepting the command
if (!GP5) { // GP5 state
switch (sequence) { // set ports according to the sequence number
case 1: sGP0 = ~sGP0; break; // toggle port 0
case 2: sGP1 = ~sGP1; break; // toggle port 1
case 3: sGP2 = ~sGP2; break; // toggle port 2
case 4: sGP4 = ~sGP4; break; // toggle port 4
}
}
else {
switch (sequence) { // set ports according to the sequence number
case 5: sGP0 = ~sGP0; break; // toggle port 0
case 6: sGP1 = ~sGP1; break; // toggle port 1
case 7: sGP2 = ~sGP2; break; // toggle port 2
case 8: sGP4 = ~sGP4; break; // toggle port 4
}
}
sequence = 0;
}
}
|
L'instruction "switch" permet d'éviter une série de "if" et "else" lorsque nous devons évaluer plusieurs fois la même variable.
À la fin de la fonction, nous rangeons la position de l'interrupteur dans la variable "prev" :
Le fichier source complet est ici.
Ce montage dérive du montage n° 1, mais au lieu d'être commandé par un manche proportionnel, il est commandé par un interrupteur à deux positions.
Quand l'interrupteur est dans une position, un port est positionné à 1 et un autre port est positionné à 0.
Quand l'interrupteur est basculé, le port qui était à 1 passe à 0, celui qui était à 0 passe à 1.
Dans le code pour le décodage du temps de signal, nous n'allons plus nommer les ports de manière individuelle, mais les positionner
directement dans le registre GPIO en valorisant les bits correspondants :
if (cnt < 78) { // 1400 usec
GPIO = 0b000001; // GP0 on, all other outputs ports off
}
else if (cnt > 98) { // 1600 usec
GPIO = 0b100000; // GP5 on, all other outputs ports off
}
|
En utilisant ce registre, il est aussi possible de positionner plusieurs ports à la fois, en une seule instruction. Par exemple :
if (cnt < 78) { // 1400 usec
GPIO = 0b000011; // GP0 and GP1 on, all other outputs ports off
}
else if (cnt > 98) { // 1600 usec
GPIO = 0b110000; // GP4 and GP5 on, all other outputs ports off
}
|
Le fichier source complet est ici.
Ce montage dérive du montage n° 7, mais nous allons utiliser trois interrupteurs, chacun commandant une sortie en tout-ou-rien.
Nous allons commencer par déclarer 3 variables, au lieu de la seule variable "cnt " :
unsigned char cnt1, cnt2, cnt3;
|
Nous allons aussi déclarer les ports GP3, GP4 et GP5 en entrée dans le registre TRIS :
TRIS = 0b111000; // GP3, GP4 and GP5 as input, all other as output
|
Dans la boucle de mesure du temps, pour avoir une mesure la plus précise possible, nous allons commencer par mesurer,
l'un après l'autre, le temps de chaque entrée :
for (;;) { // loop for ever
do {} while (!GP3); // wait start of pulse
TMR0 = 255 - 100; // wait at least 100 * 8 = 800 usec
do {} while (TMR0 >= 2); // time elapsed, TMR0 has reached 0
do {} while (GP3); // wait end of pulse
cnt1 = TMR0; // TMR0 is now between 18 and 156, neutral is 88
do {} while (!GP4); // wait start of pulse
TMR0 = 255 - 100; // wait at least 100 * 8 = 800 usec
do {} while (TMR0 >= 2); // time elapsed, TMR0 has reached 0
do {} while (GP4); // wait end of pulse
cnt2 = TMR0; // TMR0 is now between 18 and 156, neutral is 88
do {} while (!GP5); // wait start of pulse
TMR0 = 255 - 100; // wait at least 100 * 8 = 800 usec
do {} while (TMR0 >= 2); // time elapsed, TMR0 has reached 0
do {} while (GP5); // wait end of pulse
cnt3 = TMR0; // TMR0 is now between 18 and 156, neutral is 88
|
Plutôt que d'écrire trois fois la même suite d'instructions sensiblement identiques, nous allons nous simplifier la vie et
écrire deux fonctions, l'une pour mesurer le temps, l'autre pour positionner le port. Le fait d'utiliser des fonctions va allonger légérement le
temps d'exécution et fausser les mesures de temps, mais cela ne va pas gêner pour cette application.
Nous commençons par les déclarer, derrière la déclaration des variables :
unsigned char cnt1, cnt2, cnt3;
unsigned char count_pulse (unsigned char port);
void set_port (unsigned char port, unsigned char count);
|
La fonction "count_pulse " ne lit pas le port directement, elle commence par lire le registre GPIO, puis effectue
une opération logique AND avec le paramètre qui lui a été passé pour évaluer la valeur du port.
unsigned char count_pulse (unsigned char port)
{
do {} while (!(GPIO & port)); // wait start of pulse
TMR0 = 255 - 100; // wait at least 100 * 8 = 800 usec
do {} while (TMR0 >= 2); // time elapsed, TMR0 has reached 0
do {} while ((GPIO & port) // wait end of pulse
&& TMR0 >= 2); // or timer overflow
return TMR0; // TMR0 is now between 18 and 156, neutral is 88
}
|
La fonction "set_port " positionne le port en fonction des paramètres qui lui sont passés, en effectuant
également une opération logique sur le registre GPIO, plutôt que d'adresser le port directement.
void set_port (unsigned char port, unsigned char count)
{
if (count > 88) { // more than 1500 usec
GPIO |= port; // set port on
}
else { // less than 1500 usec
GPIO &= ~port; // set port off
}
}
|
Dans le programme principal, on trouve maintenant trois appels à la fonction de comptage du temps, suivis de trois appels à la fonction de
positionnement des ports :
cnt1 = count_pulse (0b001000); // count pulse time for GP3
cnt2 = count_pulse (0b010000); // count pulse time for GP4
cnt3 = count_pulse (0b100000); // count pulse time for GP5
set_port (0b000001, cnt1); // set GP0 according to time value of GP3
set_port (0b000010, cnt2); // set GP1 according to time value of GP4
set_port (0b000100, cnt3); // set GP2 according to time value of GP4
|
Si vous n'utilisez pas toutes les entrées, celles qui sont inutilisées devront être reliées au +5 V, il ne faut pas qu'elles restent débranchées.
Par contre, le port GP3 doit toujours être relié à une voie du récepteur, sinon plus rien ne marche.
Il peut y avoir un léger décalage dans le temps de réponse des allumages si les entrées ne sont pas reliées aux ports dans leur ordre d'arrivée,
par exemple, si le port relié à GP4 arrive avant le port relié à GP3, un temps d'attente de 22500 usec va être ajouté.
L'ordre d'arrivée des voies sur les récepteurs FlySky (Turnigy, Eurgle, etc.) 8CH est le suivant :
1, 3, 2, 4, 5, 6, 7, 8.
Pour les autres marques, je n'en ai aucune idée !
Le fichier source complet est ici.
Dans ce montage je vais décrire une centrale d'éclairage, avec deux ports clignotants, un port pour un feu à éclats
et un port fixe.
Une action à fond sur le manche met en service l'un des clignotants, cette action est fugitive ou à bascule, en fonction d'un port
d'entrée.
Une action à mi-course met en service le feu à éclats, une action à mi-course de l'autre côté met en service le feu fixe.
Ces deux fonctions sont des bascules : une action dans une position va activer la fonction, une deuxième action dans la
même position va la désactiver.
Pour générer un clignotement, nous utilisons le signal reçu de l'émetteur. Ce signal est une impulsion, variant de 1000 à 2000 usec,
répétée toutes les 20000 ou 22500 usec, suivant l'émetteur.
Voilà à quoi ça ressemble, avec l'analyseur logique du programmateur PICKit 2 :
Le curseur X indique la durée de l'impulsion (1.6 ms) et le curseur Y le temps de répétition (22.1 ms).
Le temps de clignotement dépendra du nombre d'impulsions reçues, 50 impulsions donnant un temps de 1 seconde ou de 1.125 secondes,
suivant l'émetteur.
Nous déclarons, avec des directives #define, différentes constantes pour ajuster les différents temps de clignotement :
#define _XTAL_FREQ 4000000
#define TEMPO 15
#define TEMPO_LIGHT 20
#define TEMPO_FLASH_ON 18
#define TEMPO_FLASH_OFF 40
|
La constante _XTAL_FREQ est utilisée par le compilateur pour calculer des délais, ici la valeur 4000000 indique la féquence du MCU (4 Mhz).
La constante TEMPO est utilisée pour détecter que le manche est à mi-course (déjà vu dans le montage n° 6).
La constante TEMPO_LIGHT indique le nombre d'impulsions pendant lesquelles les clignotants seront alternativement allumés ou éteints.
La constante TEMPO_FLASH_ON indique la durée, en millisecondes, pendant laquelle le feu à éclats sera allumé.
La constante TEMPO_FLASH_OFF indique le nombre d'impulsions pendant lesquelles le feu à éclats sera éteint.
Nous déclarons les variables nécessaires :
unsigned char cnt, tempo, tempo_light = 0, tempo_flash;
bit neutral, light_on = 0, l1 = 0, l2 = 0, l3 = 0, l4 = 0;
|
Le début du code est semblable, à peu de choses près, aux montages précédents.
Les deux premières conditions sur la position du manche, à fond d'un côté ou de l'autre, sont les suivantes :
if (cnt < 34) { // 1000 usec
if (neutral) {
if (GP0) {
GP1 = l1 = light_on = 1;
tempo_light = TEMPO_LIGHT;
}
else
GP1 = l1 = ~l1;
neutral = 0;
}
}
else if (cnt > 140) { // 2000 usec
if (neutral) {
neutral = 0;
if (GP0) {
GP2 = l2 = light_on = 1;
tempo_light = TEMPO_LIGHT;
}
else
GP2 = l2 = ~l2;
}
}
|
Suivant la position du manche, on allume les clignotants. Si le port GP0 est relié au 0 V, les manches agissent comme des bascules,
sinon, ils agissent de manière fugitive, c'est à dire que les clignotants s'arrêtent quand le manche revient au neutre.
La gestion de l'indicateur du feu à éclats est détaillée ci-dessous :
else if (cnt > 36 && cnt < 80) { // 1250 usec
if (neutral && !tempo--) { // wait some milliseconds before accepting the command
l3 = ~l3;
if (l3)
tempo_flash = 0;
neutral = 0; // set neutral state
}
}
|
On se contente ici de positionner un compteur à zéro, l'allumage et l'extinction sont gérés un peu plus loin dans le programme.
La gestion du feu fixe est maintenant un classique de la gestion d'une bascule :
else if (cnt > 96 && cnt < 138) { // 1750 usec
if (neutral && !tempo--) { // wait some milliseconds before accepting the command
GP5 = l4 = ~l4;
neutral = 0; // set neutral state
}
}
|
Lors du retour du manche au milieu, en fonction de la valeur de GP0, les clignotants sont arrêtés ou non :
else if (cnt > 82 && cnt < 94) { // 1500 usec, neutral
neutral = 1;
tempo = TEMPO;
if (GP0) {
GP1 = l1 = 0; // reset indicators
GP2 = l2 = 0;
light_on = 0;
}
}
|
Voyons maintenant comment sont gérés l'allumage et l'extinction des clignotants :
if (!tempo_light) {
light_on = ~light_on;
if (l1)
GP1 = light_on;
if (l2)
GP2 = light_on;
tempo_light = TEMPO_LIGHT;
}
else
tempo_light--;
|
La variable "tempo_light" contient le nombre d'impulsions à attendre, lorsque ce nombre est atteint, on inverse l'indicateur
d'allumage ("light_on"), on positionne les ports correspondants et on remet le compteur d'impulsions à la valeur initiale.
Sinon, on diminue le compteur d'impulsions d'une unité.
La gestion du feu à éclats est identique, le compteur s'appelle "tempo_flash" :
if (l3) {
if (!tempo_flash) {
GP4 = 1;
__delay_ms (TEMPO_FLASH_ON);
GP4 = 0;
tempo_flash = TEMPO_FLASH_OFF;
}
else
tempo_flash--;
}
|
À noter l'appel de la fonction de bibliothèque "__delay_ms()", qui attend que le nombre de millisecondes qui lui est passé
en paramètre soit écoulé. Ce genre d'appel doit être utilisé avec parcimonie lorsqu'on travaille avec un PIC12F509,
car le MCU ne fait rien pendant ce temps.
Le fichier source complet est ici.
Ce montage et sa variante permettent de commander de un à huit ports en utilisant un bouton poussoir fugitif
ou un manche avec retour au centre.
Pour allumer un port 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.
Le port GP0 est utilisé pour déterminer le mode de fonctionnement : quand il n'est pas connecté ou branché au +5 V,
le port s'éteint dès qu'on relâche le bouton ou que le manche revient au centre, quand il est branché au 0 V, une première action allume le port, une deuxième action l'éteint.
Le code source est sensiblement identique à celui du montage n° 6, seule la séquence de décodage est différente :
void decode_sequence (unsigned char pval)
{
if (prev != 2) { // initial value
if (prev ^= pval) { // each time the value is changed,
seq += pval; // the sequence number is incremented
if (GP0 && !seq) { // return to idle state
GPIO = 0; // reset all ports
sGP1=sGP2=sGP4=sGP5=0; // reset all toggles
}
tempo = TEMPO; // initialize temporisation
}
else if (!tempo--) { // when the value doesn't change, wait before accepting the command
if (pval) { // if pulse present
if (GP0) { // fugitive mode
switch (seq) { // set ports according to the sequence number
case 1: sGP1=1; break; // set port 1
case 2: sGP2=1; break; // set port 2
case 3: sGP4=1; break; // set port 4
case 4: sGP5=1; break; // set port 5
}
}
else { // toggle mode
switch (seq) { // set ports according to the sequence number
case 1: sGP1=~sGP1; break; // toggle port 1
case 2: sGP2=~sGP2; break; // toggle port 2
case 3: sGP4=~sGP4; break; // toggle port 4
case 4: sGP5=~sGP5; break; // toggle port 5
}
}
}
seq = 0; // reset sequence number
}
}
prev = pval;
}
|
Ce programme permet de commander quatre ports en envoyant de 1 à 4 impulsions. Lorsque nous voulons commander plus de ports avec
la même technique, nous utilisons un deuxième programme, dont le montage est en parallèle avec le premier, qui compte les impulsions
suivantes. Dans le programme de ce montage, les instructions "case" sont modifiées pour prendre en compte les impulsions 5 à 8 :
if (GP0) { // fugitive mode
switch (seq) { // set ports according to the sequence number
case 5: sGP1=1; break; // set port 1
case 6: sGP2=1; break; // set port 2
case 7: sGP4=1; break; // set port 4
case 8: sGP5=1; break; // set port 5
}
}
else { // toggle mode
switch (seq) { // set ports according to the sequence number
case 5: sGP1=~sGP1; break; // toggle port 1
case 6: sGP2=~sGP2; break; // toggle port 2
case 7: sGP4=~sGP4; break; // toggle port 4
case 8: sGP5=~sGP5; break; // toggle port 5
}
}
}
|
Le fichier source complet est ici, il contient les deux programmes :
- Le fichier "4-channels_tuto-10_1.c" qui compte les impulsions 1 à 4.
- Le fichier "4-channels_tuto-10_2.c" qui compte les impulsions 5 à 8.
|