mirror of
https://github.com/hathach/tinyusb.git
synced 2026-08-18 11:02:16 +00:00
skills: add adding-superspeed-port and update usb debug/recover/usbtest
This commit is contained in:
113
.claude/skills/adding-superspeed-port/SKILL.md
Normal file
113
.claude/skills/adding-superspeed-port/SKILL.md
Normal file
@ -0,0 +1,113 @@
|
||||
---
|
||||
name: adding-superspeed-port
|
||||
description: Use when adding USB3 SuperSpeed device support for a new MCU to TinyUSB (a dcd port, usually with runtime USB2 high-speed fallback), extending SuperSpeed descriptors to more class drivers or examples, or debugging SuperSpeed enumeration failures (link stuck in Polling, "can't read configurations, error -110", hot-reset loops, host never sees 5000M).
|
||||
---
|
||||
|
||||
# Adding a SuperSpeed Device Port to TinyUSB
|
||||
|
||||
## Overview
|
||||
|
||||
TinyUSB gained SuperSpeed device support with the WCH CH569 port. The work is layered so
|
||||
that a new SS MCU only writes **one dcd file + a BSP**: the core stack (usbd, types,
|
||||
templates), the class drivers and the bulk/interrupt examples are already SS-aware.
|
||||
Reference implementation for everything below: `src/portable/wch/dcd_ch56x_usb30.c`
|
||||
(+ `ch56x_usb30_reg.h`, `dcd_ch56x.h`, `dcd_ch56x_usbhs.c`, `hw/bsp/ch569/`).
|
||||
|
||||
**Core principle:** to the stack, SuperSpeed is just another speed. The dcd hides all
|
||||
link-layer work (LTSSM, LMP, resets) and reports ordinary dcd events; usbd only needs to
|
||||
know the operating speed to size EP0 transactions and pick descriptors.
|
||||
|
||||
## The core contract (already in the tree — your dcd must satisfy it)
|
||||
|
||||
| Item | Definition | Semantics |
|
||||
|---|---|---|
|
||||
| `OPT_MODE_SUPER_SPEED` (0x0800) | `src/tusb_option.h` | ORed into `CFG_TUD_MAX_SPEED` |
|
||||
| `TUD_OPT_SUPER_SPEED` | `src/tusb_option.h` | build-time "SS possible" switch |
|
||||
| `TUP_RHPORT_SUPERSPEED` | your block in `src/common/tusb_mcu.h` | declares port capability (default 0) |
|
||||
| `TUSB_SPEED_SUPER` (=3) | `src/common/tusb_types.h` | report via `dcd_event_bus_reset` |
|
||||
| EP0 = 512 fixed | `CFG_TUD_ENDPOINT0_SIZE` must be 512 on SS builds | usbd chunks EP0 with `ep0_xact_limit()` (512 when `speed==SUPER`, else 64) |
|
||||
| Bulk mps = exactly 1024 | enforced by `tu_edpt_validate` | int/iso ≤ 1024 |
|
||||
| SS descriptor structs | `tusb_desc_ss_ep_companion_t` etc. in `tusb_types.h` | companion follows EVERY endpoint descriptor |
|
||||
| `SET_SEL` / `SET_ISOCH_DELAY` / U1,U2 features | handled in `usbd.c` | nothing to do in the dcd |
|
||||
| Companion skipping | `usbd_open_edpt_pair` + `usbd_skip_ss_ep_companion()` (`usbd_pvt.h`) | class drivers already tolerate companions and count them in `drv_len` |
|
||||
| SS templates | `TUD_{CDC,MSC,VENDOR,HID,MIDI,MTP,PRINTER,NCM,USBTMC}_SS_*` in `usbd.h` | bulk sizes hardcoded 1024, `_maxburst` = bursts−1 |
|
||||
|
||||
The dcd reports the **actual operating speed** in every `dcd_event_bus_reset`; the
|
||||
speed-switched example descriptor callbacks (`tud_speed_get()`) do the rest.
|
||||
|
||||
## Steps for a new SuperSpeed MCU
|
||||
|
||||
Work in stages; each is independently buildable and hardware-verifiable.
|
||||
|
||||
1. **Plumbing** — `OPT_MCU_*` in `tusb_option.h`; an mcu block in `tusb_mcu.h` setting
|
||||
`TUP_USBIP_*` tokens, `TUP_RHPORT_HIGHSPEED 1`, `TUP_RHPORT_SUPERSPEED 1`,
|
||||
`TUP_DCD_ENDPOINT_MAX`, and a default `CFG_TUD_ENDPOINT0_SIZE 512`. If the chip has
|
||||
separate USB2/USB3 controllers, add selector defines (see the CH569 block:
|
||||
`CFG_TUD_WCH_USBIP_USB30` / `..._USBHS` / `..._USB30_FALLBACK`).
|
||||
2. **USB2 dcd first** (`SPEED=high`) — port or reuse the family's high-speed dcd, but
|
||||
expose its internals as non-static `<mcu>_usb2_*` functions through a small private
|
||||
header so stage 4 can drive them (`dcd_ch56x.h` pattern). Get it enumerating on
|
||||
hardware before touching USB3.
|
||||
3. **USB3 dcd** (`SPEED=super`) — one file. Structure it like `dcd_ch56x_usb30.c`:
|
||||
- `link_*` static functions for the LTSSM/LMP work, isolated from the endpoint
|
||||
engine (this is the extraction seam if a sibling chip shares the link IP — e.g.
|
||||
CH32H417 shares the CH569 LINK block but has a different endpoint engine).
|
||||
- All link/LMP register work happens **in the ISR** (timing critical, µs budgets);
|
||||
only `dcd_event_*` calls defer to task context.
|
||||
- "Bus reset" for the stack = the link first reaching U0
|
||||
(`dcd_event_bus_reset(rhport, TUSB_SPEED_SUPER, true)` after the LMP exchange starts).
|
||||
- SET_ADDRESS applies at the **status stage**, not when the request arrives.
|
||||
- DMA constraints (dedicated RAM regions, alignment) → per-endpoint bounce slots from
|
||||
a small pool; zero-copy only when the app buffer qualifies.
|
||||
4. **Runtime fallback** (single rhport, `SPEED=super` default) — a hardware timer bounds
|
||||
SS training; expiry tears USB3 down and brings the USB2 controller up on the same
|
||||
rhport; usbd re-inits transparently on the USB2 bus reset. See the `_fb_state`
|
||||
machine in `dcd_ch56x_usb30.c`. Make the deadline **termination-aware**: if far-end
|
||||
RX terminations were ever seen, an SS host exists — retry training (fresh detect
|
||||
cycle) longer before settling for HS; no terminations = USB2-only host = fall back
|
||||
fast. Provide a `..._FALLBACK=0` build for SuperSpeed-only (train indefinitely).
|
||||
5. **BSP/board** — nothing SS-specific beyond IRQ forwarding and (if needed) placing
|
||||
`CFG_TUSB_MEM_SECTION` in the DMA-visible RAM. Examples/descriptors need no work:
|
||||
they are already speed-switched.
|
||||
|
||||
## Validated gotchas (each cost hours on real hardware)
|
||||
|
||||
| Symptom | Cause / rule |
|
||||
|---|---|
|
||||
| `can't read configurations, error -110`; device desc reads OK, config full read times out | `CFG_TUD_ENDPOINT0_SIZE` left at 64: usbd sends a 64-byte chunk = **short packet** on the 512-mps SS EP0 → host ends the data stage early → status stage deadlocks. EP0 must be 512 (example configs use `(TUD_OPT_SUPER_SPEED ? 512 : 64)` and clamp the FS/HS device-descriptor field back to 64). |
|
||||
| Host warm-resets in a loop right after training | Wrong LMP payload. PORT_CAPABILITY and PORT_CFG_RES are different subtypes — verify against a reference manual, not guesswork. |
|
||||
| Link trains (hub port shows U0) but enumeration never starts; RDY/HOT_RESET/RECOV ISR flags cycle | LMP PORT_CONFIGURATION must be answered within µs — do it directly in the ISR. After a **hot reset** do NOT resend PORT_CAP (config is retained); after a **warm reset** the exchange restarts. |
|
||||
| SS trains on some boots only; hub port stuck in `Polling` | Some hubs (Renesas uPD720201) need a training **re-attempt**: on timer tick, `hw_deinit(); hw_init();` for a fresh RX-detect cycle instead of waiting passively. |
|
||||
| Device dead after switching to fallback; queue-full asserts | `dcd_int_enable/disable` run on **every** osal_none queue op — they must dispatch on the fallback state, and controller switch must clear stale pending IRQs, or old flags refire forever. |
|
||||
| Stack corruption, crash after first IRQ | gcc identical-code-folding rewrote one `__attribute__((interrupt))` handler into a `jal` to another (inner `mret` skips the outer epilogue). Multiple identical vectors must be **aliases of one handler**. |
|
||||
| Burst (NUMP>1) data corruption or config failure | Re-arm DMA every burst (address regs auto-increment); validate burst with read-back integrity tests, and respect the link's header-packet buffer count (CH569: 4 → burst ≤ 4). |
|
||||
| Enumeration works, wrong power draw shown | `TUD_CONFIG_SS_DESCRIPTOR` takes mA like the FS one but encodes 8 mA units — don't pre-divide. |
|
||||
|
||||
## Adding SS descriptors to a new class driver / example
|
||||
|
||||
- **Driver:** its `open()` walk must skip `TUSB_DESC_SUPERSPEED_ENDPOINT_COMPANION` and
|
||||
include companion bytes in the returned `drv_len` (helper: `usbd_skip_ss_ep_companion`;
|
||||
pattern examples: `cdc_device.c` pointer-walk, `msc_device.c` fixed-size probe).
|
||||
Wire order is EP → companion → CS-EP descriptors.
|
||||
- **Example:** copy the canonical pattern from `examples/device/cdc_msc/src/usb_descriptors.c`:
|
||||
EP0 clamp (`EP0_SIZE_FSHS`), `desc_device_ss` (bcdUSB 0x0320, `bMaxPacketSize0 = 9` — an
|
||||
exponent), SS config array from `TUD_*_SS_DESCRIPTOR` templates, BOS
|
||||
(USB2-ext + SuperSpeed caps; extend an existing BOS rather than duplicating), and
|
||||
`tud_speed_get()` branches in the device/config/BOS callbacks. ISO-class examples
|
||||
(audio/video/uac2/bth) are out of scope until a dcd supports SS ISO.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- Build the full example set for both `SPEED=super` and `SPEED=high`.
|
||||
- Confirm `wTotalLength == sizeof(desc_ss_configuration)` in the built ELFs (a length
|
||||
bug enumerates wrong at runtime but never fails the build).
|
||||
- `ceedling test:all` — `test_usbd.c` has SS cases (edpt_validate, companion skipping,
|
||||
SET_SEL/SET_ISOCH_DELAY).
|
||||
- Code-size compare on a non-SS board must be **0-delta** (all SS code folds out under
|
||||
`TUD_OPT_SUPER_SPEED == 0`).
|
||||
- Hardware: `dmesg` shows "new SuperSpeed USB device … 5000M"; `lsusb -t` 5000M;
|
||||
replug/suspend cycles; then data-integrity before throughput numbers.
|
||||
- Debugging aids: hub-port LTSSM state via `sudo uhubctl` (`Rx.Detect` = no terminations
|
||||
seen, `Polling` = LFPS but not trained, `U0` = trained); raw control-transfer truth via
|
||||
usbmon text (`sudo cat /sys/kernel/debug/usb/usbmon/<bus>u`); count link-ISR flags into
|
||||
spare RAM when the wire view isn't enough.
|
||||
@ -11,7 +11,8 @@ description: Use when running, debugging, or porting the Linux usbtest/testusb b
|
||||
(gadget-zero source/sink protocol): 30 cases over bulk, EP0, interrupt, and isochronous, including
|
||||
halt, data-toggle, and unlink storms. It is the most adversarial exerciser a DCD gets — every port
|
||||
so far surfaced at least one real driver bug. Host runner: `test/hil/usbtest.py`; HIL integration
|
||||
runs it per board and reports `✅ 30/30` cells.
|
||||
runs it per board and reports `✅ 30/30` cells (quirk-skipped cases leave the denominator, e.g.
|
||||
`✅ 27/27`).
|
||||
|
||||
**Core principle: the battery is a DCD test, not a firmware test.** When a case fails, suspect the
|
||||
DCD path it exercises (table below), reproduce that one case, and root-cause on hardware before
|
||||
@ -34,6 +35,10 @@ python3 test/hil/usbtest.py --serial <uid> --keep-binding --tests 29 # one case
|
||||
through its roster probe (non-destructive, ~130 ms) and reflashes only if that does not
|
||||
clear the wedge (see usb-kernel-recover). Manual runs without those flags leave a HUNG
|
||||
device wedged and skip cleanup — expected; reset or reflash it yourself.
|
||||
- **Never hand-write a bare `echo "vid pid" > usbtest/new_id`**: a dynamic id without the 4-field
|
||||
ref form (`vid pid 0 0525 a4a0`, copying Gadget Zero's driver_info) probes with driver_info=NULL
|
||||
→ kernel NULL-deref Oops → D-state testusb → forced reboot. Binding a foreign device (e.g. a
|
||||
vendor demo for an A/B) uses the same 4-field form.
|
||||
- Always settle a few seconds after flashing — enumeration can bounce once; testusb into the gap sees
|
||||
the device drop mid-case.
|
||||
- On a CI rig: stop the actions runner before touching hardware; restart after. Never run two
|
||||
@ -54,6 +59,14 @@ python3 test/hil/usbtest.py --serial <uid> --keep-binding --tests 29 # one case
|
||||
nothing on a flaky bring-up; deterministic partial counts (e.g. exactly 1-in-8 lost) are a
|
||||
signature, not noise — chase them.
|
||||
5. Register the board in `test/hil/tinyusb.json` so the HIL suite runs it.
|
||||
6. **Silicon-impossible cases** get a quirk flag, not a dodge: once proven a silicon erratum
|
||||
(vendor-stack A/B, debug ladder step 7), advertise it in bcdDevice bits 4–7 (`USBTEST_QUIRKS`
|
||||
in `src/usb_descriptors.h`; skip table `QUIRK_SKIPS` in `usbtest.py`) so the battery skips the
|
||||
cases *visibly* ("27/27 passed, 3 skipped") instead of flaking. Never mask an erratum by tuning
|
||||
params or skipping the whole board. Precedent — CH569 at SuperSpeed advertises 0x30 (0x10|0x20,
|
||||
skipping 13/14/21) and CH32H417 advertises 0x20 (skipping 13); the CH569 does 30/30 at HS.
|
||||
Prove an erratum on more than one host before *removing* a quirk: quirk 0x10's strike rate is
|
||||
host-dependent (see the case 14/21 row below).
|
||||
|
||||
## Case → DCD subsystem map
|
||||
|
||||
@ -62,9 +75,9 @@ python3 test/hil/usbtest.py --serial <uid> --keep-binding --tests 29 # one case
|
||||
| 9, 10 | EP0 control storms | EP0 state machine, ZLP/status stage, control starvation under load |
|
||||
| 1–8, 17–20, 27, 28 | bulk source/sink, sg, perf | FIFO handling, multi-packet, ZLP tolerance |
|
||||
| 11, 12, 24 | URB unlink mid-transfer | abort/close paths leaving state half-armed |
|
||||
| 13 | set/clear halt | stall must kill the transfer; halt on armed IN must flush the TX FIFO |
|
||||
| 13 | set/clear halt | stall must kill the transfer; halt on armed IN must flush the TX FIFO. CH569 SS silicon: a halted EP answers exactly ONE STALL TP, unfixable (quirk 0x20; the CH32H417 has RB_EP_TX/RX_HALT and is affected all the same, so it advertises 0x20 too) |
|
||||
| **29** | clear-halt on an **armed, un-halted** ep | **the classic**: `dcd_edpt_clear_stall` resets toggle but disarms the queued receive → NAKs forever, errno 110. Fix: reset toggle to DATA0 *and* re-arm/preserve the pending transfer. Found independently on rp2040, fsdev, ch32_usbhs, rusb2 |
|
||||
| 14, 21 | vendor EP0 write/readback | multi-packet control-OUT chunking, DCP flow control |
|
||||
| 14, 21 | vendor EP0 write/readback | multi-packet control-OUT chunking, DCP flow control. CH569 SS silicon: EP0 OUT data stages with `wLength % 4 == 1` are intermittently dropped (quirk 0x10, re-added 2026-08-11 with wire evidence — transaction error, full 489-byte residual). The strike rate is host-dependent: a Renesas uPD720201 fails most ctrl_out runs while an onboard AMD xHCI ran 15/15 clean, which is what briefly retired the quirk |
|
||||
| 25, 26 | interrupt src/sink | usually free once bulk works |
|
||||
| 15, 16, 22, 23 | isochronous | see iso rules below |
|
||||
|
||||
@ -125,6 +138,15 @@ curl -sO "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain
|
||||
per CLAUDE.md, and because comments/assumptions in DCDs have been wrong about hardware caps.
|
||||
6. Check the vendor's **silicon errata** early for timing/DMA hangs (an unimplemented erratum
|
||||
workaround caused a case-10 hang on one port).
|
||||
7. **Prove (or refute) "it's the silicon" with a vendor-stack A/B**: build the vendor's own
|
||||
reference firmware, patch in the gadget-zero 0x5b/0x5c ctrl store (mirror its existing
|
||||
control-OUT request, ~10 lines), flash to the same board/port, bind usbtest to the foreign
|
||||
VID:PID and run the failing case. Identical failure = erratum (document + quirk-skip); clean =
|
||||
the difference is in our DCD, keep digging. This is the only argument that closes the debate.
|
||||
8. **Flakes may be value-gated, not random**: hammer one parameter at a time.
|
||||
`testusb -D <node> -t 14 -c 4000 -s L -v $((L-1))` alternates lengths 1,L — sweeping L exposed
|
||||
a wLength-mod-4 gate that stock params (`-s 512 -v 61`, only one hot length in its 9-cycle)
|
||||
diluted into "0.3% random".
|
||||
|
||||
## Traps that pass gcc/desk review but fail elsewhere
|
||||
|
||||
|
||||
Reference in New Issue
Block a user