Status: validated live — 1 TB exFAT SSD (full performance profile), 64 GB exFAT card, 8 GB FAT32 card, hot-plug of a mounted volume.
What you need
- A P2 board with the USB serial-host header (base pin 16 assumed below)
- USB storage: thumb drive, SD-card reader, or SSD bridge — formatted exFAT (preferred) or FAT32
- Build gates:
USB_MSC+USB_EXFAT(addUSB_FAT32for auto-detect fallback to FAT32 media)
pnut-ts -d -I <path-to>/src -D USB_MSC -D USB_EXFAT my_files.spin2
The smallest program
#IFDEF USB_MSC
#PRAGMA EXPORTDEF USB_MSC
#ENDIF
#IFDEF USB_EXFAT
#PRAGMA EXPORTDEF USB_EXFAT
#ENDIF
CON
_clkfreq = 200_000_000
OBJ
usb : "usb_app"
VAR
byte buf[64]
PUB main() | h, n
usb.begin(16)
ifnot usb.await_drive(15_000) ' SSD bridges take 6-9 s to even
debug("no drive") ' assert connect - 15 s is generous
repeat
h := usb.fs_create(string("/HELLO.TXT")) ' filesystem mounts itself on
if h >= 0 ' first use - no mount call needed
usb.fs_write(h, @msg, 13)
usb.fs_close(h)
h := usb.fs_open_read(string("/HELLO.TXT"))
if h >= 0
n := usb.fs_read(h, @buf, 63)
buf[n] := 0
debug("read back: ", zstr_(@buf))
usb.fs_close(h)
repeat
usb.poll() ' hot-plug: a yanked drive is
waitms(50) ' abandoned safely, remount is
' automatic on next fs_* use
DAT
msg BYTE "hello, disk", 13, 0
What bring-up actually does
usb.begin(16)enumerates; the first mass-storage device binds.await_drive()waits for SCSI readiness too — a cold card reader takes seconds to mount its card; the driver polls INQUIRY/TEST-UNIT- READY instead of guessing with delays.- The first
fs_*call probes the media: exFAT first (boot-region checksum verified), FAT32 fallback if both gates are compiled.fs_kind()tells you which one owns the mount. - Paths are root-relative (
/LOGS/RUN1.TXT) on exFAT; FAT32 uses 8.3 names.fs_open_writeappends;fs_createfails if the file exists; negative returns are error codes (README table). - Timestamps: bind an RTC before writing with
fs_time_source(@rtc)— contract in the README — or files get a fixed build-time stamp.
Worth knowing
- Wear policy is built in: both filesystems allocate next-fit across
the whole volume (never re-hammering freed clusters), and
fs_sync()persists the FAT32 allocation hint — call it at checkpoints. - Long operations without blocking: the opt-in
usb_fs_jobworker cog runs whole file jobs (copy, write, delete) with progress and cancel while your control loop keeps its cadence — see the API manual §3. - Raw blocks:
disk_read/write(lba, n, buf)under the filesystem, 512-byte blocks, if you're bringing your own format. - Throughput to plan around: ~600–645 KB/s through exFAT with 16 KB calls; chunk size barely matters above one sector.
Troubleshooting
| Symptom | Likely cause |
|---|---|
await_drive times out |
Media still booting (SSD bridges 6–9 s — raise the window); unpowered hub; drive on the other bus. |
| Mount error −81 | Media isn't exFAT — add -D USB_FAT32, or reformat. |
| −87 after replug | Stale handle from before the detach — reopen by path. |
| First op after attach is slow | That's the card reader mounting its card (measured ~2.6 s) — it's the device, not the driver. |
| Mount fails, sector 0 reads all-zero, even a desktop format "succeeds" but changes nothing | Dead flash: the card ACKs writes and silently discards them (bench-caught 2026-08-13: write CSW clean, read-back zeros). harness/top_lunprobe.spin2's write-persistence check proves it in seconds. Replace the card. |
| Reads/writes start failing and don't come back | The drive has wedged. Branch on disk_wedged(), and see recovery.md — it covers telling a hub-disabled port from a wedged device, the recovery ladder, and how to prove a drive really came back. |
| Capacity/TUR fine but every read fails sense $B/47/01 | The reader's SD data lines to the card are failing CRC (command line still works — capacity comes from it). Reseat/clean, or swap the reader. |
Next
- Full worked example:
examples/top_fs_example.spin2(incl. RTC binding, directory listing) - Hot-plug-resilient logger skeleton:
examples/top_hotplug_example.spin2 - Async jobs under a live control loop:
harness/top_async_job.spin2 - Recovering a wedged drive: recovery.md — reset/re-bind ladder, opt-in auto-recovery, content verification, and the evidence ring
- Performance profile: docs/perf-1tb-ssd.md; API: docs/api/usb-app-api.md §3