Session 1: Async Patterns for Sensor Sampling
Synopsis
Covers periodic sensor reads, sampling intervals, filtering considerations, and balancing freshness, accuracy, and CPU usage in async loops.
Session Content
Session 1: Async Patterns for Sensor Sampling
Session Overview
Duration: ~45 minutes
Topic: Async Patterns for Sensor Sampling
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny
Learning Objectives
By the end of this session, learners will be able to:
- Explain why asynchronous programming is useful for sensor sampling on microcontrollers.
- Set up a MicroPython development environment in Thonny for the Pico 2 W.
- Use uasyncio to sample multiple sensors without blocking other tasks.
- Combine periodic sensor reads with LED status updates and serial logging.
- Structure a simple event-driven MicroPython application for IoT use cases.
Prerequisites
- Basic Python knowledge
- Raspberry Pi Pico 2 W with MicroPython installed
- Thonny IDE installed
- Breadboard, jumper wires
- 1x LED + 220Ω resistor
- 1x button
- 1x potentiometer or analog sensor
- Optional: DHT11/DHT22 or another digital sensor
1) Session Agenda
0–5 min: Introduction
- What asynchronous programming solves in embedded systems
- Blocking vs non-blocking sensor sampling
- Typical Pico 2 W use cases:
- periodic sensor monitoring
- responsive UI/button handling
- networking and telemetry
5–15 min: Theory
- The MicroPython event loop
- Coroutines with
async def awaitand cooperative multitasking- Common async patterns:
- periodic tasks
- producer/consumer
- event flags
- task cancellation and cleanup
15–25 min: Environment Setup
- Configure Thonny for Pico 2 W
- Verify MicroPython firmware
- Run a basic async test
- Connect a sensor and output device
25–40 min: Hands-on Exercise
- Build an async sensor sampler:
- read a digital sensor periodically
- blink an LED independently
- print readings to the REPL
- keep the system responsive
40–45 min: Review and Wrap-up
- Discuss best practices
- Review common async mistakes
- Preview next session
2) Theory: Why Async Matters on Pico 2 W
Microcontrollers have limited CPU, memory, and timing flexibility compared to desktop systems. When code uses blocking delays like time.sleep(), the entire program pauses. That can cause:
- missed button presses
- delayed sensor updates
- sluggish LED or display behavior
- networking timeouts
With uasyncio, tasks cooperate by yielding control using await. This allows multiple activities to progress smoothly:
- sample a sensor every second
- blink an LED every 250 ms
- listen for input events
- prepare for Wi-Fi messaging
Blocking Example
import time
from machine import Pin
led = Pin("LED", Pin.OUT)
while True:
led.toggle()
print("LED toggled")
time.sleep(1)
This works, but nothing else can run during sleep().
Async Equivalent
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def blink():
while True:
led.toggle()
print("LED toggled")
await asyncio.sleep(1)
asyncio.run(blink())
This yields control back to the event loop during await asyncio.sleep(1).
3) Development Environment Setup in Thonny
Install and Configure Thonny
- Install Thonny from the official website.
- Connect the Pico 2 W via USB.
- Open Thonny.
- Go to:
- Run > Select interpreter
- Choose MicroPython (Raspberry Pi Pico)
- Select the correct port if needed.
- Open the Shell pane to verify the REPL.
Verify MicroPython
In the Thonny shell:
import sys
print(sys.implementation)
Example output:
(name='micropython', version=(1, 24, 0), mpy=...)
Confirm uasyncio Availability
import uasyncio as asyncio
print(asyncio)
Example output:
<module 'uasyncio'>
4) Wiring for the Exercise
Required Components
- Pico 2 W
- External LED + resistor
- Button
- Potentiometer or analog sensor
Suggested Connections
LED
- LED anode (+) → GPIO 15 through 220Ω resistor
- LED cathode (−) → GND
Button
- One side → GPIO 14
- Other side → GND
Use internal pull-up resistor in code.
Potentiometer
- One end → 3V3
- Other end → GND
- Middle pin → GPIO 26 / ADC0
5) Hands-on Exercise: Async Sensor Sampling
Goal
Create a MicroPython application that: - samples an analog sensor periodically - monitors a button without blocking - blinks the onboard LED independently - prints status updates cleanly
6) Code: Async Sensor Sampler
Save this as main.py on the Pico.
# Async Sensor Sampling Demo for Raspberry Pi Pico 2 W
# - Samples an analog input periodically
# - Monitors a button without blocking
# - Blinks the onboard LED independently
#
# Hardware:
# - Potentiometer middle pin -> GPIO 26 (ADC0)
# - Potentiometer ends -> 3V3 and GND
# - Button -> GPIO 14 and GND
# - LED -> onboard LED
import uasyncio as asyncio
from machine import Pin, ADC
# ----------------------------
# Hardware setup
# ----------------------------
led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)
adc = ADC(26) # ADC0 on GPIO26
# ----------------------------
# Shared state
# ----------------------------
button_pressed = False
# ----------------------------
# Async tasks
# ----------------------------
async def blink_led():
"""Blink the onboard LED every 0.5 seconds."""
while True:
led.toggle()
await asyncio.sleep(0.5)
async def sample_sensor():
"""Read the ADC periodically and print a scaled value."""
while True:
raw = adc.read_u16() # 0..65535
voltage_ratio = raw / 65535
print("ADC raw:", raw, "ratio:", round(voltage_ratio, 3))
await asyncio.sleep(1)
async def monitor_button():
"""Poll the button and detect presses without blocking."""
global button_pressed
last_state = button.value()
while True:
current_state = button.value()
# Detect a press: pull-up means 1 -> 0 is a press
if last_state == 1 and current_state == 0:
button_pressed = True
print("Button pressed!")
last_state = current_state
await asyncio.sleep_ms(50)
async def report_status():
"""Report combined system status."""
global button_pressed
while True:
if button_pressed:
print("Status: button event handled")
button_pressed = False
else:
print("Status: running normally")
await asyncio.sleep(2)
# ----------------------------
# Main entry point
# ----------------------------
async def main():
"""Run all tasks concurrently."""
print("Starting async sensor sampling demo...")
tasks = [
asyncio.create_task(blink_led()),
asyncio.create_task(sample_sensor()),
asyncio.create_task(monitor_button()),
asyncio.create_task(report_status()),
]
# Keep the program alive
await asyncio.gather(*tasks)
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
7) Expected Output
When the board is running, the Thonny REPL may show output similar to:
Starting async sensor sampling demo...
Status: running normally
ADC raw: 32890 ratio: 0.502
Button pressed!
Status: button event handled
ADC raw: 41210 ratio: 0.629
Status: running normally
The onboard LED should blink every 0.5 seconds while sensor readings continue in the background.
8) Exercise Steps
Step 1: Upload the Code
- Open Thonny
- Paste the code into a new file
- Save it as
main.pyon the Pico 2 W
Step 2: Run the Program
- Click Run
- Observe LED blinking and serial output
Step 3: Adjust Sampling Rates
Modify these lines:
- await asyncio.sleep(1) in sample_sensor()
- await asyncio.sleep(0.5) in blink_led()
- await asyncio.sleep_ms(50) in monitor_button()
Try: - faster sensor sampling - slower LED blinking - different button polling intervals
Step 4: Observe Responsiveness
Press the button while the LED is blinking and sensor data is printing. Notice that all tasks continue to run without blocking.
9) Guided Extension: Add a Threshold Alarm
Add logic to detect when the analog reading exceeds a threshold.
Modified Sensor Task
async def sample_sensor():
"""Read the ADC periodically and print an alarm if threshold is exceeded."""
threshold = 40000
while True:
raw = adc.read_u16()
print("ADC raw:", raw)
if raw > threshold:
print("ALARM: Sensor value above threshold!")
await asyncio.sleep(1)
Example Output
ADC raw: 39120
ADC raw: 42111
ALARM: Sensor value above threshold!
ADC raw: 43890
ALARM: Sensor value above threshold!
10) Best Practices for Async MicroPython on Pico 2 W
- Keep each task short and cooperative.
- Use
await asyncio.sleep()instead of long blocking loops. - Prefer
sleep_ms()for fine-grained polling. - Avoid doing heavy computation inside tasks.
- Use shared state carefully and keep it simple.
- Structure code into small, testable coroutines.
- Add cleanup with
try/finallywhen usingasyncio.run().
11) Common Mistakes
Mistake 1: Using time.sleep() in Async Code
This blocks all tasks.
Mistake 2: Forgetting to await
Coroutines must yield control regularly.
Mistake 3: Busy Waiting
A loop with no sleep can starve other tasks.
Mistake 4: Overly Complex Shared State
Keep communication between tasks simple in early projects.
12) Quick Knowledge Check
- Why is
time.sleep()problematic in an async program? - What does
await asyncio.sleep()do? - How does the button task detect a press?
- Why is polling acceptable in this example?
- Which task would you change to make the LED blink faster?
13) Mini Challenge
Modify the program so that: - the LED blinks faster when the sensor value is high - the button resets the alarm state - sensor readings are displayed as percentages
Hint
Use a shared variable like sensor_value and update the LED task based on it.
14) Session Wrap-up
Key Takeaways
- Async programming improves responsiveness on microcontrollers.
uasyncioenables concurrent tasks on the Pico 2 W.- Periodic sensor sampling is a great use case for coroutines.
- Small, cooperative tasks are easier to manage and extend.
Next Session Preview
- Using async patterns with Wi-Fi connectivity
- Sending sensor data to an IoT endpoint
- Combining timers, network requests, and sensor reads