Session 2: Understanding the uasyncio Event Loop
Synopsis
Explains how the event loop schedules tasks, what cooperative multitasking means in practice, and how task fairness and yielding affect program behavior.
Session Content
Session 2: Understanding the uasyncio 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 Objectives
By the end of this session, learners will be able to:
- Explain what an event loop is and why it is useful in embedded systems
- Understand the role of uasyncio in MicroPython
- Write simple concurrent tasks using uasyncio
- Use await, async, sleep, and create_task()
- Run multiple non-blocking tasks on the Pico 2 W
- Recognize and avoid common blocking-code pitfalls
Prerequisites
- Raspberry Pi Pico 2 W
- USB cable
- Thonny IDE installed
- MicroPython firmware installed on the Pico 2 W
- Basic familiarity with Python functions, variables, and loops
Required Hardware
- Raspberry Pi Pico 2 W
- Onboard LED (built into the Pico 2 W)
- Optional: external LED + 220Ω resistor + breadboard + jumper wires
1. Introduction to the Event Loop
What is an Event Loop?
An event loop is the part of an application that repeatedly checks for work to do and runs tasks when they are ready.
In MicroPython, uasyncio provides a lightweight asynchronous framework:
- Tasks can pause while waiting
- Other tasks can continue running
- Useful for handling LEDs, sensors, buttons, and network requests without freezing the program
Why it matters on the Pico 2 W
Microcontrollers have limited resources, so blocking code can: - Freeze the UI - Delay sensor readings - Miss button presses - Interrupt network handling
Blocking vs Non-Blocking
Blocking example
from machine import Pin
from time import sleep
led = Pin("LED", Pin.OUT)
while True:
led.toggle()
sleep(1)
This works, but nothing else can happen while sleep(1) runs.
Asynchronous example
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def blink():
while True:
led.toggle()
await asyncio.sleep(1)
asyncio.run(blink())
Here, the task yields control during await asyncio.sleep(1), allowing other tasks to run.
2. MicroPython uasyncio Basics
Key Concepts
async def: defines an asynchronous functionawait: pauses the current task until the awaited operation completesasyncio.sleep(): non-blocking delayasyncio.create_task(): schedules concurrent tasksasyncio.run(): starts the event loop
Mental model
Think of the event loop as a manager that:
1. Starts tasks
2. Switches between tasks when they await
3. Keeps doing this until all tasks finish or the program stops
3. Development Environment Setup in Thonny
Connect and Configure
- Install Thonny from the official website
- Connect the Pico 2 W via USB
- Open Thonny
- Go to Tools > Options > Interpreter
- Select:
- MicroPython (Raspberry Pi Pico)
- Choose the correct port for the Pico 2 W
- Click OK
Verify MicroPython
Open the shell and run:
import sys
print(sys.platform)
Expected output:
rp2
4. Hands-On Exercise 1: Blink the Onboard LED with uasyncio
Goal
Create a non-blocking LED blink task.
Code
Save this as main.py on the Pico:
import uasyncio as asyncio
from machine import Pin
# Onboard LED on Raspberry Pi Pico 2 W
led = Pin("LED", Pin.OUT)
async def blink_led():
"""Toggle the onboard LED every second."""
while True:
led.toggle()
print("LED toggled:", led.value())
await asyncio.sleep(1)
async def main():
"""Main coroutine that starts the blink task."""
await blink_led()
# Start the event loop
asyncio.run(main())
Expected Output
LED toggled: 1
LED toggled: 0
LED toggled: 1
LED toggled: 0
What to Observe
- The LED toggles every second
- The shell remains responsive between toggles
- The code uses
awaitinstead ofsleep
5. Hands-On Exercise 2: Run Two Tasks at the Same Time
Goal
Run a blinking LED task and a status-printing task concurrently.
Code
Save as main.py:
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def blink_led():
"""Blink the onboard LED every 500 ms."""
while True:
led.toggle()
print("Blink task: LED =", led.value())
await asyncio.sleep(0.5)
async def status_report():
"""Print a status message every 2 seconds."""
counter = 0
while True:
counter += 1
print("Status task: heartbeat", counter)
await asyncio.sleep(2)
async def main():
"""Create and run concurrent tasks."""
asyncio.create_task(blink_led())
asyncio.create_task(status_report())
while True:
await asyncio.sleep(10)
asyncio.run(main())
Expected Output
Blink task: LED = 1
Status task: heartbeat 1
Blink task: LED = 0
Blink task: LED = 1
Blink task: LED = 0
Status task: heartbeat 2
Blink task: LED = 1
Discussion
- Both tasks run without blocking each other
- The
main()coroutine keeps the loop alive create_task()allows concurrency
6. Hands-On Exercise 3: Simulate a Button Check Without Blocking
Goal
Understand how a task can periodically poll a pin while other tasks continue running.
Hardware Setup
Optional: - Connect a button to GPIO 15 and GND - Use the internal pull-up resistor
Wiring
- One side of pushbutton to GP15
- Other side to GND
Code
import uasyncio as asyncio
from machine import Pin
button = Pin(15, Pin.IN, Pin.PULL_UP)
led = Pin("LED", Pin.OUT)
async def blink_led():
while True:
led.toggle()
await asyncio.sleep(1)
async def monitor_button():
while True:
if button.value() == 0:
print("Button pressed")
led.on()
await asyncio.sleep(0.2) # simple debounce
await asyncio.sleep(0.05)
async def main():
asyncio.create_task(blink_led())
asyncio.create_task(monitor_button())
while True:
await asyncio.sleep(1)
asyncio.run(main())
Expected Output
Button pressed
Button pressed
What to Observe
- The LED keeps blinking even while checking the button
- The button check does not freeze the program
- The short delay helps reduce repeated triggers
7. Theory: Best Practices for uasyncio
Do
- Keep tasks short and cooperative
- Use
await asyncio.sleep()instead oftime.sleep() - Use one task per responsibility
- Cleanly structure code into small async functions
Avoid
- Long blocking loops without
await - Using
time.sleep()inside async tasks - Doing heavy computation inside the event loop
- Creating tasks and then letting the program exit immediately
Common Mistake
import uasyncio as asyncio
from time import sleep
async def bad_task():
while True:
print("This blocks the event loop")
sleep(1) # Wrong in async code
Correct Version
import uasyncio as asyncio
async def good_task():
while True:
print("This cooperates with the event loop")
await asyncio.sleep(1)
8. Mini-Lab: Two LEDs, Two Timing Patterns
Goal
Use concurrency to control two outputs independently.
Hardware Setup
Optional: - External LEDs on GP16 and GP17 with 220Ω resistors
Wiring
- GP16 → resistor → LED → GND
- GP17 → resistor → LED → GND
Code
import uasyncio as asyncio
from machine import Pin
led1 = Pin(16, Pin.OUT)
led2 = Pin(17, Pin.OUT)
async def blink_fast():
while True:
led1.toggle()
print("Fast LED:", led1.value())
await asyncio.sleep(0.3)
async def blink_slow():
while True:
led2.toggle()
print("Slow LED:", led2.value())
await asyncio.sleep(1.0)
async def main():
asyncio.create_task(blink_fast())
asyncio.create_task(blink_slow())
while True:
await asyncio.sleep(5)
asyncio.run(main())
Expected Output
Fast LED: 1
Slow LED: 1
Fast LED: 0
Fast LED: 1
Slow LED: 0
Learning Point
Different tasks can run at different intervals without interfering with each other.
9. Troubleshooting
Problem: Code runs once and stops
- Ensure the tasks are in an infinite loop, or keep
main()alive with a loop
Problem: LED does not blink
- Check the board is connected correctly
- Verify the correct pin name: onboard LED is usually
"LED" - Ensure
main.pyis saved on the Pico
Problem: ModuleNotFoundError: uasyncio
- Confirm MicroPython firmware is installed
- Restart the board in Thonny
- Check the interpreter is set to MicroPython for Pico
Problem: Code freezes
- Look for
time.sleep()or long-running code withoutawait - Replace blocking calls with
await asyncio.sleep()
10. Knowledge Check
Questions
- What is the purpose of an event loop?
- Why is
await asyncio.sleep()preferred overtime.sleep()in async code? - What does
asyncio.create_task()do? - Why must tasks cooperate with the event loop?
- What happens if a task never yields control?
Suggested Answers
- It manages and schedules tasks.
- It pauses without blocking other tasks.
- It starts a task concurrently.
- So the loop can switch between tasks.
- Other tasks cannot run properly.
11. Session Wrap-Up
Key Takeaways
uasyncioenables cooperative multitasking on MicroPython- The event loop switches between tasks when they
await - Non-blocking code is essential for responsive embedded applications
- Multiple hardware interactions can be managed cleanly with separate tasks
Next Session Preview
In the next session, you will build on this foundation by combining asynchronous tasks with real hardware input and output, such as buttons, LEDs, and sensors.
Appendix: Reference Code Template
import uasyncio as asyncio
async def task_one():
while True:
print("Task one running")
await asyncio.sleep(1)
async def task_two():
while True:
print("Task two running")
await asyncio.sleep(2)
async def main():
asyncio.create_task(task_one())
asyncio.create_task(task_two())
while True:
await asyncio.sleep(10)
asyncio.run(main())
Back to Chapter | Back to Master Plan | Previous Session | Next Session