Arduino : comment gérer les rebonds d’un interrupteur dans vos programmes ?

Niveau : intermédiaire

f-leb

Les interrupteurs mécaniques en général (boutons-poussoirs, interrupteurs à bascule, à glissière, à levier, etc.) ont une fâcheuse habitude : ils sont sujets aux rebonds à la fermeture ou à l’ouverture du circuit. Ces rebonds sont parfois néfastes pour le fonctionnement de votre application, et il faut trouver des systèmes matériels ou logiciels pour les éviter. Ce tutoriel vous propose de découvrir les principes de ces systèmes « antirebonds ».

Commentez

- Titre : Arduino : comment gérer les rebonds d’un interrupteur dans vos programmes ?

- Auteur : f-leb

- Parution : 3 novembre 2022

- Licence : Copyright ® 2022 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Les Cahiers Pratiques Arduino, une série de tutoriels pour débuter

Sommaire

- I. Mise en évidence du problème des rebonds

- II. Détection des fronts

- II-A. Mise en évidence des effets des rebonds

- II-B. Mise en œuvre d’une pause « antirebonds »

- II-B-1. Avec delay()

- II-B-2. Pour aller plus loin, avec millis()

- III. Filtrage numérique du signal

- IV. Utilisation d’une bibliothèque dédiée

- V. Ressources sur Developpez.com

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) :

Schéma tinkercad.com

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 :

test1.ino

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.

pause-antirebonds-delay.ino

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().

pause-antirebonds-millis.ino

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.

test_onebutton.ino

// 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.