Skip to content

Session 1: Blinking Without Blocking

Synopsis

Uses LEDs and timed patterns to demonstrate concurrent hardware actions and reinforce the difference between blocking loops and cooperative scheduling.

Session Content

Session 1: Blinking Without Blocking

Session Overview

Duration: ~45 minutes
Goal: Learn how to write non-blocking MicroPython code on Raspberry Pi Pico 2 W using uasyncio, so your LED blinking continues while other tasks run at the same time.


Learning Objectives

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

  • Explain why time.sleep() can be a problem in embedded programs
  • Use uasyncio to run concurrent tasks in MicroPython
  • Blink an LED without blocking other code
  • Start a second task that runs alongside blinking
  • Understand basic event-loop concepts for Pico development

Prerequisites

  • Basic Python knowledge
  • Raspberry Pi Pico 2 W
  • Micro USB or USB-C cable suitable for the board
  • Thonny IDE installed on a computer
  • One onboard LED or an external LED with a resistor
  • Access to a MicroPython firmware image for RP2040/RP2350-based Pico boards

Environment Setup

1. Install Thonny

  1. Download and install Thonny.
  2. Open Thonny.
  3. Select the interpreter:
  4. Tools > Options > Interpreter
  5. Choose MicroPython (Raspberry Pi Pico) or the appropriate Pico MicroPython interpreter
  6. Connect the Pico 2 W via USB.
  7. Select the correct port if prompted.

2. Flash MicroPython to the Pico 2 W

  1. Hold the BOOTSEL button while connecting the board to USB.
  2. The Pico appears as a USB storage device.
  3. Copy the latest MicroPython .uf2 firmware file for the Pico series to the device.
  4. The board will reboot automatically.

3. Confirm Connection

Open the Thonny Shell and run:

print("Hello, Pico 2 W!")

Expected output:

Hello, Pico 2 W!

Theory: Why “Without Blocking” Matters

In embedded programming, a blocking delay pauses the entire program. For example:

import time
time.sleep(1)

While sleeping, the device cannot do anything else.

This is a problem when you want to: - Blink an LED - Read sensors - Check buttons - Send data over Wi-Fi - Respond to incoming network requests

Using uasyncio, your code can perform multiple tasks “concurrently” without freezing the whole program.


Key Concepts

Blocking Code

A blocking function stops the rest of the program from running until it finishes.

Example:

time.sleep(2)

Non-Blocking Code

A non-blocking task gives control back to the event loop using await.

Example:

await asyncio.sleep(2)

Event Loop

The event loop schedules tasks and lets them run in turn.


Hardware

  • Use the onboard LED if available on your Pico 2 W
  • If using an external LED:
  • Connect LED anode to a GPIO pin through a 220Ω resistor
  • Connect LED cathode to GND

Pin Note

Many Pico boards expose the onboard LED on Pin("LED"). If your board firmware supports it, this is the simplest option.

Create a new file in Thonny and save it as main.py on the Pico:

# main.py
# Non-blocking LED blink example using uasyncio
# Raspberry Pi Pico 2 W + MicroPython

import uasyncio as asyncio
from machine import Pin


# Use the onboard LED if supported by your board firmware
led = Pin("LED", Pin.OUT)


async def blink_led():
    """Blink the LED forever without blocking other tasks."""
    while True:
        led.on()
        print("LED ON")
        await asyncio.sleep(0.5)

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


async def main():
    """Main coroutine that starts application tasks."""
    await blink_led()


# Start the asyncio event loop
asyncio.run(main())

Expected Output

In Thonny Shell:

LED ON
LED OFF
LED ON
LED OFF
...

Now we will run two tasks at the same time: 1. Blink the LED 2. Print a counter every second

Code: Two Concurrent Tasks

Replace main.py with the following:

# main.py
# Two concurrent tasks using uasyncio

import uasyncio as asyncio
from machine import Pin


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


async def blink_led():
    """Toggle the LED every 0.5 seconds."""
    while True:
        led.toggle()
        print("LED toggled")
        await asyncio.sleep(0.5)


async def report_status():
    """Print a status message every 1 second."""
    counter = 0
    while True:
        counter += 1
        print("Status tick:", counter)
        await asyncio.sleep(1)


async def main():
    """Create and run multiple tasks concurrently."""
    asyncio.create_task(blink_led())
    asyncio.create_task(report_status())

    # Keep the program alive forever
    while True:
        await asyncio.sleep(10)


asyncio.run(main())

Expected Output

LED toggled
Status tick: 1
LED toggled
LED toggled
Status tick: 2
LED toggled
Status tick: 3
...

Exercise Discussion

What changed?

  • The LED keeps blinking
  • The counter prints at its own pace
  • Neither task blocks the other

Why does this matter?

This is the foundation for: - Sensor polling - Button handling - Wi-Fi communication - Web servers - Data logging


Hands-On Exercise 3: Add a Button Without Blocking

Hardware

  • One pushbutton
  • One 10kΩ pull-down resistor or use internal pull-up

Wiring Example

If using internal pull-up: - Button connected between GPIO and GND

Code: Button-Triggered Message

This example checks a button state periodically while blinking continues.

# main.py
# Non-blocking LED blink plus button monitoring

import uasyncio as asyncio
from machine import Pin


led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)  # Button between GPIO14 and GND


async def blink_led():
    """Blink the onboard LED."""
    while True:
        led.on()
        await asyncio.sleep(0.5)
        led.off()
        await asyncio.sleep(0.5)


async def monitor_button():
    """Check button state without blocking other tasks."""
    last_state = button.value()

    while True:
        current_state = button.value()

        # Detect button press: pull-up means 1 -> 0 when pressed
        if last_state == 1 and current_state == 0:
            print("Button pressed!")

        last_state = current_state
        await asyncio.sleep_ms(50)


async def main():
    """Run LED blink and button monitor concurrently."""
    asyncio.create_task(blink_led())
    asyncio.create_task(monitor_button())

    while True:
        await asyncio.sleep(10)


asyncio.run(main())

Example Output

Button pressed!
Button pressed!

Common Mistakes

1. Using time.sleep() inside async code

Avoid this:

import time
await asyncio.sleep(1)  # good
time.sleep(1)           # bad in async tasks

2. Forgetting await

Async functions must yield control back to the event loop.

3. Exiting main() too soon

If main() finishes, the program stops unless tasks are kept alive.

4. Incorrect pin numbering

Always confirm the correct GPIO pin for your wiring.


Best Practices

  • Use await asyncio.sleep() instead of blocking delays
  • Keep each task small and focused
  • Use descriptive function names
  • Comment hardware setup clearly
  • Test one task at a time before combining tasks
  • Use create_task() for background jobs

Mini Challenge

Modify the blink interval so that: - The LED turns on for 100 ms - The LED turns off for 900 ms

Hint

Use:

await asyncio.sleep_ms(100)
await asyncio.sleep_ms(900)

Review Questions

  1. Why is time.sleep() problematic in embedded async programs?
  2. What does await do?
  3. What is the purpose of the event loop?
  4. Why use create_task()?
  5. How does non-blocking code help with IoT applications?

Summary

In this session, you learned how to: - Avoid blocking delays - Use uasyncio on the Pico 2 W - Run multiple tasks at once - Blink an LED while handling other work - Build the foundation for responsive embedded applications


Suggested Next Session Preparation

  • Read about GPIO input handling
  • Learn how to debounce buttons
  • Experiment with uasyncio timing functions
  • Prepare a sensor like DHT22, PIR, or HC-SR501 for the next session

Back to Chapter | Back to Master Plan | Next Session