Skip to content

Session 1: From Sequential Code to Coroutines

Synopsis

Introduces async and await in MicroPython, how coroutines suspend and resume, and how to refactor simple blocking programs into asynchronous ones.

Session Content

Session 1: From Sequential Code to Coroutines

Session Overview

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

Session Goal

Introduce the mindset shift from ordinary sequential code to asynchronous, cooperative multitasking using coroutines in MicroPython. By the end of the session, learners will understand why blocking code is a problem on microcontrollers and how async / await enables responsive, concurrent behavior.


Learning Outcomes

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

  • Explain the difference between sequential execution and cooperative multitasking
  • Identify blocking operations in MicroPython code
  • Define and run simple coroutines using async def
  • Use await asyncio.sleep() to yield control
  • Run multiple coroutines concurrently with asyncio.gather()
  • Apply asynchronous thinking to Pico 2 W hardware projects

Prerequisites

  • Basic Python knowledge:
  • variables
  • loops
  • functions
  • simple if statements
  • Raspberry Pi Pico 2 W running MicroPython
  • Thonny installed on a computer
  • USB cable for the Pico
  • Optional: onboard LED access only; no external hardware required for main exercises

Development Environment Setup

1. Install Thonny

  • Download and install Thonny from: https://thonny.org/

2. Flash MicroPython onto Pico 2 W

  • Download the latest MicroPython UF2 for Raspberry Pi Pico 2 W
  • Hold BOOTSEL while connecting the Pico to USB
  • Copy the UF2 file onto the Pico drive
  • The board restarts and appears as a MicroPython device

3. Configure Thonny

  • Open Thonny
  • Go to Run > Select interpreter
  • Choose MicroPython (Raspberry Pi Pico)
  • Select the correct serial port

4. Confirm Connection

In the Thonny Shell, run:

print("Hello Pico 2 W")

Expected output:

Hello Pico 2 W

1. Theory: Why Sequential Code Becomes a Problem

Sequential Execution

In standard Python, code runs one step at a time.

print("Task A starts")
print("Task A ends")
print("Task B starts")
print("Task B ends")

This is fine for short programs, but on a microcontroller it can be limiting when: - reading sensors while blinking LEDs - responding to button presses - maintaining Wi-Fi connections - sending data to a server - keeping user interfaces responsive

Blocking Code Example

A blocking delay stops the whole program from doing anything else.

import time

print("Start")
time.sleep(2)
print("After 2 seconds")

During time.sleep(2), nothing else can happen.

Why This Matters on Pico 2 W

If you want: - one task to blink an LED - another task to read a sensor - another to send data over Wi-Fi

then blocking code makes the device feel unresponsive.


2. Coroutines: The Asynchronous Building Block

What Is a Coroutine?

A coroutine is a function defined with async def that can pause and resume.

async def my_task():
    print("Step 1")
    await asyncio.sleep(1)
    print("Step 2")

Key Idea

A coroutine can yield control at an await point, allowing other coroutines to run.

This is called cooperative multitasking: - tasks cooperate by pausing voluntarily - no task monopolizes the CPU - code stays responsive


3. Hands-On Exercise 1: Your First Coroutine

Objective

Write and run a simple coroutine that pauses and resumes.

Code

Create a new file in Thonny and enter:

import uasyncio as asyncio

async def hello_task():
    print("Hello from coroutine 1")
    await asyncio.sleep(1)
    print("Coroutine 1 resumed")

async def main():
    await hello_task()

asyncio.run(main())

Explanation

  • uasyncio is the MicroPython version of asyncio
  • async def defines a coroutine
  • await asyncio.sleep(1) pauses without blocking the whole system
  • asyncio.run(main()) starts the event loop

Expected Output

Hello from coroutine 1
Coroutine 1 resumed

4. Hands-On Exercise 2: Two Coroutines Running Together

Objective

See how asynchronous code allows multiple tasks to progress cooperatively.

Code

import uasyncio as asyncio

async def task_a():
    for i in range(5):
        print("Task A:", i)
        await asyncio.sleep(1)

async def task_b():
    for i in range(3):
        print("  Task B:", i)
        await asyncio.sleep(1.5)

async def main():
    await asyncio.gather(
        task_a(),
        task_b()
    )

asyncio.run(main())

What to Observe

  • task_a() and task_b() run concurrently
  • Neither task blocks the other for long
  • Output appears interleaved

Expected Output

Task A: 0
  Task B: 0
Task A: 1
  Task B: 1
Task A: 2
Task A: 3
  Task B: 2
Task A: 4

5. Hands-On Exercise 3: Visualizing Cooperative Multitasking with the Onboard LED

Objective

Use the Pico 2 W onboard LED to show how coroutines can coordinate without blocking.

Wiring

  • No external wiring required
  • Use the onboard LED

Code

from machine import Pin
import uasyncio as asyncio

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

async def blink_led():
    while True:
        led.value(1)
        print("LED ON")
        await asyncio.sleep(0.5)

        led.value(0)
        print("LED OFF")
        await asyncio.sleep(0.5)

async def status_message():
    while True:
        print("System is alive")
        await asyncio.sleep(1.2)

async def main():
    await asyncio.gather(
        blink_led(),
        status_message()
    )

asyncio.run(main())

Expected Behavior

  • The LED blinks continuously
  • Status messages print every 1.2 seconds
  • Both tasks continue without freezing each other

6. Why await Matters

Without await

A coroutine that never awaits can block the event loop.

async def bad_task():
    while True:
        print("This will block other tasks")

This is a common mistake. Even though the function is async, it does not yield control.

With await

Always ensure long-running loops include pauses or I/O operations that yield control.

async def good_task():
    while True:
        print("Cooperative task")
        await asyncio.sleep(1)

7. Mini Discussion: From Functions to Tasks

Sequential Mental Model

Traditional thinking: - run step 1 - run step 2 - run step 3

Coroutine Mental Model

Asynchronous thinking: - start task A - start task B - task A pauses - task B runs - task A resumes

This is especially useful when: - waiting for sensors - waiting for Wi-Fi - handling button presses - updating displays - controlling actuators


8. Hands-On Exercise 4: A Simple Two-Task Scheduler Demo

Objective

Demonstrate how tasks can be independently timed.

Code

import uasyncio as asyncio

async def fast_task():
    for i in range(10):
        print("Fast task iteration", i)
        await asyncio.sleep_ms(200)

async def slow_task():
    for i in range(4):
        print("    Slow task iteration", i)
        await asyncio.sleep_ms(500)

async def main():
    await asyncio.gather(
        fast_task(),
        slow_task()
    )

asyncio.run(main())

Expected Output

Fast task iteration 0
    Slow task iteration 0
Fast task iteration 1
Fast task iteration 2
    Slow task iteration 1
Fast task iteration 3
Fast task iteration 4
    Slow task iteration 2
Fast task iteration 5
Fast task iteration 6
Fast task iteration 7
    Slow task iteration 3
Fast task iteration 8
Fast task iteration 9

9. Best Practices for Coroutine-Based Code

Do

  • keep each coroutine focused on one responsibility
  • include await in long-running loops
  • use asyncio.gather() for concurrent tasks
  • prefer non-blocking timing with asyncio.sleep()

Avoid

  • long CPU-bound loops without await
  • using time.sleep() inside async code
  • putting too many unrelated responsibilities into one coroutine

10. Common Mistakes

Mistake 1: Using time.sleep() in Async Code

import time
import uasyncio as asyncio

async def bad_blink():
    while True:
        print("LED ON")
        time.sleep(1)
        print("LED OFF")
        time.sleep(1)

This blocks the event loop.

Correct Approach

import uasyncio as asyncio

async def good_blink():
    while True:
        print("LED ON")
        await asyncio.sleep(1)
        print("LED OFF")
        await asyncio.sleep(1)

Mistake 2: Forgetting to Run the Event Loop

Defining coroutines is not enough. They must be started with asyncio.run() or equivalent.


11. Quick Knowledge Check

Questions

  1. What is the main difference between a normal function and a coroutine?
  2. Why is await important?
  3. What happens if a coroutine never yields control?
  4. Why is time.sleep() discouraged inside async code?
  5. What does asyncio.gather() do?

Suggested Answers

  1. A coroutine can pause and resume.
  2. It allows the event loop to run other tasks.
  3. It blocks other coroutines.
  4. It blocks the whole program.
  5. It runs multiple coroutines concurrently.

12. Session Wrap-Up

Key Takeaways

  • Sequential code runs one step at a time
  • Blocking calls prevent responsiveness
  • Coroutines allow cooperative multitasking
  • await is the mechanism for yielding control
  • asyncio.gather() runs multiple tasks together
  • Asynchronous thinking is essential for responsive Pico 2 W projects

13. Optional Extension Exercise

Objective

Modify the LED demo to change blink speed dynamically.

Starter Code

from machine import Pin
import uasyncio as asyncio

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

async def blink_led(delay):
    while True:
        led.toggle()
        print("LED toggled")
        await asyncio.sleep(delay)

async def main():
    await asyncio.gather(
        blink_led(0.3),
        blink_led(0.8)
    )

asyncio.run(main())

Note

This version is intentionally problematic because two tasks control the same LED. Use it to discuss resource sharing and why tasks should not conflict over the same hardware output.


14. Further Reading

  • MicroPython Wiki: https://github.com/micropython/micropython/wiki
  • MicroPython Quick Reference: https://docs.micropython.org/en/latest/rp2/quickref.html
  • Raspberry Pi Pico MicroPython Documentation: https://www.raspberrypi.com/documentation/microcontrollers/micropython.html
  • Raspberry Pi Pico Series Documentation: https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html#pico2

15. Summary

In This Session, You Learned

  • how sequential execution differs from cooperative multitasking
  • how coroutines are created and executed in MicroPython
  • how to use await asyncio.sleep() to avoid blocking
  • how to run multiple tasks with asyncio.gather()
  • how asynchronous code improves responsiveness on the Pico 2 W

Next Session Preview

You will build on this foundation by learning how to structure async programs with tasks, event loops, and hardware interaction patterns for real-world Pico 2 W applications.


Back to Chapter | Back to Master Plan | Next Session