IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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

Niveau : intermédiaire

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

17 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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

Image non disponible

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

Image non disponible
Image non disponible
Schéma tinkercad.com

Le but est d’incrémenter un compteur à chaque appui sur le bouton-poussoir suivant le chronogramme ci-dessous :

Image non disponible

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
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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
Sélectionnez
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) :

Image non disponible

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.

 
Sélectionnez
#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) :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

Image non disponible

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
Sélectionnez
// 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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2022 Team developpez.com Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.