Parallax Propeller2 USB driver - the MashUp

1 TB SSD over USB MSC + exFAT: validation and performance profile
Login

1 TB SSD over USB MSC + exFAT: validation and performance profile

2026-08-11 update (ticket 1170, whole-transfer engine): the per-packet bulk loop moved from interpreted Spin into the PHY cog (UH_CMD_INX/OUTX, in-cog CRC16 both directions). Same suite, same SSD, same 200 MHz clock, RESULT PASS all phases (harness/logs/headless_260811-093240.log and the post-clamp rerun): raw 8-block reads 809-819 KB/s (was ~50), raw writes 512-683 KB/s, exFAT 16 KB-chunk file I/O 643 / 603 KB/s write/read (was 46/49), single-block latency ≈ 1 ms (was ≈ 12 ms), mount 54 ms (was 209), mkdir 295 ms (was 3.08 s). At 300 MHz raw sequential reaches 888 / 941 KB/s read/write. The 2088 async worker path (192 KB file copy + concurrent Alicat polling) went from 9.3 s to 501 ms (~392 KB/s) with cadence slip down from 176 ms to 1 ms. The figures below are the pre-1170 baseline, kept for the record.

Ticket 2087 (44b3845e66). All numbers measured on real hardware, 2026-08-10: P2 Eval at 200 MHz, serial-host header (base pin 16), 4-port bus-powered hub, the SSD attached through the hub alongside a CDC serial gadget, a keyboard/mouse dongle, and a USB SD-card reader — all enumerated concurrently. Full log: harness/logs/headless_260810-012803.log; harness: harness/top_ssd_profile.spin2.

Device

Bridge ASMedia SATA-USB, VID $174C PID $A89A
Capacity 2,000,409,264 × 512-byte blocks (≈ 1 TB, 976,762 MB)
Filesystem exFAT, 7,813,842 clusters × 128 KB, MBR partition at LBA 2048
Volume label P2SSD

This device is interesting to the stack for three reasons: its top LBA (2,000,409,263) sits just under the signed-32-bit edge, so it exercises the full 32-bit LBA range through clus2lba() and READ(10)/WRITE(10); its cluster count needs all 8 of the driver's mapped bitmap clusters (BMP_MAX_CLUS); and its bridge chip behaves differently from the SD-card reader used for all prior validation.

Functional results — all pass

Performance

USB full speed is 12 Mbit/s on the wire; the practical ceiling of this stack is set by the Bulk-Only-Transport protocol overhead (CBW/data/CSW per command) and the one-cog Spin-orchestrated 64-byte packet loop, not by the media — the SD-card reader profiles within a few percent of the SSD on the same rig. Where the SSD clearly wins is command latency (disk_open 5 ms vs 150 ms, no NOT-READY retry dance) and write consistency (no multi-second flash-GC stalls observed).

Raw block layer (MSC READ(10)/WRITE(10), 4 KB BOT chunks)

Operation Total Time Per-op Throughput
Single-block reads ×32 16 KB 374 ms 11.7 ms 42.8 KB/s
8-block reads ×16 64 KB 1,264 ms 79 ms 50.6 KB/s
64-block reads ×4 128 KB 2,528 ms 632 ms 50.6 KB/s
Single-block reads, device-spanning ×16 8 KB 188 ms 11.8 ms 42.6 KB/s
8-block write ×1 4 KB 87 ms 87 ms 46.0 KB/s
Single-block writes ×8 4 KB 95 ms 11.9 ms 42.1 KB/s

Single-block latency is flat (~11.7 ms) across the whole device — no LBA-position penalty. Multi-block commands amortize BOT overhead and settle at ≈ 50 KB/s regardless of transfer size beyond 4 KB.

Filesystem layer (exFAT fs_* API)

Operation Total Time Per-op Throughput
fs_mount 209 ms
fs_unmount 23 ms
fs_mkdir 3,080 ms 128 KB cluster zero-fill
create + 64 B write + close, ×4 256 B 557 ms 139 ms/file metadata-bound
fs_write in 512 B calls 64 KB 1,583 ms 12.4 ms/call 40.4 KB/s
fs_read in 512 B calls 64 KB 1,588 ms 12.4 ms/call 40.3 KB/s
fs_write in 16 KB calls 512 KB 11,387 ms 356 ms/call 45.0 KB/s
fs_read in 16 KB calls 512 KB 10,720 ms 335 ms/call 47.8 KB/s
fs_seek + 512 B read, ×8 4 KB 99 ms 12.4 ms

Takeaways:

Defects found on the way (all fixed, ticket 2087 has the full ledger)

  1. PHY TX corruption, content-dependent — the silicon-level star of this ticket. A transmitted byte was corrupted whenever the outgoing bit-stuff geometry placed a stuffed 0 immediately after the first bit of the byte following $FF (lead byte with an MSB-side 1-run of exactly 3, e.g. $E3 $FF). Devices dropped the corrupt packet silently (per spec) and the abandoned transfer wedged the BOT state machine. Root-caused by single-block content bisection (harness/top_txpat.spin2), fixed by raising the AKPIN→WYPIN gap from 3 to 8 clocks at ≥ 168 MHz — a hardware sweep (harness/top_twksweep.spin2) showed 0/3 fail and 6–20 all pass at 200 MHz. The reference OBEX 4198 driver's 3-clock value was never wrong for it — it only ever transmits 8-byte control payloads.
  2. Bulk data-stage patience — storage devices stall data stages (NAK or silence) for seconds while flash/GC is busy; the old ~64 ms per-packet retry budget abandoned transfers mid-flight. MSC now runs a ~10–20 s budget on its client instance (usb_client.set_nak_budget), matching the existing multi-second CSW patience; control paths keep the fast default.
  3. BOT reset recovery on the steady-state pathdisk_read/disk_write now run the BOT 6.6.2 recovery sequence (drain, class reset, clear both halts) and retry once on failure; previously only disk_open recovered.
  4. Late-attaching devices — SSDs can take seconds to assert connect; one-shot enumeration missed the drive entirely. usb_hub_host gained port_status(pn) (raw GET_STATUS(port)) so callers can poll for late connects and enumerate with enum_one_port(pn).

Reproducing

cd harness
pnut-ts -d -I ../src -D USB_MSC -D USB_EXFAT top_ssd_profile.spin2
pnut-term-ts --headless -r top_ssd_profile.bin -p <PORT> -b 2000000 --end-marker "SSD_PROF_END"

The harness self-selects a non-SD-reader MSC device when several are present, waits up to 30 s for late attaches, and prints PROF lines (bytes= ms= ops= Bps=) for every measurement plus a final RESULT PASS/FAIL. top_txpat.spin2 (hostile-content canary) and top_twksweep.spin2 (TX-gap sweep) remain in harness/ as regression tools for the TX fix.