X. Les entrées numériques▲
Dans cet exercice, on s'attache à étudier les entrées numériques du point de vue du code et du point de vue matériel pour le microcontrôleur. Typiquement, le microcontrôleur aura besoin de réagir à un changement d'état (vrai/faux) provenant d'un événement extérieur, suite à une action d'un utilisateur ou d'un capteur. Par exemple, un utilisateur peut appuyer sur un bouton-poussoir dans l'intention de démarrer ou stopper un processus comme couper un signal audio ou mettre en marche un moteur électrique. Les broches d'entrées numériques d'un contrôleur sont conçues précisément pour faire réagir à ce type de signaux (NDLR en tout-ou-rien).
Les signaux en entrée doivent être compatibles avec les niveaux logiques haut et bas du contrôleur afin de déclencher un événement. Dans le cas de l'Arduino Uno, les signaux doivent obéir aux niveaux standards 0 V et 5 V. Les sources des signaux peuvent être classées en deux grandes catégories : active et passive. Une source active pourrait être un composant tels une porte logique ou un comparateur qui amène la broche d'entrée au niveau de tension 5 V ou 0 V. Pour certaines applications, cette solution est trop compliquée, et on cherchera des solutions plus simples, à savoir des solutions dites passives. Pour ces dernières, une résistance de tirage interne est activée sur la broche d'entrée du contrôleur. Cette résistance de valeur assez élevée est connectée entre l'alimentation et la broche d'entrée. Elle amènera le niveau logique haut à l'entrée (on dit qu'elle « tire » l'entrée à l'état haut, NDLR d'où son nom de « résistance de tirage »). Pour amener au niveau bas, on pourra utiliser un simple interrupteur placé entre l'entrée et la masse. Aucun circuit avec une alimentation externe n'est indispensable ici. Bien entendu, la résistance de tirage doit être désactivée lorsque l'entrée est reliée à une source active.
Note de la rédaction
La résistance de tirage (entre 20 et 50 kΩ d'après la datasheet) est entourée en rouge sur la figure ci-dessous. Pour l'activer grâce au transistor, il faut PUD=LOW, WDx=LOW, WRx=HIGH :
Pour lire le niveau logique sur une entrée, deux choses doivent être faites. Premièrement, le registre de direction de données (data direction register) doit être défini de façon à configurer la broche concernée en entrée afin de la préparer en lecture (et, optionnellement, pour y activer la résistance de tirage interne). Une fois cela fait, on pourra lire l'état du bit de données sur le port concerné. Il convient de remarquer que sur certains microcontrôleurs, le même registre est employé pour à la fois lire en entrée et écrire en sortie (par exemple le registre PORTB de l'exercice sur l'écriture sur une sortie numérique). Sur les Atmel AVR (comme l'ATmega 328P de la Uno), deux adresses différentes sont utilisées : PORTx pour l'écriture, et PINx pour la lecture. L'erreur classique pour les nouveaux développeurs sur AVR est d'essayer de lire l'état depuis PORTB alors qu'ils souhaiteraient en fait le lire depuis PINB.
Dans notre premier exemple, on fera basculer l'état d'une broche d'entrée avec un simple interrupteur SPST (single-pole, single-throw – le plus basique des interrupteurs on-off) et en activant la résistance de tirage interne. L'état de l'entrée sera visualisé dans le Moniteur Série. Au départ, les bibliothèques de l'environnement Arduino seront utilisées mais ensuite on regardera en détail des formes plus génériques.
Soudez deux fils aux bornes d'un interrupteur, puis reliez-les au connecteur 8 et la masse de l'Arduino. Plus simplement, vous pouvez insérer un fil au connecteur 8 puis vous insérerez l'autre extrémité à une des masses au moment utile (NDLR pour simuler la fermeture de l'interrupteur).
Note de la rédaction
Tapez le code suivant dans l'éditeur :
/* Read Digital Input V1. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série */
void
setup
(
)
{
pinMode
(
8
, INPUT_PULLUP);
Serial.begin
(
9600
);
}
void
loop
(
)
{
int
i;
i =
digitalRead
(
8
);
Serial.println
(
i);
}
C'est aussi simple que cela. Dans un premier temps, le port série est initialisé et la fonction pinMode() initialise le registre de direction des données afin de configurer le connecteur 8 en entrée, avec la résistance de tirage activée (avec le mode INPUT seul, la résistance est désactivée). Dans la boucle, on lit l'état du connecteur 8 et on reporte son état pour affichage dans le Moniteur Série. Compilez et téléversez le code dans la carte Arduino. Ouvrez le Moniteur Série, vous devriez voir défiler une série de « 1 » marquant l'état logique haut. Maintenant, insérez l'autre extrémité du fil à une des masses disponibles sur le port de la carte. Vous devriez maintenant voir défiler des « 0 », marquant l'état logique bas. En connectant et déconnectant le fil (ou en basculant l'interrupteur), vous devriez voir les valeurs basculer.
/* Read Digital Input V2. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique */
// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01
void
setup
(
)
{
DDRB &=
(~
BITMASK); // Configurer le connecteur en entrée
PORTB |=
BITMASK; // Activer la résistance de tirage pull-up
Serial.begin
(
9600
);
}
void
loop
(
)
{
int
i;
i =
PINB &
BITMASK;
Serial.println
(
i);
}
Par rapport au code original de la première version, les deux appels pour configurer l'entrée semblent demander un peu plus de travail. Pourtant, le code machine généré est remarquablement plus compact (sous la v1.0.4 de l'EDI, la barre d'état donne 2726 octets pour la première version du programme contre 2340 seulement dans la seconde version).
À noter que dans le cas général, le calcul PINB & BITMASK retourne 0 si l'entrée est à la masse et une valeur non nulle si l'entrée est au 5 V (et pas forcément la valeur 1). Ce n'est pas un problème ici car en langage C toute valeur non nulle est traitée comme étant à Vrai. On ne testera donc pas si une variable est à Vrai en la comparant avec la valeur 1 mais plutôt en testant si elle « existe ».
if
(
v==
1
) // Ne faites pas cela pour tester si v est Vrai
if
(
v) // Faites plutôt ceci
if
(!
v) // et cela pour tester si v est Faux
Tapez la seconde version du programme Read Digital Input, compilez et téléversez le programme afin de le tester. Le défilement très rapide dans le Moniteur Série est quelque peu pénible. Actuellement, c'est comme si la Uno vous informait : « l'interrupteur est fermé », « l'interrupteur est toujours fermé », « … toujours fermé », encore et encore… Ce serait plus approprié pour l'utilisateur s'il était prévenu seulement lorsqu'il y a changement d'état. Pour cela, le programme doit mémoriser l'état en cours de la broche afin d'être comparé à son état un instant plus tard. Considérez l'évolution suivante du programme :
/* Read Digital Input V3. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés */
// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01
int
priorstate =
BITMASK;
void
setup
(
)
{
DDRB &=
(~
BITMASK); // Configurer le connecteur en entrée
PORTB |=
BITMASK; // Activer la résistance de tirage pull-up
Serial.begin
(
9600
);
}
void
loop
(
)
{
int
state;
state =
PINB &
BITMASK;
if
(
state !=
priorstate )
{
Serial.println
(
state);
priorstate =
state;
}
}
On initialise la variable priorstate à BITMASK puisque c'est l'état initial de l'entrée. Le contenu de la variable state est affiché dans le Moniteur Série seulement s'il y a eu un changement d'état. Saisissez le code, téléversez-le et testez la nouvelle version. Remarquez que le déferlement de valeurs est maintenant remplacé par un affichage plus sobre et bien plus pratique. Si vous basculez plusieurs fois l'interrupteur dans les deux sens en regardant dans le Moniteur Série, vous devriez constater quelque chose d'étrange. Vous noterez probablement qu'il y a davantage de changements d'état que de basculements de l'interrupteur. Mais d'où viennent ces changements d'état aléatoires supplémentaires ?
Suppression des rebonds (debounce)
Le comportement réel des boutons et interrupteurs mécaniques n'est pas idéal. Quand un interrupteur ou un bouton commute, après la fermeture initiale du contact se produit une succession de « rebonds » mécaniques conduisant à une série de microruptures des contacts. Cette phase peut durer 10 millisecondes, voire plus selon la conception de l'interrupteur ou du bouton. Des exemples de rebonds d'interrupteur sont visualisés à l'oscilloscope sur les figures ci-dessous.
Le problème est que le microcontrôleur peut lire l'état de la broche d'entrée en une fraction de milliseconde (NDLR et dans une boucle loop(), l'état de l'entrée peut être relevé plusieurs fois pendant la succession de rebonds), ainsi, chaque rebond peut être interprété comme une fermeture/ouverture très rapide de l'interrupteur. Dans certaines situations ce n'est pas gênant, par exemple quand il s'agit d'allumer ou éteindre une LED en accord avec l'état pressé ou relâché d'un bouton (La LED reste allumée tant que le bouton est pressé, et s'éteint lorsque le bouton est relâché). Dans ce cas, les rebonds du bouton se traduiront seulement par des clignotements de la LED imperceptibles par l’œil humain. Mais dans d'autres situations, on pourrait par exemple avoir à programmer et faire en sorte d'augmenter un niveau sonore ou un niveau de luminosité à chaque appui sur un bouton. Ici, un seul appui pourrait faire franchir plusieurs niveaux d'un coup à cause des multiples rebonds du bouton, et le système produirait un résultat incohérent en réponse à l'utilisateur.
Il existe deux systèmes « anti-rebond » simples. La première façon de faire est purement logicielle, tandis que la seconde est matérielle. Jetons un œil à la première solution.
La clé pour un anti-rebond logiciel consiste à étendre la durée d'observation de l'état du bouton. En d'autres termes, on veut observer l'état sur une durée suffisamment longue, le temps de laisser passer les rebonds. Par exemple, si on remarque que l'état n'a pas changé pendant une certaine durée, disons dix millisecondes, on peut assumer sans risque que l'on n'est pas en train de lire une entrée pendant la phase des rebonds. Considérez la variante suivante du code. Il ressemble beaucoup à la version V3, mais une nouvelle fonction get_state(), essentielle pour gérer les rebonds, a été ajoutée.
/* Read Digital Input V4. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés, avec système anti-rebond logiciel */
// prototype de fonction
int
get_state
(
void
);
// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01
int
priorstate =
BITMASK;
void
setup
(
)
{
DDRB &=
(~
BITMASK); // Configurer le connecteur en entrée
PORTB |=
BITMASK; // Activer la résistance de tirage pull-up
Serial.begin
(
9600
);
}
void
loop
(
)
{
int
state;
state =
get_state
(
);
if
(
state !=
priorstate )
{
Serial.println
(
state);
priorstate =
state;
}
}
int
get_state
(
void
)
{
int
priorstate, state, count=
0
, i=
0
;
priorstate =
PINB &
BITMASK;
while
(
i <
30
)
{
state =
PINB &
BITMASK;
if
(
state ==
priorstate )
count++
;
else
{
count =
0
;
priorstate =
state;
}
if
(
count >=
10
)
break
;
delay
(
1
);
i++
;
}
return
(
state );
}
L'idée derrière cette nouvelle fonction est de continuellement relever l'état de l'entrée et de continuer jusqu'à obtenir un état consistant pendant dix millisecondes. À chaque fois que l'état courant est identique à l'état précédent, on incrémente un compteur. Quand ce compteur atteint la valeur 10, on sait qu'une durée de dix millisecondes sans changement d'état du signal a été obtenue (grâce à la temporisation delay() d'une milliseconde lancée en fin de boucle). On sort alors de la boucle en retournant l'état de l'entrée. Si l'état de l'entrée bascule à cause d'un rebond, state n'est pas égal à priorstate et le compteur count est remis à zéro. Si l'interrupteur ou le bouton est défectueux, la phase de rebonds pourrait durer plus longtemps, et afin d'empêcher le code d'être bloqué à l'intérieur de la fonction, un maximum de 30 itérations (c'est-à-dire une période de 30 millisecondes) est institué grâce à la variable i.
Saisissez la version 4 et sauvegardez-la sous un nouveau nom, car on reviendra un peu plus tard sur la version 3. Compilez et téléversez le code et faites des tests en utilisant le Moniteur Série. Cette version devrait produire un résultat propre et cohérent en sortie dans le Moniteur Série, sans les rebonds parasites de la version 3 précédente. Il est aussi important de remarquer que le code machine produit est encore plus compact que le code original avec la bibliothèque Arduino malgré son avance en matière de fonctionnalités.
La seconde méthode pour vaincre les rebonds d'un interrupteur passe par des moyens matériels. En général, cela prend la forme d'un circuit RC connecté à l'interrupteur suivi d'une bascule de Schmitt (ou Trigger de Schmitt) (74HC14). Le circuit RC va filtrer les variations rapides du signal et l’hystérésis de la bascule de Schmitt s'occupera des basculements intempestifs autour du seuil. Le montage de la figure ci-dessous est un exemple classique (notez l'inverseur en sortie de bascule) :
Note de la rédaction
Allure des signaux en sortie du circuit RC, puis en sortie de bascule de Schmitt :
Les valeurs typiques pour ce genre de circuit devraient être autour de 4,7 kΩ pour R1, 470 kΩ pour R2 et 100 nF pour C. La constante de temps τ du circuit RC (τ=RC) vaut environ 50 millisecondes et les seuils de la bascule seront atteints en 200 millisecondes (moins de 5τ), ce qui permettra même de prendre en charge le pire des boutons mécaniques. Remarquez aussi que le signal d'entrée est actif et donc que la résistance de tirage (pull-up) n'est pas requise ici. Retournons à la version 3 (sans anti-rebond logiciel) et apportons une modification pour l’adapter au système anti-rebond matériel :
/* Read Digital Input V5. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés, avec système anti-rebond matériel */
// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01
int
priorstate =
0
;
void
setup
(
)
{
DDRB &=
(~
BITMASK); // Configurer le connecteur en entrée
// Ici, la résistance de tirage pull-up n'est pas activée
Serial.begin
(
9600
);
}
void
loop
(
)
{
int
state;
state =
PINB &
BITMASK;
if
(
state !=
priorstate )
{
Serial.println
(
state);
priorstate =
state;
}
}
Retirez le bouton de la plaque d'essais. Réalisez le circuit anti-rebond de la figure précédente et reliez-le au connecteur 8 de l'Arduino. Compilez et téléversez le code. Ouvrez le Moniteur Série et constatez les résultats. Ils devraient être similaires à ceux obtenus par la version 4 avec l'anti-rebond logiciel. Remarquez également que le code occupe 300 octets de moins. On a effectivement confié la gestion anti-rebond à un circuit extérieur matériel.
Il y a des avantages à gérer les rebonds de façon logicielle plutôt que par le matériel. Premièrement, il y a de la souplesse à pouvoir modifier le code pour affiner les performances. Deuxièmement, il n'y a pas de coût additionnel du fait de composants ou de cartes additionnelles supplémentaires pour les accueillir. Il y a également quelques désavantages, comme la mémoire supplémentaire requise due à la fonction de gestion des rebonds (souvenez-vous que les microcontrôleurs, contrairement aux PC, ont une capacité mémoire très modeste). L'autre problème est le temps gaspillé à attendre que le signal du bouton s'installe. Dix ou vingt millisecondes peuvent paraître courts mais pour certaines applications cela peut être une éternité. Le choix d'un anti-rebond logiciel ou matériel dépend en fait d'autres facteurs propres au système.
Évidemment, suivre l'état de la broche dans le Moniteur Série est bien pratique pour déboguer ou apprendre comment fonctionne le composant mais ce n'est pas ce que l'on rencontre le plus souvent dans une application. Essayons d'évoluer vers quelque chose d'un peu plus utile.
Plutôt que d'envoyer l'état de la broche en sortie dans le Moniteur Série, on va refléter cet état en allumant ou en éteignant une LED externe reliée au connecteur 12 de l'Arduino, c'est-à-dire le bit 4 du port B. Le circuit anti-rebond continuera d'être utilisé avec le nouveau code suivant :
/* Read Digital Input V6. Lire en boucle le connecteur D8 et refléter l'état à une LED extérieure, forme générique, avec système anti-rebond matériel au niveau du connecteur d'entrée. */
// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01
// LED sur connecteur D12 de l'Arduino, c'est-à-dire Port B, bit 4
#define LEDMASK 0x10
int
priorstate =
0
;
void
setup
(
)
{
DDRB &=
(~
BITMASK); // Configurer le connecteur relié au 74HC14 en entrée
DDRB |=
LEDMASK; // Configurer le connecteur relié à la LED en sortie
delay
(
1
);
}
void
loop
(
)
{
int
state;
state =
PINB &
BITMASK;
if
(
state !=
priorstate ) // Si changement d'état
{
priorstate =
state;
if
(
state )
PORTB |=
LEDMASK; // Allumer la LED
else
PORTB &=
(~
LEDMASK); // Éteindre la LED
}
}
Préparez un circuit de pilotage de LED (les versions avec un transistor NPN et PNP sont représentées sur la figure ci-dessous) et reliez-le au connecteur 12 de l'Arduino. Déterminez la valeur de Rc pour un courant qui traverse la LED de 10 milliampères environ, en supposant que sa tension de seuil est de 2 volts. Déterminez Rb pour un courant sur la base d'environ 0,5 milliampère.
Une fois le circuit monté, entrez le code, compilez-le et téléversez-le. L'état de la LED devrait être le reflet de la position de l'interrupteur. Si vous ne l'avez pas encore remarqué, jetez un œil dans la barre d'état de l'éditeur. Comme le Moniteur Série n'est plus utilisé ainsi que la bibliothèque associée, la taille du code est descendue à un petit 510 octets !
Défi
Il peut arriver dans certains cas que l'état de la LED ne reflète pas directement la position de l'interrupteur. Comme cela a été mentionné plus tôt, un bouton-poussoir (NDLR à appui momentané) peut être utilisé pour basculer l'état d'un dispositif. Par exemple, on pousse le bouton pour allumer le dispositif et on réappuie pour l'éteindre. Proposez une modification du code de façon à ce que le bouton fasse basculer l'état de la LED plutôt qu'une LED qui indique la position enfoncée ou relâchée du bouton. Autrement dit, la LED devra changer d'état seulement sur un front montant du signal d'entrée délivré par le bouton (ou seulement sur un front descendant). Vous pourrez commencer par dessiner le schéma électrique avec Multisim ou un logiciel de conception de schéma électrique équivalent.