Skip to content

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

  1. Install Thonny on your computer.
  2. Connect Pico 2 W via USB.
  3. In Thonny:
  4. go to Tools > Options > Interpreter
  5. select MicroPython (Raspberry Pi Pico)
  6. choose the correct port
  7. 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

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 while blink_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 uasyncio for 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
  • uasyncio is 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.


Back to Chapter | Back to Master Plan | Previous Session