sleep_us()/sleep_until() hangs forever on RP2350 (SDK 2.3.0): sleep_until() waits indefinitely with hardware alarm unarmed #3078

Closed
opened 2026-07-18 10:37:46 -05:00 by uranum12 · 1 comment
uranum12 commented 2026-07-18 10:37:46 -05:00 (Migrated from github.com)

Environment

  • pico-sdk: 2.3.0
  • Target: RP2350 (Pico 2)
  • Toolchain: arm-none-eabi-gcc (Arm GNU Toolchain 15.2.Rel1 (Build arm-15.86)) 15.2.1 20251203
  • Host OS: macOS (Homebrew)
  • Build system: CMake + Ninja
  • Reproduces with both:
    • -O3 -DNDEBUG (CMAKE_BUILD_TYPE=Release)
    • -Og (CMAKE_BUILD_TYPE=Debug)
  • Reproduces both:
    • with a debugger attached
    • standalone after flashing, with no debugger attached

Debugging setup used during investigation (not required to reproduce):

  • OpenOCD: raspberrypi/openocd, branch rpi-common, commit acff23ffd100479d6481a1155123774b79a990b9
    (0.12.0+dev-00004-gacff23ffd)
  • GDB: GNU gdb 17.2

Comparison test:

  • RP2040 (Raspberry Pi Pico): could not reproduce with the same code and SDK version
  • RP2350 (Pico 2): reproduces as described above

Minimal repro

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(sleep-bug)

pico_sdk_init()

add_executable(main main.c)
target_link_libraries(main pico_stdlib)

pico_enable_stdio_usb(main 0)
pico_enable_stdio_uart(main 1)

pico_add_extra_outputs(main)

main.c

#include <stdio.h>

#include <pico/stdio.h>
#include <pico/time.h>

int main() {
    stdio_init_all();
    printf("start\n");

    for (int i = 0; i < 10; ++i) {
        sleep_us(10);
    }

    printf("end\n");
    return 0;
}

Steps to reproduce

  1. Build for RP2350 (PICO_BOARD=pico2)
  2. Flash to the board
  3. Observe UART output

Expected behavior

start is printed, followed shortly by end.

Actual behavior

start is printed, but end never appears.

When inspected with a debugger, execution is stuck in the sleep_us() → sleep_until() path and does not return from the sleep call.

Reproduction characteristics

The issue is sensitive to the number of consecutive sleep_us() calls.

Observed on my setup:

  • 1 call: could not reproduce
  • 3 consecutive calls: reproduces roughly 50% of runs
  • 10 consecutive calls: reproduces nearly every run

The loop in the reproducer is only intended to make the failure more reproducible.

The issue is not specific to the for loop itself. Replacing the loop with multiple consecutive sleep_us() calls produces the same behavior.

Debugging details

Backtrace obtained via OpenOCD + GDB (-Og build):

#0  sleep_until (t=<optimized out>) at /pico-sdk/src/common/pico_time/time.c:418
#1  0x10000ed8 in sleep_us (us=<optimized out>) at /pico-sdk/src/common/pico_time/include/pico/time.h:120
#2  0x100001f8 in main () at /sleep-bug/main.c:11

The PC is consistently observed around the __wfe() wait path reached from:

if (add_alarm_at(t_before, sleep_until_callback, NULL, false) >= 0) {
    // able to add alarm for just before the time
    while (!time_reached(t_before)) {
        uint32_t save = spin_lock_blocking(sleep_notifier.spin_lock);
        lock_internal_spin_unlock_with_wait(&sleep_notifier, save);
    }
}

With -O3, more code is inlined, but repeated debugger interrupts still place execution immediately after a wfe instruction inside the same wait path.

Register dump while hung (TIMER0 @ 0x400B0000)

(gdb) p/x ((timer_hw_t*)0x400B0000)->alarm[3]
$1 = 0x528

(gdb) p/x ((timer_hw_t*)0x400B0000)->inte
$2 = 0x8

(gdb) p/x ((timer_hw_t*)0x400B0000)->ints
$3 = 0x0

(gdb) p/x ((timer_hw_t*)0x400B0000)->armed
$4 = 0x0

(gdb) p/x ((timer_hw_t*)0x400B0000)->timerawl
$5 = 0x49ace2

Observed state:

  • alarm[3] (default alarm pool hardware alarm) contains 0x528 (1,320 us).
  • timerawl contains 0x49ace2 (4,828,386 us), far past the programmed alarm value.
  • armed == 0x0; alarm 3 is not armed.
  • inte == 0x8; alarm 3 interrupts are enabled.
  • ints == 0x0; no alarm interrupt is pending.
  • timerawl was observed to continue incrementing across multiple reads, indicating that the timer itself is still running.

At the point where execution is stuck in sleep_until(), the timer counter continues to advance, the programmed alarm value lies far in the past, and the corresponding hardware alarm is not armed.

I have not yet determined how this state is reached.

Additional notes

  • Replacing sleep_us() with busy_wait_us() avoids the hang.
  • The issue reproduces with and without a debugger attached.
  • The issue reproduces in both Debug and Release builds.

Question

Given the observed state:

  • execution blocked in the sleep_until() wait path,
  • timer counter still advancing,
  • alarm value already in the past,
  • no pending interrupt,
  • hardware alarm not armed,

does this match any known RP2350 or pico-sdk alarm scheduling issue?

## Environment - pico-sdk: 2.3.0 - Target: RP2350 (Pico 2) - Toolchain: `arm-none-eabi-gcc (Arm GNU Toolchain 15.2.Rel1 (Build arm-15.86)) 15.2.1 20251203` - Host OS: macOS (Homebrew) - Build system: CMake + Ninja - Reproduces with both: - `-O3 -DNDEBUG` (`CMAKE_BUILD_TYPE=Release`) - `-Og` (`CMAKE_BUILD_TYPE=Debug`) - Reproduces both: - with a debugger attached - standalone after flashing, with no debugger attached Debugging setup used during investigation (not required to reproduce): - OpenOCD: `raspberrypi/openocd`, branch `rpi-common`, commit `acff23ffd100479d6481a1155123774b79a990b9` (`0.12.0+dev-00004-gacff23ffd`) - GDB: GNU gdb 17.2 Comparison test: - RP2040 (Raspberry Pi Pico): could not reproduce with the same code and SDK version - RP2350 (Pico 2): reproduces as described above ## Minimal repro `CMakeLists.txt` ```cmake cmake_minimum_required(VERSION 3.13) include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake) project(sleep-bug) pico_sdk_init() add_executable(main main.c) target_link_libraries(main pico_stdlib) pico_enable_stdio_usb(main 0) pico_enable_stdio_uart(main 1) pico_add_extra_outputs(main) ``` `main.c` ```c #include <stdio.h> #include <pico/stdio.h> #include <pico/time.h> int main() { stdio_init_all(); printf("start\n"); for (int i = 0; i < 10; ++i) { sleep_us(10); } printf("end\n"); return 0; } ``` ## Steps to reproduce 1. Build for RP2350 (`PICO_BOARD=pico2`) 2. Flash to the board 3. Observe UART output ## Expected behavior `start` is printed, followed shortly by `end`. ## Actual behavior `start` is printed, but `end` never appears. When inspected with a debugger, execution is stuck in the `sleep_us()` → `sleep_until()` path and does not return from the sleep call. ## Reproduction characteristics The issue is sensitive to the number of consecutive `sleep_us()` calls. Observed on my setup: - 1 call: could not reproduce - 3 consecutive calls: reproduces roughly 50% of runs - 10 consecutive calls: reproduces nearly every run The loop in the reproducer is only intended to make the failure more reproducible. The issue is not specific to the `for` loop itself. Replacing the loop with multiple consecutive `sleep_us()` calls produces the same behavior. ## Debugging details Backtrace obtained via OpenOCD + GDB (`-Og` build): ```text #0 sleep_until (t=<optimized out>) at /pico-sdk/src/common/pico_time/time.c:418 #1 0x10000ed8 in sleep_us (us=<optimized out>) at /pico-sdk/src/common/pico_time/include/pico/time.h:120 #2 0x100001f8 in main () at /sleep-bug/main.c:11 ``` The PC is consistently observed around the `__wfe()` wait path reached from: ```c if (add_alarm_at(t_before, sleep_until_callback, NULL, false) >= 0) { // able to add alarm for just before the time while (!time_reached(t_before)) { uint32_t save = spin_lock_blocking(sleep_notifier.spin_lock); lock_internal_spin_unlock_with_wait(&sleep_notifier, save); } } ``` With `-O3`, more code is inlined, but repeated debugger interrupts still place execution immediately after a `wfe` instruction inside the same wait path. ## Register dump while hung (TIMER0 @ 0x400B0000) ```text (gdb) p/x ((timer_hw_t*)0x400B0000)->alarm[3] $1 = 0x528 (gdb) p/x ((timer_hw_t*)0x400B0000)->inte $2 = 0x8 (gdb) p/x ((timer_hw_t*)0x400B0000)->ints $3 = 0x0 (gdb) p/x ((timer_hw_t*)0x400B0000)->armed $4 = 0x0 (gdb) p/x ((timer_hw_t*)0x400B0000)->timerawl $5 = 0x49ace2 ``` Observed state: - `alarm[3]` (default alarm pool hardware alarm) contains `0x528` (1,320 us). - `timerawl` contains `0x49ace2` (4,828,386 us), far past the programmed alarm value. - `armed == 0x0`; alarm 3 is not armed. - `inte == 0x8`; alarm 3 interrupts are enabled. - `ints == 0x0`; no alarm interrupt is pending. - `timerawl` was observed to continue incrementing across multiple reads, indicating that the timer itself is still running. At the point where execution is stuck in `sleep_until()`, the timer counter continues to advance, the programmed alarm value lies far in the past, and the corresponding hardware alarm is not armed. I have not yet determined how this state is reached. ## Additional notes - Replacing `sleep_us()` with `busy_wait_us()` avoids the hang. - The issue reproduces with and without a debugger attached. - The issue reproduces in both Debug and Release builds. ## Question Given the observed state: - execution blocked in the `sleep_until()` wait path, - timer counter still advancing, - alarm value already in the past, - no pending interrupt, - hardware alarm not armed, does this match any known RP2350 or pico-sdk alarm scheduling issue?
kilograham commented 2026-08-19 18:22:13 -05:00 (Migrated from github.com)

fix merged into develop

fix merged into develop
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-raspberrypi/pico-sdk#3078
No description provided.