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
uasyncioto 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, andsleep_ms() - Breadboard, jumper wires, one pushbutton, one LED, and resistors
- Optional: PIR motion sensor, buzzer, or another digital sensor
Development Environment Setup
Thonny Setup
- Install Thonny on your computer.
- Connect the Raspberry Pi Pico 2 W via USB.
- Open Thonny.
- Go to Tools → Options → Interpreter.
- Select MicroPython (Raspberry Pi Pico).
- Choose the correct serial port.
- Confirm the MicroPython firmware is installed on the Pico.
- 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.ThreadSafeFlagmicropython.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()andtime.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 defdefines a coroutineawaitpauses execution until another task can runuasyncio.create_task()starts a background taskuasyncio.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_UPis enabled - Confirm the button is not wired incorrectly across the breadboard gap
LED Does Not Blink
- 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()andtime.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
uasynciois ideal for responsive Pico applications- Combining interrupts with async yields clean, efficient IoT-style designs
11) Review Questions
- Why should an ISR be short?
- What is the purpose of a shared flag between ISR and main code?
- Why is
time.sleep()inappropriate in async code? - What is the benefit of
uasyncioon a Pico? - 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