Skip to content

Design History

delabrcd edited this page Jul 23, 2026 · 4 revisions

Design history

Why the firmware looks the way it does. This is the one place the historical narrative lives; for how it works today, see Architecture.

From AVR to RP2040

openrb-pico began as a rewrite of openrb-instruments — the original DIY Rock Band MIDI pro-drums adapter for Xbox One/Series, an Arduino/AVR build (ATmega32u4 Leonardo, on LUFA). That firmware is the mature, shipping reference this project targets; the goal was to move it onto the more capable RP2040, initially on the Adafruit Feather RP2040 USB Host.

The move's central challenge was the USB stack. The RP2040 has a single native USB controller, so openrb-pico bit-bangs a second USB host in PIO (Pico-PIO-USB) to read the instrument while the native USB serves the console-facing device side — two USB roles at once, split across the chip's two cores.

The firmware then evolved through three phases on the RP2040, each covered below: a C superloop, then a FreeRTOS-SMP task model, then a rewrite to modern C++23 with a proper HAL. Getting the PIO-USB host reliable runs through the first of those.

Host-stack bring-up

The port started by getting an Xbox controller to enumerate through a CH334R hub on a bit-banged PIO-USB host, which meant bumping the whole USB stack: pico-sdk 1.5.1 → 2.2.0, TinyUSB 0.16 → 0.18, Pico-PIO-USB 0.5.3 → 0.7.2, usb_midi_host → 2.0.0. The firmware was then a dual-core bare-metal superloop written in C: core0 ran the device + features, core1 was dedicated to tuh_task.

Two root-cause bugs had to be fixed to get reliable enumeration:

  1. core1 early-launch race — multicore_launch_core1 could fall back to the bootrom if the inter-core handshake raced; fixed with a settle/reset dance.
  2. PIO-USB timing margin through the hub repeater — the extra hop cost signal margin; originally worked around by overclocking to 240 MHz to double the PIO sub-bit timing resolution.

The 240 → 120 MHz decision

Overclocking was later dropped. Upgrading Pico-PIO-USB to its upstream main (post-0.7.2 bus-turnaround / handshake timing fixes) made enumeration reliable every time from a cold start at 120 MHz — the clock the PIO-USB library is actually designed for. The old 0.7.2 needed 240 MHz only because it lacked those fixes; with them, 240 MHz and an accompanying PID-mismatch patch were both retired. 120 MHz has been the clock ever since, unchanged by the FreeRTOS port.

The evidence (rev 0.2 hardware, boot-success rate over 10 resets; "success" = the controller's IN endpoint returns reports):

Config Result
pico_pio_usb 0.7.2, 240 MHz, both patches 10/10
pico_pio_usb 0.7.2, 120 MHz, both patches 0/10 — IN data corrupted
upstream main, 120 MHz, hub patch only enumerates every time

With the old 0.7.2, 120 MHz was a hard blocker regardless of which core hosted; the upstream upgrade is what made it viable and let the overclock and the PID-mismatch patch go — only the hub-descriptor patch (wValue=0x2900) remains. (For contrast, Santroller reaches 120 MHz a different way — forked pico_pio_usb timing with the host on core0 — not via patches; its fork is coupled to TinyUSB 0.20 glue and won't compose partially.)

Hardware A/B testing caveats

Two pitfalls have repeatedly produced false enumeration results — honor them before trusting any A/B:

  • SWD reset ≠ the physical RESET button for this board's USB-wedge behaviour. Ground truth is a physical reset while watching the controller LED (LED on = enumerated).
  • The CH334R hub stays powered across RP2040 resets (downstream VBUS is hardwired +5 V on rev 0.2), so it accumulates bad state and wedges until a full power cycle. Keep reset batches small and power-cycle between candidates.

Composing versions (current)

  • pico-sdk 2.2.0 (fork delabrcd/pico-sdk@openrb)
  • TinyUSB 0.20 (fork delabrcd/tinyusb@openrb) — supersedes the 0.18 named in older notes; brings USBH_CONTROL_RETRY_MAX
  • Pico-PIO-USB upstream main (fork delabrcd/Pico-PIO-USB@openrb)
  • FreeRTOS-Kernel V11.2.0 (RP2040 SMP port)
  • usb_midi_host 2.0.0

One submodule patch remains (a hub-descriptor wValue fix); the 240 MHz PID patch is gone.

FreeRTOS SMP (2026)

The bare-metal superloop was moved onto a FreeRTOS-SMP task model: both cores run under one scheduler, core1 is a single usb_host task, and everything else is core0 tasks (CFG_TUSB_OS=OPT_OS_FREERTOS). This was a concurrency-model change only — clock, patches, and library composition were untouched.

The old core1 launch race became a new hazard in this model: a sleep_ms in pre-scheduler boot code deadlocks (the SDK sleep needs the scheduler), so boot code uses busy_wait_ms. That was "the single nastiest bug of the port."

Modern C++ and a proper HAL

With the concurrency model settled, the firmware was rewritten from its original C (a flat src/*.c + inc/* layout) to modern C++23, restructured into the layered modules/ tree it has today. The rewrite brought static dependency injection with a single composition root — no singletons, no heap — and a zero-cost compile-time HAL (hardware access through concepts and value types) that cleanly separates the portable feature/protocol logic from the pico-sdk, with the layering enforced in CI. See Architecture for the end state.

Warm-reset / hub-wedge recovery

On the rev-0.2 board the downstream hub VBUS is hardwired to +5 V, so firmware can't power-cycle the controller. Across an RP2040 warm reset the controller keeps stale USB session state and comes back as a "silent zombie" — it mounts but never sends input. Recovery is layered (recovery.cpp, host_controller.cpp):

  1. Runtime hub-RESET# pulse (preferred, no reboot) — on core1, when a wedge is detected (a long IN-error streak, or 30 s of silence from a mounted controller), pulse the hub's active-low RESET# to force a clean re-enumeration. Bounded to 3 attempts, self-clearing on a fresh heartbeat, and it stands down after exhaustion so a real unplug can't thrash the hub.
  2. Watchdog reboot (last resort) — on core0, auth-gated and bounded to 4 reboots (the count survives across reboots in a watchdog scratch register, cleared on a real power-on), for a mounted-but-silent controller that the hub reset didn't recover.

The real fix is a hardware respin adding a firmware-controlled downstream VBUS switch; the FEATHER board already has one (GPIO18), which is why it doesn't need this machinery.

openrb-pico

Getting started

Features

Background

Repo

Clone this wiki locally