(Updated at 01/23/2023)
In electronics, an interrupt is a signal sent to a processor to indicate that an important task must be executed immediately, thus interrupting the execution of the program in progress.
Interrupts can be generated in different ways, for example following an external event, a
timer they allow certain tasks to be executed asynchronously, i.e. independently of the main program.
In practice, interruptions are generally used:
To execute portions of critical code when an external event occurs. For example, when a button is pressed, a Python function will automatically execute.
To perform functions periodically. For example, to flash an LED every 5 seconds.
You are going to tell me that we can already do this kind of script, without using interrupts. Yes, it’s true, but there are 2 major flaws without using them. Let’s take the example of a script that turns on a LED when the button is pressed:
from machine import Pin pin_button = Pin(14, mode=Pin.IN, pull=Pin.PULL_UP) pin_led = Pin(2, mode=Pin.OUT) while True: if not pin_button.value(): pin_led.on() else: pin_led.off()
The first flaw is that the script spends its time scanning the value of the
pin_button pin to know if the button has been pressed. The script can’t do other things in addition because otherwise the second flaw will occur: missing events. If the script performs other tasks in the loop, it may not be able to detect the temporary button press.
The advantage of using a hardware interrupt is that the detection is completely detached from the processor (and therefore from the Python script). With an interrupt the
while loop of the script will be empty. The hardware block in charge of the detection is also much more reactive than the MicroPython script.
With interrupts, there is no need to constantly scan the value of a pin: a function is executed automatically when a change is detected.
Whether with a timer or an external event, the interrupt is triggered following a signal change. Let’s discover the different possible variations 😊.
The detection of an event is based on the shape of the signal that arrives at the pin.
Here are the different types of possible detection of an interruption:
Pin.IRQ_LOW_LEVELtriggers the interrupt as soon as the signal is at 0V
Pin.IRQ_HIGH_LEVELtriggers the interrupt as soon as the signal is at 3.3V
Pin.IRQ_RISING: Triggers the interrupt as soon as the signal goes from
HIGH(From 0 to 3.3V)
Pin.IRQ_FALLINGtriggers the interrupt as soon as the signal changes from
LOW(From 3.3V to 0)
FALLING modes are the most used. Note that if you use the
HIGH modes, the interrupt will be triggered in a loop as long as the signal does not change state.
Here is a skeleton code, to trigger an interrupt via an external signal on your ESP32 board with MicroPython :
from machine import Pin pin_button = Pin(14, mode=Pin.IN, pull=Pin.PULL_UP) def interruption_handler(pin): ... pin_button.irq(trigger=Pin.IRQ_FALLING,handler=interruption_handler) while True: ...
The script uses the function
Pin.irq() function which allows to create an interrupt request on a falling edge of the signal present on the pin
irq stands for Interrupt Request to request an interrupt request.
When an interrupt is triggered the
interruption_handler() function will be executed automatically. The interrupt routine will have as input argument the pin on which the event was detected.
It is a good practice to have an interrupt function (
isr ) as fast as possible to avoid disturbing the main program which has been interrupted. For example, we will avoid sending data via I2C, SPI directly from an interrupt. We can use flags in the form of boolean to store the detection of an event and then process it in the main loop.
Interrupt handling in microPython will always be much slower than in Arduino or pure C code! However, it is possible to minimize this latency by using advanced parameters .
We try to limit the number of actions done in an interrupt. It is common to increment a variable in the``isr`` and to do the long tasks in the main code according to the value of this variable. Here is an example that counts the number of times you press a push button.
import time from machine import Pin button_pressed_count = 0 # global variable pin_button = Pin(14, mode=Pin.IN, pull=Pin.PULL_UP) def button_isr(pin): global button_pressed_count button_pressed_count += 1 if __name__ == "__main__": button_pressed_count_old = 0 pin_button.irq(trigger=Pin.IRQ_FALLING,handler=button_isr) while True: if button_pressed_count_old != button_pressed_count: print('Button value:', button_pressed_count) button_pressed_count_old = button_pressed_count if button_pressed_count > 10: # heavy task here ...
We use a global variable to be able to write to it in the interrupt routine.
Even if the variable is defined at the very top of the Python script, you must add the keyword
global when the variable is used in a function. This tells the Python interpreter to use the global variable instead of creating a local variable (with the same name), which would be used only in the execution context of the function.
When running the MicroPython script, the increment value is much larger than the number of presses we did. Weird 🤔? It’s because of the transitions of the logic levels at the push button that are not perfect..
You may have noticed that with push buttons, there are false positives: the interrupt routine runs more times than it should. This is because the signal received by the ESP32 is not perfect: it is as if it had received a “double press” of the button:
This is called the bounce effect. We can reduce the bounce of a push-button via the Python script directly. This is called debouncing software. It consists in not taking into account the transitory period between the 2 logical states by waiting a certain delay.