Skip to content

rp2/PICO_COMPUTER_3: add USB mass-storage host (machine.USBDrive) - #1

Open
Bryan1club wants to merge 6 commits into
UKTailwind:pico-computer-3from
Bryan1club:usb-msc-host
Open

rp2/PICO_COMPUTER_3: add USB mass-storage host (machine.USBDrive)#1
Bryan1club wants to merge 6 commits into
UKTailwind:pico-computer-3from
Bryan1club:usb-msc-host

Conversation

@Bryan1club

Copy link
Copy Markdown

Summary

The host USB stack already covers HID keyboard/mouse/touch (mp_usbh.c) and CDC-serial (usb_cdc.c), but CFG_TUH_MSC was never enabled, so a USB flash drive plugged into the host port is invisible to MicroPython even though the vendored TinyUSB already ships the tuh_msc class driver.

This adds machine.USBDrive, a block device mountable with vfs.VfsFat exactly like machine.SDCard, plus a pcusb.py that mounts it at /usb automatically at boot (mirrors pcsd.py's /sd).

  • tusb_config.h: enable CFG_TUH_MSC / CFG_TUH_MSC_MAXLUN (1 LUN — enough for the flash drives this board targets).
  • usb_msc.[ch] (new): TinyUSB MSC-host glue — mount/unmount tracking plus blocking read10/write10 wrappers. TinyUSB's are callback-based; these pump tuh_task() until the SCSI command completes, the drive disappears, or a 3s timeout elapses.
  • machine_usbdrive.c (new): machine.USBDrive, structured the same way as machine_sdcard.c. It's actually simpler than the SD driver — USB enumeration gives a real connect/disconnect event, so instead of SD's twice-a-second poll (pcsd.py), USBDrive just exposes on_change(fn).
  • pcusb.py (new) + _boot_board.py / manifest.py wiring: mounts /usb at boot and on hotplug.
  • USER_MANUAL.md: three small doc touch-ups (host-port description, pin table, storage section) so this isn't undocumented.

Test plan

I don't have a local pico-sdk/arm-gcc toolchain set up, so this hasn't been built or run on real hardware — I checked it carefully against the pinned TinyUSB submodule (3af1bec, matching msc_host.h's actual API) and against this branch's own conventions (usb_cdc.c/usb_cdc_mod.c split, machine_sdcard.c's block-device shape), but a compile + hardware smoke test (plug in a stick, ls("/usb"), copy a file) is still needed. Happy to adjust based on what that turns up, or if you'd rather fold this in yourselves given you can test on real boards.

  • Builds cleanly for PICO_COMPUTER_3
  • A FAT-formatted USB stick enumerates and mounts at /usb
  • ls("/usb"), read, and write all work
  • Unplug/replug is handled cleanly (no crash, remounts)

TinyUSB's host stack already covers HID keyboard/mouse/touch (mp_usbh.c)
and CDC-serial (usb_cdc.c) for this board, but never enabled the MSC
(mass-storage) host class, so a USB flash drive plugged into the host
port was invisible to MicroPython even though TinyUSB ships tuh_msc
already.

- Enable CFG_TUH_MSC / CFG_TUH_MSC_MAXLUN in tusb_config.h.
- usb_msc.[ch]: TinyUSB MSC-host glue -- mount/unmount tracking plus
  blocking read10/write10 wrappers (TinyUSB's are callback-based; these
  pump tuh_task() until the SCSI command completes or times out).
- machine_usbdrive.c: machine.USBDrive, a block device mountable with
  vfs.VfsFat, structured like machine.SDCard. Unlike the SD card (a bare
  SPI card polled twice a second in pcsd.py for hot-swap), USB gives a
  real connect/disconnect event, so USBDrive exposes on_change(fn)
  instead of a poll.
- pcusb.py (+ _boot_board.py/manifest.py wiring): mounts /usb the same
  way pcsd.py mounts /sd.
- USER_MANUAL.md: document the new host-port capability and /usb mount.

Tested by inspection against this branch's tinyusb submodule (pinned at
3af1bec) -- no local pico-sdk/arm-gcc toolchain was available to build
and flash a UF2, so this hasn't been run on real hardware yet. Happy to
adjust based on how it behaves once built.
…ID input

Small transport-agnostic hooks so a Bluetooth LE HID-over-GATT keyboard/
mouse (or any other non-USB input source) can feed the exact same report
decoders USB keyboards/mice already use:

- keyboard.inject_report(data): feeds an 8-byte HID boot keyboard report
  into kbd_process_report(), which has no USB dependency at all.
- mouse.inject_mount(desc) / inject_report(data) / inject_umount(): feed
  a HID report descriptor + reports into the existing descriptor-driven
  mouse decoder, using a fixed USB_MOUSE_BLE_ADDR sentinel so it's never
  confused with a real USB mouse. usb_mouse.c's two TinyUSB-only control
  transfers (set/get protocol) are skipped for that sentinel address,
  since a non-USB source has no TinyUSB device behind it.

This is groundwork for a BLE HID keyboard/mouse module (aioble- or raw
bluetooth-module-based) -- not included here. No behavior change for
existing USB keyboards/mice.
mp_sched_schedule() always invokes its target with exactly one argument,
but usb_msc.c's notify_change() scheduled pcusb.py's _on_change() (which
took zero parameters) -- every mount/unmount raised "TypeError: function
takes 0 positional arguments but 1 were given" from the scheduler, which
pcusb.py's own try/except didn't catch (the crash was in the callback
dispatch, not inside the try block), silently killing the auto-mount.

Confirmed on real hardware: the MSC layer itself was fine the whole time
(a real stick enumerated correctly -- "USB drive: mounted addr=3 lun0
60620800 x 512 bytes" -- confirmed via manual `vfs.mount(vfs.VfsFat(d),
"/usb")` at the REPL) -- only the automatic on-connect mount was broken.

Fix: notify_change() now takes and passes through a connected bool, like
every other on_change/on_usb_event callback elsewhere in this port
(usb_cdc.c, mp_usbh.c) already does. pcusb.py's _on_change(connected)
updated to match; it still re-checks present() rather than trusting the
argument, so a stale/coalesced event can't leave the mount state wrong.
Stream audio out to a Bluetooth speaker/headset. This is a different
Bluetooth transport from the board's existing BLE support (aioble,
keyboard/mouse hooks) -- A2DP is Classic Bluetooth (BR/EDR), which
MicroPython's `bluetooth` module has zero bindings for on any port.

btaudio.c is adapted directly from BTstack's own official
a2dp_source_demo.c (already vendored in lib/btstack, and already fully
wired up as pico-sdk's own pico_btstack_classic CMake target -- this
board just never turned it on), trimmed to source-only (no AVRCP, no
stream reconfiguration) and with the demo's synthetic sine/mod audio
replaced by a plain ring buffer fed from Python via write().

CMakeLists.txt adds the specific classic-profile + SBC-encoder source
files directly rather than linking pico_btstack_classic, because that
library pulls in its own copy of hci.c/l2cap.c -- duplicating the ones
micropy_extmod_btstack already compiles for BLE. Classic and LE share
ONE BTstack/HCI instance by design; hci.c/l2cap.c just need
ENABLE_CLASSIC=1 on their single compilation, which
target_compile_definitions on the firmware target itself achieves.

Python API: btaudio.scan()/on_found()/connect()/on_connect()/
disconnect()/connected()/sample_rate()/write()/writable(). Requires
bluetooth.BLE().active(True) to have already brought up the shared
stack; btaudio.c never calls hci_init()/l2cap_init()/hci_power_control()
itself.

NOT YET TESTED ON HARDWARE -- no Bluetooth speaker in hand yet. Written
carefully against BTstack's official demo and header APIs, but the
SBC/AVDTP timing and buffer sizing will likely need real-world tuning.
- avdtp_configuration_sbc_t is the correct struct name in this vendored
  BTstack version (a2dp_source_demo.c's media_codec_configuration_sbc_t
  is from a newer BTstack release than what's pinned here); dropped its
  .reconfigure/.num_channels fields and their event getters, which don't
  exist in this version either and weren't used by this simplified
  source-only implementation anyway.
- Added avdtp_acceptor.c and sdp_client.c: even a pure A2DP SOURCE role
  needs the AVDTP acceptor state machine (the sink can send signaling
  requests back) and an SDP client (to query the sink's AVDTP PSM).
- ringbuf_init() doesn't exist in this port's py/ringbuf.h -- set the
  buf/size/iget/iput fields directly, matching usb_cdc.c's own pattern.
- mp_raise_OSError_msg()/mp_raise_OSError_msg_varg() don't exist; used
  mp_raise_msg()/mp_raise_msg_varg() with &mp_type_OSError instead.
- Shrunk the PCM feed ring buffer 16KB -> 4KB: this board's SRAM GC arena
  is already tuned to a documented tight minimum (see the ASSERT in
  memmap_rp2350/section_extra_post_platform_end.incl), and classic
  Bluetooth's own static buffers (AVDTP/SDP state, SBC encoder state)
  ate most of the slack upstream left. Revisit only alongside re-checking
  that assert, and only if real playback shows underruns.

Now builds clean: 0 warnings, RAM 87.76%, FLASH 41.65%. Still not tested
on real hardware -- no Bluetooth speaker in hand yet.
Pure Python, no firmware/C changes needed -- MicroPython's raw
bluetooth module already has everything this needs (central scan/
connect, GATT client, pairing) built in. Uses the same
keyboard.inject_report()/mouse.inject_report() hooks added earlier in
this branch to feed BLE HID reports into the exact decoders USB
keyboards/mice already use.

Scans/pairs on demand (not at boot, unlike pcsd.py/pcusb.py -- pairing
is a deliberate user action, not an automatic mount). Negotiates Boot
Protocol Mode and subscribes to the standard Boot Keyboard/Mouse Input
Report characteristics.

Known v1 limitations, documented in the file: one device at a time, no
persistent bonding across reboots (BTstack, used on this board, doesn't
support the bonding-store IRQ events MicroPython's docs mark
"NimBLE only"), boot-mode-only (no arbitrary Report-mode parsing), and
mutually exclusive with aioble (both need the single bluetooth.BLE()
irq() slot -- nothing else in this app uses aioble currently).

NOT YET TESTED ON REAL HARDWARE -- no BLE keyboard/mouse in hand to
verify against. Written against MicroPython's documented raw bluetooth
API and the Bluetooth SIG's standard HOGP UUIDs; passed mpy-cross
compilation (syntax-valid) but the GATT descriptor handle-range
bookkeeping in particular is the most likely spot a real device's exact
attribute layout could trip up.
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.

1 participant