Skip to content

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(), and asyncio to 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

  1. Install and open Thonny
  2. Connect the Pico 2 W via USB
  3. In Thonny, go to:
  4. Tools → Options → Interpreter
  5. Select:
  6. MicroPython (Raspberry Pi Pico)
  7. Port: the detected USB serial port
  8. Confirm the REPL works by running:
print("Pico ready")

File Management

  • Save code to the Pico as:
  • main.py for 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 counter
  • time.ticks_us() → returns microsecond tick counter
  • time.ticks_diff(a, b) → safe difference between two tick values
  • time.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
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

Objective

Replace blocking delays with a non-blocking timing loop.

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 of await 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

  • heartbeat prints 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:

  1. Are you using blocking calls?
  2. time.sleep() inside async code is a common mistake

  3. Are delays too long?

  4. Shorten them while testing

  5. Are you using ticks_diff() for time comparisons?

  6. Prevents wraparound errors

  7. Are all tasks yielding regularly?

  8. Use await asyncio.sleep(...)

  9. Are print statements obscuring the problem?

  10. Too much logging can slow execution

  11. Is a hardware input being polled too slowly?

  12. 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

  1. Why is time.sleep() problematic in asynchronous code?
  2. What does time.ticks_diff() protect against?
  3. How does a non-blocking loop improve responsiveness?
  4. What is the role of await in uasyncio?
  5. 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

Back to Chapter | Back to Master Plan | Next Session