summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-06-24 15:44:25 +0700
committerhathach <[email protected]>2026-06-24 15:44:25 +0700
commitbc9cf55927d2dcd633350316f9b1f8b40a30a7be (patch)
tree6b65aad897a3356ab5103573a51e426fe7050351
parent467f638e98d5b76ecb970272912a1b1f63a7ca19 (diff)
docs: add usbmon skill for capturing/debugging USB bus traffic
Adds a Claude Code skill that wraps usbmon + tshark to capture host-side URBs into a Wireshark pcapng and decode them, for debugging TinyUSB device enumeration/transfer issues on real Linux hardware. Includes a usbcap.sh capture helper (bus / VID:PID / auto) plus filter, errno, and symptom->check reference tables. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
-rw-r--r--.claude/skills/usbmon/SKILL.md80
-rwxr-xr-x.claude/skills/usbmon/scripts/usbcap.sh27
2 files changed, 107 insertions, 0 deletions
diff --git a/.claude/skills/usbmon/SKILL.md b/.claude/skills/usbmon/SKILL.md
new file mode 100644
index 000000000..4164dcf2e
--- /dev/null
+++ b/.claude/skills/usbmon/SKILL.md
@@ -0,0 +1,80 @@
+---
+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".
+---
+
+# 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.)
+
+**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`.
+
+If you were just added to `wireshark` (e.g. `usermod -aG`), the running shell/agent still has the old group set (group adds only apply to a fresh login). Don't restart — wrap each capture in `sg wireshark -c '…'`, which re-reads `/etc/group` immediately: `sg wireshark -c 'tshark -i usbmon3 -s 128 -a duration:30 -w /tmp/cap.pcapng'`. (Reading a finished `.pcapng` with `tshark -r` needs no special group.) For long/high-throughput captures add `-s 128` (snaplen) to keep only URB headers/status, not payloads.
+
+## Capture
+
+```bash
+.claude/skills/usbmon/scripts/usbcap.sh <bus|VID:PID|auto> [seconds] [outfile]
+# examples
+.claude/skills/usbmon/scripts/usbcap.sh cafe: 10 # auto-find a TinyUSB (VID 0xcafe) device
+.claude/skills/usbmon/scripts/usbcap.sh 3 8 /tmp/enum.pcapng # bus 3, 8 s
+```
+`lsusb` shows `Bus 00N` → interface `usbmonN`; `usbmon0` = all buses. Capture the device's own bus. To catch enumeration, start the capture, then replug the device.
+
+## Analyze
+
+```bash
+tshark -r cap.pcapng # one line per URB
+tshark -r cap.pcapng -V | less # full dissection (descriptors decoded)
+tshark -r cap.pcapng -Y 'usb.device_address==26' # filter to one device
+```
+
+## Filter (`-Y '<expr>'`)
+
+| Goal | Expression |
+|---|---|
+| One device / endpoint | `usb.device_address==26` / `usb.endpoint_address==0x81` |
+| IN (to host) / OUT (from host) | `usb.endpoint_address.direction==1` / `==0` |
+| Submit / Complete event | `usb.urb_type=='S'` / `=='C'` (char literal: single quotes) |
+| Control / bulk / interrupt / iso | `usb.transfer_type==2` / `3` / `1` / `0` |
+| Only transfers carrying data | `usb.data_len>0` |
+| GET_DESCRIPTOR / SET_ADDRESS / SET_CONFIGURATION | `usb.setup.bRequest==6` / `5` / `9` |
+| SET_INTERFACE / CLEAR_FEATURE (clear-halt) | `usb.setup.bRequest==11` / `1` |
+| Descriptor type DEVICE/CONFIG/STRING/HID-report | `usb.bDescriptorType==1` / `2` / `3` / `0x22` |
+| Class / vendor requests | `usb.bmRequestType.type!=0` |
+| STALLs / errors | `usb.urb_status!=0 && usb.urb_status!=-115` |
+
+Combine with `&&` — e.g. one endpoint's data: `usb.endpoint_address==0x02 && usb.data_len>0`.
+
+**`usb.urb_status` codes** (errno): `0` success · `-115` `-EINPROGRESS` (every Submit) · `-71` `-EPROTO` (transaction error — device answered wrong or didn't service in time, after the HC's retries) · `-32` `-EPIPE` (STALL) · `-110` `-ETIMEDOUT` · `-75` `-EOVERFLOW` (babble). A **control** transfer completing `-71` ≠ a STALL: the device mis-/under-served it (e.g. EP0 starved under load), not a `tud_*_control_xfer_cb` returning false.
+
+**Decoding class requests:** the plain `-e usb.setup.bRequest` field is often blank for class requests (CDC `SET_LINE_CODING` 0x20 / `SET_CONTROL_LINE_STATE` 0x22, `bmRequestType==0x21`) — Wireshark routes them to class fields. Use `-V` on the Submit frame to get the request name + `wValue` (for `SET_CONTROL_LINE_STATE`, `wValue` bit0=DTR, bit1=RTS).
+
+**Hard limit — usbmon is URB-level, not wire-level.** It cannot show data toggle (DATA0/DATA1) or NAKs. A device-side stall and a toggle desync both look identical: Submits on an endpoint with no Completes. To tell them apart, pair usbmon with **on-device GDB** (e.g. `openocd` + `gdb`): read the EP control register (response/toggle bits) and the DCD/USBD/class structs (`data.xfer[ep][dir]`, `_usbd_dev.ep_status`, `_cdcd_itf[].line_state`) at the moment of the hang. Build `MinSizeRel` still ships DWARF, so `p`/struct access works.
+
+## Host-side kernel logs (dynamic debug)
+
+usbmon shows the URBs; the kernel's **dynamic debug** shows the host driver's *reasoning* that usbmon can't — port resets, enumeration retries, address (re)assignment, EP halts, xHCI ring/command errors. Turn it on, reproduce, read `dmesg`:
+
+```bash
+# pick the module from `lsusb -t` Driver= : xhci_hcd (USB3 / most modern HCs), ehci_hcd (USB2),
+# plus usbcore for enumeration / hub / descriptor logic (controller-independent).
+echo 'module usbcore +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
+echo 'module xhci_hcd +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
+dmesg -w # follow live; replug to catch enumeration
+echo 'module xhci_hcd -p' | sudo tee /sys/kernel/debug/dynamic_debug/control # OFF when done — very noisy
+```
+
+`+p` enables the print flag at every call-site in that module (`+pfl` also tags func+line); `grep <module> /sys/kernel/debug/dynamic_debug/control` lists active sites. The control file is debugfs (`/sys/kernel/debug`), root-only → use `sudo tee` (a plain `sudo echo >` redirects as your user, not root). Needs `CONFIG_DYNAMIC_DEBUG` (standard on distro kernels). Best for **re-enumerates / enumeration stalls / port-reset storms**, where usbmon shows the resets but not the host's reason.
+
+## Troubleshoot — symptom → what to check
+
+| Symptom | Look for |
+|---|---|
+| Not recognized / re-enumerates | Is `GET DESCRIPTOR (DEVICE)` answered, `bMaxPacketSize0` sane? Repeated SET_ADDRESS / resets = device too slow to respond. Enable `usbcore`/`xhci_hcd` dynamic debug (above) for the host's reset reason. |
+| Enumeration stalls | Find the last good control transfer; the next request (often CONFIG, a string, or the first class request) is what your `tud_descriptor_*` / control callback mishandled. |
+| Control STALL | `usb.urb_status!=0` on a control URB = a `tud_*_control_xfer_cb` returned `false` or didn't handle that `bRequest` (decode it with `-V`). |
+| Bulk / interrupt missing or short | On the endpoint, do Submits get Completes? an unexpected short (`usb.data_len < wMaxPacketSize`) = FIFO/length bug; no completions = the class never wrote. |
+| CDC read (`dd`/cat) hangs, IN endpoint idle | Don't assume a bulk-IN bug. Check EP0: did `SET_CONTROL_LINE_STATE` (`bmRequestType==0x21`) complete `0` or fail `-71`? If it failed, the device never saw DTR → `tud_cdc_connected()` is false → the app stops sourcing TX. A device that streams fine in isolation but stalls right after a bulk **write** phase points here (control transfer starved by concurrent bulk). Confirm device-side with GDB: `_cdcd_itf[0].line_state` bit0 (DTR). |
+| ISO / audio dropouts | ISO URB cadence (~1/ms at full-speed) and payload lengths; zero-length frames = device starved the endpoint. |
+| Wrong descriptors | `-V` decodes them; check `bLength` / `wTotalLength` against your `tud_descriptor_configuration_cb`. |
diff --git a/.claude/skills/usbmon/scripts/usbcap.sh b/.claude/skills/usbmon/scripts/usbcap.sh
new file mode 100755
index 000000000..888da6de0
--- /dev/null
+++ b/.claude/skills/usbmon/scripts/usbcap.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# usbcap.sh - capture a USB bus via usbmon into a Wireshark-readable pcapng.
+# Usage: usbcap.sh <bus|VID:PID|auto> [seconds] [outfile]
+# <bus> numeric USB bus (lsusb "Bus 00N" -> N); 0 or "auto" = all buses.
+# <VID:PID> resolve the bus from a plugged-in device, e.g. cafe: or 1a86:8010.
+# Assumes usbmon is loaded and /dev/usbmon* is readable by your wireshark group
+# (see SKILL.md), so tshark captures with no sudo.
+set -euo pipefail
+
+target="${1:?usage: usbcap.sh <bus|VID:PID|auto> [seconds] [outfile]}"
+dur="${2:-10}"
+out="${3:-/tmp/usbcap-$$.pcapng}"
+[[ "$dur" =~ ^[0-9]+$ ]] || { echo "seconds must be an integer" >&2; exit 2; }
+
+case "$target" in
+ auto) bus=0 ;;
+ *:*) line=$(lsusb | grep -iF "ID ${target}" | head -1) || true
+ bus=$(printf '%s' "$line" | sed -E 's/^Bus 0*([0-9]+).*/\1/')
+ [ -n "$bus" ] || { echo "no device matching '$target' (plugged in?)" >&2; exit 1; } ;;
+ *) bus="$target" ;;
+esac
+[[ "$bus" =~ ^[0-9]+$ ]] || { echo "bad bus '$target'" >&2; exit 2; }
+
+echo "capturing usbmon${bus} for ${dur}s -> $out"
+tshark -i "usbmon${bus}" -a "duration:${dur}" -w "$out"
+echo "saved $out ($(tshark -r "$out" 2>/dev/null | wc -l | tr -d ' ') packets)"
+echo "analyze: tshark -r $out | tshark -r $out -V"