pico_cyw43_driver: cybt: re-read transient corrupt ring index #3027
No reviewers
Labels
No labels
bazel
bluetooth
board settings
boot_stage2
bug
build
c++
can't reproduce
clang
clib-interface
cmsis
code cleanliness
code size
compiler version issues
cyw43_driver
debug
discussion
documentation
duplicate
dynamic_clock
elf2uf2
enhancement
errata
filesystem
flash layout
floating point
FreeRTOS
hardware_adc
hardware_claim
hardware_clocks
hardware_dma
hardware_exception
hardware_flash
hardware_gpio
hardware_i2c
hardware_irq
hardware_pio
hardware_pwm
hardware_rcp
hardware_rtc
hardware_spi
hardware_sync
hardware_uart
hardware_vreg
hardware_watchdog
hardware_xosc
help wanted
host
IAR
installation
invalid
linker scripts
lto
lwip
mbedtls
missing API
needs reporter detail
networking
newlib
newlib-multicore
non-c
non-cmake
non-gcc
openocd
orphan PR
OS integration
pico_async_context
pico_base
pico_binary_info
pico_flash
picolibc
pico_lwip
pico_malloc
pico_multicore
pico_printf
pico_runtime
pico_stdio
pico_stdio_usb
pico_sync
pico_time
pico_util
pioasm
port
question
review
review2
rtos interop
runtime init
static library
svd
test
tinyusb
vscode
wifi integration
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-raspberrypi/pico-sdk!3027
Loading…
Reference in a new issue
No description provided.
Delete branch "beriberikix/cybt-reread-corrupt-index"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
cybt_get_bt_buf_index()reads the BT controller'shost2bt/bt2hostshared-memory ring indices over the gSPI backplane and
assert()s if anyindex is out of range. The gSPI bus is shared with WiFi traffic, so under
bus contention a single read can return a transiently corrupt (out-of-range)
value. The assert then turns a recoverable glitch into a full system abort.
Change
Re-read the index up to
CYBT_BUF_INDEX_REREAD_MAX(8) times — thecorruption does not persist — and only if it still does not validate return
CYBT_ERR_HCI_READ_FAILED, so the caller drops the cycle gracefully insteadof aborting. No new dependencies; behaviour is unchanged on a valid read.
Testing
Reproduced on a CYW43439 (Raspberry Pi Pico 2 W) under concurrent WiFi+BT
load: the assert fired both at boot and under sustained traffic. With the
re-read the controller recovers and BT stays up across a 2-hour zero-fault
WiFi+BLE coexistence soak.
This is a transport-level robustness fix that benefits all
cybtusers, notjust coexistence.
How easy is it to reproduce this?
Reliably, under concurrent WiFi + BT load on the shared gSPI bus — it's load-proportional rather than a rare race. In my soak testing (a BLE connection streaming notifications while WiFi runs a continuous ping load) roughly a third of connection cycles hit the assert before this change, anywhere from a few seconds to ~19 min in.
The key detail from the gdb backtrace: it fires even with the cyw43 bus mutex held across the whole BT read. So it isn't a host-side locking gap — the ring index reads back transiently out-of-range at/below the gSPI transport itself (looks like F1-overflow / controller-DMA contention on the BT backplane read while WLAN is active). The corruption doesn't survive a re-read, which is why fixing it at the read site works where more locking didn't. After the change the same workload ran a 2-hour WiFi+BLE soak with zero faults.
One caveat: I hit this on a multi-threaded Zephyr port of the driver, where WLAN and BT bus access run as separate threads — more interleaving than the pico-sdk
async_contextmodel. So it may be harder to trigger on stock pico-sdk + BTstack. But since the change is behaviour-neutral on a valid read (the first-read success path is unchanged), it's pure downside protection regardless of how often it triggers.I'm happy to share the gdb backtrace and soak logs if useful.
The BT data is routed via WiFi. I believe it was done this way to support SPI on Pico devices. So I suspect you need to be careful to ensure there's no concurrent use of WiFi and BT. Is there any locking to prevent this?
Good question, and you're right about the architecture — BT rides the cyw43 backplane and the cyw43 bus mutex is the serialization point. Yes, I do lock it: in my port a shared recursive bus lock is held across each whole BT read-modify-write, so WiFi and BT don't interleave at the host level.
Two things I found that are relevant here:
While tracing this I hit a genuine locking gap in the stock integration:
cyw43_bluetooth_hci_write()is the one bus path that isn't wrapped inCYW43_THREAD_ENTER, so a BT HCI TX (e.g. a notification ACL) can drive SPI outside the lock, concurrent with the poll thread — and the cyw43 bus code yields/sleeps mid-transfer, so they do interleave. Closing that removed a whole class of these faults. It might be worth a look in pico-sdk independently of this PR.But the case this PR covers survived even with locking complete. gdb caught the assert on the BT TX thread with the cyw43 bus mutex held (
lock_count=1) — i.e. no concurrent host-side bus access at that moment — and the ring index still read back out of range. So my read is that the glitch is below the host lock: a transient on the gSPI backplane read of the controller's shared memory (looks like F1-overflow / on-chip DMA contention), not something the mutex can prevent. That's why I went with a re-read at the read site rather than more locking.So: locking is necessary (and I'd argue 1. above is a real gap worth fixing), but in my testing it wasn't sufficient on its own — hence this belt-and-braces re-read, which is a no-op on the normal path.
Yes, cyw43_bluetooth_hci_read and cyw43_bluetooth_hci_write should call CYW43_THREAD_ENTER / CYW43_THREAD_EXIT. We always run Wifi and BTStack in the same "scheduler" so presumably never hit a problem.
This does suggest a issue with the firmware on the 43439. It would be nice to have a way to reproduce it (ideally just with pico-sdk). But it's unlikely we'd get that fixed quickly - if at all - so your workaround is probably a good idea if it works.
That matches what I see exactly — in a single shared scheduler the mutex is effectively always held and WLAN/BT never preempt mid-transfer, so the window never opens. It's the threaded port (separate WLAN and BT threads) that exposes it, and the missing
CYW43_THREAD_ENTERis what makes the threaded case unsafe until it's wrapped.Two offers to take the load off your side:
I'm happy to send a small separate PR adding
CYW43_THREAD_ENTER/CYW43_THREAD_EXITtocyw43_bluetooth_hci_read/write. It's a no-op for the async_context model and just makes those two functions safe for non-cooperative integrations.On a pico-sdk-only repro: I can try, but I'll be upfront that all my evidence is from the threaded port, and in the single-scheduler model it may genuinely never fire (or only on a true firmware-side backplane glitch, which would be rare). If it'd help your decision, I'd point a stock
picow_btexample at heavy concurrent WLAN throughput (e.g. iperf) while BT streams, run it under SWD, and report back either way.And agreed on the firmware — since that's unlikely to change soon, the re-read is the pragmatic cover, and it's a no-op on the normal path so it costs the async_context users nothing even though they'll likely never hit the corruption.
I can try and reproduce it sometime. Feel free to raise a PR on the SDK.
BTW: Was this analysis by an AI? Not that it matters - just interested.
Thanks — I'll raise the SDK PR for the
CYW43_THREAD_ENTER/CYW43_THREAD_EXITwrapping shortly.And yes — full transparency: this was done with an AI assistant (Claude Code) in the loop. It helped drive the gdb sessions, characterize the fault, and draft these write-ups. But every claim here is from real hardware, not generated: the backtraces, the
lock_count=1capture at the fault, and the soak runs are all actual captures on a Pico 2 W. Happy to share any of the raw logs.For fuller context, this came out of a downstream Zephyr effort to bring up WiFi+BLE coexistence on the CYW43439 over gSPI — this re-read is one of four transport-level fixes. The RFC tracks the whole thing: https://github.com/zephyrproject-rtos/zephyr/issues/111811
Raised it: georgerobotics/cyw43-driver#153.
One note on placement — since
cyw43_bluetooth_hci_read/writeare defined in thecyw43-driversubmodule rather than the SDK tree, I put the PR there. The SDK's own btstack transport already wraps both calls inCYW43_THREAD_ENTER/EXIT, so stock pico-sdk + BTstack is unaffected; this just makes the functions self-serialising for direct API callers (non-btstack / multi-threaded ports). Happy to move or reshape it if you'd rather handle it SDK-side.Hardware-validated the companion locking change (georgerobotics/cyw43-driver#153) on a Pico 2 W (RP2350): a patched georgerobotics-backend build brings up BT over the HCI path and holds a 60s BLE notification stream (560 notifications, 0 stalls, no disconnect) under concurrent bidirectional WiFi load, board alive afterward — no fault/assert. Stock pico-sdk + BTstack is unaffected (its transport already wraps these calls); this just makes the functions self-serialising for direct callers.
@peterharperuk should this be included in 2.3.1?
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.