initial commit

This commit is contained in:
shell committed 2026-10-11 21:48:59 +08:00
commit b44ce7bd77
2716 files changed
+559005

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
---
name: build-doc
description: Use when building, previewing, or testing the TinyUSB Sphinx docs locally (docs/ → HTML), chasing Sphinx warnings, understanding how example READMEs get into the docs, or regenerating the auto-generated reference files after adding a board or dependency (boards.rst, dependencies.rst, BoardPresets.json, CMakePresets.json).
---
# Build TinyUSB Docs
## Build & preview
```bash
pip install -r docs/requirements.txt # one-time
python3 tools/build_doc.py -o # build docs/_build/ and open it
```
`tools/build_doc.py` wraps `sphinx-build`: `-c` clean, `-W` fail on warnings, `-o` open. Raw form: `sphinx-build -b html docs docs/_build`.
- Pages can be `.rst` or `.md` (MyST). Example `README.md`s under `examples/{device,host,dual}/*/` are **auto-collected** at build time into `docs/examples/` (per-group `index` pages, git-ignored) — add/rename an example and just rebuild; edit the source README, never the generated copies.
- Watch the output for `WARNING:` (broken refs, missing toctree entries).
## Regenerate after adding a board or dependency
Run from the repo root; `docs/reference/*.rst` and the preset JSONs are **generated** — don't hand-edit.
| Added | Run |
|---|---|
| Board (`hw/bsp/FAMILY/boards/`) | `python3 tools/gen_doc.py` + `python3 tools/gen_presets.py` |
| Dependency (edited `tools/get_deps.py`) | `python3 tools/gen_doc.py` |
- `gen_doc.py` → `docs/reference/boards.rst` + `dependencies.rst`. Needs `pandas` + `tabulate` (not in `requirements.txt`) — `pip install pandas tabulate` if it errors.
- `gen_presets.py` → `hw/bsp/BoardPresets.json` + per-example `CMakePresets.json`.
Then rebuild and `git diff` the regenerated files; commit them with the board/dep change.
+76
View File
@@ -0,0 +1,76 @@
---
name: code-size
description: Use when comparing TinyUSB code size between a base ref (master by default) and the current branch to evaluate the size impact of changes. Three granularities — single example on one board (with optional bloaty), all examples on one board, or all examples across CI families combined.
---
# Code Size Comparison
Compare TinyUSB code size between a base ref (default `master`) and the current branch using `tools/metrics_compare_base.py`. Three granularities — pick the narrowest one that exercises your change:
| Granularity | When to use | Command |
|---|---|---|
| **single example, one board** | Focused change touching one feature | `-b BOARD -e device/cdc_msc` |
| **all examples, one board** | Per-board regression sweep | `-b BOARD` |
| **all examples, all CI families (combined)** | Pre-merge full check | `--ci` |
The script handles the full base-vs-branch dance:
1. Creates a temporary git worktree of the base ref under `cmake-metrics/_worktree/`.
2. Builds the base in `cmake-metrics/<board>/base/`.
3. Builds the current tree in `cmake-metrics/<board>/build/`.
4. Runs `tools/metrics.py compare` and writes `cmake-metrics/<board>/metrics_compare.md`.
5. Removes the worktree on exit.
`--combined` (auto-set by `--ci`) also produces `cmake-metrics/_combined/metrics_compare.md` aggregating across all boards.
## Choosing arguments
Infer from the user's request:
- **Board(s):** named board → `-b BOARD` (repeatable). "All boards" / "CI" / "full sweep" → `--ci` (first board of each arm-gcc family). Default to a fast board (`raspberry_pi_pico`) if unspecified for an iterative check.
- **Example:** named example → `-e <group>/<name>` (e.g. `-e device/cdc_msc`). "All examples" → omit `-e`.
- **Bloaty:** only with `-e`. Use when the user wants a section/symbol-level breakdown for a single binary.
- **Base ref:** default `master`. Override with `--base-branch <ref>` (tag or commit also works).
- **Filter:** default is the absolute path of each side's `<checkout>/src/` directory, which uniquely identifies TinyUSB stack code without matching vendored deps that also have a `src/` (e.g. `pico-sdk/src/`). Override with one or more `-f SUBSTRING` flags to use repo-relative substrings instead. Change only if asked.
## Common invocations
```bash
# Single example, one board (linkermap, fastest):
python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc
# Same with bloaty for section/symbol breakdown:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc --bloaty
# All examples for one board:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico
# Multiple boards, one combined report:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico -b raspberry_pi_pico2 --combined
# Full CI sweep (first board per arm-gcc family, combined):
python3 tools/metrics_compare_base.py --ci
# Compare against a tag/commit instead of master:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico --base-branch v0.18.0
```
## Outputs
- **Per-board:** `cmake-metrics/<board>/metrics_compare.md` (and `_<example>.md` when `-e` is set)
- **Combined (with `--combined`/`--ci`):** `cmake-metrics/_combined/metrics_compare.md`
- **Bloaty:** printed to stdout as section + symbol diffs
## Timing
- Single example, single board: ~30 s
- All examples, single board: ~60-90 s
- `--ci` (all arm-gcc families, first board each): 4-8 minutes — sequential sweep across boards (Ninja parallelizes within each board, not across)
Use timeouts ≥ 10 minutes (600000 ms) for `--ci`.
## Reporting results
After running:
- Show the markdown report's summary table to the user.
- Highlight any rows with non-zero `% diff` — under the default filter every row is a TinyUSB stack source file (e.g. `usbd.c`, `cdc_device.c`, `dcd_<port>.c`), so any non-zero delta is a real stack-size impact.
- If the diff is unexpected, follow up with a single-example `--bloaty` run to localize.
+64
View File
@@ -0,0 +1,64 @@
---
name: hil
description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers per-host config selection (htpc uses local.json, ci uses tinyusb.json), local execution on either htpc or ci, remote execution over SSH from htpc, and debugging tips.
---
# Hardware-in-the-Loop (HIL) Testing
Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it tells you which host you are on, which determines the default config and whether remote mode is possible.
| Host | Local config | Remote (SSH → ci.lan)? |
|------|--------------|------------------------|
| `htpc` (dev PC) | `test/hil/local.json` | yes (large pool, `test/hil/tinyusb.json`) |
| `ci` (the rig) | `test/hil/tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local |
Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`.
## Prerequisites
Examples must be built for the target board(s) — see AGENTS.md "Build" → "All examples for a board" (produces `examples/cmake-build-<board>/`). `-B examples` points `hil_test.py` at that parent folder.
## Arguments
- **Board:** `-b BOARD_NAME` for one board; omit to run all boards in the config.
- **Pass-through:** `-v`, `-r N`, etc. forwarded unchanged.
If `local.json` is missing on `htpc`, ask the user to supply one (only fall back to `tinyusb.json` if told to).
## Local execution
Set `CONFIG` from `hostname` first (`test/hil/local.json` on htpc, `test/hil/tinyusb.json` on ci):
```bash
CONFIG=test/hil/local.json # on ci use: CONFIG=test/hil/tinyusb.json
# All boards in the config:
python3 test/hil/hil_test.py -B examples "$CONFIG"
# A single board (replace stm32f723disco):
python3 test/hil/hil_test.py -b stm32f723disco -B examples "$CONFIG"
```
Append pass-through flags (`-v`, `-r 1`, …) to either command as needed.
## Remote execution (htpc → ci.lan only)
`test/hil/hil_ci.sh` handles dir setup, scp of test scripts, rsync of firmware (`.elf`/`.bin`/`.hex`), and runs `hil_test.py` on `ci.lan` with `tinyusb.json`:
```bash
# All boards:
bash test/hil/hil_ci.sh
# A single board, with pass-through flags:
bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1
```
Env overrides: `REMOTE`, `REMOTE_DIR`, `CONFIG`. Fails fast if the build dir/repo layout is missing.
## Timing
Runs take 2-5 min. Use a timeout ≥ 20 min (1200000 ms). NEVER cancel early.
## Reporting
Show the output, summarize pass/fail per board. On failure, retry with `-v`; if that's not enough, add temporary debug prints to `hil_test.py`.
@@ -0,0 +1,71 @@
---
name: make-release
description: Use when cutting a new TinyUSB release — version bump, regenerated files, the per-release changelog, and validation before the maintainer commits and tags.
---
# Cut a TinyUSB Release
**Don't commit or tag during prep** — leave changes unstaged for the maintainer (step 4). Work in a worktree. Agree the version `X.Y.Z` with the maintainer first (their call, not derivable from the diff).
## 1. Bump + regenerate
```bash
# set version = 'X.Y.Z' in tools/make_release.py, then FROM REPO ROOT:
python3 tools/make_release.py
```
Refreshes `tusb_option.h`, `repository.yml`, `library.json`, `sonar-project.properties`, and (via gen_doc/gen_presets) `docs/reference/{boards,dependencies}.rst` + preset JSONs (presets/docs change only if boards/deps changed).
Gotchas: `gen_doc` needs `pandas`+`tabulate` (not in requirements) → `pip install pandas tabulate`; `boards.rst` lands with no trailing newline → let pre-commit fix it (step 3).
## 2. Changelog — `docs/changelog/` (the hard part)
New file `docs/changelog/X.Y.Z.md`, listed **first** in `docs/changelog/index.rst`. Get the PR set by **commit reachability, not merge date** (a date query wrongly pulls in the prior release's changelog PR at the boundary):
```bash
PREV=0.20.0
git merge-base --is-ancestor $PREV HEAD && echo OK # else stop: range invalid
git log --first-parent $PREV..HEAD --pretty=%s > /tmp/fp.txt
{ sed -nE 's/^Merge pull request #([0-9]+).*/\1/p' /tmp/fp.txt # merge-button
sed -nE 's/.*\(#([0-9]+)\)$/\1/p' /tmp/fp.txt ; } | sort -un > /tmp/prs.txt # squash
grep -vE '^Merge pull request #[0-9]+|\(#[0-9]+\)$' /tmp/fp.txt # guard: must be empty (direct pushes)
xargs -P8 -I{} gh pr view {} --json number,title,labels \
--jq '"#\(.number)\t\(.title)\t[\(.labels|map(.name)|join(","))]"' < /tmp/prs.txt # categorize
```
`--first-parent` skips dev-merges; the two `sed`s catch merge-button + squash. A PR merged into a *feature branch* folds into its parent (won't appear alone) — reflect its final state in the parent's bullet.
**Curate** into the prior file's exact Markdown (MyST) style:
- Title = version (`# X.Y.Z`), then italic date (ask if unknown). Add to top of `index.rst`.
- Section order: **General** (New MCUs and Boards / Code Quality and Build / Documentation) → **API Changes** → **Device Stack** (per class) → **Host Stack** → **Controller Driver (DCD & HCD)** (per driver) → **Testing** → **Contributors**. Sections are `##`; each class/driver group is a `###` sub-heading, not a bullet.
- Single backticks for symbols; group related PRs into one bullet (don't dump). `Port *`/driver labels help bucket DCD/HCD.
- **Contributors**: unique non-bot PR authors, alphabetical (the only contributor credit — no separate page):
```bash
xargs -P8 -I{} gh pr view {} --json author --jq '.author.login' < /tmp/prs.txt \
| grep -viE '\[bot\]$|^(copilot|claude|dependabot|github-actions)$' | sort -uf | sed 's/^/@/' | paste -sd, - | sed 's/,/, /g' # drop any CI/service accounts
```
## 3. Validate (leave unstaged)
```bash
pre-commit run --files docs/changelog/X.Y.Z.md docs/changelog/index.rst \
docs/reference/boards.rst docs/reference/dependencies.rst \
library.json repository.yml sonar-project.properties src/tusb_option.h tools/make_release.py
python3 tools/build_doc.py -c # docs build clean (see build-doc skill)
( cd test/unit-test && ceedling test:all )
( cd examples/device/cdc_msc && rm -rf build && mkdir build && cd build && \
cmake -DBOARD=stm32f407disco -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. && cmake --build . )
git diff --stat -- ':!.idea' # .idea/* is IDE noise
```
Confirm the version matches across `tusb_option.h` / `library.json` / `repository.yml` / `sonar-project.properties`.
## 4. Finalize (maintainer)
```bash
git add -A -- ':!.idea' && git commit -m "Bump version to X.Y.Z"
git tag -a X.Y.Z -m "Release X.Y.Z" # tags are unprefixed (0.20.0, not v0.20.0)
git push origin <branch> X.Y.Z
```
Then create the GitHub release from the tag.
## 5. Code size (automatic)
On the release event, CI's `code-metrics` job diffs against the previous tag's `metrics.json` and uploads `metrics.json` + a compare to the release — **only if the previous release has a `metrics.json` asset**. Confirm both appeared.
+36
View File
@@ -0,0 +1,36 @@
---
name: pvs
description: Use when running PVS-Studio static analysis (SAST + MISRA C:2023 / C++:2008) on TinyUSB for a given board. Builds the examples with an exported compile_commands.json, runs pvs-studio-analyzer against .PVS-Studio/.pvsconfig, and emits readable + SARIF output.
---
# PVS-Studio Static Analysis
`run_pvs.sh <BOARD>` builds all examples for the board (with
`compile_commands.json` exported), runs `pvs-studio-analyzer` against
`.PVS-Studio/.pvsconfig`, then writes `pvs-<board>.log` and `pvs-<board>.sarif`
plus printed errorfile findings. Needs a license file or `$PVS_STUDIO_CREDENTIALS`.
```bash
.claude/skills/pvs/run_pvs.sh raspberry_pi_pico # whole project for a board
# Scope to specific files — -S takes a plaintext list (one path per line),
# NOT a source file directly. Extra args pass through to the analyzer.
printf 'src/tusb.c\nsrc/class/cdc/cdc_device.c\n' > /tmp/files.txt
.claude/skills/pvs/run_pvs.sh stm32f407disco -S /tmp/files.txt
```
## Notes
- **Board:** `raspberry_pi_pico` mirrors CI (needs Pico SDK deps);
`stm32f407disco` is fastest (no external SDK). Any board works once its deps
are fetched (`python3 tools/get_deps.py -b <board>`).
- `.pvsconfig` already excludes vendored code and suppresses accepted MISRA
deviations — don't re-add those on the command line. Surviving `src/` findings
are genuinely new.
- Timing is dominated by the build (deps may push it past a minute); the analysis
pass is ~10-30 s. Use a timeout ≥ 10 min when deps must be fetched first.
- `--dump-files` is intentionally omitted — it scatters `.PVS-Studio.i/.cfg`
dumps across the tree (FP-debugging only); add it back via the passthrough args.
After a run, summarize findings by rule/severity from the errorfile output and
point the user at `pvs-<board>.sarif`.
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Run PVS-Studio static analysis on TinyUSB for a given BOARD.
#
# Mirrors the "Static Analysis (PVS-Studio)" section in AGENTS.md / CLAUDE.md:
# - build all examples for BOARD with compile_commands.json exported
# - run pvs-studio-analyzer against that compile DB using .PVS-Studio/.pvsconfig
# - convert the log to human-readable (errorfile) and SARIF output
#
# Usage:
# .claude/skills/pvs/run_pvs.sh <BOARD> [extra pvs-studio-analyzer args...]
#
# Examples:
# .claude/skills/pvs/run_pvs.sh raspberry_pi_pico
# # Scope to specific files via a plaintext list (one path per line):
# printf 'src/tusb.c\nsrc/class/cdc/cdc_device.c\n' > /tmp/files.txt
# .claude/skills/pvs/run_pvs.sh stm32f407disco -S /tmp/files.txt
set -euo pipefail
BOARD="${1:-}"
if [ -z "$BOARD" ]; then
echo "Usage: $0 <BOARD> [extra pvs-studio-analyzer args...]" >&2
echo "Example: $0 raspberry_pi_pico" >&2
exit 2
fi
shift
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
cd "$REPO_ROOT"
BUILD_DIR="examples/cmake-build-${BOARD}"
COMPILE_DB="${BUILD_DIR}/compile_commands.json"
REPORT_LOG="pvs-${BOARD}.log"
SARIF_OUT="pvs-${BOARD}.sarif"
JOBS="$(nproc 2>/dev/null || echo 4)"
# --- License ---------------------------------------------------------------
# Use an existing license file, else materialize one from PVS_STUDIO_CREDENTIALS
# (format: "<name> <key>", as supplied by the CI secret of the same name).
if ! pvs-studio-analyzer lic-info >/dev/null 2>&1; then
if [ -n "${PVS_STUDIO_CREDENTIALS:-}" ]; then
echo ">>> Registering PVS-Studio license from PVS_STUDIO_CREDENTIALS"
# Split "<name> <key>" into exactly two fields, quoted, so a key containing
# glob chars or extra spaces can't be word-split/expanded.
read -r _pvs_name _pvs_key <<< "$PVS_STUDIO_CREDENTIALS"
pvs-studio-analyzer credentials "$_pvs_name" "$_pvs_key"
else
echo "ERROR: no PVS-Studio license found and PVS_STUDIO_CREDENTIALS is unset." >&2
exit 1
fi
fi
# --- Build (exports compile_commands.json) ---------------------------------
echo ">>> Building all examples for ${BOARD} (this also fetches the compile DB)"
cmake examples -B "${BUILD_DIR}" -G Ninja \
-DBOARD="${BOARD}" \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build "${BUILD_DIR}"
if [ ! -f "${COMPILE_DB}" ]; then
echo "ERROR: ${COMPILE_DB} was not produced by the build." >&2
exit 1
fi
# --- Analyze ---------------------------------------------------------------
echo ">>> Running PVS-Studio analyzer (-j${JOBS})"
# Note: AGENTS.md shows --dump-files, but that scatters .PVS-Studio.i/.cfg dump
# files across the source tree (only useful for debugging false positives). It is
# omitted here to keep the working tree clean; add it back via "$@" if needed.
pvs-studio-analyzer analyze \
-f "${COMPILE_DB}" \
-R .PVS-Studio/.pvsconfig \
-o "${REPORT_LOG}" -j"${JOBS}" \
--security-related-issues \
--misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser \
"$@"
# --- Report ----------------------------------------------------------------
echo ">>> General-analysis + MISRA findings (errorfile):"
plog-converter -a GA:1,2 -t errorfile "${REPORT_LOG}" || true
plog-converter -t sarif -o "${SARIF_OUT}" "${REPORT_LOG}" >/dev/null
echo ">>> Done."
echo " Raw log : ${REPORT_LOG}"
echo " SARIF : ${SARIF_OUT}"
+80
View File
@@ -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`. |
@@ -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"