summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-07-23 10:18:11 +0700
committerhathach <[email protected]>2026-07-24 14:55:59 +0700
commit8918c4fec4a99b174d480c043000fd4405678282 (patch)
treeea49141343aedb97a9dabaac256915c945a27828
parent1b5c26b76e7194d05b82b0cccf76684716d3374d (diff)
docs(skills): rename debug skills, drop the PC-host/TinyUSB-device assumption
Rename usb-target-debug -> target-debug, usb-debug -> usb-kernel-debug, usb-recover -> usb-kernel-recover (script filenames unchanged), and make all debug skills/agents decide tool applicability by which end of the link runs Linux: TinyUSB may run the device or host stack, and its peer may be a Linux PC, another TinyUSB board, or a Linux gadget (e.g. Raspberry Pi UDC). - usbmon: exists only when a Linux PC is the link's host - usb-kernel-debug: either Linux end; allowlist gains dwc3/libcomposite/udc_core for the gadget side of a Linux peer - usb-sniffer: the only full-visibility capture when TinyUSB is the host - target-debug: covers dcd_* and hcd_*/tuh_ debugging; channel choice by topology - update target-debugger/hil-operator agents, pre-pr, hil-validate.js, and the USB_RECOVER path constant in test/hil/usbtest.py - CLAUDE.md: fold the dcd/hcd datasheet cross-check rule into the read-doc line
-rw-r--r--.claude/agents/hil-operator.md6
-rw-r--r--.claude/agents/target-debugger.md31
-rw-r--r--.claude/skills/pre-pr/SKILL.md2
-rw-r--r--.claude/skills/target-debug/SKILL.md (renamed from .claude/skills/usb-target-debug/SKILL.md)42
-rw-r--r--.claude/skills/usb-debug/SKILL.md36
-rw-r--r--.claude/skills/usb-kernel-debug/SKILL.md47
-rwxr-xr-x.claude/skills/usb-kernel-debug/scripts/usb_dyndbg.sh (renamed from .claude/skills/usb-debug/scripts/usb_dyndbg.sh)11
-rw-r--r--.claude/skills/usb-kernel-recover/SKILL.md (renamed from .claude/skills/usb-recover/SKILL.md)6
-rwxr-xr-x.claude/skills/usb-kernel-recover/scripts/usb_recover.sh (renamed from .claude/skills/usb-recover/scripts/usb_recover.sh)0
-rw-r--r--.claude/skills/usb-sniffer/SKILL.md14
-rw-r--r--.claude/skills/usbmon/SKILL.md4
-rw-r--r--.claude/skills/usbtest/SKILL.md2
-rw-r--r--.claude/workflows/hil-validate.js2
-rw-r--r--CLAUDE.md2
-rwxr-xr-xtest/hil/usbtest.py6
15 files changed, 120 insertions, 91 deletions
diff --git a/.claude/agents/hil-operator.md b/.claude/agents/hil-operator.md
index c48ceb8bd..d19eca047 100644
--- a/.claude/agents/hil-operator.md
+++ b/.claude/agents/hil-operator.md
@@ -8,8 +8,8 @@ model: sonnet
You operate physical USB test hardware. These repo skills are your source of truth — read the relevant one BEFORE acting:
- `.claude/skills/hil/SKILL.md` — run `hostname` first (host `ci` = local mode with `test/hil/tinyusb.json`; host `htpc` = local `local.json` or remote via `test/hil/hil_ci.sh`); the board lock protocol; exact `hil_test.py` invocations.
-- `.claude/skills/usb-recover/SKILL.md` — only when a device/fixture is wedged or processes hang in D state.
-- `.claude/skills/usb-debug/SKILL.md` — only when you need to explain WHY the host rejected a device (dmesg analysis).
+- `.claude/skills/usb-kernel-recover/SKILL.md` — only when a device/fixture on the rig's Linux host is wedged or processes hang in D state.
+- `.claude/skills/usb-kernel-debug/SKILL.md` — only when you need to explain WHY the Linux kernel rejected a device (dmesg analysis).
## Board lock protocol (CI runs concurrently — NEVER stop the actions-runner)
@@ -30,7 +30,7 @@ The GitHub Actions runner keeps running during your work. Per-board flock locks
- HIL runs take 2–5 min per board: use Bash timeouts >= 20 min (1200000 ms) and NEVER cancel early.
- One hardware action at a time. You are never run concurrently with another hil-operator.
-- On test failure: retry once with `-v -r 1` appended (one verbose attempt for diagnosis — the first run already did the flake-retries). If a board/fixture stops enumerating or tools hang in D state, consult usb-recover and capture `dmesg | tail -50` into `detail`; set `wedged` true.
+- On test failure: retry once with `-v -r 1` appended (one verbose attempt for diagnosis — the first run already did the flake-retries). If a board/fixture stops enumerating or tools hang in D state, consult usb-kernel-recover and capture `dmesg | tail -50` into `detail`; set `wedged` true.
## Output contract
diff --git a/.claude/agents/target-debugger.md b/.claude/agents/target-debugger.md
index c1df47cb2..861600428 100644
--- a/.claude/agents/target-debugger.md
+++ b/.claude/agents/target-debugger.md
@@ -1,25 +1,32 @@
---
name: target-debugger
-description: Root-cause one USB misbehavior on real HIL hardware by instrumenting the TinyUSB device side — TU_LOG/RTT, RAM ring-buffer trace, GDB autopsy, J-Link PC-sampling — correlated with host-side and wire-level capture. Long serial debug loop under one held board lock; strictly one instance. Produces a diagnosis with on-target evidence (plus a candidate fix when one emerges), never a merged patch.
+description: Root-cause one USB misbehavior on real HIL hardware by instrumenting the TinyUSB target — device or host stack — with TU_LOG/RTT, RAM ring-buffer trace, GDB autopsy, J-Link PC-sampling, correlated with capture from the link's other end (Linux PC host, another TinyUSB board, or a Linux gadget peer) and the wire. Long serial debug loop under one held board lock; strictly one instance. Produces a diagnosis with on-target evidence (plus a candidate fix when one emerges), never a merged patch.
model: opus
---
You debug one failing USB behavior on one physical board until you can name the
-mechanism — or report exactly what you ruled out. These repo skills are your
-source of truth; read the relevant SKILL.md BEFORE acting:
+mechanism — or report exactly what you ruled out. The target may run the device
+stack, the host stack, or both; its link peer may be the Linux PC, another
+TinyUSB board, or a Linux gadget (e.g. a Raspberry Pi) — pick capture channels
+by which end runs Linux, not by habit. These repo skills are your source of
+truth; read the relevant SKILL.md BEFORE acting:
-- `.claude/skills/usb-target-debug/SKILL.md` — your primary playbook: technique
- choice by intrusiveness, capture recipes, GDB autopsy, all rig warnings.
+- `.claude/skills/target-debug/SKILL.md` — your primary playbook: technique
+ choice by intrusiveness, channel choice by link topology, capture recipes,
+ GDB autopsy, all rig warnings.
- `.claude/skills/hil/SKILL.md` — host/config selection, board lock protocol,
`hil_test.py` invocation.
-- `.claude/skills/usbmon/SKILL.md` — host-side URB capture (the default posture
- is dual-side: host + target simultaneously).
+- `.claude/skills/usbmon/SKILL.md` — Linux-host URB capture; exists only when a
+ Linux PC is the link's host (the default posture is dual-side: both ends
+ simultaneously).
- `.claude/skills/usb-sniffer/SKILL.md` — wire-level capture with the hardware
- tap, when the host can't see the bus (device never enumerates, pre-URB
- failures) or when usbmon and device logs disagree — the wire arbitrates.
-- `.claude/skills/usb-debug/SKILL.md` — why the host acted (dmesg/dynamic debug).
-- `.claude/skills/usb-recover/SKILL.md` — only when the DUT or fixture wedges
- the host stack.
+ tap: when the host can't see the bus (device never enumerates, pre-URB
+ failures), when usbmon and target logs disagree — the wire arbitrates — or
+ when TinyUSB is the host and no end has usbmon.
+- `.claude/skills/usb-kernel-debug/SKILL.md` — why the Linux kernel acted
+ (dmesg/dynamic debug); the PC host, or a Linux gadget peer's device side.
+- `.claude/skills/usb-kernel-recover/SKILL.md` — only when the DUT or fixture
+ wedges the rig PC's Linux host stack.
## The loop (deliberately serial — no fan-out)
diff --git a/.claude/skills/pre-pr/SKILL.md b/.claude/skills/pre-pr/SKILL.md
index d4c35f7e5..3f062db78 100644
--- a/.claude/skills/pre-pr/SKILL.md
+++ b/.claude/skills/pre-pr/SKILL.md
@@ -38,5 +38,5 @@ Invoke the Workflow tool:
- Per-stage table: unit / build:<board> / size / pvs, then HIL per board — pass/fail with the first error for each failure.
- If the hardware result has non-empty `locked` (a CI job held those boards): ask the user with AskUserQuestion — **Force now** (re-invoke `hil-validate` with `force: true` for those boards; user accepts the risk of colliding with a mid-test CI job), **Keep waiting** (re-invoke `hil-validate` for them after a few minutes; ask again if still locked), or **Accept** the partial verdict. Never force without the user's answer.
-- Wedged boards: point at `.claude/skills/usb-recover/SKILL.md`.
+- Wedged boards: point at `.claude/skills/usb-kernel-recover/SKILL.md`.
- End with a clear ship / no-ship verdict and what to fix first.
diff --git a/.claude/skills/usb-target-debug/SKILL.md b/.claude/skills/target-debug/SKILL.md
index 71fac98f2..c82237ea3 100644
--- a/.claude/skills/usb-target-debug/SKILL.md
+++ b/.claude/skills/target-debug/SKILL.md
@@ -1,23 +1,28 @@
---
-name: usb-target-debug
-description: Use when a TinyUSB device misbehaves on real hardware and host-side capture can't explain it — a HIL test fails but usbmon shows only Submits with no Completes, the device silently NAKs, wedges, STALLs, babbles, or drops data, EP0 starves, an ISR or DCD/HCD state bug is suspected — and you need device-side evidence: TU_LOG/RTT logs, GDB state dumps, a RAM ring-buffer event trace, or PC-sampling of where the core spins.
+name: target-debug
+description: Use when TinyUSB firmware — device or host stack — misbehaves on real hardware and capture from the other end can't explain it: a HIL test fails but usbmon shows only Submits with no Completes, the device silently NAKs, wedges, STALLs, babbles, or drops data, EP0 starves, tuh_ enumeration of an attached device fails, an ISR or DCD/HCD state bug is suspected — and you need target-side evidence: TU_LOG/RTT logs, GDB state dumps, a RAM ring-buffer event trace, or PC-sampling of where the core spins.
---
-# usb-target-debug — device-side capture & debugging on the HIL rig
+# target-debug — target-side capture & debugging on the HIL rig
-Completes the debugging trio (the `usb-sniffer` skill adds a fourth,
-wire-level view when hardware tapping is available):
+The **target** is whichever MCU runs TinyUSB — device stack (`dcd_*`), host
+stack (`hcd_*`/`tuh_*`), or both. Its link peer is not always a Linux PC: a
+TinyUSB host may face another TinyUSB board or a Linux gadget (e.g. a
+Raspberry Pi). Pick capture channels by which end runs Linux, not by habit:
-| Skill | Answers |
-|---|---|
-| `usbmon` | what the host actually exchanged (URBs) |
-| `usb-debug` | why the host acted (dmesg / dynamic debug) |
-| **`usb-target-debug`** | **what the device did** (logs, driver state, PC) |
-| `usb-sniffer` | what crossed the wire (PIDs, handshakes, resets — hardware tap) |
+| Skill | Answers | Exists when |
+|---|---|---|
+| `usbmon` | what the Linux host exchanged (URBs) | a Linux PC is the link's host |
+| `usb-kernel-debug` | why the Linux kernel acted (dmesg / dynamic debug) | Linux on either end: PC host or Linux gadget peer |
+| **`target-debug`** | **what the target did** (logs, driver state, PC) | always — either role, needs a debug probe |
+| `usb-sniffer` | what crossed the wire (PIDs, handshakes, resets) | hardware tap cabled in — role-agnostic |
For enumeration/transfer bugs the default posture is **dual-side capture** —
-usbmon on the host *and* a target-side channel, simultaneously — not
-host-first-then-escalate.
+both ends simultaneously, not one-side-first-then-escalate: usbmon plus a
+target channel when a Linux PC is the host. When TinyUSB is the host there is
+no usbmon on either end — pair the target channel with the wire
+(`usb-sniffer`) and, if the peer is a Linux gadget, `usb-kernel-debug` on the
+peer.
## Rig discipline — lock first, always
@@ -91,7 +96,8 @@ wedged; do not rebuild while the wedge is still on the board. The debug-loop
specifics:
```gdb
-p/x _usbd_dev.ep_status # usbd core [epnum][dir] (1=IN): busy/stalled/claimed
+p/x _usbd_dev.ep_status # device stack: usbd [epnum][dir] (1=IN): busy/stalled/claimed
+p _usbh_devices[0] # host stack: usbh per-device state (addr, enum/config)
p/x <port's private state> # per-port names — read the board's dcd_*.c first
x/32wx <USB peripheral base> # raw EP/FIFO regs; base = the macro the dcd uses
watch xfer_status[2][1].total_len # HW watchpoint (Cortex-M: ~4); dwc2 names shown
@@ -153,7 +159,9 @@ top entries are the spin site; a flat histogram = core is servicing normally.
## Dual-side capture — the default for enumeration/transfer bugs
-Start both channels, then trigger the failing test:
+Start both channels, then trigger the failing test (Linux-PC-host link shown;
+TinyUSB-as-host: swap the usbmon line for a `usb-sniffer` capture, plus
+`usb-kernel-debug` on the peer if it is a Linux gadget):
```bash
.claude/skills/usbmon/scripts/usbcap.sh cafe: 30 /tmp/host.pcapng & # host URBs (usbmon skill)
@@ -172,8 +180,8 @@ the wire itself: `usb-sniffer` skill (hardware tap, PID-level).
- **Halting/resetting via the probe does NOT disconnect the device**: a DWC2
soft-connect pullup stays up through core halt *and* reset, so the host's
- stuck URBs stay stuck and a wedged DUT stays wedged — recover the host side
- with the `usb-recover` skill.
+ stuck URBs stay stuck and a wedged DUT stays wedged — recover the Linux
+ host side with the `usb-kernel-recover` skill.
- **A bug that vanishes under LOG=2 is a timing bug**, not fixed: switch to
the ring buffer; if it vanishes under GDB too, PC-sampling only.
- **UART TU_LOG blocks in the write path** (worst perturbation, including
diff --git a/.claude/skills/usb-debug/SKILL.md b/.claude/skills/usb-debug/SKILL.md
deleted file mode 100644
index 20ab7d764..000000000
--- a/.claude/skills/usb-debug/SKILL.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-name: usb-debug
-description: Use when USB enumeration fails or misbehaves and usbmon alone can't explain WHY the host acted — port reset storms, repeated re-enumeration, address errors, xHCI ring/command errors, "device descriptor read error", babble, or when you need the host driver's own reasoning from dmesg on the ci HIL rig.
----
-
-# usb-debug — host-side kernel dynamic debug for USB
-
-usbmon shows the URBs; kernel **dynamic debug** shows the host driver's
-*reasoning* usbmon can't: port resets and their causes, enumeration retries,
-address (re)assignment, EP halts, xHCI ring/command errors.
-
-Run this skill's `scripts/usb_dyndbg.sh` with `sudo` (abbreviated to
-`usb_dyndbg.sh` in the examples below). It flips the dynamic-debug print flag
-for an allowlisted set of USB host modules only:
-
-```bash
-sudo usb_dyndbg.sh on usbcore xhci_hcd # enable +p; pick modules from `lsusb -t` Driver=
-sudo usb_dyndbg.sh status [module] # list enabled print sites
-sudo usb_dyndbg.sh off usbcore xhci_hcd # ALWAYS turn off when done — very noisy
-```
-
-Allowlisted modules: `usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd
-ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 cdc_acm usb_storage uas`.
-
-## Workflow
-
-1. `sudo usb_dyndbg.sh on usbcore <hcd-module>` — `usbcore` for enumeration/hub
- logic, plus the controller module (`lsusb -t` shows the driver per bus).
-2. Reproduce (replug / re-enumerate / rerun the failing test) while following
- `sudo dmesg -w` (or grab `sudo dmesg | tail` afterwards).
-3. `sudo usb_dyndbg.sh off ...` — leaving it on floods the log and skews timing.
-
-Pair with the `usbmon` skill: usbmon for what crossed the bus, dynamic debug for
-why the host reacted. For a wedged device/bus use the `usb-recover` skill.
-
-Requires `CONFIG_DYNAMIC_DEBUG` and mounted debugfs (standard on distro kernels).
diff --git a/.claude/skills/usb-kernel-debug/SKILL.md b/.claude/skills/usb-kernel-debug/SKILL.md
new file mode 100644
index 000000000..e4169b049
--- /dev/null
+++ b/.claude/skills/usb-kernel-debug/SKILL.md
@@ -0,0 +1,47 @@
+---
+name: usb-kernel-debug
+description: Use when USB enumeration fails or misbehaves and packet/URB capture can't explain WHY the Linux kernel acted — port reset storms, repeated re-enumeration, address errors, xHCI ring/command errors, "device descriptor read error", babble — on whichever end of the link runs Linux: the PC host when testing a TinyUSB device, or a Linux gadget peer (e.g. Raspberry Pi) when testing the TinyUSB host stack.
+---
+
+# usb-kernel-debug — Linux kernel dynamic debug for USB
+
+Kernel **dynamic debug** shows the Linux side's *reasoning* that packet
+capture can't: port resets and their causes, enumeration retries, address
+(re)assignment, EP halts, xHCI ring/command errors. It applies wherever Linux
+sits in the link — the rig PC when it is the host, or a Linux gadget peer
+(dwc2/UDC + gadget modules) when TinyUSB is the host. It cannot see inside
+the TinyUSB MCU — that is the `target-debug` skill.
+
+Run this skill's `scripts/usb_dyndbg.sh` with `sudo` (abbreviated to
+`usb_dyndbg.sh` in the examples below). It flips the dynamic-debug print flag
+for an allowlisted set of USB modules only:
+
+```bash
+sudo usb_dyndbg.sh on usbcore xhci_hcd # enable +p; pick modules from `lsusb -t` Driver=
+sudo usb_dyndbg.sh status [module] # list enabled print sites
+sudo usb_dyndbg.sh off usbcore xhci_hcd # ALWAYS turn off when done — very noisy
+```
+
+Allowlisted modules: `usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd
+ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 dwc3 cdc_acm usb_storage uas
+libcomposite udc_core` (`dwc2`/`dwc3` + the last two cover a Linux gadget
+peer's device side).
+
+## Workflow
+
+1. `sudo usb_dyndbg.sh on usbcore <hcd-module>` — `usbcore` for enumeration/hub
+ logic, plus the controller module (`lsusb -t` shows the driver per bus).
+ On a gadget peer: `dwc2` (or `dwc3`) + `udc_core` + `libcomposite` instead —
+ run on the peer itself (its SSH/serial console); the script is self-contained,
+ copy it over or use the raw `dynamic_debug/control` writes from the `usbmon`
+ skill.
+2. Reproduce (replug / re-enumerate / rerun the failing test) while following
+ `sudo dmesg -w` (or grab `sudo dmesg | tail` afterwards).
+3. `sudo usb_dyndbg.sh off ...` — leaving it on floods the log and skews timing.
+
+On a Linux-PC-host link, pair with the `usbmon` skill: usbmon for what crossed
+the bus, dynamic debug for why the kernel reacted. A gadget peer's UDC has no
+usbmon — pair with `usb-sniffer` on the wire instead. For a wedged device/bus
+on the rig PC use the `usb-kernel-recover` skill.
+
+Requires `CONFIG_DYNAMIC_DEBUG` and mounted debugfs (standard on distro kernels).
diff --git a/.claude/skills/usb-debug/scripts/usb_dyndbg.sh b/.claude/skills/usb-kernel-debug/scripts/usb_dyndbg.sh
index 0dc880469..3923cdc6d 100755
--- a/.claude/skills/usb-debug/scripts/usb_dyndbg.sh
+++ b/.claude/skills/usb-kernel-debug/scripts/usb_dyndbg.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
-# usb_dyndbg.sh — toggle kernel dynamic-debug on USB host drivers; run with sudo.
-# Flips +p/-p only on an allowlisted set of USB modules, so it can't reach
-# arbitrary kernel debug or unrelated subsystems.
+# usb_dyndbg.sh — toggle kernel dynamic-debug on USB drivers (host or gadget
+# side); run with sudo. Flips +p/-p only on an allowlisted set of USB modules,
+# so it can't reach arbitrary kernel debug or unrelated subsystems.
#
# Usage:
# sudo usb_dyndbg.sh on <module>... # enable +p (e.g. usbcore xhci_hcd)
@@ -10,8 +10,9 @@
set -euo pipefail
CTL=/sys/kernel/debug/dynamic_debug/control
-# Allowlist: USB host-controller + core + common host class drivers.
-ALLOW='usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 cdc_acm usb_storage uas'
+# Allowlist: USB core + host-controller + common class drivers, plus the
+# gadget/UDC side of a Linux peer (dwc2/dwc3, udc_core, libcomposite).
+ALLOW='usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 dwc3 cdc_acm usb_storage uas libcomposite udc_core'
die() { echo "usb_dyndbg: $*" >&2; exit 1; }
usage() {
diff --git a/.claude/skills/usb-recover/SKILL.md b/.claude/skills/usb-kernel-recover/SKILL.md
index beb6fd862..3f03722fe 100644
--- a/.claude/skills/usb-recover/SKILL.md
+++ b/.claude/skills/usb-kernel-recover/SKILL.md
@@ -1,9 +1,9 @@
---
-name: usb-recover
-description: Use when a USB device or fixture on the ci HIL rig is stuck, hung, not enumerating, or wedged after a failed flash or test, or when processes touching USB (testusb, JLinkExe, uhubctl, libusb tools) start hanging in D state.
+name: usb-kernel-recover
+description: Use when a USB device or fixture attached to the ci HIL rig's Linux host is stuck, hung, not enumerating, or wedged after a failed flash or test, or when processes touching USB (testusb, JLinkExe, uhubctl, libusb tools) start hanging in D state. Linux-kernel-side only — a bus owned by a TinyUSB host is out of reach (reset the target / cycle its VBUS instead); the rig's probes and serial fixtures always remain in scope.
---
-# USB Recovery on the HIL Rig
+# USB Recovery on the HIL Rig (Linux kernel side)
Run this skill's `scripts/usb_recover.sh` with `sudo` (abbreviated to
`usb_recover.sh` in the examples below). It wraps the sysfs reset actions, a
diff --git a/.claude/skills/usb-recover/scripts/usb_recover.sh b/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh
index 7652253fa..7652253fa 100755
--- a/.claude/skills/usb-recover/scripts/usb_recover.sh
+++ b/.claude/skills/usb-kernel-recover/scripts/usb_recover.sh
diff --git a/.claude/skills/usb-sniffer/SKILL.md b/.claude/skills/usb-sniffer/SKILL.md
index 7c2cd2644..a20aecf05 100644
--- a/.claude/skills/usb-sniffer/SKILL.md
+++ b/.claude/skills/usb-sniffer/SKILL.md
@@ -1,6 +1,6 @@
---
name: usb-sniffer
-description: Use when you need wire-level USB evidence that host-side capture can't provide — a device that never enumerates (usbmon shows nothing or only Submits), suspected NAK storms/STALL/babble/bad handshakes, bus-reset or enumeration timing, split-transaction issues, or a usbmon-vs-device-log disagreement the wire must arbitrate. Captures LS/FS/HS packets (PIDs, tokens, handshakes, SE0/line states) with the ataradov usb-sniffer hardware into Wireshark pcapng.
+description: Use when you need wire-level USB evidence that host-side capture can't provide — a device that never enumerates (usbmon shows nothing or only Submits), suspected NAK storms/STALL/babble/bad handshakes, bus-reset or enumeration timing, split-transaction issues, a usbmon-vs-device-log disagreement the wire must arbitrate, or any link where TinyUSB is the host (no Linux PC host to run usbmon on). Captures LS/FS/HS packets (PIDs, tokens, handshakes, SE0/line states) with the ataradov usb-sniffer hardware into Wireshark pcapng.
---
# usb-sniffer — wire-level capture with the ataradov hardware analyzer
@@ -9,14 +9,16 @@ Extends the debugging trio with the layer below URBs:
| Skill | Answers |
|---|---|
-| `usbmon` | what the host software exchanged (URBs) |
-| `usb-debug` | why the host acted (dmesg / dynamic debug) |
-| `usb-target-debug` | what the device firmware did |
+| `usbmon` | what a Linux PC host exchanged (URBs) |
+| `usb-kernel-debug` | why the Linux kernel acted (dmesg / dynamic debug) |
+| `target-debug` | what the TinyUSB target did (device or host role) |
| **`usb-sniffer`** | **what actually crossed D+/D-** (PIDs, handshakes, resets, timing) |
Reach for it when usbmon can't see (device never binds, pre-enumeration
-failures) or can't be trusted (URB completed but did the wire really ACK?).
-For everything visible in URBs, usbmon is cheaper — no hardware, no locks.
+failures), can't be trusted (URB completed but did the wire really ACK?), or
+doesn't exist — a link where TinyUSB is the host has no usbmon on either end
+(an MCU host runs no kernel; a Linux gadget peer's UDC bypasses usbmon).
+Where a Linux PC is the host, usbmon is cheaper — no hardware, no locks.
## Rig inventory — find the sniffer and what it taps
diff --git a/.claude/skills/usbmon/SKILL.md b/.claude/skills/usbmon/SKILL.md
index 85ec33248..a2e7c1ac1 100644
--- a/.claude/skills/usbmon/SKILL.md
+++ b/.claude/skills/usbmon/SKILL.md
@@ -1,11 +1,11 @@
---
name: usbmon
-description: Use when capturing, analyzing, or debugging USB bus traffic for TinyUSB device development on Linux — enumeration failures, STALLed control transfers, missing/short bulk or interrupt transfers, isochronous/audio dropouts, or descriptor problems. Captures host-side URBs with usbmon + tshark into a Wireshark pcapng and decodes them. Use whenever you need to see what the host actually exchanged with a device on real hardware, even if the user just says "sniff USB", "capture the enumeration", or "why won't my device enumerate".
+description: Use when capturing, analyzing, or debugging USB bus traffic on a link where a Linux PC is the host (TinyUSB in device role) — enumeration failures, STALLed control transfers, missing/short bulk or interrupt transfers, isochronous/audio dropouts, or descriptor problems. Captures host-side URBs with usbmon + tshark into a Wireshark pcapng and decodes them. Not applicable when TinyUSB is the host — no URBs traverse the PC (use usb-sniffer / target-debug). Use whenever you need to see what the Linux host actually exchanged with a device on real hardware, even if the user just says "sniff USB", "capture the enumeration", or "why won't my device enumerate".
---
# usbmon — capture & debug USB traffic
-`usbmon` records host-side **URBs** — control / bulk / interrupt / isochronous transfers, descriptors, class requests, STALLs, short packets — i.e. exactly what the host exchanged with a device. Use it to debug a TinyUSB device on real hardware. (It's host/URB-level, not wire-level; for SOF/ACK/electrical use a hardware analyzer.)
+`usbmon` records host-side **URBs** — control / bulk / interrupt / isochronous transfers, descriptors, class requests, STALLs, short packets — i.e. exactly what the host exchanged with a device. Use it to debug a TinyUSB device on real hardware. (It's host/URB-level, not wire-level; for SOF/ACK/electrical use a hardware analyzer.) It exists only on the Linux host side of a link: when TinyUSB runs the *host* stack (peer = another TinyUSB board or a Linux gadget, e.g. a Raspberry Pi), neither end has usbmon — capture the wire (`usb-sniffer` skill) or instrument the target (`target-debug` skill).
**Setup (assumed in place):** `usbmon` loaded and a udev rule `SUBSYSTEM=="usbmon", GROUP="wireshark", MODE="0640"` with your user in the `wireshark` group — so `tshark` captures with no `sudo`. Freshly added to the group? The running shell doesn't have it yet (group adds need a new login) — wrap captures in `sg wireshark -c 'tshark -i usbmon3 -s 128 -a duration:30 -w /tmp/cap.pcapng'`; reading a finished `.pcapng` (`tshark -r`) needs no group. `-s 128` (snaplen) keeps only URB headers/status, not payloads — use it for long/high-throughput captures.
diff --git a/.claude/skills/usbtest/SKILL.md b/.claude/skills/usbtest/SKILL.md
index 8146d94e4..850781d6e 100644
--- a/.claude/skills/usbtest/SKILL.md
+++ b/.claude/skills/usbtest/SKILL.md
@@ -1,6 +1,6 @@
---
name: usbtest
-description: Use when running, debugging, or porting the Linux usbtest/testusb battery (examples/device/usbtest, cafe:4010) — device "did not bind", SET_CONFIGURATION fails, a case fails with errno 110/32/5/71, toggle-clear/halt/unlink/iso failures, iso packets dropped, or a new MCU/DCD needs the full 30/30 sign-off.
+description: Use when running, debugging, or porting the Linux usbtest/testusb battery (examples/device/usbtest, cafe:4010) — device "did not bind", SET_CONFIGURATION fails, a case fails with errno 110/32/5/71, toggle-clear/halt/unlink/iso failures, iso packets dropped, or a new MCU/DCD needs the full 30/30 sign-off. Needs a Linux PC as the link's host driving TinyUSB in device role — it exercises the DCD, not the TinyUSB host stack.
---
# usbtest — porting & debugging the Linux kernel USB battery
diff --git a/.claude/workflows/hil-validate.js b/.claude/workflows/hil-validate.js
index aa0556abc..50559135f 100644
--- a/.claude/workflows/hil-validate.js
+++ b/.claude/workflows/hil-validate.js
@@ -52,7 +52,7 @@ if (!args.force) {
}
const wedged = results.filter(r => r.wedged).map(r => r.board)
-if (wedged.length) log(`WEDGED boards needing usb-recover: ${wedged.join(', ')}`)
+if (wedged.length) log(`WEDGED boards needing usb-kernel-recover: ${wedged.join(', ')}`)
// Workers cannot prompt the user — surface still-locked boards for the main
// session to ask: force (re-invoke with force: true), wait, or accept.
const locked = args.force ? [] : results.filter(r => !r.pass && r.detail.startsWith('board locked')).map(r => r.board)
diff --git a/CLAUDE.md b/CLAUDE.md
index 2acdc3a63..77dab4565 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -116,7 +116,7 @@ Cutting a release — version bump, regenerated files, the per-release changelog
## References
-- MCU reference manuals, datasheets, schematics: before answering register/bitfield/pinout/errata/timing questions from memory or the web, use the `read-doc` skill (`.claude/skills/read-doc/SKILL.md`) to search and read them from `$HOME/Documents/calibre-library` (skill no-ops if the library is absent).
+- MCU reference manuals, datasheets, schematics: before answering register/bitfield/pinout/errata/timing questions from memory or the web — or changing a specific dcd/hcd driver — use the `read-doc` skill (`.claude/skills/read-doc/SKILL.md`) to cross-check against docs in `$HOME/Documents/calibre-library`; tell the user if the needed document is missing (skill no-ops if the library is absent).
- Supported MCUs/boards: `hw/bsp/` and `docs/reference/boards.rst`.
- USB classes: `src/class/{cdc,hid,msc,audio,…}/` — each has `*_device.c` and `*_host.c`.
- Key files: `src/tusb.h`, `src/tusb_config.h`, `tools/get_deps.py`, `tools/build.py`, `test/unit-test/project.yml`.
diff --git a/test/hil/usbtest.py b/test/hil/usbtest.py
index a2841f0b6..e17705a48 100755
--- a/test/hil/usbtest.py
+++ b/test/hil/usbtest.py
@@ -38,7 +38,7 @@ PID = '4010'
GZ_REF = '0525 a4a0' # copy Gadget Zero's capability profile (ctrl_out+iso+intr)
SYS_USB = Path('/sys/bus/usb/devices')
DRIVER = Path('/sys/bus/usb/drivers/usbtest')
-USB_RECOVER = Path(__file__).resolve().parents[2] / '.claude/skills/usb-recover/scripts/usb_recover.sh'
+USB_RECOVER = Path(__file__).resolve().parents[2] / '.claude/skills/usb-kernel-recover/scripts/usb_recover.sh'
PATTERN_PARAM = Path('/sys/module/usbtest/parameters/pattern')
# Battery per tier, in run order: control sanity first, then simple bulk,
@@ -391,7 +391,7 @@ def main():
if pci:
print(f'aborting battery: kernel-side hang, device wedged mid-transfer.\n'
f'auto-recovering: sudo {USB_RECOVER} pci-reset {pci} '
- f'(see .claude/skills/usb-recover)', file=sys.stderr)
+ f'(see .claude/skills/usb-kernel-recover)', file=sys.stderr)
# FLR frees the D-state ioctl without the device lock; must run BEFORE
# any unbind/remove_id, which would deadlock the bus otherwise
if sudo([str(USB_RECOVER), 'pci-reset', pci]).returncode != 0:
@@ -418,7 +418,7 @@ def main():
try:
if unrecovered_hang:
# testusb is still stuck in a usbfs ioctl holding the device lock; remove_id/unbind
- # would join the convoy and deadlock the bus (see usb-recover skill) — leave it be
+ # would join the convoy and deadlock the bus (see usb-kernel-recover skill) — leave it be
print('skipping cleanup after unrecovered hang: reboot required to release the bus',
file=sys.stderr)
elif not args.keep_binding: