From 1e6b03da9336c61d9051c558ee85833b89d77721 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 14 Jul 2026 09:50:40 -0700 Subject: [PATCH] raspberrypi: keep core1's mutex-contention wait path out of flash When core1 posts a USB event while core0 holds the TinyUSB queue mutex, core1 falls into the pico-sdk blocking wait, which lived in flash. Core1 cannot execute from flash, so a contended moment hard faults core1 and USB host goes silent. Fixes #11116. On USB host builds only, --wrap best_effort_wfe_or_timeout and time_us_64 with RAM-resident implementations in usb_host/Port.c. The best_effort replacement mirrors the SDK's own documented PICO_TIME_DEFAULT_ALARM_POOL_DISABLED fallback (poll the clock, allow early return), so no alarm pool machinery is pulled into RAM. The time_us_64 replacement keeps the SDK's hi/lo/hi rollover re-read loop. Also RAM-place pico_int64_ops_aeabi.o (50 bytes) next to divider.o in the linker scripts: mutex_enter_timeout_ms multiplies ms to us through __aeabi_lmul, and the SDK already wraps that symbol so it cannot be wrapped again per-feature. Cost: +64 bytes RAM on USB host builds, 50 bytes on builds with CIRCUITPY_USB_HOST=0 (Pico baseline 70320 -> 70384). The previous approach in this branch cost 1216 bytes on every build. With this, the core1 flash-call checker (#11115) passes on Feather RP2040 USB Host, Fruit Jam, and Raspberry Pi Pico builds. Smoke tested on the Feather: 20 SCSI read/write cycles to a flash drive. Co-Authored-By: Claude Fable 5 --- ports/raspberrypi/Makefile | 6 ++++ ports/raspberrypi/common-hal/usb_host/Port.c | 34 ++++++++++++++++++++ ports/raspberrypi/link-rp2040.ld | 2 +- ports/raspberrypi/link-rp2350.ld | 2 +- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ports/raspberrypi/Makefile b/ports/raspberrypi/Makefile index 96ba25ce30c..6d9b557e0ba 100644 --- a/ports/raspberrypi/Makefile +++ b/ports/raspberrypi/Makefile @@ -557,6 +557,12 @@ SRC_C += \ INC += \ -isystem lib/Pico-PIO-USB/src + +# Core1 posts USB events while core0 may hold the TinyUSB queue mutex. The +# contended wait path calls these two flash-resident SDK functions, and core1 +# must not execute from flash. Replace them with RAM-resident implementations +# in common-hal/usb_host/Port.c on USB host builds only. See issue #11116. +PICO_LDFLAGS += -Wl,--wrap=best_effort_wfe_or_timeout -Wl,--wrap=time_us_64 endif ifeq ($(CIRCUITPY_PICODVI),1) diff --git a/ports/raspberrypi/common-hal/usb_host/Port.c b/ports/raspberrypi/common-hal/usb_host/Port.c index da09fdc91c0..f06e9e5d71d 100644 --- a/ports/raspberrypi/common-hal/usb_host/Port.c +++ b/ports/raspberrypi/common-hal/usb_host/Port.c @@ -34,6 +34,40 @@ usb_host_port_obj_t usb_host_instance; volatile bool _core1_ready = false; +// Core1 posts TinyUSB events while core0 may hold the queue FIFO mutex. The +// contended wait path calls best_effort_wfe_or_timeout() and time_us_64(), +// which normally live in flash, but core1 must not touch flash (see the MPU +// setup in core1_main below). These RAM-resident replacements are linked in +// with --wrap on USB host builds only (see the port Makefile). Issue #11116. +// +uint64_t __wrap_time_us_64(void); +bool __wrap_best_effort_wfe_or_timeout(absolute_time_t timeout_timestamp); + +// The same hi/lo/hi re-read loop as the SDK's timer_time_us_64, to defeat +// rollover between the two 32-bit halves. +uint64_t __not_in_flash_func(__wrap_time_us_64)(void) { + uint32_t hi = timer_hw->timerawh; + uint32_t lo; + do { + lo = timer_hw->timerawl; + uint32_t next_hi = timer_hw->timerawh; + if (hi == next_hi) { + break; + } + hi = next_hi; + } while (true); + return ((uint64_t)hi << 32) | lo; +} + +// Mirrors the SDK's own PICO_TIME_DEFAULT_ALARM_POOL_DISABLED fallback +// (a poll of the clock with no __wfe): "best effort" explicitly permits +// returning early, and every SDK caller re-checks in a loop. Polling honors +// the timeout exactly and avoids dragging the alarm pool machinery into RAM. +bool __not_in_flash_func(__wrap_best_effort_wfe_or_timeout)(absolute_time_t timeout_timestamp) { + tight_loop_contents(); + return __wrap_time_us_64() >= to_us_since_boot(timeout_timestamp); +} + static void __not_in_flash_func(core1_main)(void) { // The MPU is reset before this starts. SysTick->LOAD = (uint32_t)((common_hal_mcu_processor_get_frequency() / 1000) - 1UL); diff --git a/ports/raspberrypi/link-rp2040.ld b/ports/raspberrypi/link-rp2040.ld index 6e7bd15a7b6..189c3d7c338 100644 --- a/ports/raspberrypi/link-rp2040.ld +++ b/ports/raspberrypi/link-rp2040.ld @@ -82,7 +82,7 @@ SECTIONS *(.property_getset) __property_getset_end = .; - *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a: *interp.o *divider.o *tusb_fifo.o *mem_ops_aeabi.o *usbh.o) .text*) + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a: *interp.o *divider.o *pico_int64_ops_aeabi.o *tusb_fifo.o *mem_ops_aeabi.o *usbh.o) .text*) /* Allow everything in usbh.o except tuh_task_event_ready because we read it from core 1. */ *usbh.o (.text.[_uphc]* .text.tuh_[cmved]* .text.tuh_task_ext*) *(.fini) diff --git a/ports/raspberrypi/link-rp2350.ld b/ports/raspberrypi/link-rp2350.ld index a2cc62909e6..3c339b03fe2 100644 --- a/ports/raspberrypi/link-rp2350.ld +++ b/ports/raspberrypi/link-rp2350.ld @@ -63,7 +63,7 @@ SECTIONS *(.property_getset) __property_getset_end = .; - *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a: *interp.o *divider.o *tusb_fifo.o *mem_ops_aeabi.o *usbh.o *string0.o) .text*) + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a: *interp.o *divider.o *pico_int64_ops_aeabi.o *tusb_fifo.o *mem_ops_aeabi.o *usbh.o *string0.o) .text*) /* Allow everything in usbh.o except tuh_task_event_ready because we read it from core 1. */ *usbh.o (.text.[_uphc]* .text.tuh_[cmved]* .text.tuh_task_ext*) *(.fini)