Ardu? No!Les boutons ≫ Les rebonds

Les rebonds

Quand on appuie sur un bouton, il y a toujours un petit souci de rebonds. Supposons le montage suivant:

Un poussoir et sa pullup

La résistance est indispensable, on le verra en détail dans la page "Un bouton par broche". Quand le bouton est au repos, il ne passe aucun courant nulle part, la tension aux bornes de la résistance est nulle et la sortie est donc à VCC. On dira pour les amateurs Arduino HIGH. Si le bouton est appuyé, il va y avoir 0V à ses bornes, un courant qui ne nous intéresse pas vraiment va circuler dans la résistance, et la tension de sortie est à LOW. Ce serait si sympathique que le niveau passe directement de HIGH à LOW, mais ce n'est pas toujours ce que l'on observe. C'est un peu comme quand une balle tombe sur le sol, avant qu'elle y soit stable, elle rebondit un moment. Si on trace un diagramme temporel, on peut observer ceci:

Diagrame temporel d'un rebond

Il va y avoir un petit retard entre le moment ou le poussoir bascule et le premier contact que l'on ignore, car il est court devant le mouvement humain et que cela ne pose en général pas de problèmes. Par contre si on lit la broche avec le micro-contrôleur, on va trouver une succession d'états HIGH et LOW intermédiaires. Le temps des rebonds dépend beaucoup de la technologie du bouton, et on considère souvent qu'il est inférieur à 20ms.

Si on soigne une belle boucle de lecture avec l'utilisation de digitalReadFast, on peut compter plusieurs dizaines d'états intermédiaires ce qui peut être très gênant si on veut compter le nombre d'appuis ou si un appui sur deux allume une led et un appui sur deux l'éteint. On obtient alors n'importe quoi.

Si on fait un programme classique avec un digitalRead, qui prend plus de temps, on échantillonnera moins souvent et on verra donc moins de rebonds. Mais cela peut rester problématique.

Si on utilise un poussoir pour mettre en marche et un poussoir différent pour faire un arrêt, ou si le programme va suffisamment lentement, les rebonds n'auront aucune importance car ils seront terminés longtemps avant l'appui sur l'autre bouton.

Il ne faut donc pas forcément traiter les rebonds, cela va dépendre de l'utilisation que l'on a. Nous allons voir de plus que si on doit traiter les rebonds, il n'y a pas de solution universelle.

Il y a en général moins de rebonds au relâchement du bouton, mais il y a toujours le risque d'en avoir.

Pour observer les rebonds, j'ai utilisé un bouton placé entre l'entrée 2 d'une uno, en observant ce que la carte à compris en recopiant l'état sur la broche 10. Pour aller le plus vite possible, le programme contenait en fait 1000 fois la même instruction de copie, le programme ci-dessous n'en comportant que 100:

void setup()
{
  pinMode(2, INPUT_PULLUP);
  pinMode(10, OUTPUT);
}

void loop()
{
  PORTB = PIND; // Recopie l'entrée 2 sur la sortie 10

  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
  PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND; PORTB = PIND;
}

Avec un bouton neuf d'une matrice à 2€, je n'obtiens pas du tout de rebonds. Par contre avec un bouton de récupération (d'une imprimante), j'obtiens en utilisant un analyseur logique lors d'un appui:

Rebonds lors de l'appui

Et lors du relâchement:

Rebonds lors du relâchement

Avec ce bouton, on a lors du relâchement des rebonds pendant environ 2ms. En prenat un temps d'anti-rebonds de 20ms, on est large.

Anti-rebond par condensateur

Une solution est de placer un condensateur en parallèle avec le bouton:

Anti-rebond avec condensateur

Si le bouton n'est pas appuyé, le condensateur est chargé à VCC et la tension lue vaut HIGH. Quand on appuie sur le bouton, on décharge complètement le condensateur. Pendant les rebonds, il va parfois se charger lentement grâce à la résistance, et parfois se décharger à fond à cause du bouton. Mais si on choisit comme constante de temps 3ms, un seul rebond durant beaucoup moins, la tension lue sera toujours LOW. Tout est parfait lors de l'appui. Au relâchement, après les rebonds éventuels qui vont maintenir le LOW, la tension va remonter (exponentiellement) et on finira par lire HIGH au bout d'un temps voisin de la constante de temps. Il va donc y avoir un changement immédiat lors de l'appui et un léger retard lors du relâchement. Avec une constante de temps de 3ms ce retard est très très inférieur au mouvement humain qui relâche le bouton et n'est donc pas du tout perceptible.

Avec une AVR, on peut utiliser la résistance interne appelée PULL_UP qui est de l'ordre de 35kΩ. Pour avoir une constante de temps (RC) de 10ms, on peut prendre un condensateur de valeur C=3ms/35kΩ soit environ 100nF. 10 fois moins ou 100 fois plus ne devrait pas se voir, on prendra ce que l'on a dans le tiroir.

Cette solution fonctionne très bien pour le montage ci-dessus, mais n'est pas universelle. Elle ne fonctionne très mal pour le montage 2 boutons par broche:

Schéma pour 2 boutons mais 1 seule broche

Un condensateur en parallèle sur le bouton du bas obligerait à mettre une temporisation pour lire le bouton du haut. Et cette solution ne fonctionne pas du tout avec un clavier matriciel classique:

Schéma pour la matrice carrée simple

Mettre des condensateurs en parallèle avec toutes les touches mettrait des condensateurs entre les lignes et les colonnes et pour la lecture, mettre une colonne à LOW mettrait momentanément toutes les lignes à LOW. Il faudrait alors attendre la fin des charges des condensateurs, et cela reviendrait à utiliser un anti-rebond logiciel en plus.

Cette solution ne fonctionne pas mieux pour les lecture analogiques.

Anti-rebond par post-attente

Reprenons le premier montage:

Un poussoir et sa pullup

Supposons que le bouton soit non appuyé et qu'on le sache (on a par exemple mémorisé le dernier état). Quand on appuie sur le bouton, on lit un état LOW. A ce moment on affirme que le nouvel état est LOW et on mémorise le temps (avec millis() par exemple). On ne tiendra pas compte d'un nouveau changement si l'intervalle de temps (temps actuel - temps mémorisé) est inférieur au temps maximal des rebonds. En anglais ce temps est souvent appelé bounce.

On peut aussi si on veut détecter un appui sur un bouton, utiliser une interruption matérielle mise en place par attachInterrupt(). On appellera la fonction d'interruption lors d'un "FALLING". Il faudra de même attendre au moins le temps bounce avant de reprendre en compte un nouveau front descendant.

Exemple avec attachInterrupt(). Quand un appui est détecté, on mémorise le temps et tout nouvel appui sera ignoré tant qu'on n'a pas attendu les 20ms. Pour bien montrer que l'on passe par interruption, on va faire clignoter la led de la carte, et la prise en compte du boutons fonctionnera même au milieu du delay():

const byte INTERRUPTEUR = 2; // GND - inter - pin2
const unsigned long BOUNCE = 20; // 20 ms pour l'anti-rebond

word compteur; // Donne le nombre d'appui sur le bouton
unsigned long dernierAppui; // Heure en ms du dernier appui

void affiche(void)
{
  if (millis() - dernierAppui > BOUNCE) // On ne fait rien tant que la temporisation n'est pas finie
  {
    Serial.print("On a appyué ");
    Serial.print(++compteur); // Incrémente puis affichage
    Serial.println(" fois sur le bouton");
    dernierAppui = millis(); // Début de la temporisation
  }
}

void setup()
{
  Serial.begin(115200);
  pinMode(INTERRUPTEUR, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(INTERRUPTEUR), affiche, FALLING); // Interruption avec post temporisation
}

void loop()
{
  // Blink with delay
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(900);
}

Cette solution est souvent celle qui est prise par les bibliothèques de gestions des boutons, car cela permet une utilisation simple pour l'utilisateur.

Mais cette solution n'est pas non plus universelle, car elle ne fonctionne pas si on veut détecter les appuis et les relâchements, ou si on veut lire plusieurs boutons par interruptions avec une Nano. Cette dernière ne permet l'interruption mise en place par attachInterrupt() que sur 2 broches seulement. Si on en veut plus, on peut passer par une interruption sur une vingtaine de broches, mais on ne dispose plus du mode "FALLING". Dans les deux cas, il faut utiliser le mode "CHANGE", et cela donne des erreurs. Lors d'un changement, par exemple le passage HIGH vers LOW, on sait qu'il y a eu changement (car on entre dans la routine d'interruption), mais on ne sait pas que est le nouvel état. Si on lit la valeur dès l'entrée dans la routine d'interruption, on lira la valeur d'un rebond qui peut être aussi bien HIGH que LOW. Entre le moment du changement et la lecture possible, on a un retard dû à la synchronisation du signal, la fin éventuelle de l'instruction en cours, la sauvegarde des registres et du compteur programme, et le saut au début de la fonction.

Anti-rebond par pré-attente

Une solution, qui fonctionnerait dans tous les cas, est d'utiliser une pré-attente. Lorsque l'on détecte un changement, il suffit d'attendre le temps bounce avant de faire la lecture. Ainsi les rebonds sont terminés et on lit une bonne valeur.

Première solution: programme bloquant! Nous passons par interruption déclenchée par "CHANGE". En début d'interruption nous attendons un temps bounce puis nous lisons la broche et nous la traitons. On ne pourra pas utiliser delay() qui ne fonctionne pas par défaut dans le fonction d'interruption, mais il y a delayMicroseconds() que l'on peut utiliser. Mais le temps d'attente est souvent de l'ordre de 20ms qui va tout bloquer, y compris la remise à l'heure de l'horloge système. Dans bien des cas, on peut s'en moquer, mais pas toujours.

const byte INTERRUPTEUR = 2; // GND - inter - pin2
const unsigned long BOUNCE = 20; // 20 ms pour l'anti-rebond
byte dernierEtat = HIGH;

void affiche(void)
{
  delay(BOUNCE); // On attend que le signal se stabilise
  if (digitalRead(INTERRUPTEUR) != dernierEtat) // Il peut y avoir une mémorisation pendant le delay
  { // Evite un avertissement
    if (dernierEtat == HIGH)
    {
      dernierEtat = LOW;
      Serial.println("Appui sur le bouton");
    } 
    else 
    {
      dernierEtat = HIGH;
      Serial.println("On vient de relâcher le bouton");
    }
  }
}

void setup()
{
  Serial.begin(115200);
  pinMode(INTERRUPTEUR, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(INTERRUPTEUR), affiche, CHANGE); // Interruption avec pré temporisation
}

void loop()
{
  // Blink with delay
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(900);
}

Deuxième solution: programme partiellement bloquant. On va procéder de la même manière, mais en début de fonction d'interruption, on va désactiver la lecture des boutons et autoriser les interruptions. Le programme attend toujours, il est toujours bloquant, mais cela ne gêne plus l'horloge système et les autres interruptions. Mais c'est nettement plus compliqué à mettre en place.

Troisième solution: le timer. Quand un bouton change, on déclenche un timer pour le temps bounce. Et c'est quand le timer a fini que l'on lit et traite la valeur. Non seulement c'est plus compliqué, mais il faut en plus un timer utilisable (encore que l'on peut utiliser le timer 0, celui de l'horloge système qui n'est pas utilisé complètement).

Certains montages ne permettent pas le déclenchement d'une interruption!

Dans les trois cas, cela va introduire un léger retard dans la prise en compte du bouton, mais qui normalement n'est pas perceptible.

Conclusion

En conclusion, avec des boutons, il y aura des rebonds, mais il n'y a pas forcément nécessité de les traiter. Et si on doit le faire, il n'y a pas de solution universelle.