Skip to content

Session 3: Managing Multiple I/O Activities at Once

Synopsis

Combines outputs and inputs into small multitasking programs, such as status indicators, user interaction loops, and sensor checks running concurrently.

Session Content

Session 3: Managing Multiple I/O Activities at Once

Session Overview

Duration: ~45 minutes
Topic: Managing Multiple I/O Activities at Once
Focus: Using asynchronous programming in MicroPython to handle multiple hardware activities without blocking the main loop.

By the end of this session, learners will be able to: - Explain why blocking code is a problem in embedded applications. - Use uasyncio in MicroPython to coordinate multiple tasks. - Manage simultaneous I/O activities such as LED blinking, button monitoring, and sensor polling. - Build a simple multi-task Pico 2 W application that remains responsive.


Prerequisites

  • Raspberry Pi Pico 2 W
  • Micro-USB cable
  • Thonny IDE installed
  • MicroPython firmware installed on the Pico 2 W
  • Breadboard, jumper wires
  • 1 LED
  • 1 resistor (220Ω to 330Ω)
  • 1 push button
  • Optional: DHT11 or another simple sensor

Development Environment Setup

Thonny Setup

  1. Connect the Raspberry Pi Pico 2 W to your computer using USB.
  2. Open Thonny.
  3. Go to Tools > Options > Interpreter.
  4. Select MicroPython (Raspberry Pi Pico).
  5. Choose the correct serial port.
  6. Click OK.
  7. Verify the REPL shows the MicroPython prompt: ```python

    ```

File Workflow

  • Use main.py for the program that should run automatically on boot.
  • Use code.py or other helper files only if needed.
  • Save files directly to the Pico filesystem.

Session Agenda

1. Theory: Why Multiple I/O Activities Matter

Embedded systems often need to do more than one thing at a time: - Blink an LED - Read a button state - Poll a sensor - Send data over Wi-Fi

If each task uses sleep() or long loops, the device becomes unresponsive.
Asynchronous programming allows tasks to cooperate by yielding control frequently.

2. Theory: Cooperative Multitasking with uasyncio

MicroPython provides uasyncio, a lightweight asynchronous framework.

Key ideas: - Coroutine: a function defined with async def - Await: pauses a coroutine without blocking other tasks - Task: a scheduled coroutine managed by the event loop - Event loop: coordinates all active tasks


This first example shows how to blink an LED without freezing other work.

Wiring

  • LED anode to GP15 through a 220Ω resistor
  • LED cathode to GND

Code: main.py

import uasyncio as asyncio
from machine import Pin

# Built-in LED on many Pico boards is often on GP25.
# Here we use an external LED on GP15 for clarity.
led = Pin(15, 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 main():
    await blink_led()

try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

LED ON
LED OFF
LED ON
LED OFF

What to Observe

  • The program does not use a blocking while True with sleep().
  • await asyncio.sleep() lets the system remain responsive.

Now add a second activity: reading a button while blinking continues.

Wiring

  • Button one side to GP14
  • Button other side to GND
  • Use the internal pull-up resistor

Code: main.py

import uasyncio as asyncio
from machine import Pin

led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

async def blink_led():
    while True:
        led.toggle()
        print("LED state:", led.value())
        await asyncio.sleep(0.5)

async def monitor_button():
    last_state = button.value()

    while True:
        current_state = button.value()
        if current_state != last_state:
            if current_state == 0:
                print("Button pressed")
            else:
                print("Button released")
            last_state = current_state
        await asyncio.sleep_ms(50)

async def main():
    task1 = asyncio.create_task(blink_led())
    task2 = asyncio.create_task(monitor_button())
    await asyncio.gather(task1, task2)

try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

LED state: 1
LED state: 0
Button pressed
Button released
LED state: 1

What to Observe

  • The LED continues blinking even while the button is being monitored.
  • Button changes are detected without interrupting the LED task.

Hands-On Exercise 3: Three Concurrent Activities

Add a third task to simulate sensor polling.

Code: main.py

import uasyncio as asyncio
from machine import Pin
import random

led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

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 monitor_button():
    last_state = button.value()

    while True:
        current_state = button.value()
        if current_state != last_state:
            if current_state == 0:
                print("Button pressed")
            else:
                print("Button released")
            last_state = current_state
        await asyncio.sleep_ms(25)

async def poll_sensor():
    while True:
        # Simulated sensor value
        reading = random.randint(20, 30)
        print("Sensor reading:", reading)
        await asyncio.sleep(2)

async def main():
    tasks = [
        asyncio.create_task(blink_led()),
        asyncio.create_task(monitor_button()),
        asyncio.create_task(poll_sensor())
    ]
    await asyncio.gather(*tasks)

try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

LED ON
Sensor reading: 24
LED OFF
Button pressed
Sensor reading: 27
LED ON

What to Observe

  • All tasks run in parallel cooperatively.
  • No task prevents the others from executing.

Theory: Best Practices for Multiple Async Tasks

  • Keep each task short and responsive.
  • Use await asyncio.sleep() frequently to yield control.
  • Use small polling intervals for buttons and sensors.
  • Avoid long blocking loops inside coroutines.
  • Use descriptive names for tasks.
  • Clean up tasks when your program ends or resets.

Hands-On Exercise 4: Event-Style Task Coordination

This example uses a shared variable to coordinate actions.

Goal

  • Blink an LED continuously.
  • Press the button to change the blink speed.

Code: main.py

import uasyncio as asyncio
from machine import Pin

led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

blink_delay = 0.5

async def blink_led():
    global blink_delay
    while True:
        led.toggle()
        print("Blink delay:", blink_delay)
        await asyncio.sleep(blink_delay)

async def button_controller():
    global blink_delay
    last_state = button.value()

    while True:
        current_state = button.value()
        if current_state != last_state:
            if current_state == 0:
                if blink_delay == 0.5:
                    blink_delay = 0.1
                    print("Fast blink mode")
                else:
                    blink_delay = 0.5
                    print("Normal blink mode")
            last_state = current_state
        await asyncio.sleep_ms(50)

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

try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

Blink delay: 0.5
Blink delay: 0.5
Fast blink mode
Blink delay: 0.1
Blink delay: 0.1
Normal blink mode

What to Observe

  • One task changes shared state.
  • Another task reacts to the shared state.
  • This is a simple pattern for user input controlling output.

Common Mistakes

  • Using sleep() instead of await asyncio.sleep()
  • Creating tasks but forgetting to keep the event loop running
  • Polling too slowly and missing button presses
  • Accessing shared variables without thinking about task interaction
  • Writing tasks that never yield control

Mini Challenge

Extend the program so that: - The LED blinks normally by default. - A button press switches the LED to fast blink mode. - A second button press switches it back to normal. - A sensor value is printed every 3 seconds.


Knowledge Check

  1. What is the difference between blocking and non-blocking code?
  2. Why is await asyncio.sleep() preferred in async tasks?
  3. What does asyncio.create_task() do?
  4. Why is it important for every task to yield control?
  5. How can shared variables be used between async tasks?

Summary

In this session, learners practiced managing multiple I/O activities using uasyncio in MicroPython. They learned how to: - Replace blocking delays with async pauses - Run multiple tasks cooperatively - Monitor buttons while blinking LEDs - Simulate and coordinate multiple concurrent activities


Next Session Preview

Session 4: Using Async Tasks with Real Hardware Inputs and Outputs
We will extend these patterns to real sensors and actuators, including more robust task coordination and practical IoT-style device behavior.


Back to Chapter | Back to Master Plan | Previous Session | Next Session