Manage buttons with interrupts

This tutorial explains how to interact with buttons with the interrupt mechanism. This example completes the tutorial available here.

Some insights on interruptions

The microprocessors that drive the STM32WB55 are two Cortex M, designed by ARM. The Cortex-M ARM architecture is dominant in microcontrollers for embedded applications mainly due to its excellent energy efficiency and high performance interruption management.

The component integrated in the Cortex-M and responsible for interrupt management is the NVIC for «Nested Vectored Interrupt Controller». To be exact, there is a second component integrated in the STM32WB55 for interrupt management, it is the EXTI for “Extended Interrupts and Events Controller”. The EXTI can be seen as a “hub” (a hub) driven by the NVIC for interrupts coming from the GPIO.

Programming a microcontroller with interruptions allows it to process almost instantaneously signals coming from peripherals (sensors for example) by executing contingency programs as a priority.

Specifically:

  • The program executed by the microcontroller just before the interruption is first paused when it is received.
  • The microcontroller executes in its place another program, dedicated to the processing of the interruption, called “interrupt service routine” (ISR)
  • Once the ISR has been processed, the microcontroller resumes the execution of the initial program.

In addition to instantaneous (almost) reactivity, interruptions also save energy. They replace the infinite loops in a main program advantageously with SSIs that are only executed when the events that interest us occur. On the other hand, the code of an ISR must be as simple as possible so as not to delay the execution of other ISRs that could occur or suspend the main program for too long, which, as we know, remains on pause until all the ISRs are processed.

First example: command an LED

The following example shows how to configure the GPIO to control an LED with an interrupt controlled button. When the button is pressed, the interrupt management (ISR) function of the handle_interrupt button is executed and inverts the status of the LED. Note that there is no “infinite” while True loop in the script.

Some will object that it is not necessary to use a microcontroller to perform this function; we would get the same result directly by connecting the switch to the LED power supply circuit. But we are here to explain the programming of microcontrollers!

Required Hardware

LED

Image credit : Seeed Studio

Connect the LED module to D2 and the button module to D4.

MicroPython code

The following scripts are available in the download area.

Edit the main.py script contained in the NUCLEO-WB55 PYBFLASH virtual USB drive directory:

# Object of the script: Switch on or off a LED with a button.
# The button is managed with an interruption.
# A first press of the button turns the LED on, a second one turns it off.
# Hardware required in addition to NUCLEO-WB55: one button connected to the
# D4 pin and one LED connected to the D2 pin.

from pyb import Pin # Class to manage GPIO

# Configure the input button (IN) on pin D4.
# The chosen mode is PULL UP: the potential of D4 is forced to +3.3V
# when the button is not pressed.

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

# Configure the LED on push-pull output LED (OUT_PP) on pin D2.
# The chosen mode is PULL NONE: the potential of D2 is not fixed.

led_out = Pin('D2', Pin.OUT_PP, Pin.PULL_NONE) # LED pin

statut = 0 # Variable to store LED status (lit or not)
led_out.value(statut) # LED initially off

# Function for Button interrupt management 
def ISR(pin):
	global statut
	statut = not statut # inverts the state of the variable (0->1 ou 1->0)
	led_out.value(statut) # Inverts state of LED

# The ISR is "attached" to the button pin. It will activate when the button 
# goes down and the voltage goes from 3.3V to 0V (IRQ_FALLING).
bouton_in.irq(trigger=bouton_in.IRQ_FALLING, handler=ISR)

LThe following drawings summarize what our program does

We start from the situation where the button is released and has not yet been pressed (Figure 1). The variable status is then equal to zero. Because of the interrupt management, two components internal to the STM32WB55 are activated, the EXTI and the NVIC. The CPU is not running any user programs. The LED is off.

Pressing the (Figure 2) button has the following consequences:

  • The button switches pin D4 to 0V. The potential drop activates the EXTI, which will activate the NVIC itself.
  • The NVIC commands the CPU to pause a possible running program (there is none in our example) and the force to immediately execute the interrupt service function, ISR.
  • This changes the status variable to 1, and turns on the LED. The ISR function is executed only once and the CPU then resumes the course of the program it was processing before (still none in this example!).

Once the button is released (Figure 3), the variable status keeps the value 1 and the LED remains lit.

LED

Example 2: Track with Interrupts

We will resume and improve the chaser code by adding the ability to press the SW1 button to pause it or the SW2 button to change its direction. The advantage of using the buttons as an interruption is that the action on the buttons will be treated as a priority and taken into account while the track is on.

Equipment Required

The NUCLEO-WB55 board, its LEDs and integrated buttons SW1, SW2 and SW3:

LED

MicroPython code

The following scripts are available in the download area.

Edit the main.py script contained in the NUCLEO-WB55 PYBFLASH virtual USB drive directory:

# Object of the script: Create a "chaser" with interrupts
# Example of GPIO configuration for management of the NUCLEO-WB55 integrated LEDs

from machine import Pin # For pin access
from pyb import LED, ExtInt # Pin interrupt and LED management
from time import sleep_ms # For system breaks

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

# LED Initialization
led_bleu = LED(3) # LED1 screen printed on the PCB
led_vert = LED(2) # LED2 screen printed on the PCB
led_rouge = LED(1) # LED3 screen printed on the PCB

# Global variables Initialization
compteur_de_led = 0

# interrupts flag
pause = 0
inv = 0

# Button Initialization (SW1 & 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)

# Interrupt service function for SW1 (pauses the chaser)
def Pause(line):
	global pause
	if(pause == 0):
		pause = 1
		print("Pause")
	else:
		pause = 0

# Interrupt Service Function for SW2 (Reverse chaser direction)
def Inversion(line):
	global inv
	if(inv == 0):
		inv = 1
		print("Inversion")
	else:
		inv = 0

# We "attach" the ISR of the interruptions to the pins of the buttons
irq_1 = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Pause)
irq_2 = ExtInt(sw2, ExtInt.IRQ_FALLING, Pin.PULL_UP, Inversion)

while True: # Create an "infinite" loop with actions only if the system is not paused

	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()
		
		# We want to turn on the next LED at the next iteration of the loop with direction management
		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) # 500 milliseconds Timeout

To use interrupts, it is necessary to initialize the buttons as we saw earlier. Next, you must define the interrupt vectors specific to each button with the function pyb.ExtInt() and the functions that will be called at each interrupt.

Since an interrupt function has to be short in order not to slow down the program, we use global variables called “flag” which are modified with each entry in an interrupt service function and which have implications in the main function.

Moving Forward: Managing Bounces

Buttons of poor quality often pose a bounce problem. When you press such a button, it does not immediately change from the “open” state to the “closed” state but it can oscillate several times between the two and generate random behaviors on your montage.

This classic problem can be solved by waiting a few milliseconds for the button state to be stable. You will find here a debouncing algorithm in MicroPython that can be adapted to ISR buttons. This topic is also covered in this tutorial.