I. Mise en évidence du problème des rebonds▲
Lorsqu’un interrupteur est basculé, un bouton-poussoir pressé ou relâché, on s’attend à ce que le signal bascule aussi d’état de façon instantanée. Dans les faits, les signaux visualisés à l’oscilloscope ci-dessous montrent qu’il n’en est rien (image d’après le manuel de laboratoire Arduino) :
Fermer un circuit avec un interrupteur mécanique revient à mettre en contact des parties métalliques de l’interrupteur entre elles. Or, l’établissement du contact n’est pas instantané, et des rebonds qui se traduisent par des signaux parasités peuvent durer parfois quelques millisecondes avant la stabilisation du signal. Ce phénomène de rebonds s’observe souvent aussi bien à la fermeture qu’à l’ouverture du circuit.
Dans de nombreux cas, la durée des rebonds n’a aucune influence sur le fonctionnement du système. Mais quand un interrupteur est relié à une entrée d’un microcontrôleur suffisamment performant, ces rebonds peuvent être perçus comme autant de cycles de fermeture/ouverture très rapides et mettre en défaut le fonctionnement prévu.
Une étude très poussée menée par Jack Ganssle sur une multitude d’interrupteurs mécaniques a révélé une durée moyenne des rebonds de 1557 μs (microsecondes), avec un maximum de 6200 μs (sur un échantillon de 16 types d’interrupteurs, mais après en avoir éliminé tout de même deux dont la durée des rebonds était phénoménale).
C’est pourquoi vous aurez parfois besoin de mettre au point un dispositif « antirebonds », géré par logiciel ou par du matériel spécifique.
Les solutions matérielles ne seront pas étudiées dans ce tutoriel, mais vous pouvez en découvrir une à base de filtre RC et de bascule de Schmitt dans le manuel de laboratoire Arduino. Les solutions logicielles peuvent détecter les rebonds et les filtrer, ou plus simplement les ignorer.
II. Détection des fronts▲
Le plus souvent, on cherche à faire réagir le programme lorsqu’un événement est détecté, appui ou relâchement du bouton par exemple. Il faut donc détecter un front montant ou descendant du signal.
Comme démonstration, on propose le montage suivant d’un bouton-poussoir à appui momentané et une résistance de rappel (pull-down) :
Le but est d’incrémenter un compteur à chaque appui sur le bouton-poussoir suivant le chronogramme ci-dessous :
II-A. Mise en évidence des effets des rebonds▲
Le code ci-dessous s’attache dans un premier temps à détecter un front montant du signal qui se produit théoriquement à chaque appui du bouton-poussoir :
const
byte
brocheBouton =
2
;
int
compteur =
0
;
byte
etatBoutonSauvegarde =
LOW
; // montage pull-down, bouton relâché initialement
void
setup() {
pinMode
(brocheBouton, INPUT
);
Serial
.begin(115200
);
}
void
loop() {
byte
etatBouton =
digitalRead
(brocheBouton);
if
(etatBouton !=
etatBoutonSauvegarde) {
// si changement d'état du bouton (front)
if
(etatBouton ==
HIGH
) {
// si front montant
compteur++
;
Serial
.println(compteur);
}
etatBoutonSauvegarde =
etatBouton; // mémorisation de l'état du bouton
}
}
Pour détecter un front du signal, il faut sauvegarder tout changement d’état du bouton. Quand au prochain passage dans la boucle, l’état en cours est différent de l’état sauvegardé, c’est qu’un front du signal, montant ou descendant, s’est produit.
Le compteur est incrémenté, puis affiché dans le Moniteur Série uniquement sur front montant du signal, conformément au chronogramme.
Le résultat dépend évidemment du bouton utilisé, mais le constat est souvent le même :
- le compteur augmente en général d’une unité à chaque appui comme prévu, mais pas toujours… Avec pourtant un seul appui sur le bouton, le compteur augmente de temps en temps de 2, 3 unités d’un coup, voire plus ;
- pire, le compteur s’incrémente parfois aussi lorsque le bouton est relâché.
Ces incohérences aléatoires sur l’incrémentation du compteur sont dues aux rebonds qui produisent des fronts montants parasites sur appui du bouton, mais aussi sur son relâchement.
II-B. Mise en œuvre d’une pause « antirebonds »▲
Avec des variantes, la stratégie antirebonds s’appuie principalement sur une fenêtre de temps après la détection d’un premier front où les rebonds doivent être ignorés.
Le plus simple reste alors de mettre en pause la lecture du bouton pendant ce laps de temps, en espérant qu’après cette courte pause, la phase des rebonds soit terminée.
Les codes suivants sont de simples démonstrations pour expliquer les différentes stratégies possibles de gestion des rebonds. Les principes expliqués doivent être adaptés et les codes optimisés en fonction des situations réelles rencontrées dans vos applications.
II-B-1. Avec delay()▲
Lorsqu’un premier front montant est détecté, une pause de 20 ms est effectuée avec delay
(). Si le bouton est toujours appuyé à l’issue de cette pause (ce deuxième test est nécessaire, car un front montant peut aussi se produire lors du relâchement du bouton à cause d’un rebond), on incrémente le compteur. La phase des rebonds étant censée être terminée, le compteur ne s’incrémentera qu’une seule fois à chaque appui.
const
byte
brocheBouton =
2
;
const
byte
pauseAntiRebonds =
20
; // en millisecondes
int
compteur =
0
;
byte
etatBoutonSauvegarde =
LOW
; // montage pull-down, bouton relâché initialement
void
setup() {
pinMode
(brocheBouton, INPUT
);
Serial
.begin(115200
);
}
void
loop() {
byte
etatBouton =
digitalRead
(brocheBouton);
if
(etatBouton !=
etatBoutonSauvegarde) {
// si changement d'état du bouton (front)
if
(etatBouton ==
HIGH
) {
// si front montant
delay
(pauseAntiRebonds);
if
(digitalRead
(brocheBouton) ==
HIGH
) {
// si le bouton est toujours pressé après la pause
compteur++
;
Serial
.println(compteur);
}
}
etatBoutonSauvegarde =
etatBouton; // mémorisation de l'état du bouton
}
}
Le principe de la « pause » est très simple, mais en situation réelle, au lieu de bloquer le programme avec un delay
(), il serait plus pertinent de demander au microcontrôleur d’effectuer d’autres tâches.
II-B-2. Pour aller plus loin, avec millis()▲
Comme vous le savez déjà (voir le tutoriel Comment gérer le temps ?), delay
() est bloquant et il se peut que votre système s’accommode mal d’un système aveugle de tout évènement extérieur (aveugle ou sourd, c’est selon…) et inactif pendant la pause antirebonds. Une solution pour conserver un système réactif est de passer par les fonctions de gestion du temps et de chronométrage comme millis
().
const
byte
brocheBouton =
2
;
const
byte
pauseAntiRebonds =
20
; // en milisecondes
enum
{
LECTURE_BOUTON, DETECTION_APPUI_BOUTON, ACTION_APPUI_BOUTON}
etat;
byte
etatBouton, etatBoutonSauvegarde =
LOW
; // montage pull-down, bouton relâché initialement
unsigned
long
departChrono;
int
compteur =
0
;
void
setup() {
pinMode
(brocheBouton, INPUT
);
Serial
.begin(115200
);
}
void
loop() {
switch
(etat) {
case
LECTURE_BOUTON:
etatBouton =
digitalRead
(brocheBouton);
if
(etatBouton !=
etatBoutonSauvegarde) {
// si changement d'état du bouton
etatBoutonSauvegarde =
etatBouton; // mémorisation de l'état du bouton
if
(etatBouton ==
HIGH
) {
// si front montant
departChrono =
millis
(); // lancement du chrononomètre
etat =
DETECTION_APPUI_BOUTON;
}
}
break
;
case
DETECTION_APPUI_BOUTON:
if
(millis
() -
departChrono >
pauseAntiRebonds) {
// si la pause est terminée, les rebonds sont derrière
if
(digitalRead
(brocheBouton) ==
HIGH
) {
// et si le bouton est toujours maintenu
etat =
ACTION_APPUI_BOUTON;
}
else
{
etat =
LECTURE_BOUTON;
}
}
break
;
case
ACTION_APPUI_BOUTON:
compteur++
;
Serial
.println(compteur);
etat =
LECTURE_BOUTON;
break
;
}
// Autres actions à programmer ici...
delay
(1
);
}
Le code est implanté sous la forme d’une « machine à états finis » (ici avec seulement trois états). Chaque état doit avoir une durée très courte et être non bloquant. Si le temps écoulé pendant la pause antirebonds et relevé par millis
() est encore dans la fenêtre des rebonds, le code peut se poursuivre pour faire d’autres actions.
III. Filtrage numérique du signal▲
Un filtre sert en priorité à protéger votre système du « bruit » qui engendre de « fausses » impulsions sur un signal. On peut utiliser le principe des filtres pour supprimer les rebonds.
Un principe très simple pour filtrer le signal consiste à scruter l’état de l’interrupteur à intervalles réguliers, par exemple toutes les 5 ms. L’état est mis à jour seulement s’il est conservé à l’identique cinq fois de suite (fenêtre de stabilité = 20 ms) :
Dans la démonstration qui suit, on utilise un timer pour déclencher la lecture périodique de l’état d’un bouton grâce à la bibliothèque Ticker (simple d’utilisation, mais on peut aussi gérer ce déclenchement périodique avec millis
()). La variable globale etatBoutonFiltre renvoie à l’état du bouton après filtrage du signal.
#include
<Ticker.h>
const
byte
brocheBouton =
2
; // bouton sur entrée D2, montage pull-down
const
byte
periodeLecture =
5
; // lecture du bouton toutes les 5 millisecondes
const
byte
fenetreStabilite =
20
; // le signal est stable s'il n'y a pas de changement d'état pendant 20 millisecondes
byte
etatBoutonFiltre =
LOW
; // signal du bouton filtré, initialement à l'état bas
void
digitalRead_filtrage();
Ticker timer(digitalRead_filtrage, periodeLecture); // fonction de rappel à intervalles réguliers
void
setup() {
pinMode
(brocheBouton, INPUT
);
Serial
.begin(115200
);
timer.start(); // Démarrage du timer
}
void
loop() {
timer.update(); // mise à jour timer
Serial
.println(etatBoutonFiltre); // état du bouton filtré
}
void
digitalRead_filtrage() {
// fonction rappelée à intervalles réguliers
static
byte
compteur =
0
;
static
byte
etatBoutonPrecedent =
LOW
;
byte
etatBouton =
digitalRead
(brocheBouton);
if
(etatBouton ==
etatBoutonPrecedent) {
if
(++
compteur ==
fenetreStabilite /
periodeLecture) {
compteur =
0
;
etatBoutonFiltre =
etatBouton;
}
}
else
{
compteur =
0
;
}
etatBoutonPrecedent =
etatBouton;
}
Ce programme commence déjà à consommer pas mal de ressources pour un simple interrupteur. Mais certains développeurs ont pensé à relever les différents états successifs de l’entrée sur un bit (0 ou 1) mémorisé dans un unique octet (1 octet = 8 états successifs mémorisés) :
etat =
(etat <<
1
) |
(digitalRead
(brocheBouton) ==
HIGH
? 0x01
: 0x00
);
À chaque appel, l’octet est décalé d’un bit vers la gauche, et le bit de poids faible est l’image de l’état courant de l’entrée. Si notre octet vaut par exemple xxx1 1111 (x=0 ou 1), cela signifie que l’entrée a pris l’état haut cinq fois d’affilée, et on considérera que l’état est stable à l’état haut.
Le programme devient :
#include
<Ticker.h>
const
byte
brocheBouton =
2
; // bouton sur entrée D2, montage pull-down
const
byte
periodeLecture =
5
; // lecture du bouton toutes les 5 millisecondes
byte
etatBoutonFiltre =
LOW
; // signal du bouton filtré, initialement à l'état bas
void
digitalRead_filtrage();
Ticker timer(digitalRead_filtrage, periodeLecture); // fonction de rappel à intervalles réguliers
void
setup() {
pinMode
(brocheBouton, INPUT
);
Serial
.begin(115200
);
timer.start(); // Démarrage du timer
}
void
loop() {
timer.update(); // mise à jour timer
Serial
.println(etatBoutonFiltre); // état du bouton filtré
}
void
digitalRead_filtrage() {
// fonction rappelée à intervalles réguliers
static
byte
etat =
0x00
;
etat =
(etat <<
1
) |
(digitalRead
(brocheBouton) ==
HIGH
? 0x01
: 0x00
);
byte
masque =
0b00011111
; // stable si cinq valeurs identiques d'affilée
byte
etat_masque =
etat &
masque;
if
(etat_masque ==
masque) {
etatBoutonFiltre =
HIGH
;
}
else
if
(etat_masque ==
0x00
) {
etatBoutonFiltre =
LOW
;
}
}
IV. Utilisation d’une bibliothèque dédiée▲
Mêler les codes de gestion d’un bouton en les imbriquant dans le code principal de votre application ne va déjà pas de soi. Surtout qu’en général, la gestion d’un bouton n’est pas la tâche essentielle de votre application. Il faudrait au moins mettre le code dans des fonctions ou des méthodes de classe (au sens de la programmation orientée objet) dans un fichier de bibliothèque à inclure dans le programme principal.
Mais si vous devez gérer plusieurs interrupteurs et de différents types, avec des fonctionnalités pour gérer les appuis longs ou courts et les doubles-clics, sachez que la communauté Arduino propose aussi de nombreuses bibliothèques clés en main.
Le mot-clé « button » tapé dans le champ de recherche du Gestionnaire de bibliothèques de l’EDI Arduino vous dévoilera une liste de solutions déjà impressionnante :
La bibliothèque OneButton par exemple propose des fonctionnalités de lecture de bouton non bloquantes implémentées dans une machine à états finis (voir le diagramme états-transitions dans la documentation). Plusieurs types d’évènements sont détectés : appui simple, double-appui, appui multiple, et appui long.
Les rebonds sont évidemment gérés.
// démo OneButton
// sur un double-clic du bouton connecté sur la broche 2 :
// - affichage du message "double-clic détecté" dans le Moniteur Série
// - bascule de l'état de la LED 13, allumée/éteinte
#include
"OneButton.h"
const
byte
brocheBouton =
2
;
const
byte
brocheLed =
13
;
// Instanciation d'un bouton
OneButton monBouton(brocheBouton,
false
, // bouton à l'état HIGH si appuyé
false
// résistance de tirage pull-up désactivée
);
int
etatLed =
LOW
;
void
setup() {
Serial
.begin(115200
);
pinMode
(brocheLed, OUTPUT
);
digitalWrite
(brocheLed, etatLed);
monBouton.attachDoubleClick(doubleClic); // liaison de l'évènement avec la fonction doubleclic
monBouton.setDebounceTicks(20
); // pause antirebonds 20 ms
}
void
loop() {
monBouton.tick(); // appel au gestionnaire du bouton
// d'autres actions peuvent être programmées ici
delay
(1
);
}
void
doubleClic() {
// fonction appelée en cas de double-clic
Serial
.println("double-clic détecté"
);
etatLed =
!
etatLed;
digitalWrite
(brocheLed, etatLed);
}
V. Ressources sur Developpez.com▲
- Arduino : comment effectuer le branchement d’un interrupteur ?
- Branchement d’un bouton-poussoir
- Petits exercices : bouton-poussoir et LED qui clignote
- Les entrées numériques
- Branchement d’un interrupteur, quelle valeur pour la résistance pull-up/pull-down ?
Et si vous avez encore des questions, n’hésitez pas à ouvrir une discussion dans le forum Arduino.