Session 1: Debugging Timing and Scheduling Problems
Synopsis
Focuses on identifying starvation, hidden blocking, missed events, and task interaction issues that are common in cooperative multitasking systems.
Session Content
Session 1: Debugging Timing and Scheduling Problems
Duration: ~45 minutes
Audience: Python developers with basic programming knowledge
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny
1. Session Goals
By the end of this session, learners will be able to:
- Recognize common timing and scheduling problems in MicroPython on Pico 2 W
- Debug issues caused by blocking code, poor task ordering, and incorrect delays
- Use
time.ticks_ms(),time.ticks_diff(), andasyncioto build reliable timing behavior - Compare blocking vs asynchronous approaches
- Apply practical debugging techniques to hardware projects
2. Prerequisites
- Raspberry Pi Pico 2 W with MicroPython installed
- Thonny IDE installed on a computer
- USB cable for Pico connection
- Basic knowledge of Python syntax
- Optional hardware for exercises:
- 1 LED
- 1 resistor (220Ω or 330Ω)
- 1 push button
- Breadboard and jumper wires
3. Development Environment Setup
Thonny Setup
- Install and open Thonny
- Connect the Pico 2 W via USB
- In Thonny, go to:
- Tools → Options → Interpreter
- Select:
- MicroPython (Raspberry Pi Pico)
- Port: the detected USB serial port
- Confirm the REPL works by running:
print("Pico ready")
File Management
- Save code to the Pico as:
main.pyfor automatic startup- separate test files such as
debug_timing.py - Use the Thonny shell to inspect runtime errors and printed timestamps
4. Theory: What Causes Timing and Scheduling Problems?
Common Problems
1. Blocking Delays
Using time.sleep() or long loops can prevent other tasks from running.
Example symptom: - LED stops responding - Button presses are missed - Network communication becomes unreliable
2. Incorrect Timing Comparisons
Using plain subtraction on millisecond counters can fail when timer values wrap around.
Correct approach:
- Use time.ticks_ms()
- Use time.ticks_diff()
3. Task Starvation
A long-running task can prevent other tasks from getting CPU time.
Example symptom: - One sensor updates, but another never does
4. Race-like Behavior in Cooperative Code
When tasks depend on shared timing or state, poor scheduling can create inconsistent results.
5. Key Debugging Tools
Useful MicroPython Timing Functions
time.ticks_ms()→ returns millisecond tick countertime.ticks_us()→ returns microsecond tick countertime.ticks_diff(a, b)→ safe difference between two tick valuestime.sleep(ms)/time.sleep_us(us)→ blocking delays
Debugging Techniques
- Print timestamps around critical sections
- Measure loop iteration duration
- Isolate one task at a time
- Reduce code to the smallest reproducible example
- Use LED blink patterns as visible timing indicators
6. Hands-On Exercise 1: Observe Blocking Behavior
Objective
Demonstrate how blocking delays affect responsiveness.
Hardware
- Pico 2 W
- Onboard LED or external LED
Wiring for External LED
- LED anode → GP15 through 220Ω resistor
- LED cathode → GND
Code: Blocking Blink with Long Delay
from machine import Pin
import time
# Use the onboard LED on many Pico boards.
# If needed, replace "LED" with a GPIO pin number like 15.
led = Pin("LED", Pin.OUT)
while True:
print("LED ON")
led.value(1)
time.sleep(2) # Blocking delay: other work cannot run here
print("LED OFF")
led.value(0)
time.sleep(2) # Blocking delay again
Expected Output
LED ON
LED OFF
LED ON
LED OFF
Discussion Points
- Why does nothing else happen during
sleep(2)? - What would happen if a button handler were added here?
- Why can long sleeps be a problem in IoT devices?
7. Hands-On Exercise 2: Measure Loop Timing
Objective
Track loop execution time and identify unexpected delays.
Code: Timing a Loop
import time
# Record the start time using the millisecond tick counter.
last_time = time.ticks_ms()
while True:
now = time.ticks_ms()
elapsed = time.ticks_diff(now, last_time)
print("Loop took", elapsed, "ms")
# Simulate work
time.sleep_ms(200)
# Update timestamp for the next loop
last_time = now
Expected Output
Loop took 0 ms
Loop took 200 ms
Loop took 200 ms
Loop took 200 ms
Notes
- The first line may differ slightly depending on execution timing
time.ticks_diff()is the correct way to compare tick values safely
8. Theory: Why ticks_diff() Matters
Millisecond counters eventually wrap around. If you compare values using normal subtraction in the wrong way, your logic may fail after long uptime.
Safe Pattern
import time
start = time.ticks_ms()
# ... later ...
elapsed = time.ticks_diff(time.ticks_ms(), start)
if elapsed > 1000:
print("One second has passed")
Avoid This Pattern for Long-Uptime Timing
elapsed = time.ticks_ms() - start
9. Hands-On Exercise 3: Non-Blocking LED Blink
Objective
Replace blocking delays with a non-blocking timing loop.
Code: Non-Blocking Blink
from machine import Pin
import time
led = Pin("LED", Pin.OUT)
# Store the time when the LED last changed state
last_toggle = time.ticks_ms()
# Start with LED off
led_state = 0
led.value(led_state)
while True:
now = time.ticks_ms()
# Check if 500 ms have passed since the last toggle
if time.ticks_diff(now, last_toggle) >= 500:
led_state = 1 - led_state # Toggle between 0 and 1
led.value(led_state)
last_toggle = now
print("LED state:", "ON" if led_state else "OFF", "| time:", now)
# Do other work here without blocking
# This could be reading a sensor, checking Wi-Fi, etc.
time.sleep_ms(10)
Expected Output
LED state: ON | time: 123456
LED state: OFF | time: 123956
LED state: ON | time: 124456
Learning Points
- The loop stays responsive
- Multiple tasks can be interleaved
- Short sleeps can reduce CPU usage without blocking behavior too much
10. Theory: Cooperative Scheduling with uasyncio
MicroPython supports asynchronous programming with uasyncio, which allows multiple tasks to cooperate by yielding control with await.
Benefits
- Better structure for multiple concurrent activities
- Easier management of periodic tasks
- Better responsiveness than large blocking loops
Key Idea
Each task must regularly yield control using await asyncio.sleep(...) or similar.
11. Hands-On Exercise 4: Debugging an Async Timing Task
Objective
Build two concurrent tasks and observe scheduling behavior.
Code: Two Async Tasks
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def blink_task():
"""Blink the LED every 1 second."""
while True:
led.toggle()
print("blink_task: LED =", led.value())
await asyncio.sleep(1)
async def logger_task():
"""Print a heartbeat every 300 ms."""
count = 0
while True:
count += 1
print("logger_task:", count)
await asyncio.sleep(0.3)
async def main():
# Run both tasks concurrently
await asyncio.gather(
blink_task(),
logger_task()
)
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Expected Output
blink_task: LED = 1
logger_task: 1
logger_task: 2
logger_task: 3
blink_task: LED = 0
logger_task: 4
logger_task: 5
Debugging Questions
- Do both tasks continue to run?
- What happens if one task uses
time.sleep(2)instead ofawait asyncio.sleep(2)? - Why is yielding important?
12. Hands-On Exercise 5: Introduce and Fix a Scheduling Bug
Objective
See how blocking code breaks async scheduling, then fix it.
Broken Code: Blocking Inside an Async Task
import uasyncio as asyncio
from machine import Pin
import time
led = Pin("LED", Pin.OUT)
async def bad_blink_task():
while True:
led.toggle()
print("bad_blink_task: LED =", led.value())
# Wrong: blocks the whole event loop
time.sleep(2)
async def heartbeat_task():
while True:
print("heartbeat")
await asyncio.sleep(0.5)
async def main():
await asyncio.gather(
bad_blink_task(),
heartbeat_task()
)
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Expected Behavior
heartbeatprints stop or become severely delayed
Fixed Code
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def good_blink_task():
while True:
led.toggle()
print("good_blink_task: LED =", led.value())
# Correct: yields control to other tasks
await asyncio.sleep(2)
async def heartbeat_task():
while True:
print("heartbeat")
await asyncio.sleep(0.5)
async def main():
await asyncio.gather(
good_blink_task(),
heartbeat_task()
)
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Expected Output
good_blink_task: LED = 1
heartbeat
heartbeat
heartbeat
good_blink_task: LED = 0
heartbeat
heartbeat
13. Practical Debugging Checklist
When timing or scheduling goes wrong, check:
- Are you using blocking calls?
-
time.sleep()inside async code is a common mistake -
Are delays too long?
-
Shorten them while testing
-
Are you using
ticks_diff()for time comparisons? -
Prevents wraparound errors
-
Are all tasks yielding regularly?
-
Use
await asyncio.sleep(...) -
Are print statements obscuring the problem?
-
Too much logging can slow execution
-
Is a hardware input being polled too slowly?
- Reduce loop delay or use asynchronous polling
14. Mini Challenge: Responsive Button and LED
Objective
Create a responsive loop that reads a button while blinking the LED.
Wiring
- Button one side → GP14
- Button other side → GND
- Use internal pull-up resistor in software
Code
from machine import Pin
import time
led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)
last_toggle = time.ticks_ms()
led_state = 0
while True:
now = time.ticks_ms()
# Blink every 500 ms
if time.ticks_diff(now, last_toggle) >= 500:
led_state = 1 - led_state
led.value(led_state)
last_toggle = now
# Read button without blocking
if button.value() == 0:
print("Button pressed")
led.value(1)
else:
led.value(led_state)
time.sleep_ms(10)
Expected Behavior
- LED blinks continuously
- Pressing the button forces the LED on
- Releasing the button returns LED to blink control
15. Review Questions
- Why is
time.sleep()problematic in asynchronous code? - What does
time.ticks_diff()protect against? - How does a non-blocking loop improve responsiveness?
- What is the role of
awaitinuasyncio? - How can print statements help debug timing issues?
16. Session Summary
In this session, you learned how to identify and debug timing and scheduling problems on the Pico 2 W. You practiced:
- Detecting blocking behavior
- Measuring loop durations
- Using safe tick-based timing
- Building non-blocking loops
- Writing and debugging asynchronous tasks with
uasyncio
These skills are foundational for reliable sensor polling, responsive input handling, and robust IoT applications.
17. Suggested Follow-Up Practice
- Convert a blocking sensor-reading loop into a non-blocking loop
- Add a second asynchronous task to send periodic data
- Replace print-based debugging with LED status patterns
- Experiment with different sleep intervals and observe task behavior