Programmer un bouton et problématique des rebonds

Ce tutoriel explique comment programmer un bouton et gérér la problématique des rebonds en MicroPython. Il complète ce tutoriel qui aborde plus en détails la configuration des broches d’entrées-sorties.
La problématique des rebonds est un grand classique de la programmation embarquée : lorsqu’on actionne un bouton ou bien qu’on le relâche, il oscille généralement pendant quelques millisecondes entre l’état ouvert et l’état fermé. En conséquence, pendant quelques fractions de seconde le bouton peut basculer de façon aléatoire entre l’état ouvert et l’état fermé avant de se stabiliser dans l’état attendu, comme illustré par la figure ci-dessous :


Problématique des rebonds


Crédit image : create.arduino.cc

En général, elle est résolue par un circuit dédié comportant un condensateur qui introduit une temporisation.
Un exemple bien expliqué est disponible sur ce site pour du code C/C++ avec l’API Arduino ; nous allons traiter ce même sujet avec l’API MicroPython.

Matériel requis

  • La carte NUCLEO-WB55
  • Un Shield de base Grove
  • Un module LED Grove
  • Un module Bouton Grove


LED


Crédit image : Seeed Studio

Connectez le module LED sur D2 et le module bouton sur D4.

Attention nous vous rappelons que les LED sont polarisées ; si vous les branchez incorrectement, vous les détruirez probablement. La patte la plus longue de la LED que vous utiliserez devra être insérée dans la borne “+” du module Grove et la plus courte dans sa borne “-“.

1 - Bouton géré en scrutation sans temporisation

Nous utilisons un module bouton Grove et un module LED Grove et nous souhaitons allumer ou éteindre la LED (selon son état précédent), chaque fois que l’on appuie sur le bouton. Le code qui vient immédiatement à l’esprit procède par scrutation (ou “polling” en anglais) : une boucle infinie surveille l’état du bouton.

Le code MicroPython

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 PYBFLASH et copiez-y le code qui suit :

# Objet du script : Allumer ou éteindre une LED en appuyant sur un bouton.
# Le bouton est géré en "polling" (scrutation) : une boucle infinie
# surveille l'état du bouton.

from pyb import Pin # Pour gérer les GPIO

# On configure le bouton en entrée (IN) sur la broche D4.
# Le mode choisi est PULL UP : le potentiel de D4 est forcé à +3.3V
# lorsque le bouton n'est pas appuyé.

bouton_in = Pin('D4', Pin.IN, Pin.PULL_UP)

# On configure la LED en sortie Push-Pull (OUT_PP) sur la broche D2.
# Le mode choisi est PULL NONE : le potentiel de D2 n'est pas fixé.
led_out = Pin('D2', Pin.OUT_PP, Pin.PULL_NONE) # Broche de la LED

# On commence avec la LED éteinte
led_state = 0
led_out.value(led_state)

n = 0 # Pour compter le nombre de changement d'état

# Boucle sans clause de sortie ("infinie")
while True :
	# Dans notre configuration bouton_in.value() vaut 1 lorsque le bouton est enfoncé, 
	# ce qui inverse l'état de la LED

	if bouton_in.value() == 1:
		n += 1
		if led_state == 1:
			led_state = 0
		else:
			led_state = 1
		print("Basculement ", n, " - Statut de la LED : ", led_state)
	
	led_out.value(led_state)

Comportement du programme : ça ne marche pas !

Lancez le script avec CTRL-D, appuyez sur le bouton et observez les messages sur le terminal PuTTY :


bouton et LED


A l’évidence, le programme ne fonctionne pas comme nous le souhaitons : lors de chaque appui les messages “Basculement xx - Statut de la LED : …” s’enchaînent et la LED clignote plusieurs dizaines de fois plutôt que simplement s’allumer (respectivement s’éteindre) si elle était éteinte (respectivement allumée) avant que le bouton ne soit pressé.
La raison est facile à deviner : la boucle while “tourne” très vite et a le temps d’inverser un grand nombre de fois l’état de la LED pendant la fraction seconde où le bouton reste enfoncé. Ce qui manque à notre script, c’est en fait une temporisation qui mettrait en attente la boucle “while” dès que le bouton est enfoncé, afin que la variable led_state ne change qu’une seule fois pendant le laps de temps où il reste dans cet état.

2 - Bouton géré en scrutation, avec temporisation

Le script qui suit introduit une fonction de temporisation, copiée directement depuis cette source et conçue initialement pour “filtrer” d’éventuels rebonds du bouton. Dans notre cas, elle est de toutes façons indispensable au bon fonctionnement de notre montage pour la raison expliquée au paragraphe qui précède : il ne faut pas que la LED change constamment d’état pendant que le bouton reste enfoncé. La fonction qui gère la temporisation, wait_pin_change, est assez subtile : elle lit l’état initial du bouton puis attend que cette valeur change. Le nouvel état du bouton doit persister pendant 20 millisecondes pour que la fonction se termine et laisse le programme se dérouler plus loin.

Le code MicroPython

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 PYBFLASH :

# Objet du script : Eteindre une LED en maintenant un bouton appuyé.
# Le bouton est géré en "polling" (scrutation).
# Ajout d'une fonction de temporisation selon la source :
# https://docs.micropython.org/en/latest/pyboard/tutorial/debounce.html

from pyb import Pin # Pour gérer les GPIO
from time import sleep_ms

WAIT_MS = const(20)
WAIT_STEP_MS = const(1)

# Fonction de temporisation
def attente_bouton_stable(pin):
	# Attend que la valeur de la broche ait changée
	# Elle doit rester stable pendant au moins WAIT_MS millisecondes
	global button_state
	button_state = pin.value()
	print("Statut du bouton : ", button_state)
	temps_ecoule = 0
	while temps_ecoule < WAIT_MS:
		if pin.value() != button_state:
			temps_ecoule += WAIT_STEP_MS
		else:
			temps_ecoule = 0
		sleep_ms(WAIT_STEP_MS)

BUT_PIN = 'D4' # Broche du bouton
LED_PIN = 'D2' # Broche de la LED

# On configure le bouton en entrée (IN) sur la broche D4 en "PULL UP"
bouton_in = Pin(BUT_PIN, Pin.IN, Pin.PULL_UP)

# On configure la LED en sortie Push-Pull (OUT_PP) sur la broche D2.
# Le mode choisi est PULL NONE : le potentiel de D2 n'est pas fixé.
led_out = Pin(LED_PIN, Pin.OUT_PP, Pin.PULL_NONE)

# On commence avec la LED éteinte
button_state = 0 # Variable contenant l'état du bouton
led_state = 0 # Variable contenant l'état de la LED
led_out.value(led_state) # Eteint la LED

while True : # Boucle sans clause de sortie ("infinie")

	# Attend que l'état de la broche du bouton soit stable
	attente_bouton_stable(bouton_in)

	# Dans notre configuration button_state vaut 1 lorsque le bouton est enfoncé, 
	# ce qui inverse l'état de la LED
	if button_state == 1:
		if led_state == 1:
			led_state = 0
		else:
			led_state = 1
		print("Statut de la LED : ", led_state)
		
	led_out.value(led_state)

Comportement du programme : ça marche !

Cette fois-ci, le programme fonctionne comme nous le souhaitions : chaque appui sur le bouton (passage du statut 0 au statut 1) inverse l’état de la LED une seule fois ; les messages sur le terminal PuTTY le confirment :


bouton et LED


3 - Bouton géré avec une interruption, sans filtre anti-rebond

Pour réaliser la fonction qui nous intéresse, il est bien plus efficace de gérer les changements d’états du bouton avec une interruption plutôt qu’en scrutation. Lorsque le bouton est enfoncé, une fonction est éxécutée, qui inverse l’état de la LED. On appelle cette fonction la routine de service de l’interruption (ou ISR pour “Interrupt Service Routine” en anglais) du bouton.

Le code MicroPython

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 PYBFLASH et copiez-y le code qui suit :

# Objet du script : Allumer ou éteindre une LED avec un bouton.
# Le bouton est géré avec une interruption.
# Un premier appui sur le bouton allume la LED, un deuxième l'éteint.
# Matériel requis en plus de la NUCLEO-WB55 : un bouton connecté à la broche
# D4 et une LED connectée à la broche D2.

from pyb import Pin # Classe pour gérer les GPIO

# On configure le bouton en entrée (IN) sur la broche D4.
# Le mode choisi est PULL UP : le potentiel de D4 est forcé à +3.3V
# lorsque le bouton n'est pas appuyé.

bouton_in = Pin('D4', Pin.IN, Pin.PULL_UP)

# On configure la LED en sortie Push-Pull (OUT_PP) sur la broche D2.
# Le mode choisi est PULL NONE : le potentiel de D2 n'est pas fixé.

led_out = Pin('D2', Pin.OUT_PP, Pin.PULL_NONE) # Broche de la LED
led_state = 0 # Variable globale pour mémoriser l'état de la LED (allumée ou pas)
led_out.value(led_state) # LED initialement éteinte

# Variable globale qui décompte les appels à button_falling_ISR :
it_trigger_count = 0

# Fonction de gestion de l'interruption du bouton lorsqu'on l'enfonce
def button_falling_ISR(pin):
	# Mot clef "global" indispensable pour que l'ISR modifie effectivement les variables concernées
	global it_trigger_count, led_state
	led_state = not led_state # inverse l'état de la variable (0->1 ou 1->0)
	led_out.value(led_state) # Inverse l'état de la LED
	it_trigger_count +=1
	print("Interruption du bouton activée ", it_trigger_count, " fois")


# On "attache" l'ISR à la broche du bouton, elle prend effet alors que celui-ci est enfoncé (IRQ_FALLING)
bouton_in.irq(trigger=bouton_in.IRQ_FALLING, handler=button_falling_ISR)

Comportement du programme : mise en évidence des rebonds

En principe, button_falling_ISR est exécutée une seule fois par appui sur le bouton et la variable n affichée dans le terminal PuTTY ne s’incrémente que d’une seule unité. Pourtant, vous constaterez que, parfois, un seul appui sur le bouton change deux fois l’état de la LED au lieu d’une seule et que la variable n est incrémentée de 2 unités, ce qui indique que button_falling_ISR a été appelée deux fois ! Ces évènements correspondent précisément à des rebonds du bouton tels que nous les avons décrits en introduction.

4 - Bouton géré avec une interruption et un filtre anti-rebond, première version

Pour filtrer ces rebonds, il est nécessaire d’ajouter à notre programme une fonction anti-rebond inspirée de wait_pin_change déjà présentée au point 2.

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 PYBFLASH et copiez-y le code qui suit :

# Objet du script : Allumer ou éteindre une LED avec un bouton.
# Le bouton est géré avec une interruption.
# Un premier appui sur le bouton allume la LED, un deuxième l'éteint.
# Matériel requis en plus de la NUCLEO-WB55 : un bouton connecté à la broche
# D4 et une LED connectée à la broche D2.
# filtre anti-rebonds réalisé avec une tempriosation non blocante.

from pyb import Pin # Classe pour gérer les GPIO
from time import ticks_ms, ticks_diff # Bibliothèque pour gérer les temporisations
 
# On configure le bouton en entrée (IN) sur la broche D4.
# Le mode choisi est PULL UP : le potentiel de D4 est forcé à +3.3V
# lorsque le bouton n'est pas appuyé.

bouton_in = Pin('D4', Pin.IN, Pin.PULL_UP)

# On configure la LED en sortie Push-Pull (OUT_PP) sur la broche D2.
# Le mode choisi est PULL NONE : le potentiel de D2 n'est pas fixé.

led_out = Pin('D2', Pin.OUT_PP, Pin.PULL_NONE) # Broche de la LED
led_state = 0 # Variable globale pour mémoriser l'état de la LED (allumée ou pas)
led_out.value(led_state) # LED initialement éteinte

# Variable globale qui décompte les appels à button_falling_ISR :
it_trigger_count = 0

# Variable globale pour mesurer le temps écoulé entre deux interruptions du bouton
temps_ecoule = ticks_ms() 

# Fonction de gestion de l'interruption du bouton lorsqu'on l'enfonce
def button_falling_ISR(pin):
	# Mot clef "global" indispensable pour que l'ISR modifie effectivement les variables concernées
	global it_trigger_count, led_state
	global temps_ecoule
	
	it_trigger_count +=1
	print("Interruption du bouton activée ", it_trigger_count, " fois")

	# Autoriser deux activations successives si plus de 10 ms se sont écoulées 
	if ticks_diff(ticks_ms(), temps_ecoule) > 10: 
		led_state = not led_state # Inverse l'état de la variable (0->1 ou 1->0)
		led_out.value(led_state) # Inverse l'état de la LED
		
	temps_ecoule = ticks_ms()

# On "attache" l'ISR à la broche du bouton, elle prend effet alors que celui-ci est enfoncé (IRQ_FALLING)
bouton_in.irq(trigger=bouton_in.IRQ_FALLING, handler=button_falling_ISR)

Comportement du programme : rebonds filtrés !

Lancez le programme avec CTRL+D. Cette fois-ci, il fonctionne comme nous le souhaitions : chaque appui sur le bouton (passage du statut “0” au statut “1”) inverse l’état de la LED une seule fois ; les messages sur le terminal PuTTY le confirment.

5 - Bouton géré avec une interruption et un filtre anti-rebond, deuxième version

Nous implémentons à présent un filtre antirebond entièrement géré avec des interruptions. Le principe de notre programme est le suivant :

  • Un appui sur le bouton exécute la routine de service d’interruption button_falling_ISR ;
  • button_falling_ISR désactive l’interruption du bouton et, si pas déjà fait, lance un timer (un compteur) qui, une fois tous les dixièmes de secondes, exécute la routine de service d’interruption timer_overflow_ISR ;
  • timer_overflow_ISR relève l’état du bouton et vérifie s’il a changé depuis son dernier appel (0.1 secondes plus tôt). Si le bouton est resté stable depuis le dernier appel, on arrête le timer, on réactive l’interruption du bouton et on inverse l’état de la LED. Autrement, on laisse le timer compter et relancer timer_overflow_ISR 0.1 secondes plus tard pour vérifier à nouveau la stabilité du signal généré par le bouton.

Le code qui suit, un peu complexe, intercepte tous les rebonds en n’utilisant que des mécanismes d’interruptions, ce qui le rend en principe plus efficace que la solution plus “naïve” qui précède. Cependant, il est légitime de se demander si les ressoures matérielles qu’il utilise ne sont pas excessives au regard du service qu’il rend…

Le code MicroPython

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 PYBFLASH et copiez-y le code qui suit :

# Objet du script : Allumer ou éteindre une LED avec un bouton.
# Le bouton est géré avec une interruption.
# Un premier appui sur le bouton allume la LED, un deuxième l'éteint.
# Matériel requis en plus de la NUCLEO-WB55 : un bouton connecté à la broche
# D4 et une LED connectée à la broche D2.
# Filtre anti-rebonds réalisé avec les interruptions d'un timer

# Buffer alloué pour que les messages d'erreur des routines de service des interruptions
# soient notifiés correctement (peut être commenté après test du code)
import micropython
micropython.alloc_emergency_exception_buf(100)

from pyb import Pin, Timer # Pour gérer les GPIO et les timers

timer_running = 0 # Est-ce que le timer est en train de compter ?
previous_state = -1 # Etat précédent du bouton
current_state = 0 # Etat actuel du bouton 
led_state = 0 # Variable mémorisant l'état de la LED

# Routine de service de l'interruption de dépassement de compteur du timer 1.
# Elle s'exécute tous les 10-ièmes de seconde.
def timer_overflow_ISR(timer):
	# Variables globales
	global led_state, current_state, previous_state, timer_running
	# Relevé de l'état du bouton
	current_state = bouton_in.value()
	# Si l'état  n'a pas changé depuis la précédente interruption du timer
	if current_state == previous_state:
		# Arrête le timer
		timer.deinit()
		# Inverse l'état de la LED (0->1 ou 1->0)
		led_state = not led_state
		led_out.value(led_state)
		# Mets à jour les variables globales
		timer_running = 0
		previous_state = 0
		current_state = -1
		# Ré-active l'interruption du bouton
		bouton_in.irq(trigger=bouton_in.IRQ_RISING, handler=button_falling_ISR)
	else:
		# Autrement, mémorise l'état actuel du bouton
		previous_state = current_state


# On configure le bouton en entrée (IN) sur la broche D4.
# Le mode choisi est PULL UP : le potentiel de D4 est forcé à +3.3V
# lorsque le bouton n'est pas appuyé.
bouton_in = Pin('D4', Pin.IN, Pin.PULL_UP)

# On configure la LED en sortie Push-Pull (OUT_PP) sur la broche D2.
# Le mode choisi est PULL NONE : le potentiel de D2 n'est pas fixé.

led_out = Pin('D2', Pin.OUT_PP, Pin.PULL_NONE) # Broche de la LED
led_out.value(led_state) # LED initialement éteinte

# Compte le nombre de fois où l'interruption du bouton a été lancée
it_trigger_count = 0

# Routine de service de l'interruption du bouton
def button_falling_ISR(pin):
	# Démarre le timer 1 à la fréquence de 10 Hz.
	global it_trigger_count
	it_trigger_count +=1
	print("Interruption du bouton activée ", it_trigger_count, " fois")
	global timer_running # Variables globales
	if not timer_running: # Si aucun timer n'est démarré
		# Commence par désactiver l'interruption du bouton
		bouton_in.irq(trigger=bouton_in.IRQ_RISING, handler=None)
		# Démarre un timer de fréquence 10 Hz
		tim1 = pyb.Timer(1, freq=10)
		timer_running = 1
		# La routine/ISR "timer_overflow_ISR" sera exécutée toutes le 1/10 secondes, au moment de
		# l'interruption de dépassement du timer
		tim1.callback(timer_overflow_ISR)

# On "attache" l'ISR à la broche du bouton, elle prend effet alors que celui-ci est enfoncé (IRQ_FALLING)
bouton_in.irq(trigger=bouton_in.IRQ_FALLING, handler=button_falling_ISR)

6 - Pour aller plus loin

Notre dernier code est certes efficace mais il n’est pas très élégant. On peut corriger ce travers en utilisant la formidable flexibilité de MicroPython, qui présente néanmoins l’inconvénient majeur de produire un code de haut niveau très abstrait particulièrement difficile à aborder pour les néophytes et pas nécessairement plus performant. A chacun de choisir la solution qu’il préfère selon son niveau !

Vous trouverez dans l’archive téléchargeable MODULES.zip un dossier “Bouton et anti-rebond\Pour aller plus loin” contenant une bibliothèque debounce.py et un fichier main.py l’utilisant, directement adaptés depuis cette source. Ce dernier exemple réalise les mêmes actions que le script du point 5 mais en exploitant la syntaxe de haut niveau permise par MicroPython.