ESP32 In MicroPython: Interrupts
Written by Mike James & Harry Fairhead   
Wednesday, 03 April 2024
Article Index
ESP32 In MicroPython: Interrupts
Interrupts
esppython360 Interrupt Service Routine Restrictions
Race Conditions and Starvation

Interrupts

The ESP32 supports 32 distinct interrupts, but only 26 can be associated with GPIO lines – the others are used internally or for timers. You can set an interrupt service routine (ISR) for each GPIO line independently and the GPIO line events that you can use to trigger an interrupt are:

Pin.IRQ_FALLING 
Pin.IRQ_RISING

You can also use:

Pin.IRQ_FALLING | Pin.IRQ_RISING

to set an interrupt on both rising and falling edges. In this case finding out which event caused the interrupt is difficult. Notice that MicroPython for the ESP32 doesn’t support level interrupts, even though the hardware does.

MicroPython provides a simple method for working with GPIO interrupts:

pin.irq(ISR, trigger)

This sets ISR as the interrupt service handler for the event. This is a very simplified version of what happens at a lower level. The ISR function has to accept a single parameter, which is a Pin object associated with the GPIO line that caused the event. You can use a lambda function to define the ISR. To know how see Programmer’s Python: Async,ISBN:978-1871962765.

There are, in fact, two distinct implementations of an interrupt system within general MicroPython. The first is the default and the only one that the ESP32 supports. It seems to be a full interrupt queue implementation, which is good if you don’t want to miss an interrupt, but not so good for timing. The second, which the ESP currently doesn’t support, is a lighter wrapping of the interrupt mechanism which doesn’t allow an interrupt to occur within the interrupt handler and is more accurate from the point of view of timing. To use the lighter interrupt handling you have to add hard = True to the irq method:

pin.irq(ISR, trigger, hard = True)

Unfortunately, neither form of interrupt returns a timestamp for when the interrupt occurs, which limits their usefulness.

The ISR will still be called even if your MicroPython program has come to an end:

def HelloIRQ(pin):
    print("IRQ")
pin=Pin(4,Pin.IN,Pin.PULL_DOWN)
pin.irq(HelloIRQ,Pin.IRQ_RISING)

You will see IRQ printed whenever GPIO4 has a rising edge, even after the program has finished.

This seems very strange but you need to see it in terms of “my program has finished but the MicroPython interpreter is still active and will run any program when it is told to”. Setting an IRQ handler tells the MicroPython interpreter to run the function whenever the event occurs.

You can associate a different ISR to each GPIO line. For example:

def HelloIRQ1(pin):
    print("IRQ1")
    
def HelloIRQ2(pin):
    print("IRQ2")
pin1=Pin(4,Pin.IN,Pin.PULL_DOWN)
pin2=Pin(5,Pin.IN,Pin.PULL_DOWN)
pin1.irq(HelloIRQ1,Pin.IRQ_RISING)
pin2.irq(HelloIRQ2,Pin.IRQ_RISING)

You will see IRQ1 or IRQ2 printed depending on which pin the interrupt occurred on. Alternatively you can set a single ISR and work out what to do depending on the pin passed in:

def HelloIRQ(pin):
	def HelloIRQ1(pin):
    if str(pin) == "Pin(4)":
        print("IRQ1")
    else:    
        print("IRQ2")
pin1=Pin(4,Pin.IN,Pin.PULL_DOWN)
pin2=Pin(5,Pin.IN,Pin.PULL_DOWN)
pin1.irq(HelloIRQ,Pin.IRQ_RISING)
pin2.irq(HelloIRQ,Pin.IRQ_RISING)

The Interrupt Queue

The interrupt as implemented in MicroPython isn’t like the raw hardware interrupt produced by the ESP32. It differs in that interrupts are buffered rather than turned off in the interrupt routine. The problem is that interrupts can occur during the execution of an interrupt handler and this can cause problems. It is usual for interrupt handlers to disable interrupts while they are running to simplify things, but this isn’t how MicroPython’s interrupts work. What happens is that the interrupt system is kept enabled during an interrupt routine but if an interrupt occurs it is simply added to a queue. The interrupt routine then finishes and the interrupt system immediately calls it again so that no interrupt is missed. This is exactly the advantage of this approach – no interrupt is missed, but from a realtime point of view it isn’t always what you want. Often the time that the interrupt occurred is important and having a queue of pending interrupts spoils this.

To see how all this works and what effect it has consider this simple example. An interrupt signal is applied to GPIO4 that provides a rising edge every second, i.e. it is a 1Hz square wave. The interrupt handler records the time of the interrupt and prints the difference between this time and the previous interrupt time. If no interrupts are missed this should always be a little more than 1 second. However, there is a sleep for 1.5 seconds at the end of the interrupt routine and so it should miss the next rising edge but get the following one. This means that the time should be reported as 2 seconds:

import time
from machine import Pin
t = 0
def myHandler(pin):
    global t 
    temp = time.ticks_us()
    print(time.ticks_diff(temp,t))
    t = temp
    time.sleep(1.5)   
    return
pin = Pin(4, Pin.IN, Pin.PULL_DOWN)
pin.irq(myHandler, Pin.IRQ_FALLING)

If you try this program you will find that it prints a little more than 1.5 seconds. The reason is that the supposedly missed interrupt event is added to a queue and as soon as the interrupt handler is finished it is called again – hence the supposed 1.5 seconds between interrupts.

You can clear the queue of interrupts waiting to be handled by disabling interrupts within the interrupt handler and then turning them back on again:

import time
from machine import Pin
t = 0
def myHandler(pin):
    global t
    pin.irq(None, Pin.IRQ_FALLING)
    temp = time.ticks_us()
    print(time.ticks_diff(temp,t))
    t = temp
    time.sleep(1.5)
    pin.irq(myHandler, Pin.IRQ_FALLING)
    return
pin = Pin(4, Pin.IN, Pin.PULL_DOWN)
pin.irq(myHandler, Pin.IRQ_FALLING)

With this change the interrupt handler does miss the very next edge and the time displayed is slightly more than 2 seconds. Notice the use of None as an interrupt handler to turn interrupts off. How to deal with missing interrupts is a matter of what is most important – responding to all interrupts or responding at the time that the interrupt occurred.



Last Updated ( Wednesday, 03 April 2024 )