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
uasyncioto 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
- Download and install Thonny.
- Open Thonny.
- Select the interpreter:
- Tools > Options > Interpreter
- Choose MicroPython (Raspberry Pi Pico) or the appropriate Pico MicroPython interpreter
- Connect the Pico 2 W via USB.
- Select the correct port if prompted.
2. Flash MicroPython to the Pico 2 W
- Hold the BOOTSEL button while connecting the board to USB.
- The Pico appears as a USB storage device.
- Copy the latest MicroPython
.uf2firmware file for the Pico series to the device. - 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.
Hands-On Exercise 1: Simple LED Blink with uasyncio
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.
Code: Non-Blocking Blink
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
...
Hands-On Exercise 2: Blink While Doing Something Else
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
- Why is
time.sleep()problematic in embedded async programs? - What does
awaitdo? - What is the purpose of the event loop?
- Why use
create_task()? - 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
uasynciotiming functions - Prepare a sensor like DHT22, PIR, or HC-SR501 for the next session