Gérer des boutons par interruptions

Ce tutoriel explique comment interagir avec des boutons avec le mécanisme des interruptions. Cet exemple complète le tutoriel disponible ici.

Quelques notions sur les interruptions

Les deux microprocesseurs qui animent le STM32WB55 sont des Cortex M, conçus par la société ARM. L’architecture ARM Cortex-M est dominante dans les microcontrôleurs pour les applications embarquées essentiellement en raison de son excellente efficacité énergétique et de sa gestion très performante des interruptions.

Le composant intégré aux Cortex-M et chargé de la gestion des interruptions est le NVIC pour « Nested Vectored Interrupt Controller ». Pour être tout à fait exact, un deuxième composant intégré au STM32WB55 intervient pour la gestion des interruptions, il s’agit du EXTI pour “Extended Interrupts and Events Controller”. Le EXTI peut être vu comme un “hub” (un concentrateur) piloté par le NVIC pour les interruptions provenant des GPIO.

Programmer un microcontrôleur avec des interruptions lui permet de traiter presque instantanément des signaux provenant de périphériques (des capteurs par exemple) en exécutant de façon prioritaire des programmes de circonstance.

Plus précisément :

  • Le programme exécuté par le microcontrôleur juste avant l’interruption est d’abord mis en pause à la réception de celle-ci.
  • Le microcontrôleur exécute à sa place un autre programme, dédié au traitement de l’interruption, appelé “routine de service de l’interruption” (ou ISR pour “Interrupt Service Routine”, en anglais).
  • Une fois que l’ISR a été traitée, le microcontrôleur reprend le cours de l’exécution du programme initial.

Outre une réactivité (presque) instantanée, les interruptions permettent également d’économiser l’énergie. Elles remplacent avantageusement les boucles infinies dans un programme principal par des ISR qui ne sont exécutée que lorsque surviennent les évènements qui nous intéressent. En contrepartie, le code d’une ISR doit être aussi simple que possible pour ne pas retarder l’exécution d’autres ISR qui pourraient survenir ou suspendre trop longtemps le programme principal, qui, rappelons-le, reste en pause en attendant que toutes les ISR soient traitées.

Premier exemple : commander une LED

L’exemple qui suit montre comment configurer les GPIO pour commander une LED avec un bouton géré par une interruption. Lorsque le bouton est appuyé, la fonction de gestion de l’interruption (ISR) du bouton handle_interrupt est exécutée et inverse l’état de la LED. On remarque qu’il n’y a pas de boucle “infinie” while True dans le script.

D’aucuns objecteront qu’il n’est pas nécessaire d’utiliser un microcontrôleur pour réaliser cette fonction ; on obtiendrait directement le même résultat en connectant l’interrupteur au circuit d’alimentation de la LED. Mais nous sommes là pour expliquer la programmation des microcontrôleurs !

Matériel requis

LED

Crédit image : Seeed Studio

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

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

statut = 0 # Variable pour mémoriser l'état de la LED (allumée ou pas)
led_out.value(statut) # LED initialement éteinte

# Fonction de gestion de l'interruption du bouton
def ISR(pin):
	global statut
	statut = not statut # inverse l'état de la variable (0->1 ou 1->0)
	led_out.value(statut) # Inverse l'état de la LED

# On "attache" l'ISR à la broche du bouton. Elle se déclanchera lorsque le bouton sera 
# en train de s'enfoncer et que la tension passe de 3.3V à 0V (IRQ_FALLING).
bouton_in.irq(trigger=bouton_in.IRQ_FALLING, handler=ISR)

Les dessins qui suivent résument ce que fait notre programme

On part de la situation où le bouton est relâché et n’a pas encore été appuyé (figure 1). La variable statut est alors égale à zéro. Du fait de la gestion par interruptions, deux composants internes au STM32WB55 sont activés, le EXTI et le NVIC. Le CPU n’exécute aucun programme utilisateur. La LED est éteinte.

Un appui sur le bouton (figure 2) a les conséquences suivantes :

  • Le bouton met la broche D4 à 0V. La chute de potentiel active le EXTI, qui va lui-même activer le NVIC.
  • Le NVIC commande au CPU de mettre en pause un éventuel programme en cours d’exécution (il n’y en a pas dans notre exemple) et le force à exécuter immédiatement la fonction de service de l’interruption, ISR.
  • Celle-ci fait passer la variable statut à 1, et allume la LED. La fonction ISR n’est exécutée qu’une seule fois et le CPU reprend ensuite le cours du programme qu’il traitait avant (toujours aucun dans cet exemple !).

Une fois le bouton relâché (figure 3), la variable statut conserve la valeur 1 et la LED reste allumée.

LED

Deuxième exemple : chenillard avec interruptions

Nous allons reprendre et améliorer le code du chenillard en ajoutant la possibilité d’appuyer sur le bouton SW1 pour le mettre en pause ou sur le bouton SW2 pour faire changer son sens. L’intérêt d’utiliser la lecture des boutons sous forme d’interruption est que l’action sur les boutons sera traitée en priorité et prise en compte pendant que le chenillard est en marche.

Matériel requis

La carte NUCLEO-WB55, ses LED et ses boutons intégrés SW1, SW2 et SW3 :

LED

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 : Créer un "chenillard" avec interruptions
# Exemple de configuration des GPIO pour une gestion des LED intégrées de la NUCLEO-WB55

from machine import Pin # Pour les accès aux broches
from pyb import LED, ExtInt # Interruption des broches et gestions des LED
from time import sleep_ms # Pour faire des pauses systèmes

print( "Les interruptions avec MicroPython c'est facile" )

# Initialisation des LED
led_bleu = LED(3) # sérigraphiée LED1 sur le PCB
led_vert = LED(2) # sérigraphiée LED2 sur le PCB
led_rouge = LED(1) # sérigraphiée LED3 sur le PCB

# Initialisation des variables globales
compteur_de_led = 0

# flags des interruptions
pause = 0
inv = 0

# Initialisation des boutons (SW1 et SW2)
sw1 = Pin('SW1')
sw1.init(Pin.IN, Pin.PULL_UP, af=-1)
sw2 = Pin('SW2')
sw2.init(Pin.IN, Pin.PULL_UP, af=-1)

# Fonction de service de l'interruption pour SW1 (met en pause le chenillard)
def Pause(line):
	global pause
	if(pause == 0):
		pause = 1
		print("Pause")
	else:
		pause = 0

# Fonction de service de l'interruption pour SW2 (Inverse le sens du chenillard)
def Inversion(line):
	global inv
	if(inv == 0):
		inv = 1
		print("Inversion")
	else:
		inv = 0

# On "attache" les ISR des interruptions aux broches des boutons
irq_1 = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Pause)
irq_2 = ExtInt(sw2, ExtInt.IRQ_FALLING, Pin.PULL_UP, Inversion)

while True: # Création d'une boucle "infinie" avec des actions uniquement si le système n'est pas en pause

	if (pause == 0):
		if compteur_de_led == 0:
			led_bleu.on()
			led_rouge.off()
			led_vert.off()
		elif compteur_de_led == 1:
			led_bleu.off()
			led_vert.on()
			led_rouge.off()
		else :
			led_bleu.off()
			led_vert.off()
			led_rouge.on()
		
		# On veut allumer la prochaine LED à la prochaine itération de la boucle avec gestion du sens
		if inv == 0:
			compteur_de_led = compteur_de_led + 1
			if compteur_de_led > 2:
				compteur_de_led = 0
		else:
			compteur_de_led = compteur_de_led - 1
			if compteur_de_led < 0:
				compteur_de_led = 2
				
		sleep_ms(500) # Temporisation de 500 millisecondes

Pour utiliser les interruptions, il est nécessaire d’initialiser les boutons comme nous avons vu précédemment. Il faut ensuite définir les vecteurs d’interruption propres à chaque bouton avec la fonction pyb.ExtInt() et les fonctions qui seront appelées à chaque interruption.

Comme une fonction d’interruption doit être courte pour ne pas ralentir le programme, nous utilisons des variables globales appelées “flag” qui sont modifiées à chaque entrée dans une fonction de service d’interruption et qui ont des incidences dans la fonction main.

Pour aller plus loin : gérer les rebonds

Les boutons de mauvaise qualité posent souvent un problème de rebond. Lorsque vous appuyez sur un tel bouton, il ne passe pas immédiatement de l’état “ouvert” à l’état “fermé” mais il peut osciller plusieurs fois entre les deux et générer des comportements aléatoires sur votre montage.

Ce problème classique peut être résolu en attendant quelques millisecondes que l’état du bouton soit stable. Vous trouverez ici un algorithme anti-rebond (ou de debouncing en anglais) en MicroPython qui pourra être adapté aux ISR des boutons. Ce sujet est également abordé dans ce tutoriel.