Session 4: Why Async Matters on a Microcontroller
Synopsis
Explains blocking versus non-blocking behavior, responsiveness, cooperative multitasking, and why asynchronous design is especially useful on resource-constrained devices.
Session Content
Session 4: Why Async Matters on a Microcontroller
Session Overview
Duration: ~45 minutes
Audience: Python developers with basic programming knowledge
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny
Session Goals
By the end of this session, learners will be able to:
- Explain why synchronous code can be limiting on a microcontroller
- Describe how asynchronous programming helps with responsiveness
- Use uasyncio to run multiple tasks concurrently
- Build a simple non-blocking Pico application with a button and LED
- Understand where async is useful in IoT and hardware projects
1) Theory: Why Async Matters on a Microcontroller
What is the problem with synchronous code?
On a microcontroller, many tasks need attention at the same time:
- reading buttons or sensors
- updating LEDs or displays
- handling Wi-Fi connections
- sending data over the network
- reacting quickly to events
With blocking code, one task can prevent others from running.
Example of blocking behavior
If you write code like this:
- turn on LED
- wait 5 seconds
- check button
then the button may not be checked until the wait finishes.
Why this is a problem on Pico 2 W
The Pico 2 W has limited CPU and memory compared to a PC. It cannot waste time waiting if the device should remain responsive.
Async programming helps you: - avoid long blocking delays - keep the board responsive - manage multiple activities in one program - combine hardware and network tasks more effectively
Key idea
Async on a microcontroller does not mean true parallel CPU execution.
It means:
- tasks cooperate
- each task yields control when waiting
- the event loop schedules other tasks
2) Async Concepts in MicroPython
Event loop
The event loop is the engine that runs async tasks.
Coroutine
A coroutine is an async function defined with async def.
Await
await pauses a coroutine until the awaited operation completes, allowing other tasks to run.
Common use cases
- blink an LED while checking a button
- read a sensor periodically
- keep Wi-Fi alive while doing other work
- serve a small web page while monitoring hardware
3) Development Environment Setup
Required tools
- Raspberry Pi Pico 2 W
- USB cable
- Thonny IDE
- MicroPython firmware for Pico 2 W
Thonny setup
- Install Thonny on your computer.
- Connect Pico 2 W via USB.
- In Thonny:
- go to Tools > Options > Interpreter
- select MicroPython (Raspberry Pi Pico)
- choose the correct port
- If needed, flash the latest MicroPython firmware for Pico 2 W.
Quick check
In the Thonny shell, run:
import sys
print(sys.platform)
Expected output should indicate the RP2 platform, for example:
rp2
4) Hands-On Exercise 1: Blocking vs Non-Blocking LED Blink
Hardware
- Raspberry Pi Pico 2 W
- 1 LED
- 1 resistor (220Ω to 330Ω)
- breadboard and jumper wires
Wiring
- LED anode (long leg) -> GP15 through resistor
- LED cathode (short leg) -> GND
Part A: Blocking version
Upload this code to main.py:
from machine import Pin
import time
# Built-in LED is available on many Pico boards,
# but here we use an external LED on GP15.
led = Pin(15, Pin.OUT)
print("Starting blocking blink demo...")
while True:
led.on()
print("LED ON")
time.sleep(1)
led.off()
print("LED OFF")
time.sleep(1)
What to observe
- The LED blinks every second
- During
time.sleep(), the program cannot do anything else
Example output
Starting blocking blink demo...
LED ON
LED OFF
LED ON
LED OFF
Part B: Async version
Replace the code with:
import uasyncio as asyncio
from machine import Pin
# External LED on GP15
led = Pin(15, Pin.OUT)
async def blink_led():
while True:
led.on()
print("LED ON")
await asyncio.sleep(1)
led.off()
print("LED OFF")
await asyncio.sleep(1)
async def main():
# Start the blink task
asyncio.create_task(blink_led())
# Keep the event loop alive
while True:
await asyncio.sleep(5)
print("Main task still running")
# Run the event loop
asyncio.run(main())
What to observe
- The LED still blinks
- The program remains structured for additional tasks
main()can manage other work whileblink_led()runs
5) Hands-On Exercise 2: Button + LED Without Blocking
Hardware
- 1 pushbutton
- 1 LED with resistor
- Pico 2 W
- jumper wires
Wiring
- Button one side -> GP14
- Button other side -> GND
- LED anode -> GP15 through resistor
- LED cathode -> GND
Use the internal pull-up resistor on GP14.
Code: responsive button monitor
import uasyncio as asyncio
from machine import Pin
# Button uses internal pull-up, so pressed = 0
button = Pin(14, Pin.IN, Pin.PULL_UP)
# External LED on GP15
led = Pin(15, Pin.OUT)
async def watch_button():
last_state = button.value()
while True:
current_state = button.value()
# Detect a state change
if current_state != last_state:
if current_state == 0:
print("Button pressed")
led.on()
else:
print("Button released")
led.off()
last_state = current_state
# Small delay keeps the system responsive
await asyncio.sleep_ms(20)
async def heartbeat():
while True:
print("Heartbeat: system alive")
await asyncio.sleep(2)
async def main():
asyncio.create_task(watch_button())
asyncio.create_task(heartbeat())
# Keep main alive forever
while True:
await asyncio.sleep(1)
asyncio.run(main())
What to observe
- Pressing the button turns the LED on
- Releasing the button turns the LED off
- Heartbeat messages continue without blocking
Example output
Heartbeat: system alive
Button pressed
Button released
Heartbeat: system alive
Heartbeat: system alive
6) Hands-On Exercise 3: Async Task Coordination
Goal
Run two independent tasks: - blink an LED - simulate sensor reading
This demonstrates how async helps combine multiple periodic jobs.
Code
import uasyncio as asyncio
from machine import Pin
import random
led = Pin(15, Pin.OUT)
async def blink_led():
while True:
led.toggle()
print("LED toggled")
await asyncio.sleep(0.5)
async def read_fake_sensor():
while True:
# Simulated sensor value for learning purposes
value = random.randint(0, 1023)
print("Sensor value:", value)
await asyncio.sleep(3)
async def main():
asyncio.create_task(blink_led())
asyncio.create_task(read_fake_sensor())
while True:
await asyncio.sleep(1)
asyncio.run(main())
What to observe
- LED toggles quickly
- Sensor readings appear every 3 seconds
- Both tasks run together without stopping each other
Example output
LED toggled
LED toggled
Sensor value: 732
LED toggled
LED toggled
Sensor value: 184
7) Common Async Patterns
1. Use await instead of blocking delays
Prefer:
await asyncio.sleep(1)
Instead of:
time.sleep(1)
2. Split logic into tasks
Examples: - one task for LED animation - one task for button input - one task for Wi-Fi communication
3. Keep tasks short and cooperative
A task should not do long CPU-heavy work without yielding.
4. Use periodic sleep in loops
This prevents one task from monopolizing the CPU.
8) Important Notes and Best Practices
Do
- use
uasynciofor concurrent waiting tasks - keep loops cooperative
- test one task at a time before combining tasks
- add clear print statements during development
Don’t
- use long blocking
sleep()calls in async programs - put heavy computation inside a tight infinite loop
- assume async means true parallelism
Debugging tip
If your program appears frozen:
- check for a blocking call
- verify all tasks use await
- confirm the event loop is running with asyncio.run()
9) Mini-Challenge
Task
Modify the button-and-LED program so that: - short press turns LED on - second press turns LED off - heartbeat still prints every 2 seconds
Hint
Track the LED state in software and toggle it only on button press transitions.
10) Wrap-Up
Key takeaways
- Blocking code can make a microcontroller unresponsive
- Async helps multiple tasks share time on a small device
uasynciois a practical tool for Pico 2 W projects- Async is especially useful for hardware plus IoT applications
Next session preview
You will use async patterns to handle real hardware input and begin integrating sensors or network tasks into responsive programs.