Merge branch 'master' into fork/kira-live/master

This commit is contained in:
hathach
2026-04-29 13:15:24 +07:00
7 changed files with 671 additions and 427 deletions

View File

@ -1,58 +0,0 @@
# hil
Run Hardware-in-the-Loop (HIL) tests on physical boards.
## Arguments
- $ARGUMENTS: Optional flags (e.g. board name, extra args). If empty, runs all boards with default config.
## Instructions
1. Parse $ARGUMENTS:
- If $ARGUMENTS contains `-b BOARD_NAME`, run for that specific board only.
- If $ARGUMENTS is empty or has no `-b`, run for all boards in the config.
- Pass through any other flags (e.g. `-v` for verbose, `-r N` for retry count) directly to the command.
2. Determine whether to run **locally** or **remotely via SSH**:
- **Local**: boards are attached to this machine (default when `local.json` is used)
- **Remote (`ssh ci.lan`)**: boards are attached to the CI machine (when `tinyusb.json` is used)
3. **Local execution** (boards attached to this machine):
```bash
python test/hil/hil_test.py -b BOARD_NAME -B examples $HIL_CONFIG $EXTRA_ARGS
```
4. **Remote execution** (boards attached to `ci.lan`):
Only copy the minimal files needed (firmware binaries + test script + config), then run remotely.
```bash
REMOTE=ci.lan
REMOTE_DIR=/tmp/tinyusb-hil
# Create remote working directory
ssh $REMOTE "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR/test/hil"
# Copy HIL test script and its dependency
scp test/hil/hil_test.py test/hil/pymtp.py test/hil/tinyusb.json $REMOTE:$REMOTE_DIR/test/hil/
# Copy only the firmware binaries for the target board(s)
# For a specific board:
scp -r examples/cmake-build-$BOARD_NAME $REMOTE:$REMOTE_DIR/examples/
# Or for all boards that have been built:
# for dir in examples/cmake-build-*/; do scp -r "$dir" $REMOTE:$REMOTE_DIR/examples/; done
# Run the test remotely
ssh $REMOTE "cd $REMOTE_DIR && python3 test/hil/hil_test.py -b $BOARD_NAME -B examples tinyusb.json $EXTRA_ARGS"
```
Note: The remote machine (`ci.lan`) must have:
- Python 3 with `pyserial` installed (`pip install pyserial`)
- Flasher tools: `JLinkExe`, `openocd`, etc. as needed by the board
- USB access to the boards (udev rules configured)
5. Use a timeout of at least 20 minutes (600000ms). HIL tests take 2-5 minutes. NEVER cancel early.
6. After the test completes:
- Show the test output to the user.
- Summarize pass/fail results per board.
- If there are failures, suggest re-running with `-v` flag for verbose output to help debug.

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.

View File

@ -0,0 +1,66 @@
---
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 local execution and remote execution over SSH, config selection, and debugging tips.
---
# Hardware-in-the-Loop (HIL) Testing
Run TinyUSB HIL tests against real boards. Two execution modes — **local** (boards attached to this machine) and **remote** (boards attached to `ci.lan`, reached over SSH). Default to **local** unless the user specifies `remote`. Do not auto-detect.
## Prerequisites
- Examples must already be built for the target board(s). See AGENTS.md "Build" → "All examples for a board", which produces `examples/cmake-build-<board>/`.
- `-B examples` tells `hil_test.py` that `examples/` is the parent folder containing the per-board build outputs.
## Choosing arguments
Infer from the user's request:
- **Mode:** `local` (default) or `remote`. Only switch to `remote` if the user explicitly says so or names `ci.lan`.
- **Board:** if the user names a specific board, pass `-b BOARD_NAME`. Otherwise omit `-b` to run all boards in the config.
- **Pass-through flags:** `-v` (verbose), `-r N` (retry count), etc. — pass through unchanged.
Config file follows from mode:
- **Local** → `test/hil/local.json` (user-supplied; not tracked in repo — describes boards attached locally)
- **Remote** → `test/hil/tinyusb.json` (tracked; describes the `ci.lan` test rig)
If `local.json` is missing, fall back to `tinyusb.json` only when explicitly told to; otherwise stop and ask the user to supply one.
## Local execution
Boards attached to this machine:
```bash
# Specific board:
python3 test/hil/hil_test.py -b BOARD_NAME -B examples test/hil/local.json $EXTRA_ARGS
# All boards in the config (no -b):
python3 test/hil/hil_test.py -B examples test/hil/local.json $EXTRA_ARGS
```
## Remote execution (ci.lan)
Use `test/hil/hil_ci.sh` — it handles dir setup, scp of test scripts, rsync of firmware artifacts (`.elf` / `.bin` / `.hex` only), and running `hil_test.py` on `ci.lan`:
```bash
# Specific board:
bash test/hil/hil_ci.sh -b raspberry_pi_pico2
# All boards in tinyusb.json:
bash test/hil/hil_ci.sh
# Pass-through extra args (any non -b flag is forwarded to hil_test.py):
bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1
```
Overrides via env vars: `REMOTE=ci.lan`, `REMOTE_DIR=/tmp/tinyusb-hil`, `CONFIG=test/hil/tinyusb.json`.
The script fails fast if the build dir or repo layout is missing.
## Timing
HIL runs take 2-5 minutes. Use a timeout of at least 20 minutes (600000 ms). NEVER cancel early.
## Reporting results
After the test completes:
- Show the test output to the user.
- Summarize pass/fail per board.
- On failure, suggest re-running with `-v` for verbose output. If `-v` isn't enough, temporarily add debug prints to `test/hil/hil_test.py` to pinpoint the issue.

1
.gitignore vendored
View File

@ -56,3 +56,4 @@ BrowseInfo
.cmake_build
README_processed.rst
.worktrees
cmake-metrics/

491
AGENTS.md
View File

@ -1,428 +1,217 @@
# TinyUSB Agent Instructions
TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems, designed to be memory-safe with no
dynamic allocation and thread-safe with all interrupt events deferred to non-ISR task functions.
TinyUSB is a cross-platform USB Host/Device stack for embedded systems: memory-safe (no dynamic allocation) and thread-safe (ISR events deferred to task context).
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected
information that does not match the info here.
Reference these instructions first; fall back to search/bash only when reality diverges.
## Shared Ground Rules
- Keep TinyUSB memory-safe: avoid dynamic allocation, defer ISR work to task context, and follow C99 with two-space indentation/no tabs.
- Match file organization: core stack under `src`, MCU/BSP support in `hw/{mcu,bsp}`, examples under `examples/{device,host,dual}`, docs in `docs`, tests under `test/{unit-test,fuzz,hil}`.
- Use descriptive snake_case for helpers, reserve `tud_`/`tuh_` for public APIs, `TU_` for macros, and keep headers self-contained with `#if CFG_TUSB_MCU` guards where needed.
- Prefer `.clang-format` for C/C++ formatting, run `pre-commit run --all-files` before submitting, and document board/HIL coverage when applicable.
- Commit in imperative mood, keep changes scoped, and supply PRs with linked issues plus test/build evidence.
## Behavioral Guidelines
Bias toward caution over speed. For trivial tasks, use judgment.
## Bootstrap and Build Setup
- **Think first** — state assumptions; ask if unclear; present alternatives instead of picking silently.
- **Simplicity** — no features, abstractions, flexibility, or error handling beyond what was asked. If 200 lines could be 50, rewrite.
- **Surgical changes** — touch only what the task requires; match existing style; don't refactor working code; mention unrelated dead code rather than deleting it. Remove only orphans *your* changes created.
- **Goal-driven** — turn tasks into verifiable goals ("write failing test, make it pass"). For multi-step work, state a brief `step → verify` plan.
- Install ARM GCC toolchain: `sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi`
- Fetch core dependencies: `python3 tools/get_deps.py` -- takes <1 second. NEVER CANCEL.
- For specific board families: `python3 tools/get_deps.py FAMILY_NAME` (e.g., rp2040, stm32f4), or
`python3 tools/get_deps.py -b BOARD_NAME`
- Dependencies are cached in `lib/` and `hw/mcu/` directories
- For **Espressif** boards, initialize the ESP-IDF environment before any build/flash/monitor command:
`. $HOME/code/esp-idf/export.sh`
## Ground Rules
## Build Examples
- **Language/style:** C99, 2-space indent (no tabs), snake_case helpers, `UPPER_CASE` macros. Public APIs use `tud_`/`tuh_`; macros use `TU_`. Headers self-contained with `#if CFG_TUSB_MCU` guards.
- **Safety:** no dynamic allocation; defer ISR work to task context; use `TU_ASSERT()` for error checks; always check return values; include order: C stdlib → tusb common → drivers → classes.
- **Layout:** `src/` core, `hw/{mcu,bsp}/` MCU+BSP, `examples/{device,host,dual}/`, `test/{unit-test,fuzz,hil}/`, `docs/`, `tools/`.
- **Commits/PRs:** imperative mood, scoped changes, link issues, include test/build evidence.
- **Formatting/lint:** `clang-format` (`.clang-format`), `codespell` (`.codespellrc`), run `pre-commit run --all-files` before submitting.
Choose ONE of these approaches:
**Option 1: Individual Example with CMake and Ninja (RECOMMENDED)**
## Bootstrap
```bash
cd examples/device/cdc_msc
mkdir -p build && cd build
sudo apt-get install -y gcc-arm-none-eabi # ARM toolchain (2-5 min, one-time)
python3 tools/get_deps.py [FAMILY|-b BOARD] # fetch deps into lib/, hw/mcu/ (<1 s)
. $HOME/code/esp-idf/export.sh # Espressif only: before any build/flash/monitor
```
## Build
Single example (CMake+Ninja, recommended, 1-3 s):
```bash
cd examples/device/cdc_msc && mkdir -p build && cd build
cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel ..
cmake --build .
```
-- takes 1-2 seconds. NEVER CANCEL. Set timeout to 5+ minutes.
**Option 2: All Examples for a Board**
different folder than Option 1
All examples for a board (15-20 s; some objcopy failures are non-critical). Use `cmake-build-<board>` as the build dir — HIL tests expect that exact name:
```bash
cd examples/
mkdir -p build && cd build
cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel ..
cmake --build .
cd examples
cmake -B cmake-build-raspberry_pi_pico -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .
cmake --build cmake-build-raspberry_pi_pico
```
-- takes 15-20 seconds, may have some objcopy failures that are non-critical. NEVER CANCEL. Set timeout to 30+ minutes.
**Option 3: Individual Example with Make**
Single example with Make:
```bash
cd examples/device/cdc_msc
make BOARD=raspberry_pi_pico all
cd examples/device/cdc_msc && make BOARD=raspberry_pi_pico all
```
-- takes 2-3 seconds. NEVER CANCEL. Set timeout to 5+ minutes.
**Option 4: Espressif Example with ESP-IDF**
Only ESP-IDF-enabled examples are supported for Espressif boards. Use FreeRTOS examples such as `examples/device/cdc_msc_freertos`
that contain `idf_component_register()` support.
Espressif (only ESP-IDF examples like `cdc_msc_freertos`):
```bash
. $HOME/code/esp-idf/export.sh
cd examples/device/cdc_msc_freertos
idf.py -DBOARD=espressif_s3_devkitc build
```
Use `-DBOARD=...` with any supported board under `hw/bsp/espressif/boards/`. NEVER CANCEL. Set timeout to 10+ minutes.
**Build options** (CMake `-D…` / Make `…=…`):
- Debug: `CMAKE_BUILD_TYPE=Debug` / `DEBUG=1`
- Logging: `LOG=2` (add `LOGGER=rtt` for RTT)
- Root hub port: `RHPORT_DEVICE=1`
- Speed: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED`
## Flash
## Build Options
```bash
# JLink
ninja cdc_msc-jlink # CMake
make BOARD=<board> flash-jlink # Make
- **Debug build**:
- CMake: `-DCMAKE_BUILD_TYPE=Debug`
- Make: `DEBUG=1`
- **With logging**:
- CMake: `-DLOG=2`
- Make: `LOG=2`
- **With RTT logger**:
- CMake: `-DLOG=2 -DLOGGER=rtt`
- Make: `LOG=2 LOGGER=rtt`
- **RootHub port selection**:
- CMake: `-DRHPORT_DEVICE=1`
- Make: `RHPORT_DEVICE=1`
- **Port speed**:
- CMake: `-DRHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED`
- Make: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED`
# OpenOCD
ninja cdc_msc-openocd # CMake
make BOARD=<board> flash-openocd # Make
## Flashing and Deployment
# UF2
ninja cdc_msc-uf2 # CMake
make BOARD=<board> all uf2 # Make
- **Flash with JLink**:
- CMake: `ninja cdc_msc-jlink`
- Make: `make BOARD=raspberry_pi_pico flash-jlink`
- **Flash with OpenOCD**:
- CMake: `ninja cdc_msc-openocd`
- Make: `make BOARD=raspberry_pi_pico flash-openocd`
- **Generate UF2**:
- CMake: `ninja cdc_msc-uf2`
- Make: `make BOARD=raspberry_pi_pico all uf2`
- **List all targets** (CMake/Ninja): `ninja -t targets`
- **Espressif flash**:
- Run `. $HOME/code/esp-idf/export.sh`
- `cd examples/device/cdc_msc_freertos`
- `idf.py -DBOARD=espressif_s3_devkitc flash`
- **Espressif serial monitor / chip log output**:
- Run `. $HOME/code/esp-idf/export.sh`
- `cd examples/device/cdc_msc_freertos`
- `idf.py -DBOARD=espressif_s3_devkitc monitor`
ninja -t targets # list CMake targets
# Espressif (after . $HOME/code/esp-idf/export.sh)
idf.py -DBOARD=<board> flash
idf.py -DBOARD=<board> monitor
```
## GDB Debugging
Look up the board's `JLINK_DEVICE` and `OPENOCD_OPTION` from `hw/bsp/*/boards/*/board.cmake` (or `board.mk`).
Look up `JLINK_DEVICE` / `OPENOCD_OPTION` in `hw/bsp/*/boards/*/board.cmake` (CMake builds) or `board.mk` (Make builds).
### JLinkGDBServer
**Terminal 1 start the GDB server:**
**JLink — Terminal 1:**
```bash
JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 \
-port 2331 -swoport 2332 -telnetport 2333 -nogui
JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -nogui
```
**Terminal 2 connect GDB:**
```bash
arm-none-eabi-gdb /tmp/build/firmware.elf
(gdb) target remote :2331
(gdb) monitor reset halt
(gdb) load
(gdb) continue
```
To break on entry instead of running immediately:
```bash
(gdb) monitor reset halt
(gdb) load
(gdb) break main
(gdb) continue
```
### OpenOCD
**Terminal 1 start the GDB server:**
**OpenOCD — Terminal 1:**
```bash
openocd -f interface/stlink.cfg -f target/stm32h7x.cfg
# or with J-Link probe:
# or with a J-Link interface:
openocd -f interface/jlink.cfg -f target/stm32h7x.cfg
```
For **rp2040/rp2350** with a CMSIS-DAP probe (e.g. Picoprobe, debugprobe):
```bash
# rp2040/rp2350 via CMSIS-DAP:
openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000"
# or for rp2350:
openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
For boards that define `OPENOCD_OPTION` in `board.cmake`, use those options directly:
```bash
openocd $(cat hw/bsp/FAMILY/boards/BOARD/board.cmake | grep OPENOCD_OPTION | ...)
```
**Terminal 2 connect GDB (OpenOCD default port is 3333):**
**Terminal 2 — connect GDB** (replace `<port>` with `2331` for JLinkGDBServer or `3333` for OpenOCD):
```bash
arm-none-eabi-gdb /tmp/build/firmware.elf
(gdb) target remote :3333
(gdb) target remote :<port>
(gdb) monitor reset halt
(gdb) load
(gdb) break main # optional, to stop at entry
(gdb) continue
```
### RTT Logging with JLinkGDBServer
**RTT logging:** build with `LOG=2 LOGGER=rtt`, flash, then run JLinkGDBServer with `-RTTTelnetPort 19021`, and in another terminal `JLinkRTTClient` (pipe to `tee rtt.log` or use `timeout 20s JLinkRTTClient > rtt.log` for non-interactive capture).
- Build with RTT logging enabled (example):
`cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt all`
- Flash with J-Link:
`cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt flash-jlink`
- Launch GDB server with RTT port (keep this running in terminal 1):
`JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -RTTTelnetPort 19021 -nogui`
- Read RTT output (terminal 2):
`JLinkRTTClient`
- Capture RTT to file (optional):
`JLinkRTTClient | tee rtt.log`
- For non-interactive capture:
`timeout 20s JLinkRTTClient > rtt.log`
## Testing
## Unit Testing
**Unit (Ceedling, Unity+CMock, ~4 s):**
```bash
sudo gem install ceedling
cd test/unit-test && ceedling test:all # or ceedling test:test_fifo
```
- Install Ceedling: `sudo gem install ceedling`
- Run all unit tests: `cd test/unit-test && ceedling` or `cd test/unit-test && ceedling test:all` -- takes 4 seconds.
NEVER CANCEL. Set timeout to 10+ minutes.
- Run specific test: `cd test/unit-test && ceedling test:test_fifo`
- Tests use Unity framework with CMock for mocking
## Hardware-in-the-Loop (HIL) Testing
- `-B examples` means `examples` is the parent folder that contains multi-board build outputs such as `examples/cmake-build-BOARD_NAME/...`
- Select config file before running HIL tests:
- if GitHub Actions self-hosted runner service is running, use `tinyusb.json`
- otherwise use `local.json`
- example:
`HIL_CONFIG=$( (systemctl list-units --type=service --state=running 2>/dev/null; systemctl --user list-units --type=service --state=running 2>/dev/null) | grep -q 'actions\.runner' && echo tinyusb.json || echo local.json )`
- Run tests on actual hardware, one of following ways:
- test a specific board `python test/hil/hil_test.py -b BOARD_NAME -B examples $HIL_CONFIG`
- test all boards in config `python test/hil/hil_test.py -B examples $HIL_CONFIG`
- In case of error, enabled verbose mode with `-v` flag for detailed logs. Also try to observe script output, and try to
modify hil_test.py (temporarily) to add more debug prints to pinpoint the issue.
- Requires pre-built (all) examples for target boards (see Build Examples section 2)
take 2-5 minutes. NEVER CANCEL. Set timeout to 20+ minutes.
**HIL (2-5 min):** invoke the `hil` skill (`.claude/skills/hil/SKILL.md`) for the full procedure (local vs remote mode, config selection, SSH copy steps, debugging tips). Requires pre-built examples — see Build → "All examples for a board".
## Documentation
- Install requirements: `pip install -r docs/requirements.txt`
- Build docs: `cd docs && sphinx-build -b html . _build` -- takes 2-3 seconds. NEVER CANCEL. Set timeout to 10+ minutes.
```bash
pip install -r docs/requirements.txt
cd docs && sphinx-build -b html . _build # ~2.5 s
```
## Code Size Metrics
Generate and compare code size metrics to evaluate the impact of changes. This is the most common workflow
when making code changes use it to verify size impact before committing.
Verify size impact before committing. Invoke the `code-size` skill (`.claude/skills/code-size/SKILL.md`) — it wraps `tools/metrics_compare_base.py` to handle the base-vs-branch worktree + build + compare flow.
**Quick single-board metrics (preferred for iterative development):**
Quick reference:
```bash
# Single example, one board:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc
# Add --bloaty for section/symbol breakdown.
# All examples, one board:
python3 tools/metrics_compare_base.py -b raspberry_pi_pico
# All arm-gcc CI families combined (pre-merge sweep, 4-8 min):
python3 tools/metrics_compare_base.py --ci
```
Reports land in `cmake-metrics/<board>/metrics_compare.md` (per-board) and `cmake-metrics/_combined/metrics_compare.md` (with `--combined`/`--ci`).
## Static Analysis (PVS-Studio)
Requires `compile_commands.json` (CMake `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`).
```bash
rm -rf cmake-build
python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics
python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json
# Whole project:
pvs-studio-analyzer analyze \
-f examples/cmake-build-raspberry_pi_pico/compile_commands.json \
-R .PVS-Studio/.pvsconfig \
-o pvs-report.log -j12 --dump-files \
--misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser
# Specific files (add one or more `-S <file>`):
pvs-studio-analyzer analyze \
-f examples/cmake-build-raspberry_pi_pico/compile_commands.json \
-R .PVS-Studio/.pvsconfig \
-S src/foo.c -S src/bar.c \
-o pvs-report.log -j12 --dump-files \
--misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser
plog-converter -a GA:1,2 -t errorfile pvs-report.log # view results
```
This builds all examples for one board and produces `metrics.json` + `metrics.md`. Takes ~30 seconds.
NEVER CANCEL. Set timeout to 10+ minutes.
Takes ~10-30 s.
**Comparing with master (before/after workflow):**
## Validation After Changes
1. On master: build and save baseline
```bash
rm -rf cmake-build
python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics
python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json
mv metrics.json metrics_master.json
```
2. Switch to your branch: rebuild
```bash
rm -rf cmake-build
python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics
python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json
```
3. Compare: `python3 tools/metrics.py compare -m -f tinyusb/src metrics_master.json metrics.json`
Produces `metrics_compare.md` showing size differences.
1. `pre-commit run --all-files` — format, spell, unit tests (10-15 s).
2. Build at least one board's full example set (Build → "All examples for a board") for modules you touched.
3. Run relevant unit tests; add fuzz/HIL coverage for parsers or protocol state machines.
**Full CI metrics (all arm-gcc families, for thorough validation):**
**Boards good for local testing:**
- `stm32f407disco` — no external SDK
- `raspberry_pi_pico` — Pico SDK required
- Others: see `hw/bsp/FAMILY/boards/`
```bash
rm -rf cmake-build
FAMILIES=$(python3 .github/workflows/ci_set_matrix.py | python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('arm-gcc',[])))")
python3 tools/build.py --one-first --target all --target tinyusb_metrics $FAMILIES
python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json
```
Device examples need real hardware to validate runtime behavior; must at least build.
Builds the first board of each family. Takes 2-4 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
## Release
## Code Quality and Validation
**Do not commit automatically — leave changes for maintainer review.**
- Format code: `clang-format -i path/to/file.c` (uses `.clang-format` config)
- Check spelling: `pip install codespell && codespell` (uses `.codespellrc` config)
- Pre-commit hooks validate unit tests and code quality automatically
1. Bump version at top of `tools/make_release.py`.
2. Run `python3 tools/make_release.py` to refresh: `src/tusb_option.h`, `repository.yml`, `library.json`, `sonar-project.properties`, `docs/reference/boards.rst`, `hw/bsp/BoardPresets.json`.
3. Changelog `docs/info/changelog.rst`:
- `git log <last-tag>..HEAD --oneline` for commit list.
- Read merged PRs for context (`gh pr view`, or github MCP tools).
- Follow existing format: version + `======` underline, italic date, sections (General, API Changes, DCD & HCD, Device Stack, Host Stack, Testing), RST inline code for symbols.
4. Validate: `ceedling test:all`, build `cdc_msc` for `stm32f407disco`, review `git diff --stat`.
5. Leave unstaged. Maintainer commits `Bump version to X.Y.Z`, then: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin <branch> vX.Y.Z`. Create GitHub release from tag.
## Static Analysis with PVS-Studio
## References
- **Analyze whole project**:
```bash
pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser
```
- **Analyze specific source files**:
```bash
pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S path/to/file.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser
```
- **Multiple specific files**:
```bash
pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S src/file1.c -S src/file2.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser
```
- Requires `compile_commands.json` in the build directory (generated by CMake with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`)
- Use `-f` option to specify path to `compile_commands.json`
- Use `-R .PVS-Studio/.pvsconfig` to specify rule configuration file
- Use `-j12` for parallel analysis with 12 threads
- `--dump-files` saves preprocessed files for debugging
- `--misra-c-version 2023` enables MISRA C:2023 checks
- `--misra-cpp-version 2008` enables MISRA C++:2008 checks
- `--use-old-parser` uses legacy parser for compatibility
- Analysis takes ~10-30 seconds depending on project size. Set timeout to 5+ minutes.
- View results: `plog-converter -a GA:1,2 -t errorfile pvs-report.log` or open in PVS-Studio GUI
- MCU reference manuals, datasheets, schematics: `$HOME/Documents/Calibre Library`.
- 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`.
## Validation Checklist
## Common Build Issues
### ALWAYS Run These After Making Changes
1. **Pre-commit validation** (RECOMMENDED): `pre-commit run --all-files`
- Install pre-commit: `pip install pre-commit && pre-commit install`
- Runs all quality checks, unit tests, spell checking, and formatting
- Takes 10-15 seconds. NEVER CANCEL. Set timeout to 15+ minutes.
2. **Build validation**: Build at least one board with all example that exercises your changes, see Build Examples
section (option 2)
3. Run unit tests relevant to touched modules; add fuzz/HIL coverage when modifying parsers or protocol state machines.
### Manual Testing Scenarios
- **Device examples**: Cannot be fully tested without real hardware, but must build successfully
- **Unit tests**: Exercise core stack functionality - ALL tests must pass
- **Build system**: Must be able to build examples for multiple board families
### Board Selection for Testing
- **STM32F4**: `stm32f407disco` - no external SDK required, good for testing
- **RP2040**: `raspberry_pi_pico` - requires Pico SDK, commonly used
- **Other families**: Check `hw/bsp/FAMILY/boards/` for available boards
## Release Instructions
**DO NOT commit files automatically - only modify files and let the maintainer review before committing.**
1. Bump the release version variable at the top of `tools/make_release.py`.
2. Execute `python3 tools/make_release.py` to refresh:
- `src/tusb_option.h` (version defines)
- `repository.yml` (version mapping)
- `library.json` (PlatformIO version)
- `sonar-project.properties` (SonarQube version)
- `docs/reference/boards.rst` (generated board documentation)
- `hw/bsp/BoardPresets.json` (CMake presets)
3. Generate release notes for `docs/info/changelog.rst`:
- Get commit list: `git log <last-release-tag>..HEAD --oneline`
- **Visit GitHub PRs** for merged pull requests to understand context and gather details
- Use GitHub tools to search/read PRs: `github-mcp-server-list_pull_requests`, `github-mcp-server-pull_request_read`
- Extract key changes, API modifications, bug fixes, and new features from PR descriptions
- Add new changelog entry following the existing format:
- Version heading with equals underline (e.g., `0.20.0` followed by `======`)
- Release date in italics (e.g., `*November 19, 2024*`)
- Major sections: General, API Changes, Controller Driver (DCD & HCD), Device Stack, Host Stack, Testing
- Use bullet lists with descriptive categorization
- Reference function names, config macros, and file paths using RST inline code (double backticks)
- Include meaningful descriptions, not just commit messages
4. **Validation before commit**:
- Run unit tests: `cd test/unit-test && ceedling test:all`
- Build at least one example: `cd examples/device/cdc_msc && make BOARD=stm32f407disco all`
- Verify changed files look correct: `git diff --stat`
5. **Leave files unstaged** for maintainer to review, modify if needed, and commit with message: `Bump version to X.Y.Z`
6. **After maintainer commits**: Create annotated tag with `git tag -a vX.Y.Z -m "Release X.Y.Z"`
7. Push commit and tag: `git push origin <branch> && git push origin vX.Y.Z`
8. Create GitHub release from the tag with changelog content
## Repository Structure Quick Reference
```
├── src/ # Core TinyUSB stack
│ ├── class/ # USB device classes (CDC, HID, MSC, Audio, etc.)
│ ├── portable/ # MCU-specific drivers (organized by vendor)
│ ├── device/ # USB device stack core
│ ├── host/ # USB host stack core
│ └── common/ # Shared utilities (FIFO, etc.)
├── examples/ # Example applications
│ ├── device/ # Device examples (cdc_msc, hid_generic, etc.)
│ ├── host/ # Host examples
│ └── dual/ # Dual-role examples
├── hw/bsp/ # Board Support Packages
│ └── FAMILY/boards/ # Board-specific configurations
├── test/unit-test/ # Unit tests using Ceedling
├── tools/ # Build and utility scripts
└── docs/ # Sphinx documentation
```
#### Build Time Reference
- **Dependency fetch**: <1 second
- **Single example build**: 1-3 seconds
- **Unit tests**: ~4 seconds
- **Documentation build**: ~2.5 seconds
- **Full board examples**: 15-20 seconds
- **Toolchain installation**: 2-5 minutes (one-time)
#### Key Files to Know
- `tools/get_deps.py`: Manages dependencies for MCU families
- `tools/build.py`: Builds multiple examples, supports make/cmake
- `src/tusb.h`: Main TinyUSB header file
- `src/tusb_config.h`: Configuration template
- `examples/device/cdc_msc/`: Most commonly used example for testing
- `test/unit-test/project.yml`: Ceedling test configuration
#### MCU Reference Manuals and Datasheets
- Look in `$HOME/Documents/Calibre Library` for all MCU reference manuals, datasheets and board schematics.
#### Debugging Build Issues
- **Missing compiler**: Install `gcc-arm-none-eabi` package
- **Missing dependencies**: Run `python3 tools/get_deps.py FAMILY`
- **Board not found**: Check `hw/bsp/FAMILY/boards/` for valid board names
- **objcopy errors**: Often non-critical in full builds, try individual example builds
#### Working with USB Device Classes
- **CDC (Serial)**: `src/class/cdc/` - Virtual serial port
- **HID**: `src/class/hid/` - Human Interface Device (keyboard, mouse, etc.)
- **MSC**: `src/class/msc/` - Mass Storage Class (USB drive)
- **Audio**: `src/class/audio/` - USB Audio Class
- Each class has device (`*_device.c`) and host (`*_host.c`) implementations
#### MCU Family Support
- **STM32**: Largest support (F0, F1, F2, F3, F4, F7, G0, G4, H7, L4, U5, etc.)
- **Raspberry Pi**: RP2040, RP2350 with PIO-USB host support
- **NXP**: iMXRT, Kinetis, LPC families
- **Microchip**: SAM D/E/G/L families
- Check `hw/bsp/` for complete list and `docs/reference/boards.rst` for details
### Code Style Guidelines
#### General Coding Standards
- Use C99 standard
- Memory-safe: no dynamic allocation
- Thread-safe: defer all interrupt events to non-ISR task functions
- 2-space indentation, no tabs
- Use snake_case for variables/functions
- Use UPPER_CASE for macros and constants
- Follow existing variable naming patterns in files you're modifying
- Include proper header comments with MIT license
- Add descriptive comments for non-obvious functions
#### Best Practices
- When including headers, group in order: C stdlib, tusb common, drivers, classes
- Always check return values from functions that can fail
- Use TU_ASSERT() for error checking with return statements
- Follow the existing code patterns in the files you're modifying
Remember: TinyUSB is designed for embedded systems - builds are fast, tests are focused, and the codebase is optimized for resource-constrained environments.
- Missing compiler → install `gcc-arm-none-eabi`.
- Missing deps → `python3 tools/get_deps.py FAMILY`.
- Unknown board → check `hw/bsp/FAMILY/boards/`.
- `objcopy` errors in full builds are often non-critical; retry the single example.

View File

@ -1,15 +1,24 @@
#!/bin/bash
#!/usr/bin/env bash
# Run HIL test remotely on ci.lan
# Usage: test/hil/hil_ci.sh [-b BOARD] [-t TEST] [extra hil_test.py args...]
# Example:
# test/hil/hil_ci.sh -b stm32f723disco
# test/hil/hil_ci.sh -b stm32f723disco -t host/cdc_msc_hid -r 1
#
# Env overrides: REMOTE, REMOTE_DIR, CONFIG (path to HIL config json),
# ROOT_DIR (tinyusb checkout to test; defaults to the script's own checkout).
set -e
set -euo pipefail
REMOTE=ci.lan
REMOTE_DIR=/tmp/tinyusb-hil
SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
REMOTE=${REMOTE:-ci.lan}
REMOTE_DIR=${REMOTE_DIR:-/tmp/tinyusb-hil}
ROOT_DIR=${ROOT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}
CONFIG=${CONFIG:-$ROOT_DIR/test/hil/tinyusb.json}
[[ -f "$ROOT_DIR/test/hil/hil_test.py" && -d "$ROOT_DIR/examples" ]] || {
echo "error: $ROOT_DIR does not look like a tinyusb checkout" >&2
exit 1
}
# Parse -b BOARD from arguments to know which build to copy
BOARD=""
@ -17,6 +26,7 @@ ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-b)
[[ $# -ge 2 ]] || { echo "error: -b requires a BOARD argument" >&2; exit 1; }
BOARD="$2"
ARGS+=("$1" "$2")
shift 2
@ -28,42 +38,52 @@ while [[ $# -gt 0 ]]; do
esac
done
# Setup remote directory
# Setup remote directory. Use `bash -s` + heredoc so REMOTE_DIR (user-overridable)
# is passed as a positional parameter and never reinterpreted by the remote shell.
echo "==> Setting up remote $REMOTE:$REMOTE_DIR"
ssh "$REMOTE" "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR/test/hil $REMOTE_DIR/examples"
ssh "$REMOTE" bash -s -- "$REMOTE_DIR" <<'REMOTE'
set -e
rm -rf -- "$1"
mkdir -p -- "$1/test/hil" "$1/examples"
REMOTE
# Copy HIL test script and config
echo "==> Copying test scripts"
scp -q "$SCRIPT_DIR/test/hil/hil_test.py" \
"$SCRIPT_DIR/test/hil/pymtp.py" \
"$SCRIPT_DIR/test/hil/tinyusb.json" \
scp -q "$ROOT_DIR/test/hil/hil_test.py" \
"$ROOT_DIR/test/hil/pymtp.py" \
"$CONFIG" \
"$REMOTE:$REMOTE_DIR/test/hil/"
# Copy only firmware binaries (elf/bin/hex), preserving directory structure
copy_board_binaries() {
local src="$1"
local board_name
board_name=$(basename "$src")
rsync -a --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \
rsync -a --prune-empty-dirs \
--include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \
"$src" "$REMOTE:$REMOTE_DIR/examples/"
}
if [ -n "$BOARD" ]; then
BUILD_DIR="$SCRIPT_DIR/examples/cmake-build-$BOARD"
BUILD_DIR="$ROOT_DIR/examples/cmake-build-$BOARD"
if [ ! -d "$BUILD_DIR" ]; then
echo "Error: build directory not found: $BUILD_DIR"
echo "Build first with: cd examples && cmake -DBOARD=$BOARD -G Ninja -B cmake-build-$BOARD .. && cmake --build cmake-build-$BOARD"
echo "Build first with: cd examples && cmake -DBOARD=$BOARD -G Ninja -B cmake-build-$BOARD . && cmake --build cmake-build-$BOARD"
exit 1
fi
echo "==> Copying binaries for $BOARD"
copy_board_binaries "$BUILD_DIR"
else
echo "==> Copying all built binaries"
for dir in "$SCRIPT_DIR"/examples/cmake-build-*/; do
for dir in "$ROOT_DIR"/examples/cmake-build-*/; do
[ -d "$dir" ] && copy_board_binaries "$dir"
done
fi
# Run test
# Run test. Use `bash -s` so REMOTE_DIR + ARGS reach the remote shell as positional
# parameters; quoting and metacharacters in args are preserved.
CONFIG_BASENAME="$(basename "$CONFIG")"
echo "==> Running HIL test on $REMOTE"
ssh -t "$REMOTE" "cd $REMOTE_DIR && python3 -u test/hil/hil_test.py -B examples ${ARGS[*]} tinyusb.json"
ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE'
cd -- "$1"
shift
exec python3 -u test/hil/hil_test.py -B examples "$@"
REMOTE

View File

@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""Build base branch (master) and current tree, then compare code size metrics.
Creates cmake-metrics/<board>/{base,build} directories for each board.
With --combined, also writes cmake-metrics/_combined/metrics_compare.md aggregating
all boards into a single comparison.
Usage:
python tools/metrics_compare_base.py -b raspberry_pi_pico
python tools/metrics_compare_base.py -b raspberry_pi_pico -b raspberry_pi_pico2
python tools/metrics_compare_base.py -b raspberry_pi_pico -f portable/raspberrypi
python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc
python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc --bloaty
python tools/metrics_compare_base.py --ci # first board of each arm-gcc family, combined
python tools/metrics_compare_base.py -b pico -b pico2 --combined # aggregate listed boards
"""
import argparse
import glob
import json
import os
import re
import shlex
import subprocess
import sys
TINYUSB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
METRICS_DIR = os.path.join(TINYUSB_ROOT, 'cmake-metrics')
def tinyusb_src_filter(checkout_dir):
"""Return a path-substring filter that uniquely matches TinyUSB stack source files
in `checkout_dir`. The substring is the absolute path to the checkout's `src/`
dir — collision-free with vendored deps (pico-sdk, lwip, FreeRTOS, etc.) which
live at unrelated paths."""
return os.path.realpath(os.path.join(checkout_dir, 'src')) + os.sep
verbose = False
def run(cmd, **kwargs):
"""Run a command. cmd must be a list (no shell=True). On `timeout=`-induced
TimeoutExpired, return a CompletedProcess with rc=124 instead of letting the
exception propagate, so the caller can fall through to error reporting and
worktree cleanup rather than crashing with a traceback."""
if not isinstance(cmd, list):
raise TypeError('run() requires a list, got str — fix the caller')
if verbose:
print(f' $ {" ".join(shlex.quote(str(c)) for c in cmd)}')
try:
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
except subprocess.TimeoutExpired as e:
msg = f'Command timed out after {e.timeout}s: {" ".join(shlex.quote(str(c)) for c in cmd)}'
stderr = (e.stderr or '') + ('\n' if e.stderr else '') + msg
return subprocess.CompletedProcess(cmd, 124, stdout=(e.stdout or ''), stderr=stderr)
def symlink_deps(main_root, worktree_dir):
"""Symlink dependency directories (fetched by tools/get_deps.py) from the main
checkout into the temporary worktree. Without this, the base build fails because
the worktree doesn't have the untracked deps."""
def link_subdirs(rel_parent):
src_parent = os.path.join(main_root, rel_parent)
dst_parent = os.path.join(worktree_dir, rel_parent)
if not os.path.isdir(src_parent):
return
os.makedirs(dst_parent, exist_ok=True)
for entry in os.listdir(src_parent):
src = os.path.join(src_parent, entry)
dst = os.path.join(dst_parent, entry)
if os.path.isdir(src) and not os.path.exists(dst):
os.symlink(src, dst)
# lib/* and tools/* deps (e.g. lib/lwip, tools/linkermap)
link_subdirs('lib')
link_subdirs('tools')
# hw/mcu/<vendor>/<dep> (e.g. hw/mcu/raspberry_pi/Pico-PIO-USB)
hw_mcu = os.path.join(main_root, 'hw', 'mcu')
if os.path.isdir(hw_mcu):
for vendor in os.listdir(hw_mcu):
link_subdirs(os.path.join('hw', 'mcu', vendor))
def ci_first_boards():
"""Return the first board (alphabetical) of each arm-gcc CI family."""
matrix_py = os.path.join(TINYUSB_ROOT, '.github', 'workflows', 'ci_set_matrix.py')
if not os.path.isfile(matrix_py):
return []
ret = run([sys.executable, matrix_py])
if ret.returncode != 0:
return []
try:
data = json.loads(ret.stdout)
except json.JSONDecodeError:
return []
families = data.get('arm-gcc', [])
boards = []
bsp_root = os.path.join(TINYUSB_ROOT, 'hw', 'bsp')
for family in families:
family_boards = sorted(
d for d in os.listdir(os.path.join(bsp_root, family, 'boards'))
if os.path.isdir(os.path.join(bsp_root, family, 'boards', d))
) if os.path.isdir(os.path.join(bsp_root, family, 'boards')) else []
if family_boards:
boards.append(family_boards[0])
return boards
def build_board(src_dir, build_dir, board, example=None):
"""Configure and build examples for a board. Returns True on success.
When `example` is given, only that target is built (`cmake --build --target NAME`),
keeping single-example workflows fast.
"""
os.makedirs(build_dir, exist_ok=True)
ret = run(['cmake', '-B', build_dir, '-G', 'Ninja',
f'-DBOARD={board}', '-DCMAKE_BUILD_TYPE=MinSizeRel',
os.path.join(src_dir, 'examples')])
if ret.returncode != 0:
print(f' Error configuring {board}: {ret.stderr}')
return False
cmd = ['cmake', '--build', build_dir]
if example:
cmd += ['--target', os.path.basename(example)]
ret = run(cmd, timeout=600)
if ret.returncode != 0:
print(f' Error building {board}: {ret.stderr}')
return False
return True
def generate_metrics(build_dir, out_basename, filters, example=None):
"""Run metrics.py combine on .map.json files. Returns metrics json path or None.
`filters` is a list of substrings; metrics.py keeps a compile unit if its path
contains any of them.
"""
if example:
patterns = glob.glob(f'{build_dir}/{example}/*.map.json')
else:
patterns = glob.glob(f'{build_dir}/**/*.map.json', recursive=True)
if not patterns:
print(f' Error: no .map.json files in {build_dir}' + (f' for {example}' if example else ''))
return None
metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py')
cmd = [sys.executable, metrics_py, 'combine']
for f in filters:
cmd += ['-f', f]
cmd += ['-j', '-q', '-o', out_basename, *patterns]
ret = run(cmd)
if ret.returncode != 0:
print(f' Error: {ret.stderr}')
return None
return f'{out_basename}.json'
def main():
global verbose
parser = argparse.ArgumentParser(description='Compare code size metrics with base branch')
parser.add_argument('-b', '--board', action='append', default=[],
help='Board name (repeatable). Required unless --ci is given.')
parser.add_argument('-f', '--filter', action='append', default=None,
help='Path-substring filter (repeatable). When given, '
'overrides the default and is applied to BOTH base and '
'current builds. Default: each side\'s own absolute '
'<checkout>/src/ path, which uniquely matches TinyUSB '
'stack code without colliding with vendored deps.')
parser.add_argument('--base-branch', default='master',
help='Base branch to compare against (default: master)')
parser.add_argument('-e', '--example', action='append', default=None,
help='Compare specific example (repeatable, e.g. -e device/cdc_msc -e host/cdc_msc_hid)')
parser.add_argument('--bloaty', action='store_true',
help='Use bloaty for detailed section/symbol diff (requires -e)')
parser.add_argument('--ci', action='store_true',
help='Add the first board of every arm-gcc CI family. Implies --combined.')
parser.add_argument('--combined', action='store_true',
help='Aggregate map.json files across all boards into one comparison '
'(in cmake-metrics/_combined/), instead of (or in addition to) per-board.')
parser.add_argument('-v', '--verbose', action='store_true',
help='Print build commands')
args = parser.parse_args()
verbose = args.verbose
if args.bloaty and not args.example:
parser.error('--bloaty requires -e/--example')
if args.ci:
args.combined = True
ci_boards = ci_first_boards()
if not ci_boards:
parser.error('--ci: failed to derive boards from .github/workflows/ci_set_matrix.py')
# Append, dedup, preserve order
seen = set(args.board)
for b in ci_boards:
if b not in seen:
args.board.append(b)
seen.add(b)
if not args.board:
parser.error('at least one -b BOARD is required (or pass --ci)')
metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py')
worktree_dir = os.path.join(METRICS_DIR, '_worktree')
# Per-side filters: when no override is given, each build uses its own
# absolute <checkout>/src/ path so we only match TinyUSB stack code from that
# checkout (and never vendored-dep `src/` like pico-sdk/src/...).
if args.filter:
base_filters = cur_filters = list(args.filter)
else:
base_filters = [tinyusb_src_filter(worktree_dir)]
cur_filters = [tinyusb_src_filter(TINYUSB_ROOT)]
# Step 1: Create worktree for base branch
print(f'[1/5] Setting up {args.base_branch} worktree...')
if os.path.isdir(worktree_dir):
run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir])
# --detach: check out the ref at a detached HEAD instead of trying to claim the
# branch. Lets us add a worktree of `master` even if master is already checked
# out elsewhere (main repo, another worktree).
ret = run(['git', '-C', TINYUSB_ROOT, 'worktree', 'add', '--detach',
worktree_dir, args.base_branch])
if ret.returncode != 0:
print(f'Error creating worktree: {ret.stderr}')
sys.exit(1)
# Symlink dependency dirs (lib/*, hw/mcu/*/*, tools/*) so the worktree builds.
symlink_deps(TINYUSB_ROOT, worktree_dir)
try:
examples = args.example or [None]
# For --combined: track every (base_build, cur_build) pair so we can aggregate at the end.
built_pairs = []
for board in args.board:
print(f'\n=== {board} ===')
board_dir = os.path.join(METRICS_DIR, board)
base_build = os.path.join(board_dir, 'base')
cur_build = os.path.join(board_dir, 'build')
# Build only the requested examples (or all if -e not given). Single-example
# mode used to build everything and filter at metric time — that was wasted work.
board_failed = False
for example in examples:
build_label = f' --target {os.path.basename(example)}' if example else ''
print(f'[2/5] Building {args.base_branch} for {board}{build_label}...')
if not build_board(worktree_dir, base_build, board, example):
board_failed = True
break
print(f'[3/5] Building current for {board}{build_label}...')
if not build_board(TINYUSB_ROOT, cur_build, board, example):
board_failed = True
break
if board_failed:
continue
built_pairs.append((board, base_build, cur_build))
for example in examples:
suffix = f'_{example.replace("/", "_")}' if example else ''
label = f' ({example})' if example else ''
# Step 4: Generate metrics
print(f'[4/5] Generating metrics for {board}{label}...')
base_json = generate_metrics(base_build, os.path.join(board_dir, f'base_metrics{suffix}'),
base_filters, example)
cur_json = generate_metrics(cur_build, os.path.join(board_dir, f'build_metrics{suffix}'),
cur_filters, example)
if not base_json or not cur_json:
continue
# Step 5: Compare
out_base = os.path.join(board_dir, f'metrics_compare{suffix}')
print(f'[5/5] Comparing {board}{label}...')
ret = run([sys.executable, metrics_py, 'compare', '-m', '-o', out_base, base_json, cur_json])
print(ret.stdout)
# Optional: bloaty diff
if args.bloaty and example:
elf_name = os.path.basename(example)
base_elf = os.path.join(base_build, example, f'{elf_name}.elf')
cur_elf = os.path.join(cur_build, example, f'{elf_name}.elf')
if os.path.exists(base_elf) and os.path.exists(cur_elf):
# Bloaty expects one regex; OR-join all filters (current side
# for the new ELF, base side for the base ELF).
bloaty_regex = '(' + '|'.join(
re.escape(f) for f in (cur_filters + base_filters)
) + ')'
bloaty_common = ['bloaty', '--domain=vm', f'--source-filter={bloaty_regex}']
print(f'--- bloaty sections ---')
ret = run(bloaty_common + ['-d', 'compileunits,sections', cur_elf, '--', base_elf])
print(ret.stdout)
print(f'--- bloaty symbols ---')
ret = run(bloaty_common + ['-d', 'compileunits,symbols', '-s', 'vm',
cur_elf, '--', base_elf])
print(ret.stdout)
else:
print(f' bloaty: ELF not found')
# Optional combined comparison across all boards.
# Aggregates the per-board metrics JSONs (not raw map.json globs) so the argv
# stays small even with --ci spanning many boards.
if args.combined and built_pairs:
combined_dir = os.path.join(METRICS_DIR, '_combined')
os.makedirs(combined_dir, exist_ok=True)
# Use the no-suffix per-board JSONs (whole-board metrics). Combined mode
# is meant for board-level sweeps; -e/--example combinations skip combined.
base_jsons, cur_jsons = [], []
for board, _, _ in built_pairs:
bj = os.path.join(METRICS_DIR, board, 'base_metrics.json')
cj = os.path.join(METRICS_DIR, board, 'build_metrics.json')
if os.path.isfile(bj) and os.path.isfile(cj):
base_jsons.append(bj)
cur_jsons.append(cj)
if not base_jsons or not cur_jsons:
print(' combined: no per-board metrics found (did you pass -e? skip --combined with -e)')
else:
print(f'\n=== combined ({len(base_jsons)} boards) ===')
base_out = os.path.join(combined_dir, 'base_metrics')
cur_out = os.path.join(combined_dir, 'build_metrics')
# Per-board JSONs are already filtered to TinyUSB-only files; combine
# without re-filtering so we don't accidentally drop entries.
def _combine(out_basename, inputs):
cmd = [sys.executable, metrics_py, 'combine',
'-j', '-q', '-o', out_basename, *inputs]
return run(cmd)
ret = _combine(base_out, base_jsons)
if ret.returncode != 0:
print(f' combined base error: {ret.stderr}')
else:
ret = _combine(cur_out, cur_jsons)
if ret.returncode != 0:
print(f' combined current error: {ret.stderr}')
else:
out_combined = os.path.join(combined_dir, 'metrics_compare')
ret = run([sys.executable, metrics_py, 'compare', '-m',
'-o', out_combined, f'{base_out}.json', f'{cur_out}.json'])
print(ret.stdout)
print(f' combined report: {out_combined}.md')
finally:
print(f'\nCleaning up worktree...')
run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir])
if __name__ == '__main__':
main()