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
ifstatements - 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
uasynciois the MicroPython version ofasyncioasync defdefines a coroutineawait asyncio.sleep(1)pauses without blocking the whole systemasyncio.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()andtask_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
awaitin 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
- What is the main difference between a normal function and a coroutine?
- Why is
awaitimportant? - What happens if a coroutine never yields control?
- Why is
time.sleep()discouraged inside async code? - What does
asyncio.gather()do?
Suggested Answers
- A coroutine can pause and resume.
- It allows the event loop to run other tasks.
- It blocks other coroutines.
- It blocks the whole program.
- 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
awaitis the mechanism for yielding controlasyncio.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.