Board / port: PNut-Term-TS P81svj44
Dogfood host: DM 40 / DP 41
Dogfood device: DM 48 / DP 49 (ganged)
Sysclk baseline: 200 MHz (baud X = $CF5C)
Architecture: usb_host = PHY cog only; usb_client = sole Spin protocol engine
Date: 2026-08-01 (updated)
Scope vs status
| Layer | Status | Notes |
|---|---|---|
| Thin PHY + protocol engine | Done | 1000–1070, 0150 Fixed; live |
| Control enum multipacket | Done | GET_DESC n=18 mps0=8, status ACK |
| Device peer + BOT | Restructured | Per-class device cogs (not kitchen-sink) |
usb_app facade |
Done | bind_hid / bind_cdc / bind_msc (not all-at-once default) |
| Concurrent load + SOF | Done | per-class LOAD (one device cog at a time on gang) |
| Topology switch API | Done | Serial Host ch1/ch2 open+enum probe |
| Real SSD multi-sector | Open | 2030 — host path ready; needs media on Serial Host |
| Hub multi-device enum | Open | 1090 — addr tokens work; hub class not automated |
| Clock sweep to failure | Done | real ladder 140-300 MHz, honest scoring (4030 CLOSED 2026-08-09) |
| Class-faithful multi-pair emus | Partial | shared min peer plane |
| Isochronous + UAC1 audio | Done | 1180/2110 CLOSED 2026-08-12; loopback acceptance (below) |
Live dogfood evidence
Log: harness/logs/headless_260801-002613.log — per-class device cogs
PHASE HID: kbd.start cog → enum → bind_hid → kbd_poll key0=$04
LOAD HID ok=40 fail=0 sof_delta=42
PHASE CDC: cdc.start cog → enum → bind_cdc → ser_write hs=$D2 ser_read b0=$43
PHASE MSC: msc.start cog → enum → bind_msc → disk_open blocks=1024
Architecture: one host Force-1 cog; each emu owns its device cog (sequential on gang)
Concurrent load (4010 / 1110)
| Metric | Value (live) |
|---|---|
| Iterations | 40 |
| Classes exercised | HID poll, CDC bulk, MSC presence |
| SOF | enabled during load; sof_delta ≈ iteration count with 1 ms yield |
| Failures | some NAKs expected under tight poll |
Topology (4000 / 1130)
| Topo | Pins | Live |
|---|---|---|
| Dogfood | 40/41 host + 48/49 device | full stack green |
| Serial Host CH1 | 34/35 EN 33 | open ok; enum needs stick |
| Serial Host CH2 | 38/39 EN 37 | open ok; enum needs stick |
Footprint (2060 / 4050)
Hub RAM (UH_TOTAL_BYTES @ version 4 at measurement time): ctrl+toggles+
scratch + class ws. (Layout is v7 since 1180: control block 20 longs,
dual-bus toggle tables, and USB_AUDIO adds a 6,208 B ISO workspace —
current totals per usb_hub_ram.spin2 / docs/design/hub-ram-map.md.)
| Build defines | Selftest .bin bytes |
Hub total (all-on) |
|---|---|---|
USB_HID only |
20978 | scales HID 1024 |
USB_MSC only |
20880 | scales MSC 8192 |
USB_CDC only |
20572 | scales CDC 2048 |
| HID+MSC+CDC | 23044 | 11840 live |
Exclusive builds drop unused class objects and hub workspaces (#IFDEF).
Clock / baud (4030)
| Sysclk | Baud X | Notes |
|---|---|---|
| 300→50 MHz | ladder via char.clock_ladder_run |
10 legal / 1 illegal (48) logged live |
| 200 MHz | $CF5C |
live baseline; matches ladder |
| ≤48 MHz | invalid | host rejects |
CLOSED 2026-08-09 with a real clkset ladder (per-step VBUS cycles, hub + 3 real devices, honest scoring - a step fails if a compiled class leaf never binds): 165-300 MHz PASS full stack, 160 MARGINAL (card-reader config read intermittent), 140/150 FAIL (same read; enum+HID+CDC alone work to 140). Sub-160 limit is CPU-bound Spin host loops, not USB timing. The TX gap self-scales as 3/4 bit period above the ~168 MHz breakpoint (4040). Guidance: keep sysclk above 180 MHz; 200 default. Note: the sub-168 gap-0 branch has never run the hostile-content canary (2087 finding postdates the sweep), and the ladder predates the 1140/1150/1160/1170 engine rework - re-proven at 200 and 300 MHz only. 0190 (2026-08-11) then found and fixed two PHY bugs at exactly this margin: the RX ACK trailed two mailbox writes (blowing the device's 16-18 bit-time window through hub latency at 200 MHz - USB-3-fallback sticks went silent/wedged behind hubs) and SOFEN bursts starved the SOF grid into device suspend. Post-fix: the FULL dual-bus suite (canaries both buses, interleave, hot-plug) passes at 200 AND 300 MHz.
Isochronous audio (1180 / 2110, CLOSED 2026-08-12)
Device: C-Media CM108 adapter ($0D8C:$0014) behind the hub; validation rig:
loopback jumper headphone→mic, making the stack its own frequency
counter and level meter (harness/top_audio_accept.spin2, RESULT PASS):
| Measure | Result |
|---|---|
| Capture rate | exactly 48,000 Hz and 44,100 Hz (16.16 fractional path) |
| Tone matrix | 500/1k/2k/4k Hz each confirmed at its own Goertzel bin |
| Volume/mute | effective (CM108 coarse steps, master channel) |
| 60 s playback under load | 0 underruns while the 2088 job cog copied files at 251 KB/s |
| First-hardware run | 9 clean tones by ear, 169k frames sustained 1000/s (top_tone_keyboard_example) |
Campaign findings (fixed in the same pass): interrupt-IN polls carried the
bulk NAK budget (~64 ms/idle poll → ~1 ms); ISO receiver lacked the own-EOP
gate; frame guards taxed both buses with the ISO reservation (27 KB/s disk
→ audio-bus-only); aud_read clip-discard punched DSP-shredding
discontinuities (now whole-frames-only). Known characteristic: the CM108
mic is synchronous-mode ISO and loses SOF lock under jitter from a degraded
bus peer (~99.3 % held with healthy peers); adaptive-mode playback immune.
Error injection (3060 Fixed live; 4040 boundary still Open)
Log: harness/logs/headless_260801-001823.log
| Mode | Host | Evidence |
|---|---|---|
| NAK | fail hs=$5A |
INJ NAK line |
| STALL | fail hs=$1E |
stall++ |
| DROP | fail hs=$0 |
timeout |
| CRC | fail after DATA $C3 |
crc++ |
| recover | bulk_in ok | clear_inject |
inj_fail=0 (all expect_fail + recover). Sustained inject+load failure-boundary sweep still Open (4040).
Sniffer (3070)
| Path | Live |
|---|---|
sample_host_z(app.line_z) during bulk/SOF |
samples=32, act_mask has J |
spare pair start(46) |
start ok (quiet without jumper) |
MSC media (2030) — parked
| Path | Result |
|---|---|
| Dogfood BOT CAPACITY | green (1024×512) |
| Real thumb/SD in hub on Serial Host | Adequate media even if slow |
| USB kbd/mouse dongle | Useful for HID host later; same hub enum gate |
| Blocker | GET_DESC@0 / hub class-9 port power+reset (1090) not green |
Parked until root/hub leaf enum works. Do not claim 2030 Fixed on dogfood BOT alone.
Structural rules
- One protocol engine (
usb_client); one PHY cog (usb_host) - No soft-open disk; no dual transfer implementations
- Fossil tickets are status authority;
TICKETS.mdis index only - CLOSE PROTECTION: Fixed only with live log or measurement table
Build
cd harness
pnut-ts -d -I ../src -D USB_HID -D USB_MSC -D USB_CDC top_selftest.spin2
pnut-term-ts --headless -r top_selftest.bin -p P81svj44 -b 2000000 --end-marker "DEBUG_END_SESSION"