Skip to content

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 function
  • await: pauses the current task until the awaited operation completes
  • asyncio.sleep(): non-blocking delay
  • asyncio.create_task(): schedules concurrent tasks
  • asyncio.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

  1. Install Thonny from the official website
  2. Connect the Pico 2 W via USB
  3. Open Thonny
  4. Go to Tools > Options > Interpreter
  5. Select:
  6. MicroPython (Raspberry Pi Pico)
  7. Choose the correct port for the Pico 2 W
  8. Click OK

Verify MicroPython

Open the shell and run:

import sys
print(sys.platform)

Expected output:

rp2

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 await instead of sleep

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 of time.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
  • Check the board is connected correctly
  • Verify the correct pin name: onboard LED is usually "LED"
  • Ensure main.py is 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 without await
  • Replace blocking calls with await asyncio.sleep()

10. Knowledge Check

Questions

  1. What is the purpose of an event loop?
  2. Why is await asyncio.sleep() preferred over time.sleep() in async code?
  3. What does asyncio.create_task() do?
  4. Why must tasks cooperate with the event loop?
  5. What happens if a task never yields control?

Suggested Answers

  1. It manages and schedules tasks.
  2. It pauses without blocking other tasks.
  3. It starts a task concurrently.
  4. So the loop can switch between tasks.
  5. Other tasks cannot run properly.

11. Session Wrap-Up

Key Takeaways

  • uasyncio enables 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