Skip to content

Session 3: Interrupts and Async: Safe Handoffs to the Event Loop

Synopsis

Introduces interrupt service routine constraints, deferred processing, and methods for transferring hardware-triggered events into async task logic safely.

Session Content

Session 3: Interrupts and Async: Safe Handoffs to the Event Loop

Session Overview

Duration: ~45 minutes
Audience: Python developers with basic programming knowledge
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny

Learning Outcomes

By the end of this session, learners will be able to:

  • Explain why interrupt service routines (ISRs) must stay short
  • Use hardware interrupts for buttons and sensors
  • Safely communicate between interrupts and the main event loop
  • Use uasyncio to build non-blocking applications
  • Combine interrupts with asynchronous tasks for responsive IoT-style projects

Prerequisites

  • Raspberry Pi Pico 2 W running MicroPython
  • Thonny IDE installed and configured
  • Basic familiarity with GPIO, Pin, and sleep_ms()
  • Breadboard, jumper wires, one pushbutton, one LED, and resistors
  • Optional: PIR motion sensor, buzzer, or another digital sensor

Development Environment Setup

Thonny Setup

  1. Install Thonny on your computer.
  2. Connect the Raspberry Pi Pico 2 W via USB.
  3. Open Thonny.
  4. Go to Tools → Options → Interpreter.
  5. Select MicroPython (Raspberry Pi Pico).
  6. Choose the correct serial port.
  7. Confirm the MicroPython firmware is installed on the Pico.
  8. Save scripts directly to the Pico using File → Save as... → Raspberry Pi Pico.

Test the Connection

Create and run this simple test in Thonny:

from machine import Pin
from time import sleep

led = Pin("LED", Pin.OUT)

while True:
    led.toggle()
    sleep(0.5)

Expected result: The onboard LED blinks every 0.5 seconds.


Session Agenda

1. Theory: Why Interrupts and Async Matter

2. Hands-On: Button Interrupt Basics

3. Theory: Safe Communication Between ISR and Main Code

4. Hands-On: Debounced Interrupt Counter

5. Theory: Introduction to uasyncio

6. Hands-On: Event Loop with LED and Button

7. Integrated Project: Interrupt Triggered Async Task

8. Wrap-Up and Review


1) Theory: Why Interrupts and Async Matter

The Problem with Polling

A typical polling loop repeatedly checks whether something has happened:

while True:
    if button.value() == 0:
        print("Pressed")

This works, but it has drawbacks:

  • It wastes CPU time checking constantly
  • It can miss very short events
  • It becomes harder to manage when multiple tasks run together

Interrupts

An interrupt allows hardware to notify the CPU when an event occurs.

Examples: - Button press - Sensor edge change - Signal pulse

When the interrupt occurs, MicroPython runs a callback function, called an ISR (Interrupt Service Routine).

ISR Rules

An ISR should be:

  • Very short
  • Fast
  • Safe

Inside an ISR: - Avoid long delays - Avoid memory allocations - Avoid printing too much - Avoid complex logic

Async Programming

uasyncio lets you run multiple tasks cooperatively without blocking the program.

Examples: - Blink an LED - Read a sensor periodically - Handle network communication - Wait for a button event

Key Idea

Use: - Interrupts to detect events quickly - Async event loop to process those events safely


2) Hands-On: Button Interrupt Basics

Wiring

Connect a pushbutton:

  • One side of the button to GP14
  • Other side of the button to GND

Use the internal pull-up resistor in software.

Code: Basic Interrupt on Falling Edge

Create main.py:

from machine import Pin
import time

# Onboard LED
led = Pin("LED", Pin.OUT)

# Button on GP14 with internal pull-up
button = Pin(14, Pin.IN, Pin.PULL_UP)

# Shared state updated by the interrupt
button_pressed = False

def button_isr(pin):
    global button_pressed
    button_pressed = True

# Trigger on falling edge: button press connects pin to GND
button.irq(trigger=Pin.IRQ_FALLING, handler=button_isr)

print("Press the button to toggle the LED")

while True:
    if button_pressed:
        button_pressed = False
        led.toggle()
        print("Button pressed: LED toggled")
    time.sleep_ms(20)

What This Demonstrates

  • The ISR only sets a flag
  • The main loop checks the flag and performs the action
  • This avoids doing too much work in the ISR

Example Output

Press the button to toggle the LED
Button pressed: LED toggled
Button pressed: LED toggled

3) Theory: Safe Communication Between ISR and Main Code

Why Not Do Everything in the ISR?

Unsafe or discouraged actions inside ISRs may include:

  • time.sleep()
  • Network requests
  • File operations
  • Complex object creation
  • Long loops

Safe Pattern

Use the ISR to: - Set a flag - Increment a counter - Store a timestamp - Signal a task

Then handle the real work in: - Main loop - Async task - Deferred callback

Common Communication Methods

  • Boolean flag
  • Counter
  • uasyncio.ThreadSafeFlag
  • micropython.schedule()

For beginner-friendly designs, a flag + async task is a great starting point.


4) Hands-On: Debounced Interrupt Counter

Mechanical buttons often bounce, causing multiple false triggers.

Wiring

Same as before: - Button to GP14 - Button to GND

Code: Interrupt Counter with Debounce

Use a time-based debounce inside the main loop, not the ISR.

from machine import Pin
import time

button = Pin(14, Pin.IN, Pin.PULL_UP)
led = Pin("LED", Pin.OUT)

press_count = 0
button_event = False
last_press_time = 0
debounce_ms = 200

def button_isr(pin):
    global button_event
    button_event = True

button.irq(trigger=Pin.IRQ_FALLING, handler=button_isr)

print("Press the button")

while True:
    if button_event:
        button_event = False
        now = time.ticks_ms()

        if time.ticks_diff(now, last_press_time) > debounce_ms:
            last_press_time = now
            press_count += 1
            led.toggle()
            print("Press count:", press_count)

    time.sleep_ms(10)

Example Output

Press the button
Press count: 1
Press count: 2
Press count: 3

Discussion Points

  • The ISR only signals that an event occurred
  • Debouncing occurs in the main loop
  • time.ticks_ms() and time.ticks_diff() are safe and recommended for timing logic

5) Theory: Introduction to uasyncio

What Is uasyncio?

uasyncio is MicroPython’s lightweight async framework.

It lets you write code that looks sequential but runs cooperatively.

Core Concepts

  • async def defines a coroutine
  • await pauses execution until another task can run
  • uasyncio.create_task() starts a background task
  • uasyncio.run() starts the event loop

Why It Helps on Pico

  • Non-blocking LED blinking
  • Concurrent sensor reading
  • Responsive button handling
  • Better structure for IoT applications

Important Rule

Async tasks should use await often enough to let other tasks run.


6) Hands-On: Event Loop with LED and Button

This example uses: - One async task to blink the onboard LED - One async task to watch for button events - An interrupt to set the event flag

Code: Async Button Response

import uasyncio as asyncio
from machine import Pin

# Hardware setup
led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

# Shared event flag
button_event = False

def button_isr(pin):
    global button_event
    button_event = True

button.irq(trigger=Pin.IRQ_FALLING, handler=button_isr)

async def blink_task():
    while True:
        led.toggle()
        await asyncio.sleep_ms(500)

async def button_task():
    global button_event

    while True:
        if button_event:
            button_event = False
            print("Button pressed!")
            led.on()
            await asyncio.sleep_ms(100)
        await asyncio.sleep_ms(10)

async def main():
    asyncio.create_task(blink_task())
    asyncio.create_task(button_task())

    while True:
        await asyncio.sleep(1)

print("Running async event loop...")
asyncio.run(main())

Example Output

Running async event loop...
Button pressed!
Button pressed!
Button pressed!

What to Observe

  • The LED blinks continuously
  • Button presses are detected without stopping the blink task
  • The event loop remains responsive

7) Integrated Project: Interrupt-Triggered Async Task

Project Idea

Use a button interrupt to trigger a short async alert sequence: - Turn LED on immediately in response to the event - Run a short alert animation - Resume normal blinking

Code: Async Alert Triggered by Interrupt

import uasyncio as asyncio
from machine import Pin
import time

led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

button_event = False

def button_isr(pin):
    global button_event
    button_event = True

button.irq(trigger=Pin.IRQ_FALLING, handler=button_isr)

async def idle_blink():
    while True:
        led.toggle()
        await asyncio.sleep_ms(700)

async def alert_flash():
    # Short alert sequence
    for _ in range(3):
        led.on()
        await asyncio.sleep_ms(100)
        led.off()
        await asyncio.sleep_ms(100)

async def event_watcher():
    global button_event
    while True:
        if button_event:
            button_event = False
            print("Event detected: starting alert")
            await alert_flash()
        await asyncio.sleep_ms(20)

async def main():
    asyncio.create_task(idle_blink())
    asyncio.create_task(event_watcher())

    while True:
        await asyncio.sleep(1)

print("Interrupt + async demo ready")
asyncio.run(main())

Example Output

Interrupt + async demo ready
Event detected: starting alert
Event detected: starting alert

Suggested Extension

Replace the button with: - PIR motion sensor output - Reed switch - Hall effect sensor - Rain sensor digital output


8) Practical Exercise

Exercise: Build a Press-to-Alert Device

Goal

Create a system where: - A button interrupt records a press - The async loop performs a visible alert - A press counter is maintained

Requirements

  • Count button presses
  • Flash LED three times for each valid press
  • Ignore bounce
  • Keep the system responsive

Starter Structure

  • ISR sets button_event = True
  • Async task checks event flag
  • Debounce using time.ticks_ms()

Optional Challenge

Add: - A buzzer on another GPIO - A second button to reset the counter - A “long press” detection in the main loop


9) Troubleshooting

Button Does Not Work

  • Verify wiring to GP14 and GND
  • Check that Pin.PULL_UP is enabled
  • Confirm the button is not wired incorrectly across the breadboard gap
  • Ensure you are using Pin("LED", Pin.OUT) for the onboard LED
  • Confirm the script is saved as main.py
  • Reset the Pico if needed

Async Code Freezes

  • Check that async functions use await
  • Avoid time.sleep() inside async code
  • Use await asyncio.sleep_ms(...) instead

Multiple Button Presses From One Press

  • Add debounce
  • Use time.ticks_ms() and time.ticks_diff()
  • Consider reducing switch noise with better wiring

10) Key Takeaways

  • ISRs should be short and safe
  • Do not perform slow or complex work inside interrupts
  • Use a shared flag or counter to pass events to the main loop
  • uasyncio is ideal for responsive Pico applications
  • Combining interrupts with async yields clean, efficient IoT-style designs

11) Review Questions

  1. Why should an ISR be short?
  2. What is the purpose of a shared flag between ISR and main code?
  3. Why is time.sleep() inappropriate in async code?
  4. What is the benefit of uasyncio on a Pico?
  5. How does debounce help with button interrupts?

12) Suggested Home Practice

Practice Task

Modify the project so that: - The first button press starts a 5-second blinking alert - A second button press during the alert cancels it - The LED indicates the current state

Stretch Goal

Add Wi-Fi support and send an HTTP request when the button is pressed.


Back to Chapter | Back to Master Plan | Previous Session | Next Session