diff options
| author | hathach <[email protected]> | 2026-07-13 17:53:34 +0700 |
|---|---|---|
| committer | hathach <[email protected]> | 2026-07-14 13:22:48 +0700 |
| commit | e02f93158cc602ba6f20945a187172abf2546718 (patch) | |
| tree | 50355a1465a29e14accbb35ac566fb07a82ae684 /tools/check_example_pids.py | |
| parent | 24f8bce0bc4a07a69f242ff1e790da90719e984d (diff) | |
test/hil: usbtest fleet enablement, shuffled scheduling, unique PIDs
Pool/config:
- record real uids (ra8m1_ek), enable usbtest for espressif s3/p4, then
park ra6m5_ek and ra8m1_ek in boards-skip (ra6m5's usbtest/MSC traffic
can kill the uPD720201 host on its ROM firmware; ra8m1 USBHS bring-up
pending); max32666/nrf54lm20 stay enabled - their MosChip flakiness
never wedges
- re-enable device/usbtest on HS boards (mimxrt1064, ch32v307) now that
uPD720201 firmware 2.0.2.6 fixes the command-ring death; mimxrt1015
stays skipped - its HS battery killed the controller on both ROM and
2.0.2.6 firmware (board-specific); match the moved host-test bundles
(f723 <-> rt1064); skip never-passing tests on the new
nrf5340dk/nrf54lm20dk boards and the detached pico host bundle, each
documented with a comment
Host-controller quirk gating in usbtest.py (auto-skip, self-heals on a
healthy xHCI):
- MosChip MCS9990 EHCI: case 25 (int-OUT never scheduled, FRINDEX bug)
and case 11 (unlinked reads complete short/EREMOTEIO)
- Renesas uPD720201 xHCI: firmware-gated. The card must run firmware
>= 2.0.2.6 (RAM-uploaded - it reverts to ROM on every power cycle):
on older firmware the command ring dies under unlink stress (a
Configure Endpoint command stops completing; the hub worker deadlocks
holding the device lock; only a host power cycle recovers; three
boards reproduced it). usbtest.py reads the FW version register (PCI
config 0x6c) and refuses to run at all on older firmware - hil_test
surfaces that as a failed test with the reason. On current firmware
the full 30-case battery runs (validated FS+HS: metro_m4, f723,
f723-DMA all 30/30).
Scheduling (hil_test.py):
- Shuffle each (board, variant)'s test order with a seeded RNG
(HIL_SHUFFLE_SEED to replay) so usbtest batteries and flash churn spread
across the timeline instead of convoying on one controller.
- Per-controller usbtest + flash semaphores: HIL_USBTEST_PARALLEL
(default 4) concurrent usbtest batteries and HIL_FLASH_PARALLEL
(default 8) concurrent flashes per host controller. Profiled on
uPD720201 firmware 2.0.2.6 across 8/1..12/8: wall time falls
22.2/14.3/12.5/10.8 min at usbtest width 1/2/3/4 and plateaus there;
zero controller errors everywhere; first battery case failures
(leaf-hub bandwidth stretch) appear at 12/8, and flash width 12 only
amplifies flasher-hub contention flakes - so 8/4 is the optimum. A
separate battery-window flash throttle was profiled and dropped.
- Give every example a unique hardcoded USB PID (0x4001-0x4022, usbtest
keeps 0x4010) instead of the PID_MAP interface bitmap: different
examples now always re-enumerate back-to-back, even on boards whose
CPU reset does not drop D+ (WCH CH58x), so the EXAMPLE_PID table and
same-PID adjacency reordering in hil_test.py are gone; only the
variant-boundary same-example repeat needs a swap.
- Report matrix: stable columns with the metric-bearing tests pinned
first (usbtest, cdc_msc_throughput, msc_file_explorer[_freertos]),
the rest alphabetical.
Fail fast:
- enum wait budget 8 s on the first attempt, 4 s on retries; dfu waits
are deadline-based so dfu-util's own runtime counts against the
budget.
A device-absent failure now costs ~3-5x a passing test (20-30 s)
instead of 10-30x (47-150 s).
- CI runs hil_test with --retry 1 and no in-run second pass: a broken
fixture fails the job fast instead of holding the self-hosted runner
for hours and blocking other PRs' HIL jobs. hil_test still writes the
.skip sidecar, so a manual re-run attempt only retests what failed.
Review fixes (multi-agent adversarial review of this commit):
- tinyusb_win_usbser.inf: the PID rework moved five CDC examples onto
even PIDs the INF's odd-only DeviceList never matched (legacy-Windows
usbser binding) - appended 0x4006/4008/400a/4020/4022 to both lists.
- usbtest example: USBTEST_TIER is now overridable and the descriptors
and pumps are tier-conditional, so a board whose DCD cannot serve a
tier lowers it instead of skipping the whole example - RA2A1 (RUSB2
with no isochronous pipe) builds at tier 3 via its BOARD_ define; the
host battery follows the tier advertised in bcdDevice. Tier-4 output
verified byte-identical after the refactor.
- dynamic_configuration's second config derived USB_PID + 11 = 0x4018,
colliding with net_lwip_webserver - now USB_PID + 0x0100, outside the
per-example space. tools/check_example_pids.py (pre-commit hook)
enforces PID uniqueness incl. derived and literal idProduct values.
- usbtest.py firmware gate: matched by device ID (uPD720201/720202,
both use the 0x6c FW register), and an unreadable version (setpci
missing/denied) now refuses with its own message instead of
masquerading as "firmware 0x00000000"; noted the gate is necessary
but not sufficient (board-specific kills stay per-board skips).
- hil_test: deadline waits use time.monotonic(); multiprocessing
context pinned to fork (raw semaphores in Pool initargs); flash and
usbtest permits unified into one fail-closed, exception-safe
ctrl_permit (unknown controller takes every slot and logs a warning
instead of silently borrowing slot 0); an all-skipped battery
reports as skip, not "0/0" failure; slow-body polls (mtp, printer,
disk read) go through a shared deadline-based wait_until so their
bodies count against the enum budget; throughput's FS detection
compares serials case-insensitively like every other walk; a missing
MSC read-speed line now fails the host msc_file_explorer test
instead of passing with an empty metric.
Hardening:
- fail fast (15 s) when a driver-registry sysfs write blocks: a wedged
device otherwise turns every subsequent battery into an unkillable
D-state writer and silently hangs the whole run
- usb-recover skill: a VM reboot is not a reliable cure (MosChip hubs
latch up across the PCIe reset); full host power cycle is
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg
Diffstat (limited to 'tools/check_example_pids.py')
| -rw-r--r-- | tools/check_example_pids.py | 55 |
1 files changed, 55 insertions, 0 deletions
diff --git a/tools/check_example_pids.py b/tools/check_example_pids.py new file mode 100644 index 000000000..d8795af34 --- /dev/null +++ b/tools/check_example_pids.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Check that every example enumerates with a unique USB PID. + +Each example's usb_descriptors.c hardcodes its idProduct (0x40xx). Uniqueness is what +guarantees back-to-back re-enumeration on the HIL rig and a fresh host driver match, and +it is easy to break by hand: a new example copying a neighbour's PID, or an arithmetic +PID (dynamic_configuration derives a second one from USB_PID). This collects every +`#define USB_PID 0x....`, every literal `.idProduct = 0x....`, and every `USB_PID + <n>` +derivation across examples/, and fails on any duplicate value. +""" + +import re +import sys +from pathlib import Path + +EXAMPLES = Path(__file__).resolve().parents[1] / 'examples' + +RE_DEFINE = re.compile(r'#define\s+USB_PID\s+\(?(0x[0-9a-fA-F]+)\)?') +RE_LITERAL = re.compile(r'\.idProduct\s*=\s*(0x[0-9a-fA-F]+)') +RE_DERIVED = re.compile(r'\.idProduct\s*=\s*USB_PID\s*\+\s*(0x[0-9a-fA-F]+|\d+)') + + +def main() -> int: + pids: dict[int, list[str]] = {} + for f in sorted(EXAMPLES.glob('*/*/src/usb_descriptors.c')): + text = f.read_text(errors='replace') + rel = f.relative_to(EXAMPLES.parent) + base = None + m = RE_DEFINE.search(text) + if m: + base = int(m.group(1), 16) + for m in RE_LITERAL.finditer(text): + pids.setdefault(int(m.group(1), 16), []).append(str(rel)) + for m in RE_DERIVED.finditer(text): + if base is None: + print(f'{rel}: derived idProduct but no USB_PID define', file=sys.stderr) + return 1 + pids.setdefault(base + int(m.group(1), 0), []).append(f'{rel} (USB_PID + {m.group(1)})') + # examples whose descriptor uses .idProduct = USB_PID pick up the define itself + if base is not None and re.search(r'\.idProduct\s*=\s*USB_PID\s*[,;]', text): + pids.setdefault(base, []).append(str(rel)) + + dups = {pid: users for pid, users in pids.items() if len(users) > 1} + for pid, users in sorted(dups.items()): + print(f'duplicate USB PID 0x{pid:04x}:', file=sys.stderr) + for u in users: + print(f' {u}', file=sys.stderr) + if dups: + return 1 + print(f'{len(pids)} unique example USB PIDs') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) |
