Skip to content

tools: add core1 flash-call checker for rp2 USB host builds#11115

Open
mikeysklar wants to merge 6 commits into
adafruit:mainfrom
mikeysklar:core1-flash-checker
Open

tools: add core1 flash-call checker for rp2 USB host builds#11115
mikeysklar wants to merge 6 commits into
adafruit:mainfrom
mikeysklar:core1-flash-checker

Conversation

@mikeysklar

Copy link
Copy Markdown
Collaborator

Follow-up to #10243 / #11114, answering "could we have caught this at build time?" Yes.

On RP2 USB host boards, core1 may only run code that lives in RAM. If it ever steps into flash, it crashes and USB host goes silent. We keep core1's functions in RAM, but nothing ever checked what those functions call. That's how #10243 slipped in.

This script checks the finished firmware: it follows every call core1 can make and fails the build with the guilty call chain if any of them lead into flash. Runs in ~2 seconds using arm-none-eabi-objdump.

Against current builds it catches three real cases:

  1. the USB Host Issues Using Gamepads #10243 bug (fixed by raspberrypi: make tusb_time_millis_api RAM-resident; core1 calls it #11114)
  2. Pico-PIO-USB's calc_usb_crc16 lives in flash. Verified on hardware: writing to a USB drive hard-locks the board. One-word upstream fix coming in a separate PR.
  3. a rare flash call path in TinyUSB's queue mutex. Only triggers under contention; will file separately.

Not wired into CI yet, since 2 and 3 would fail the build until they're fixed.

@dhalbert

Copy link
Copy Markdown
Collaborator

Let's wait to merge this until all the bugs this detects are fixed. Then you can add the CI check as part of this PR.

@mikeysklar

Copy link
Copy Markdown
Collaborator Author

Sounds good. I'll take a look at the mutex path

@tannewt

tannewt commented Jul 14, 2026

Copy link
Copy Markdown
Member

Very cool thank you! I've vibed up changes to LLVM a bit ago for the compiler to handle this. The current mechanisms are clearly insufficient. Thanks for the reminder that we need this.

mikeysklar added a commit to mikeysklar/circuitpython that referenced this pull request Jul 14, 2026
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 adafruit#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 (adafruit#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 <noreply@anthropic.com>
mikeysklar and others added 3 commits July 14, 2026 15:57
On raspberrypi boards with USB host, core1 runs the PIO-USB frame loop
behind an MPU region that makes flash inaccessible, so everything core1
reaches must be RAM-resident. The linker script places the entry points
in RAM, but nothing verified their callees — which is how adafruit#10243
happened: an upstream TinyUSB change added a flash-resident call inside
RAM-placed tuh_task_event_ready() and core1 died at first device attach.

check_core1_flash_calls.py disassembles the linked ELF, walks the static
call graph from core1_main (following linker veneers through their
literal-pool targets), and fails if any reachable function lives in
flash, printing the offending call chain.

Run against current builds it catches the adafruit#10243 regression, the
Pico-PIO-USB calc_usb_crc16 flash placement (hard-locks the board on
any multi-packet OUT transfer, e.g. writing to a USB drive), and a
latent flash call chain in the TinyUSB queue mutex contention path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fails the build with the offending call chain if any function reachable
from core1 lives in flash. Adds about 2 seconds per board build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikeysklar mikeysklar force-pushed the core1-flash-checker branch from f7e823d to b03cc85 Compare July 14, 2026 22:57
@mikeysklar

Copy link
Copy Markdown
Collaborator Author

All bugs the checker detects are now fixed on main (#11114, #11117, #11120). Added the CI check as requested: the rp2 Makefile now runs the checker after linking any CIRCUITPY_USB_HOST build and fails the build with the offending call chain (about 2 seconds per board). Verified locally: fails correctly on pre-fix trees, passes on current main.

@mikeysklar mikeysklar marked this pull request as ready for review July 14, 2026 23:27
The picodvi Framebuffer_RP2040 core1_main calls dvi_register_irqs_this_core
and dvi_start before it enables the MPU that blocks flash access, like the
usb_host core1_main does with common_hal_mcu_processor_get_frequency.
Fixes the 10 RP2040 DVI board build failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikeysklar

Copy link
Copy Markdown
Collaborator Author

@tannewt - cred for the idea of checking at linker level goes to @dhalbert

Looks good after last run. Last fix was to mark the DVI boards as special case as they have code being called before flash gets locked down.

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggestion on when to run this test. It'd be nice to cover all uses of core1.

Comment thread ports/raspberrypi/Makefile Outdated
picodvi on RP2040 also locks flash out via the MPU and runs core1
RAM-only. Check those builds too, rooting the walk at core1_main plus
the two entry points reached only through registered pointers:
dvi_dma1_irq and core1_scanline_callback. The checker now skips roots
absent from a given build instead of skipping the whole check.

This immediately flagged libdvi's flash-resident panic() on a
"can't happen" queue overflow in the IRQ handler; allow it explicitly
since core1 halts either way and the code is vendored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Core1 with flash MPU-locked (usb_host, picodvi RP2040) could reach the
SDK's flash-resident panic(), e.g. libdvi's TMDS queue overflow path,
which would hard fault before printing. Wrap panic at link time with a
RAM-resident implementation: core0 keeps the SDK behavior (print and
exit), core1 halts in RAM via breakpoint + spin since stdio is not
usable there. The core1 flash-call checker verifies the RAM path and
allows the core0-only print half by name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dhalbert dhalbert requested a review from tannewt July 16, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants