Skip to content

test(rp2350): complete RP2350(W) loopback driver and subsystem HIL #3899

Open

Description

@zackees

Context

Finish the RP2350 and RP2350W hardware bring-up on the currently available Pico 2 W fixture, using the existing AutoResearch/fbuild path and the board's soldered two-pin feedback bridge.

The 2026-08-18 inventory shows:

Most source-side RP2350W foundation work has landed: board/deploy support, PIO0/1/2 channel selection, serial RPC, watchdog recovery, CYW43 networking, peer HTTP/JSON-RPC, OTA staging, and btstack BLE transport. What remains is a single evidence-backed closeout of every driver and subsystem reachable with the attached hardware.

Proposal

  1. Re-establish the RP2350W through fbuild port scan and the normal RP BOOTSEL/CDC deployment path. Do not pin a stale COM17 record; omit --upload-port unless multiple RP boards are actually present, or use UF2=<volume> if the board is deliberately in BOOTSEL.
  2. Run GPIO-only AutoResearch with pin discovery and record the physically observed bridge. The historical GPIO0 -> GPIO1 note is a hypothesis until the current firmware reports it.
  3. Validate both rp2350 and rp2350w profiles on the Pico 2 W so common silicon support and W-specific transports are separately covered.
  4. Exercise every RP driver reachable with the discovered bridge through the public FastLED/Channels API where available:
    • PIO0, PIO1, and RP2350-only PIO2 clockless TX with PIO RX byte/edge evidence;
    • PIO0 + PIO1 simultaneous resource/concurrency behavior;
    • UART clockless output on any hardware UART route compatible with the discovered bridge (GPIO0 -> GPIO1 is UART0 TX/RX);
    • AUTO and BIT_BANG fallback behavior through the user-facing API;
    • arbitrary-pin PIO SPI if the harness supports capture on the bridge;
    • fixed-function SPI0/SPI1 and APA102/SK9822 only when the present wiring can form the required MOSI -> MISO route; otherwise record the exact pin incompatibility as not reachable in this fixture, not as a pass.
  5. Complete the RP2350W-only gates already prepared by feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832: RPC smoke, IEEE-754, SIMD, watchdog/reconnect, RP dual-core concurrency, Wave2D, ESP32-C6 peer networking, BLE GATT, and peer OTA.
  6. Preserve exact command/output evidence on this issue and update parent feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 with the final matrix.

Acceptance criteria

  • A focused RED -> GREEN repro is recorded for every source/harness gap discovered while enabling RP2350 HIL (including any RP2040-only CLI restriction that must be generalized).
  • fbuild port scan records the live RP board identity/port and the healthy peer identity; firmware-level detection confirms the peer chip before ESP32-C6 use.
  • AutoResearch live pin discovery reports the soldered pair and testGpioConnection proves LOW/HIGH feedback; no pin mapping is accepted from historical notes alone.
  • The rp2350 and rp2350w build/deploy/serial RPC paths both pass on the attached Pico 2 W.
  • PIO0, PIO1, and PIO2 each produce decisive byte/edge loopback evidence through the public driver path.
  • PIO0 + PIO1 simultaneous execution passes without resource collision, stale state machine, or DMA ownership leak.
  • Every UART, SPI, AUTO, BIT_BANG, and PIO-SPI path physically reachable with the discovered bridge is tested; every unreachable path is listed with the exact wiring/peripheral constraint.
  • RP2350W RPC smoke, IEEE-754, SIMD, watchdog/reconnect, concurrency, and Wave2D gates pass or produce a concrete tracked implementation gap.
  • RP2350W <-> ESP32-C6 peer HTTP/JSON-RPC, >=4 KiB payloads in both directions, ten reconnect cycles, BLE GATT, and OTA recovery pass when the peer is firmware-confirmed as C6.
  • Closeout records the exact commands, commits, wiring, ports, VID:PIDs/serials, and decisive REMOTE:/RESULT: lines.
  • No raw flasher, raw pyserial path, one-off sketch, host-WiFi switch, guessed register map, or unsupported-peripheral shim is introduced.
  • Relevant lint, focused tests, RP2350/RP2350W builds, code review, PR delivery, and post-merge live rerun are complete.

Decisions

  • Treat the attached Pico 2 W as both the RP2350 common-silicon DUT and the RP2350W transport DUT; this covers both profiles without pretending a second board exists.
  • Keep this issue focused on live driver/subsystem completion; feat(rp2350w): complete pin-free AutoResearch and ESP32-C6 peer-network bring-up #3832 remains the parent for the broader pin-free/network work.
  • Treat prior GPIO0 -> GPIO1 documentation as a lead only; live discovery is the authority for the current physical bridge.
  • Count a driver as complete only with live feedback evidence through the normal API. Compile-only or raw peripheral evidence is supporting evidence, not closeout.
  • Mark physically unreachable fixed-pin routes explicitly rather than changing soldered wiring or inferring success.

Open questions

  • Which present USB node/BOOTSEL volume belongs to the Pico 2 W after the user-reported reattachment? The saved COM17 record is not currently present, so this must be resolved from a fresh scan/deploy result.
  • Whether the current two-pin bridge permits any fixed-function SPI route depends on the live-discovered pins; the historical GPIO0/GPIO1 pair supports UART0 and arbitrary-pin PIO tests but not the canonical SPI0/1 MOSI -> MISO pairs.

Related issues

Activity

  1. zackees commented on Aug 19, 2026

    @zackees
    MemberAuthor

    Hardware status update (2026-08-18):

    • PR test(rp2350): enable RP2xxx driver validation #3901 is merged and supplies RP2350(W) fixed-SPI support, indexed UART/PIO AutoResearch selection, deferred USB-detection normalization, UART affinity/metadata fixes, tests, and documentation.
    • The current fbuild port scan still reports the saved RP2350 application endpoint as phantom/non-selectable COM17; there is no RPI-RP2 BOOTSEL volume.
    • Windows PnP topology confirms that endpoint was last removed on 2026-08-01. Its two parent USB hubs are currently healthy and present, but the endpoint itself is not present.
    • The currently present Code 43 USB node is on a different root-port path and has a different container ID, so it cannot be attributed to this RP2350.

    Consequently there is still no live evidence for the bridged GPIO pair, and no RP HIL result is being claimed from historical GPIO0/GPIO1 notes. Remaining acceptance is blocked until the Pico 2 W is reconnected as either application CDC or RPI-RP2 BOOTSEL; then pin discovery and UART/PIO/SPI/concurrency validation can resume.

  2. zackees commented on Aug 19, 2026

    @zackees
    MemberAuthor

    Progress update after the attached-board investigation and merged tooling/driver work:

    • Electrically identified the two-pin bridge as GPIO0 <-> GPIO1, verified in both directions.
    • Canonical AutoResearch loopback passed on PIO0, PIO1, and PIO2 using TX GPIO1 -> RX GPIO0.
    • The bridge is compatible with UART0 (TX GPIO0 -> RX GPIO1). UART1 is not routable to this bridge, and neither fixed SPI controller exposes a compatible TX/RX pair on GPIO0/GPIO1, so those modes are not physically testable with the supplied wiring.
    • FastLED PR test(rp2350): enable RP2xxx driver validation #3901 added the RP2350 validation surface and merged as dd19c066f1.
    • fbuild PR fix(rp): complete Arduino-Pico board resolution fbuild#1301 added the common/family RP platform handling and was released as fbuild 2.5.17.
    • FastLED PR Fix RP2350W UART diagnostics and validation #3907 fixed the RP UART default receive sizing, preserves specific startup failures, exposes typed UART startup diagnostics through AutoResearch, pins fbuild 2.5.17, and adds regression coverage. It merged to master as 9e5fa8ab92.
    • Validation is green: affected Python suites 193 passed, targeted RP UART C++ test passed, full C++ suite 279/279 plus 84/84 examples passed, lint passed, and pre-push review was clean.

    Remaining hardware blocker: the RP2350W stopped enumerating before the final UART0 diagnostic rerun. Released fbuild 2.5.17 reports stale/unselectable COM18, and Windows reports an unidentified USB device at Port_#0014.Hub_#0001 with problem code 43 (descriptor request failure). Replug/BOOTSEL/reset attempts have not produced either a normal serial endpoint or PICOBOOT endpoint. No deploy/reset was attempted against the stale port.

    The issue should remain open until the board enumerates again (or is replaced) so UART0 and the remaining pin-free/peer-capable acceptance modes can be completed with physical evidence.

  3. zackees commented on Aug 21, 2026

    @zackees
    MemberAuthor

    Burn-down disposition: DEFER. The harness and several drivers are merged, but the required post-merge hardware matrix cannot be completed while the exact RP2350W no longer enumerates.

    Completed evidence includes merged PRs #3901, #3907, #3908, and #3954 plus fbuild #1301/#1304: RP2350 validation/dispatch, indexed PIO/UART selection, fixed-SPI eligibility, typed diagnostics, individual PIO loopbacks, 50 no-replug deploy/RPC cycles, watchdog recovery, and one full RP2350W?ESP32-C6 network cycle.

    Remaining acceptance:

    • Restore serial 2DCB876B587EA334; COM18 is currently phantom and Windows reports code 43 at the same direct root port.
    • Run the final UART0 loopback with typed diagnostics.
    • Prove simultaneous PIO0+PIO1 resource/concurrency behavior.
    • Record AUTO, BIT_BANG, and arbitrary-pin PIO-SPI public-API results or a precise fixture limitation.
    • Produce distinct live rp2350 and rp2350w profile build/deploy/RPC evidence.
    • Complete all ten post-fix(net): stabilize RP2350W peer lifecycle #3954 peer-network cycles with >=4 KiB in both directions.
    • Complete OTA recovery and real BLE GATT JSON-RPC (or prove the upstream backend boundary).
    • Publish the final command/commit/wiring/identity/result matrix.

    Restart after physical BOOTSEL/replug or the exact-device recovery tracked by FastLED/fbuild#1331 restores a selectable endpoint. Close only from retained post-merge HIL evidence, not compilation or stubs.

  4. added
    deferredTriage pass could not resolve this; skipped by ci/oldest_issue.py
    area: ciGitHub Actions, fbuild, toolchains
    status: blockedBlocked by external dependency or issue
    on Aug 21, 2026
  5. zackees commented on Sep 7, 2026

    @zackees
    MemberAuthor

    Hardware bring-up complete — most of this issue's checklist is now evidence-backed

    Ran the full reachable matrix on the attached fixture. Merged: #4169 (UART), #4172 (boot-loop escape + bring-up).

    The fixture is a Pico 2 W — confirmed, not assumed

    This issue recorded the W identity as historical and Windows-reported health=phantom selectable=no, so "current physical enumeration still must be established". Established:

    2e8a:f00f  'Pico 2W'                                     # rpipico2w application CDC
    status     -> platform: "Raspberry Pi Pico 2 W (RP2350)"
    wifiStatus -> {"status":"IDLE","success":true}           # UNSUPPORTED on the non-W build
    

    2E8A:F00F matches the registry identity. The phantom/unselectable condition does not reproduce on Linux — the port is healthy and openable.

    Step 2: bridge confirmed by live discovery, not by the note

    The issue required this be proven rather than inherited from #3832/#3864:

    ✅ PIN DISCOVERY SUCCESSFUL
       Found connected pins: TX (GPIO 0) → RX (GPIO 1)
    

    Driver matrix

    Driver Result
    PIO0 PASS 4/4
    PIO1 PASS 4/4
    PIO2 (RP2350-only) PASS 4/4
    UART0 not reachable on this silicon — see below
    SPI0/SPI1, APA102/SK9822 not reachable in this fixture — wiring
    Serial JSON-RPC PASS (71 methods; ping/status/drivers/payload/error handling)
    RP dual-core concurrency PASS — 200/200, recursiveMutexReady: true

    drivers reports PIO2 as RP2350 PIO2, enabled and ready, so the RP2350-only third PIO block is real and exercised.

    UART is not reachable — and not for the reason this issue assumed

    The plan expected UART to work because "GPIO0 -> GPIO1 is UART0 TX/RX". The bridge is fine; the clocking is the blocker:

    RP UART: attempted=True started=False encodedBytes=0 actualBaud=3000000
             lastError='RP UART: achieved baud outside tolerance'
    

    3,000,000 is exactly clk_peri / 16, the PL011 hardware maximum — so clk_peri is 48 MHz on arduino-pico RP2350, and WS2812 wave10 encoding needs more than 3 Mbaud. No software change can make UART clockless work on this board.

    #4169 fixed the surrounding defect: the engine hardcoded a 6.25 MHz ceiling that silently assumed ~100 MHz clocking, so the encoder picked a baud uart_init() clamped and the achieved-baud check rejected it. The ceiling is now derived from clk_peri/16 via the peripheral, and the refusal says why instead of failing opaquely mid-start.

    Recommend recording UART as "not reachable on this fixture", the same category this issue already reserves for SPI.

    SPI remains unreachable

    SPI loopback needs GPIO3→GPIO0 (SPI0) or GPIO11→GPIO8 (SPI1). The fixture is wired GPIO0→GPIO1. Recording as the issue instructs, rather than as a failure.

    Deployment

    fbuild deploy via BOOTSEL/UF2 works and returns the application port. One note for the cross-variant case: deploying rp2350w while the board runs non-W firmware fails clean, without flashing —

    deploy error: RP-series picotool deployment requires the selected runtime USB serial
    so fbuild can bind the BOOTSEL operation to one board
    

    Correct behavior; the running identity did not match the target environment. --upload-port resolves it, which is what this issue prescribes.

    Remaining

    Generated with Claude Code

    https://claude.ai/code/session_01KkufoNxfnNRU9psT3R9F51

  6. 63 remaining items

  7. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Resolved: the RX decoder does not handle 400 kHz — it is not the UART's timing deviation

    On the UART0 result I listed two candidate causes for bytes=0/3300 and declined to guess between them:

    1. the RX decode path is configured for 800 kHz geometry
    2. the 200 ns T1H shortfall puts symbols outside what the decoder accepts

    They separate cleanly with a control. PIO0 is a known-good transmitter — it passes this issue's loopback criterion at 800 kHz. Running it at the same 400 kHz geometry isolates the decoder from the transmitter:

    $ bash autoresearch rp2350w --flex-io --rp-pio-index 0 --chipset ws2811-400
    TEST PIO0 lanes=1 leds=100 FAIL 805ms class=decode_mismatch
    Captured bytes: 0/300
    Raw sample: H1200 L1300  H1200 L1300  H1250 L1300
                H500 L2000   H500 L2050   H450 L2050 ...
    

    Against TIMING_WS2811_400KHZ (T0H 500, T1H 1200, period 2500):

    symbol PIO0 measured expected
    0 high 500 ns 500 ns exact
    1 high 1200 ns 1200 ns exact
    period 2500 ns 2500 ns exact

    A textbook-perfect waveform decodes to zero bytes. So cause 2 is ruled out: the decoder fails on a signal with no timing deviation at all.

    The controlled comparison

    transmitter geometry waveform decode
    PIO0 800 kHz (ws2812) correct PASS
    PIO0 400 kHz (ws2811-400) exact 0/300 bytes
    UART0 400 kHz (ws2811-400) T1H 200 ns short 0/300 bytes

    Same board, same bridge, same driver — only the geometry differs between rows 1 and 2. The RX capture/decode path handles 800 kHz and not 400 kHz.

    What this means for the two entries

    UART0: its decode failure is explained by the decoder, not by its own timing. That makes the UART finding cleaner than I recorded it — the backend reaches the only geometry it can, transmits, and the harness cannot read it back. The 200 ns T1H shortfall remains a genuine separate finding (outside the ±150 ns datasheet tolerance) but is not what fails the test.

    A new harness gap: --chipset ws2811-400 cannot pass on any driver, because nothing the harness captures at 400 kHz decodes. That is worth knowing before someone spends time on a driver that is transmitting correctly.

    This is the "focused RED -> GREEN repro for every source/harness gap discovered" criterion — the repro is one command, and the control that makes it diagnostic is the 800 kHz run of the same driver.

    I am not proposing a fix: whether the decoder derives thresholds from the requested timing or assumes 800 kHz is a question for whoever owns the capture path, and I have guessed at enough mechanisms this session.

  8. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Refinement: the decode thresholds do adapt to 400 kHz and the signal fits them — so that is not the cause

    My previous comment concluded "the RX capture/decode path handles 800 kHz and not 400 kHz". The empirical result stands, but it invites an assumption I have now checked and refuted: that the decoder's thresholds are hardwired for 800 kHz. They are not.

    AutoResearchTest.cpp:840 builds them from the requested chipset:

    tx_timing = fl::ChipsetTiming{timing.t1_ns, timing.t2_ns, timing.t3_ns, ...};
    const uint32_t tolerance = (uses_wave8 || uses_wave3) ? 200 : 170;
    auto rx_timing = fl::make4PhaseTiming(tx_timing, tolerance);

    and make4PhaseTiming (fl/channels/rx.cpp.hpp:72) derives four windows as value ± tolerance. For TIMING_WS2811_400KHZ (T1=500, T2=700, T3=1300) at ±170 ns:

    phase derived window PIO0 measured
    t0h [330, 670] 500 inside
    t0l [1830, 2170] 2000 inside
    t1h [1030, 1370] 1200 inside
    t1l [1130, 1470] 1300 inside

    Every phase of the measured signal falls inside its window. So the thresholds adapt correctly, the signal conforms to them, and the decode still returns zero bytes. Whatever fails is downstream of threshold selection.

    What I could not get, and why it matters

    The device logs the actual reason — AutoResearchTest.cpp:866:

    AR_FL_WARN("Decode failed (error code: " << static_cast<int>(decode_result.error()) << ")");

    That line never reaches the host, because AutoResearch.ino's setup() calls fl::setLogLevel(FL_LOG_LEVEL_NONE). The one diagnostic that would name the cause is suppressed by default, and the host sees only Decode status: ok=-1 error=-1, which are unset sentinels rather than a reported error.

    That is worth noting on its own: the decode failure path has a diagnostic and the harness cannot see it. Same shape as the OTA fetch error that was being written to a closed serial link (#4318) and the UART decline that was being reported as a capture failure (#4310) — the information exists and does not reach whoever needs it.

    Corrected status

    • Established: PIO0 passes at 800 kHz and produces a datasheet-exact waveform at 400 kHz that decodes to zero bytes. Same board, same bridge, same driver.
    • Eliminated: wrong thresholds; a transmitter timing deviation (PIO0's is exact).
    • Unknown: the actual failure, which needs either the device error code or someone who knows the capture path.

    I am flagging rather than fixing, and being explicit that my earlier phrasing pointed at a cause I have since ruled out.

  9. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Retracting "the decoder does not handle 400 kHz" — it does, up to ~60 LEDs. And the advertised lane cap is not reachable.

    I said the RX path handles 800 kHz and not 400 kHz. That is wrong. Driving runSingleTest directly instead of through the harness default of 100 LEDs:

    WS2811-400KHZ leds= 10 passed=True  bytes=120
    WS2811-400KHZ leds= 25 passed=True  bytes=300
    WS2811-400KHZ leds= 50 passed=True  bytes=600
    WS2811-400KHZ leds= 60 passed=True  bytes=720
    WS2811-400KHZ leds= 65 passed=False bytes=0
    

    400 kHz decodes correctly. My conclusion came from testing one LED count — the harness default — which happens to sit above the 400 kHz ceiling. Every earlier 400 kHz "decode gap" observation, UART0's included, is this.

    The real shape: a size ceiling on both geometries

    All reproduced 3/3:

    geometry LEDs frame result
    WS2812B-V5 (800 kHz) 100 3.00 ms pass
    WS2812B-V5 (800 kHz) 103 3.09 ms fail
    WS2811-400KHZ 60 3.60 ms pass
    WS2811-400KHZ 65 3.90 ms fail

    A concrete defect: the advertised maximum does not work

    The firmware rejects above 103 with an explicit error:

    {"error": "LaneSizeTooLarge", "message": "laneSizes[0] = 110 exceeds max 103"}
    

    But 103 itself fails, silently, 3/3 — passed=false, bytes=0, no error. So the cap the device advertises is one it cannot honour; the usable maximum at 800 kHz is somewhere in (100, 103). A caller who trusts max 103 gets a silent capture failure rather than LaneSizeTooLarge, which is the worse of the two outcomes.

    What I could not determine

    The invariant. It is not frame duration — 3.09 ms fails while 3.60 ms passes. It is not bit count — 2400 bits pass while 1560 fail. It is not LED count, which differs by geometry. Something geometry-dependent in the PIO_RX capture path sets it, and black-box probing cannot separate the candidates. The device does log the decode error code (AutoResearchTest.cpp:866) but setup() sets FL_LOG_LEVEL_NONE, so it never reaches the host; setDebug exists as an RPC and the harness never calls it.

    Corrected entries

    • UART0 — the decode failure is the 100-LED default exceeding the 400 kHz ceiling, not a decoder incompatibility and not its 200 ns T1H shortfall. That shortfall remains a separate, genuine finding.
    • --chipset ws2811-400 — usable, at ≤60 LEDs. My "cannot pass on any driver" was wrong.
    • New: max 103 is advertised and 103 fails silently.

    This is the second conclusion I have had to withdraw here after testing a single point and generalising — the first being three "blocked" labels that dissolved on contact. One LED count is not a capability.

  10. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Root of the silent-ceiling defect: a fixed / 32 divisor expressing a geometry-dependent limit

    AutoResearchRemoteRunSingleTest.cpp:512:

    const int max_leds_per_lane = mState->rx_buffer.size() / 32;

    rx_buffer is fl::span<uint8_t> of 3300 bytes (matching the bytes=0/3300 in the decode diagnostics), so the advertised cap is 3300 / 32 = 103.

    That divisor cannot be right for more than one geometry. Decoded LED data is 3 bytes per LED, so 32 bytes per LED means the buffer holds raw capture samples — and the samples a frame costs scale with how long the frame lasts. A 400 kHz frame is twice as long as an 800 kHz frame of the same LED count. One constant cannot describe both, which is exactly what the measurements show (all 3/3 reproducible):

    geometry advertised cap actual usable failure mode above it
    WS2812B-V5 (800 kHz) 103 100–102 silent — passed=false, bytes=0, no error
    WS2811-400KHZ 103 60–64 silent

    Two distinct problems fall out of the one line:

    1. The advertised cap is unreachable even at 800 kHz. 103 is accepted by the guard and then fails in capture, 3/3. The usable maximum is at most 102. A caller trusting max 103 gets silence instead of LaneSizeTooLarge, which is strictly the worse failure — the guard exists precisely to convert that into a clean error.
    2. At 400 kHz the cap is ~70% too high, so everything from 65 to 103 LEDs is accepted and then silently fails. This is what made 400 kHz look like a decoder incompatibility: the harness default of 100 LEDs sits deep inside that band.

    Direction

    runSingleTest already receives the timing ("timing": "WS2811-400KHZ") and resolves it to t1_ns/t2_ns/t3_ns a few lines later for make4PhaseTiming. So the information needed to compute a per-geometry cap is present at the same call site — the bound could scale with the bit period rather than being a constant.

    I am not proposing a number. Deriving the true bytes-per-LED needs the PIO_RX sample rate and whether it is fixed or tracks the bit period, which I could not determine black-box: no single invariant fits both thresholds (3.09 ms fails while 3.60 ms passes; 2400 bits pass while 1560 fail). Whoever owns that path can size it correctly; the measurements above pin what the answer has to reproduce.

    Worth noting the guard is doing its job in shape — a bounds check that returns a clear error — it is only the bound that is wrong. Raising or lowering a constant will not fix it; the bound has to depend on the geometry the caller asked for.

  11. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Split the lane-cap defect out as #4371 so it is tracked on its own rather than living in this thread — it is a harness bug independent of the RP2350 HIL matrix, and the silent-failure band it creates is what sent me down a false trail about the 400 kHz decoder.

    Net effect on this issue's matrix, after all the corrections above:

    path status
    PIO0 / PIO1 / PIO2 PASS at 800 kHz; also pass at 400 kHz below the real ceiling
    PIO0+PIO1 simultaneous PASS (distinct TX pins + show_success, the right contract for a one-RX-pin fixture)
    UART0 transmits at 400 kHz (attempted=True started=True actualBaud=2000000); its decode failure was the 100-LED default sitting above the 400 kHz ceiling, not a decoder or timing fault. Separate genuine finding: T1H runs 1000 ns against a 1200 ns datasheet value, outside ±150 ns tolerance
    SPI0 unreachable — needs GPIO3 -> GPIO0, one wire, and GPIO0 is already the correct MISO
    SPI1 unreachable — needs GPIO11 -> GPIO8, neither pin on the bridge
    BIT_BANG not registered; if registered, a constant ~4 µs/bit overhead puts the floor near 250 kHz, below every supported chipset — tested at both 800 and 400 kHz
    AUTO not exposed in the device driver inventory

    Four things I asserted in this thread and later had to withdraw, all listed above in corrected form: BLE "fully blocked" (only the host-central half was), SPI "needs jumpers" (true, but one of them is a single wire to an already-correct pin), UART0 "backend declines" (only for the chipset I happened to run), and "the decoder does not handle 400 kHz" (it does, below the ceiling). Each came from testing one point and generalising to a capability.

  12. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    UART0 resolved: it is the T1H shortfall, by 30 ns. My previous correction over-corrected.

    When I found the lane-cap ceiling I attributed UART0's decode failure to it and said the 200 ns T1H shortfall "is not what fails the test". That was wrong — testing UART0 below the ceiling separates them cleanly.

    runSingleTest on UART0 at 400 kHz, well under the ~60-LED ceiling:

    UART0 leds=10 passed=False bytes=116
    UART0 leds=25 passed=False bytes=296
    UART0 leds=50 passed=False bytes=596
    

    It fails at every size. PIO0 passes at all three. So the ceiling was never UART0's problem.

    The failure signature

    At 10 LEDs:

    {"capturedBytes": 29, "totalBytes": 30, "decodeBytes": 0, "decodeOk": -1,
     "captureFailed": false,
     "errors": [{"led": 0, "actual": [255,255,255], "expected": [240,15,170]},
                {"led": 1, "actual": [255,255,255], "expected": [240,15,170]}, ...],
     "rawEdgeSample": "H950 L1500 H1000 L1500 H1000 L1500 H1000 L1500 ..."}

    Every byte reads 0xFF — every bit classified as a one — not partial or random corruption.

    The arithmetic

    make4PhaseTiming derives the 1-bit high window as T1+T2 ± tolerance. For WS2811_400KHZ (T1=500, T2=700) at the non-wave tolerance of 170 ns:

    value
    expected t1h 1200 ns
    accepted window [1030, 1370]
    UART0 measured t1h 1000 ns
    shortfall below the window 30 ns

    UART0's 1 symbols land 30 ns outside the bottom of the window. Its 0 symbols (H500) sit comfortably inside t0h [330, 670], which is why the capture runs to completion (captureFailed: false, 29 of 30 bytes) rather than aborting — the frame is captured, and the bit classification is what goes wrong.

    PIO0 at the identical configuration measures t1h = 1200 ns, dead centre, and passes.

    Corrected entry

    path status
    UART0 transmits correctly (attempted=True started=True actualBaud=2000000), but its T1H is 1000 ns against a 1200 ns datasheet value — 30 ns below the decoder's accepted window and 200 ns outside the ±150 ns datasheet tolerance. Fails at every lane size, independent of the #4371 ceiling.

    This is the second time I have moved UART0's cause. First I blamed the backend declining (true only for the 800 kHz chipset), then the lane-cap ceiling (real, but not what fails UART0). Both times I had a plausible explanation and stopped before separating it from the alternative. The separation here was one command — run it below the ceiling — and it should have come first.

    The underlying question for whoever owns the RP UART clockless encoder: whether a 2 Mbaud PL011 can express a 1200 ns T1H at all, or whether 1000 ns is the closest reachable quantisation. If it is the latter, the geometry is simply out of reach and the path is unreachable on this silicon rather than defective.

  13. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Tested a fix for UART0's T1H and it does not work — plus a silent decline path

    Following the 30 ns shortfall: the RP UART encoder (fl/channels/uart_wave_encoder.cpp.hpp) evaluates two frame geometries and takes the one with smaller quantisation error.

    const UartWaveFit fit5 = fitUartWave(timing, 5, max_baud_rate);
    const UartWaveFit fit4 = fitUartWave(timing, 4, max_baud_rate);
    constexpr u32 kHysteresisNs = 50;
    if (fit4.total_err_ns + kHysteresisNs < fit5.total_err_ns) { P = 4; } else { P = 5; }

    with fit.total_err_ns = err_t0h + err_t1h — a sum. For WS2811_400KHZ (period 2500 ns, T0H 500, T1H 1200):

    pulse T0H err T1H err sum worst
    P=5 (baud 2.0 M) 500 ns 500 0 1000 200 200 200
    P=4 (baud 1.6 M) 625 ns 625 125 1250 50 175 125

    P=4 needs 175 + 50 < 200, which is false, so P=5 is chosen — and its T1H lands outside the decoder window [1030, 1370] while P=4's 1250 would sit inside it. The sum metric cannot see that one phase being out of tolerance is fatal while error spread across both is not.

    That looked like a clean fix: compare worst-phase error instead of the sum.

    It does not work. Changing total_err_ns to max(err_t0h, err_t1h) — consistently, so admission and construction cannot drift — selects P=4, and the RP UART then never starts:

    UART0 leds=10/25/50  passed=False  bytes=0
    {'rpUartStartAttempted': False, 'rpUartStartSucceeded': False,
     'rpUartActualBaud': 0, 'rpUartEncodedSize': 0, 'rpUartLastError': ''}
    

    So P=5 with an out-of-window T1H is at least functional — it transmits and captures 29 of 30 bytes. P=4 does not transmit at all on this backend. Whatever rejects it is upstream of the start attempt, and the reason is not recorded.

    (My first attempt at this experiment was invalid and I want to flag it: I forced the branch with if (true), which made construction disagree with admission — the code comments warn about exactly that drift. It produced the same symptom for the wrong reason. The result above is from changing the metric itself, which keeps both paths consistent.)

    Incidental finding: a decline that reports nothing

    rpUartStartAttempted: False with rpUartLastError: ''. The documented decline path sets a message — that is how the 800 kHz case reports "chipset timing needs a baud above the UART backend maximum". This one sets nothing, so a P=4 rejection is indistinguishable from a driver that was never invoked.

    That is the fourth instance of this shape on this issue: the OTA fetch error written to a closed serial link (#4318), the UART decline classified as a capture failure (#4310), the decode error suppressed by FL_LOG_LEVEL_NONE, and now a start rejection with an empty reason.

    Where UART0 stands

    Unchanged as a result, but better understood: it transmits, its 1 symbols land 30 ns below the decoder window because 1200 ns is not expressible on the P=5 grid (multiples of 500 ns), and the alternative grid that could express it is rejected by this backend for an unreported reason. Whether that rejection is a real hardware constraint — 6 data bits at 1.6 MBaud on PL011 — or a fixable gap is the open question.

  14. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Minor: multi-lane runSingleTest reports PASS with evidence covering one lane

    Lower-confidence than the other findings here, and on a non-default path, but it bears on evidence quality so it is worth recording.

    --lanes N makes phases.py:1088 build laneSizes = [strip_size] * lane_count for runSingleTest. Driving that directly:

    laneSizes total LEDs passed captureEvidenceBytes
    [50, 50] 100 true 600
    [60, 60] 120 true 720
    [100, 100] 200 true 1200

    captureEvidenceBytes is 12 bytes per LED of one lane in every case — 600 for a 50-LED lane, 1200 for a 100-LED lane — while the response reports laneCount: 2, passed: true.

    That is expected physically: this fixture has a single RX pin (GPIO1), so only the bridged lane can be captured. The gap is that nothing in the response distinguishes "verified both lanes" from "verified one and drove the other". captureEvidenceBytes is the only tell, and reading it requires knowing the expected byte count and computing it yourself.

    runParallelTest handles the same constraint deliberately — phases.py:4376 excludes capture evidence for concurrent runs and validates distinct TX pins plus show_success instead. The single-test multi-lane path appears not to have an equivalent contract.

    Also worth noting: [100, 100] = 200 LEDs passes, while a single lane of 101 fails. So the per-lane cap in #4371 is applied per lane while rx_buffer is shared — consistent with only one lane being captured, but it means the guard's arithmetic and the capture's actual behaviour are describing different things.

    I am not proposing a change. On a single-RX fixture there is no more evidence to be had; the question is whether a PASS should say so, and that is a call for whoever owns the criterion.

  15. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    No multi-frame degradation on the RP PIO path — including at the capacity boundary

    A negative result, but on a dimension the harness has a dedicated flag for (--frames, "Exposes second-frame degradation bugs"), so it is worth recording as closed rather than untested.

    Repeated frames per request, via runSingleTest's iterations:

    geometry LEDs iterations passed captureEvidenceBytes
    WS2812B-V5 50 1 / 2 / 4 / 8 / 16 all true 600 → 9600, linear
    WS2812B-V5 100 (the cap) 2 / 4 / 8 all true 2400 / 4800 / 9600
    WS2811-400KHZ 63 (the cap) 4 / 8 all true 3024 / 6048

    Two things this establishes:

    No second-frame degradation. Sixteen consecutive frames decode cleanly, and byte counts scale exactly linearly with iteration count — no drop-off, no partial frames.

    The capture buffer is correctly reused between frames. rx_buffer is 3300 bytes, yet 100 LEDs × 8 iterations reports 9600 bytes of evidence. So the accumulation is in the reported total, not in buffer occupancy — the per-frame capacity limit from #4371 applies per frame and does not tighten with repetition. Running at the exact capacity boundary for eight consecutive frames is the strongest form of that check available here, and it holds for both geometries.

    So the PIO0 entry in the matrix above is robust across repetition, not just a single-frame pass.

  16. zackees commented on Sep 11, 2026

    @zackees
    MemberAuthor

    Minor: runSingleTest's pattern parameter is inert and unvalidated

    Small, and the practical impact cuts the safe way, but it is a diagnosability wart in the tool this issue's evidence comes from.

    runSingleTest accepts pattern, echoes it back in the response, and does nothing with it. It is extracted at AutoResearchRemoteRunSingleTest.cpp:545-547, echoed at :1112, and never mapped to a pattern id. The test always runs the full internal sweep — AutoResearchTest.cpp:1563:

    for (int pattern_id = 0; pattern_id < 4; pattern_id++) {

    Verified by passing values that cannot mean anything:

    sent='MSB_LSB_A'           passed=True  echoed='MSB_LSB_A'
    sent='MSB_LSB_B'           passed=True  echoed='MSB_LSB_B'
    sent='TOTAL_NONSENSE_XYZ'  passed=True  echoed='TOTAL_NONSENSE_XYZ'
    sent=''                    passed=True  echoed=''
    

    Nonsense and empty are both accepted without complaint and reflected back as though honoured.

    Coverage is better than the parameter implies, not worse — all four patterns run every time, including Pattern B (0x55, 0xFF, 0x00), which exercises the all-high and all-low byte boundaries. I went looking because the harness only ever sends MSB_LSB_A and I assumed patterns B and C were dead code; they are not, and I would have filed a bogus coverage gap if I had not checked.

    The risk is interpretive: someone narrowing down a pattern-specific failure will pass MSB_LSB_B, see it echoed, and believe they isolated one pattern when all four ran — so a failure that only Pattern C triggers looks like it reproduces under B as well. A typo'd name silently behaves identically.

    Either validate the value and select on it, or drop it from the accepted schema and document that the sweep is fixed. Echoing an ignored parameter back is the part that misleads.

  17. zackees commented on Sep 12, 2026

    @zackees
    MemberAuthor

    Full driver matrix, run today on master (731fb6f8ad) against the attached Pico 2 W. Every reachable path exercised, every unreachable one recorded with the exact wiring constraint it needs.

    Fixture — /dev/ttyACM0, 2E8A:F00F, serial 2DCB876B587EA334. The historical COM17 record is gone; this is the live enumeration from fbuild port scan. Peer: /dev/ttyACM2, 303A:1001, 8C:BF:EA:CF:87:B4, firmware-confirmed ESP32-C6FH4 (QFN32) rev v0.1.

    The bridge, from live evidence only

    findConnectedPins on the device, not from the historical note:

    {"found": true, "txPin": 0, "rxPin": 1, "autoApplied": true, "searchRange": [0, 8]}

    testGpioConnection, both directions plus a negative control:

    testGpioConnection(0, 1)  -> connected=true   rxWhenTxLow="LOW"   rxWhenTxHigh="HIGH"
    testGpioConnection(1, 0)  -> connected=true   rxWhenTxLow="LOW"   rxWhenTxHigh="HIGH"
    testGpioConnection(0, 20) -> connected=false  rxWhenTxLow="HIGH"  rxWhenTxHigh="HIGH"
                                 "RX pin stuck HIGH - no connection detected"
    

    The GPIO20 row is what makes the other two mean something: an unconnected pin reads stuck HIGH, so LOW-follows-TX is real feedback and not a floating input that happened to agree.

    Both profiles on the one board

    profile status.platform RPC smoke
    rp2350w Raspberry Pi Pico 2 W (RP2350) RESULT: RPC smoke PASS
    rp2350 (non-W) RP2350 RESULT: RPC smoke PASS

    Same silicon, two board configurations, both deploy → boot → serial RPC clean.

    Driver matrix

    path command result
    PIO0 --flex-io --rp-pio-index 0 TEST PIO0 lanes=1 leds=100 PASS 684ms, 4/4 patterns
    PIO1 --flex-io --rp-pio-index 1 TEST PIO1 lanes=1 leds=100 PASS 684ms, 4/4
    PIO2 (RP2350-only) --flex-io --rp-pio-index 2 TEST PIO2 lanes=1 leds=100 PASS 684ms, 4/4
    PIO0 + PIO1 parallel --flex-io --rp-pio-both --parallel PASS — see below
    UART0 --uart --rp-uart-index 0 transmits, comparison fails — #4379, fixed in #4381
    UART1 RPC, driver: "UART1" declined — not routable on this bridge
    SPI0 fixed-function --rp-spi-loopback --rp-spi-index 0 not reachable
    SPI1 fixed-function --rp-spi-loopback --rp-spi-index 1 not reachable
    SPI public API (APA102) --rp-spi-public-api not reachable
    AUTO RPC, driver: "AUTO" not a registered driver on RP
    BIT_BANG RPC, driver: "BIT_BANG" not a registered driver on RP

    PIO0 + PIO1 parallel — a resource test, and it says so

    {"passed": true, "show_success": true, "show_duration_us": 7872,
     "drivers": [{"driver":"PIO0","pinTx":0,"laneSizes":[100]},
                 {"driver":"PIO1","pinTx":1,"laneSizes":[100]}],
     "concurrent_resource_test": true,
     "captureEvidenceExpected": false, "rx_validation_attempted": false}

    Worth being explicit, because captureEvidenceBytes: 0 looks like a zero capture until you read the two lines above it: this configuration declares it is not a loopback test. Only one TX can reach the single RX pin, so re-entering capture here would test a different topology; both transmitters were already byte-validated individually in the rows above. The resource evidence is what matters, and testRpPioParallelResources supplies it:

    {"success": true, "iterations": 8, "numLeds": 256,
     "baseline": {"freeDma": 15, "freeSmsPerBlock": [3,4,4]},
     "busiestInFlight": {"freeDma": 14, "freeSmsPerBlock": [3,3,4]},
     "released": {"freeDma": 15, "freeSmsPerBlock": [3,4,4]},
     "collisionFree": true, "noOverClaim": true, "noLeak": true,
     "observedInFlight": true, "noResidueBetweenFrames": true}

    DMA and state-machine counts return exactly to baseline after eight frames, and busiestInFlight shows a block genuinely occupied mid-frame rather than the sampler missing the window. (bothHeldAtOnce: false in the same payload: across the sampled instants only one of the two ever held an SM — the shows are serialised by the manager, which is itself the answer to "no resource collision".)

    UART — one defect, one wiring constraint

    UART0 WS2811-400KHZ -> 0/4  attempted=True started=True baud=2000000  evidence=357
    UART1 WS2811-400KHZ -> 0/4  attempted=False started=False baud=0 err=''  evidence=0
    

    UART0 transmits — the engine starts and clocks out a frame — but every symbol miscompares. That is #4379: the geometry selector picks P=5, putting T1H at 1000 ns against a 1200 ns spec. Fixed in #4381, where the same board runs 4/4 at 1, 2, 5, 10, 20, 40, 60 and 63 LEDs, 3/3 each, at baud 1600000 — the first byte-exact RP UART clockless loopback.

    UART1 is a wiring constraint, not a defect. Valid TX pins are fixed per instance (channel_engine_rp_uart.cpp.hpp):

    static constexpr int kUart0TxPins[] = {0, 12, 16, 28};
    static constexpr int kUart1TxPins[] = {4, 8, 20, 24};

    The bridge drives from GPIO0, which belongs to UART0 only. UART1 cannot reach GPIO0 on any mux setting, so it is unreachable on this fixture without re-soldering. Note the decline above carries an empty err on master — that is #4375, fixed in #4376, which makes it say "RP UART: TX pin is not usable by this UART instance" instead of nothing.

    SPI — all three routes need pins this fixture does not have

    route required wiring present
    SPI0 fixed-function GPIO3 (MOSI) → GPIO0 (MISO), GPIO2 (SCK) GPIO0 ↔ GPIO1 only
    SPI1 fixed-function GPIO11 (MOSI) → GPIO8 (MISO), GPIO10 (SCK) —
    SPI public API, APA102/SK9822 GPIO11 (MOSI) → GPIO8 (MISO), GPIO10 (SCK) —

    GPIO0 is half of SPI0's route, but the other half is GPIO3 and the bridge's far end is GPIO1. No mux setting makes a two-pin 0↔1 bridge into a MOSI→MISO pair.

    The public-API run is worth quoting, because the failure is on the return path only:

    {"chipset": "APA102", "frameBytes": 1068, "actualClockHz": 8000000,
     "wireIdle": true, "success": false,
     "message": "RP SPI public API loopback failed."}

    1068 bytes is exactly right for 256 APA102 LEDs (4-byte start + 4 × 256 + 40-byte end), clocked at 8 MHz with the wire correctly idle. The transmitter is provably correct; only the MISO return the fixture cannot form is missing. That is a wiring limit, not an untested driver.

    AUTO and BIT_BANG

    AUTO     -> UnknownDriver: Driver 'AUTO' not available
    BIT_BANG -> UnknownDriver: Driver 'BIT_BANG' not available
    

    The registered set on RP2350 is exactly PIO0, PIO1, PIO2, SPI0, SPI1, UART0, UART1 (from drivers). Neither fallback is registered on this platform, so there is no public path to exercise — recorded as a platform fact rather than left as a blank row.

    Subsystem gates

    gate result
    RPC smoke (both profiles) RESULT: RPC smoke PASS (discovery, help, ping, payload, status, drivers, testNoSerial, RP concurrency, RP PIO contention, RP PIO0+PIO1 resources, error handling)
    SIMD 85/85 passed, plus testSimdBenchmark (simd_us 571 / 898 / 568)
    RP dual-core concurrency 200/200, pico-sdk-mutex+arduino-core1, core1 loop counter 8077 → 8079
    PIO contention noTheft, noLeak, 11 SMs free before and after
    C6 peer network 10/10 cycles, 12/12 both directions, 4 KiB FNV-1a each way — detail on #3832
    IEEE-754, watchdog/reconnect, Wave2D not run in this pass; not claimed
    BLE GATT lifecycle blocked — host has no BlueZ userspace; detail and the reason a two-board workaround is impossible on #3955
    Peer OTA not attempted unattended — reflashes the DUT over the air, and a partial write needs physical BOOTSEL

    Where that leaves the driver-coverage criterion

    Every driver registered on RP2350 is now either byte-validated through the public path or listed with the specific pins it would need:

    Nothing is left as "untested" without a reason a reader can check against the wiring.

  18. zackees commented on Sep 12, 2026

    @zackees
    MemberAuthor

    Subsystem gates, completing the three rows I left unclaimed in the driver matrix above. Same fixture, same master (731fb6f8ad), Pico 2 W on /dev/ttyACM0.

    IEEE-754

    ieee754CodecTest -> {"success": true, "tests_run": 35, "tests_failed": 0,
                         "first_failure": "", "expected_bits": 0, "actual_bits": 0}

    35/35, no first-failure bit pattern to report.

    Wave2D — and the listed gap is gone

    bash autoresearch rp2350w --perf-wave2d 32x32 --upload-port /dev/ttyACM0

    📌 Wave2D mode: skipping pin discovery and GPIO pre-test
    

    That line is the result. #3832's current-gaps list carries "--perf-wave2d incorrectly enters the GPIO-dependent path" — it does not any more. The mode now takes the pin-free path the way --simd does, so the gate no longer depends on a bridge it never needed. Closed with live evidence rather than by inspection.

    The run itself is clean, sanity probes included:

    perfProbeMemcpy (4096 bytes x 1000 iters)... 194.5 MB/s
    perfProbeNop    (100k iters)............... 0.0410 us/iter
    perfProbeRepeat (32x32, 50 iters x 8 rep).. 0.23% (stable)
    
    wave2dPerf:
      FivePoint          (compute)     0.3750 us/cell   2602 fps
      FivePoint          (loads_only)  0.3760 us/cell   2600 fps
      NinePointIsotropic (compute)     2.4570 us/cell    397 fps
      NinePointIsotropic (loads_only)  0.3760 us/cell   2598 fps
    
    "untrusted": false, "untrusted_reasons": []
    

    Worth reading the two loads_only rows against their compute partners, since that pairing is what makes the number trustworthy: FivePoint's compute cost is indistinguishable from its load cost (0.3750 vs 0.3760 µs/cell — memory-bound), while NinePointIsotropic's is 6.5× its loads (2.4570 vs 0.3760 — genuinely compute-bound). A solver that measured as pure load traffic in both stencils would mean the work was optimised away; it wasn't. perfProbeRepeat at 0.23% and untrusted: false say the harness itself agrees the measurement is stable enough to keep.

    Timing drift

    timingDriftTest -> {"success": true, "num_leds": 35, "iterations": 10, "show_count": 2954,
                        "show_min_us": 109, "show_max_us": 200, "show_total_us": 397822,
                        "iter_ms": [1270, 2495, 2495, 2495, 2495, 2495, 2495, 2495, 2495, 2495]}

    2954 shows with a 91 µs spread between fastest and slowest, and every iteration after the first landing on 2495 ms. The first at 1270 ms is the partial warm-up window, not drift.

    Watchdog — deliberately not induced

    The remaining gate is "watchdog acknowledgement, reset, CDC reacquisition, uptime reset, and post-reset RPC". The recovery half is evidenced continuously by this session's runs: every deploy resets the board and the next ping answers immediately with a fresh uptime and lastResetCause: "SOFTWARE", e.g. {"uptimeMs": 6476, "lastResetCause": "SOFTWARE", "lastResetWasWatchdog": false} taken right after a flash — CDC reacquired, uptime reset, RPC live.

    What I did not do is trip the watchdog on purpose. This bench is unattended for long stretches, and a deliberate hang is the one action here whose recovery I would not be present to confirm. The mechanism is already recorded live on #4173 (lastResetCause: "WATCHDOG", lastResetWasWatchdog: true, board self-recovered to IDLE), so the evidence exists — it just is not mine to re-collect safely right now. Flagging it as the one gate wanting a supervised session rather than quietly ticking it.

    Gate status after this pass

    gate result
    IEEE-754 35/35
    SIMD 85/85 + benchmarks
    Wave2D pass, pin-free path confirmed, untrusted: false
    Timing drift 2954 shows, 109–200 µs
    RP dual-core concurrency 200/200
    PIO contention / parallel resources noTheft, noLeak, baseline restored
    RPC smoke, both profiles PASS
    C6 peer network 10/10 cycles (#3832)
    Watchdog trip deliberately not induced — see above
    BLE GATT blocked on host BlueZ (#3955)
    Peer OTA not attempted unattended (#3956)
  19. zackees commented on Sep 12, 2026

    @zackees
    MemberAuthor

    Four of this issue's driver/harness fixes are now on master, so the matrix above can be re-read against shipped code rather than branches.

    landed commit what it unblocks here
    #4376 — declined RP UART channel no longer reports the previous run's diagnostics (#4375) 1987a5782a a UART decline now names its reason instead of returning a bare false with inherited numbers
    #4377 — the encoder reaches the transmitter, so UCS7604 tests the chipset (#4373) e0b793f221 UCS7604 goes from 0/4 at every length to 4/4 through 45 LEDs
    #4382 — an inline pinRx no longer captures nothing while reporting success (#4374) 8b965814eb alternate-wiring probes are trustworthy; a zero capture there now means no signal, not dma_buffer_busy
    #4310 — a declined driver is reported as declined, not as zero capture 129e4cd7d4 the distinction the parallel-PIO row in my matrix depends on

    Plus #4372 (a5fb9e670e, peer server stall budget) and #4343 (42a4d1f9e1, serial RPC dead-wait), which shorten the peer-network and RPC paths this issue's gates run on.

    Re-verified on the merged master against the attached Pico 2 W (2E8A:F00F, 2DCB876B587EA334), so this is the combination rather than each fix in isolation:

    PIO1           leds=100  4/4          RESULT PASS
    UCS7604-800KHZ leds=  1  4/4  bytes=84     (21 x 4 patterns)
    UCS7604-800KHZ leds= 10  4/4  bytes=300    (75 x 4)
    UCS7604-800KHZ leds= 45  4/4  bytes=1140   (285 x 4)
    UCS7604-800KHZ leds= 50  0/4  bytes=0      <- #4371 ceiling, PR #4378
    WS2812B-V5     leds=100  4/4  bytes=1200
    WS2818         leds= 50  4/4  bytes=600
    WS2814         leds= 50  4/4  bytes=600
    

    Every passing UCS7604 row captures exactly 15 + 6*leds bytes per pattern — the encoded frame length to the byte. Host suite on the merged tree: 305/305 unit, 84/84 examples.

    Still in flight for this issue, all with stale CHANGES_REQUESTED reviews that predate their fixes and re-review requested:

  20. zackees commented on Oct 2, 2026

    @zackees
    MemberAuthor

    Live evidence, 2026-10-02, Pico 2 (RP2350, 2E8A:000F, ser=2DCB876B587EA334), master ae0cf517c0 plus #4667 (--all prunes unregistered drivers). Command: bash autoresearch rp2350 --all --upload-port /dev/ttyACM0 --timeout 240 --skip-lint.

    Discovered bridge: GPIO0 -> GPIO1.

    --all: skipping drivers this firmware does not register: PARLIO, RMT, LCD_CLOCKLESS, LCD_SPI, LCD_RGB, OBJECT_FLED
    TEST SPI0 lanes=1 leds=100 FAIL 1217ms class=zero_capture
    TEST UART0 lanes=1 leds=100 FAIL 2016ms class=driver_declined
       RP UART: attempted=False started=False encodedBytes=0 actualBaud=0 lastError='RP UART: chipset timing needs a baud above the UART backend maximum'
    TEST PIO1 lanes=1 leds=100 PASS 634ms
    
    • PIO1: passes.
    • SPI0: expected not reachable on this fixture. SPI0 MOSI is not routable to GPIO0, so this is a wiring constraint, not a driver result.
    • UART0: GPIO0 is a valid UART0 TX, but the engine declines WS2812B-V5 because canRepresentTimingForMaxBaud finds no 4- or 5-pulse fit under RpUartPeripheral::maxBaudRate(). That value is min(clk_peri/16, kRpUartBaudCeiling = 6.25 MHz). This is the remaining UART gap for the acceptance item "UART clockless output on any hardware UART route compatible with the discovered bridge".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ciGitHub Actions, fbuild, toolchainsdeferredTriage pass could not resolve this; skipped by ci/oldest_issue.pyplatform: rp2040Raspberry Pi Picostatus: blockedBlocked by external dependency or issue

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions