initial commit
This commit is contained in:
commit
b44ce7bd77
2716 files changed
+559005
No files matched your search
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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`.
|
||||
@@ -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}"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user