diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index d61ec05e4f..0d3abf1ac1 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -11,9 +11,15 @@ Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it tells you |------|--------------|------------------------| | `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 | +| `hifiphile` (external rig) | `test/hil/hfp.json` | no outbound SSH to htpc/ci; SSH-reachable FROM both | Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`. +The `hifiphile` rig is externally hosted by TinyUSB maintainer hifiphile; its board pool is +`test/hil/hfp.json` and its HIL runs are triggered by GitHub CI (the `hil-tinyusb (hfp.json)` +matrix job). **Never run HIL against this rig during development unless the user explicitly +asks for it.** + ## Board locks — the CI runner keeps running The `ci` rig also hosts a GitHub Actions runner that flashes boards and runs HIL as part of CI. Hardware access is arbitrated **per board** with kernel flocks in `/tmp/tinyusb-hil-locks/` — do NOT stop the runner service. @@ -32,7 +38,6 @@ python3 test/hil/board_lock.py release BOARD [BOARD...] - Rig-wide operations (uhubctl power cycling, pci-rebind — bus renumbering) affect every board: `board_lock.py hold --all --reason "..."` first. - `board_lock.py status` lists holders. Locks auto-release when the holder process dies (kernel flock); `/tmp` clears on reboot. - Forcing past a lock: `HIL_NO_BOARD_LOCK=1 python3 test/hil/hil_test.py ...` bypasses the guard without killing the holder. Only with the user's explicit go-ahead — they accept the risk of colliding with whatever holds the board. -- Caveat until this branch merges to master: CI's checkout of `hil_test.py` does not yet enforce locks — keep dev hardware sessions short and check `gh run list --status in_progress` first. ## Prerequisites diff --git a/.claude/skills/usb-debug/SKILL.md b/.claude/skills/usb-debug/SKILL.md new file mode 100644 index 0000000000..20ab7d7640 --- /dev/null +++ b/.claude/skills/usb-debug/SKILL.md @@ -0,0 +1,36 @@ +--- +name: usb-debug +description: Use when USB enumeration fails or misbehaves and usbmon alone can't explain WHY the host acted — port reset storms, repeated re-enumeration, address errors, xHCI ring/command errors, "device descriptor read error", babble, or when you need the host driver's own reasoning from dmesg on the ci HIL rig. +--- + +# usb-debug — host-side kernel dynamic debug for USB + +usbmon shows the URBs; kernel **dynamic debug** shows the host driver's +*reasoning* usbmon can't: port resets and their causes, enumeration retries, +address (re)assignment, EP halts, xHCI ring/command errors. + +Run this skill's `scripts/usb_dyndbg.sh` with `sudo` (abbreviated to +`usb_dyndbg.sh` in the examples below). It flips the dynamic-debug print flag +for an allowlisted set of USB host modules only: + +```bash +sudo usb_dyndbg.sh on usbcore xhci_hcd # enable +p; pick modules from `lsusb -t` Driver= +sudo usb_dyndbg.sh status [module] # list enabled print sites +sudo usb_dyndbg.sh off usbcore xhci_hcd # ALWAYS turn off when done — very noisy +``` + +Allowlisted modules: `usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd +ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 cdc_acm usb_storage uas`. + +## Workflow + +1. `sudo usb_dyndbg.sh on usbcore ` — `usbcore` for enumeration/hub + logic, plus the controller module (`lsusb -t` shows the driver per bus). +2. Reproduce (replug / re-enumerate / rerun the failing test) while following + `sudo dmesg -w` (or grab `sudo dmesg | tail` afterwards). +3. `sudo usb_dyndbg.sh off ...` — leaving it on floods the log and skews timing. + +Pair with the `usbmon` skill: usbmon for what crossed the bus, dynamic debug for +why the host reacted. For a wedged device/bus use the `usb-recover` skill. + +Requires `CONFIG_DYNAMIC_DEBUG` and mounted debugfs (standard on distro kernels). diff --git a/.claude/skills/usb-debug/scripts/usb_dyndbg.sh b/.claude/skills/usb-debug/scripts/usb_dyndbg.sh new file mode 100755 index 0000000000..0dc8804696 --- /dev/null +++ b/.claude/skills/usb-debug/scripts/usb_dyndbg.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# usb_dyndbg.sh — toggle kernel dynamic-debug on USB host drivers; run with sudo. +# Flips +p/-p only on an allowlisted set of USB modules, so it can't reach +# arbitrary kernel debug or unrelated subsystems. +# +# Usage: +# sudo usb_dyndbg.sh on ... # enable +p (e.g. usbcore xhci_hcd) +# sudo usb_dyndbg.sh off ... # disable -p +# sudo usb_dyndbg.sh status [module] # show enabled sites (or one module's sites) +set -euo pipefail + +CTL=/sys/kernel/debug/dynamic_debug/control +# Allowlist: USB host-controller + core + common host class drivers. +ALLOW='usbcore xhci_hcd xhci_pci xhci_pci_renesas ehci_hcd ehci_pci ohci_hcd ohci_pci uhci_hcd dwc2 cdc_acm usb_storage uas' + +die() { echo "usb_dyndbg: $*" >&2; exit 1; } +usage() { + echo "usage: usb_dyndbg.sh {on|off} ... modules: $ALLOW" >&2 + echo " usb_dyndbg.sh status [module]" >&2 + exit 2 +} +allowed() { local m; for m in $ALLOW; do [ "$m" = "$1" ] && return 0; done; return 1; } + +[ -e "$CTL" ] || die "dynamic_debug unavailable (need CONFIG_DYNAMIC_DEBUG + debugfs mounted)" + +action=${1:-}; shift || true +case "$action" in + on|off) + [ "$#" -ge 1 ] || usage + flag='+p'; [ "$action" = off ] && flag='-p' + for m in "$@"; do allowed "$m" || die "module not allowlisted: $m"; done + for m in "$@"; do echo "module $m $flag" > "$CTL"; echo "dynamic debug $action: $m"; done + ;; + status) + m=${1:-} + if [ -n "$m" ]; then + allowed "$m" || die "module not allowlisted: $m" + grep -E "\[$m\]" "$CTL" || echo "(no sites for $m)" + else + grep -E '=p( |$)' "$CTL" || echo "(no print sites enabled)" + fi + ;; + *) + usage + ;; +esac diff --git a/.claude/skills/usb-recover/SKILL.md b/.claude/skills/usb-recover/SKILL.md new file mode 100644 index 0000000000..3f72b7e215 --- /dev/null +++ b/.claude/skills/usb-recover/SKILL.md @@ -0,0 +1,93 @@ +--- +name: usb-recover +description: Use when a USB device or fixture on the ci HIL rig is stuck, hung, not enumerating, or wedged after a failed flash or test, or when processes touching USB (testusb, JLinkExe, uhubctl, libusb tools) start hanging in D state. +--- + +# USB Recovery on the HIL Rig + +Run this skill's `scripts/usb_recover.sh` with `sudo` (abbreviated to +`usb_recover.sh` in the examples below). It wraps four sysfs reset actions plus +a resolver: + +```bash +sudo usb_recover.sh resolve /dev/ttyACM3 # /dev node -> busport (e.g. 3-4.7); also ttyUSB*, sg* +sudo usb_recover.sh authorized # deauthorize+reauthorize: re-enumerate, no VBUS cut +sudo usb_recover.sh rebind # usb driver unbind+bind: re-probe +sudo usb_recover.sh pci-rebind # whole HCD controller unbind+bind, e.g. 0000:02:00.0 +sudo usb_recover.sh pci-reset # PCI function-level reset: kills URBs at HW level, no device lock +sudo usb_recover.sh pci-bind [drv] # re-bind a DRIVERLESS controller (auto-tries xHCI drivers) +``` + +## Decide first: is anything stuck in D state? + +```bash +ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /D/' +``` + +**If yes** (uninterruptible sleep, typically a usbfs ioctl — e.g. testusb inside +`usb_sg_wait`): run `pci-reset` and NOTHING ELSE first: + +```bash +sudo usb_recover.sh pci-reset +``` + +FLR kills the URBs at the hardware level without taking the per-device lock; +the ioctl then returns and the convoy unwinds on its own. + +**Not every controller supports FLR.** The Renesas uPD720201 (`0000:01:00.0`) +has no reset method — `pci-reset` fails with `Inappropriate ioctl for device` +(ENOTTY). On those, there is no clean software D-state cure — a VM reboot is NOT +reliable (the MosChip downstream hubs latch up across the PCIe reset and need a +physical replug); ask the operator for a full PVE host power cycle instead. Do NOT +fall through to `pci-rebind` (see next). + +**`pci-rebind` can strand the controller driverless.** Its unbind succeeds but, +with a D-state process still holding a URB, the *re-bind* hangs — leaving the +PCI device with **no driver** (`/sys/bus/pci/devices//driver` gone) and the +whole controller's fixtures offline. A second `pci-rebind` then dies with "no +driver bound". Recover with `pci-bind ` (re-attaches the xHCI driver); +if that also hangs because the D-state URB is unkillable, only a full PVE host +power cycle (operator action) recovers. The Renesas binds via `xhci-pci-renesas` (firmware loader), others via +`xhci_hcd` — `pci-bind` auto-tries both, or pass the driver explicitly. + +**Ordering is critical.** `authorized`/`rebind`/`pci-rebind` all take the +per-device lock the stuck ioctl holds — they block and join the convoy, and +soon every libusb tool (uhubctl, JLinkExe) hangs too. Worse, a blocked +`pci-rebind` grabs the PCI device lock on its way in, which `pci-reset` also +needs: once a rebind has been attempted and is stuck, even FLR deadlocks and +**only a full PVE host power cycle recovers**. pci-reset first (if supported), and never +`pci-rebind` a D-state wedge. + +**If no** (device merely dead or silent), escalate gently: + +1. `authorized ` — re-enumerates just that device +2. `rebind ` — re-probe; also worth trying on the parent hub's busport +3. `pci-rebind ` — last resort: bounces every fixture on that controller + +## Finding targets + +```bash +grep -l /sys/bus/usb/devices/*/serial # serial -> busport (dir name) +readlink -f /sys/bus/usb/devices/usb # bus N -> its PCI addr in the path +``` + +Rig layout: buses 3+4 = `0000:02:00.0` (main fixture tree: J-Links, ST-Links, +WCH-Links, DUTs); buses 9+12 = `0000:01:00.0`, the only ones with uhubctl port +power (ganged VBUS: `sudo uhubctl -l 9 -a cycle`). Hubs on buses 1-4 have no +port power switching — uhubctl reports "No compatible devices" there. + +## Common mistakes + +- `resolve` takes a **/dev node**, not a busport or serial ("no such device node"). +- `authorized`/`rebind` take a **busport** (`3-4.7`); `pci-rebind`/`pci-reset` + take a **PCI addr**. +- Command produces no output and doesn't return → it is blocked on the device + lock: a D-state holder exists; see above. +- Trying `pci-rebind` on a D-state hang — its re-bind hangs and strands the + controller **driverless**; recover with `pci-bind `, or a PVE host power + cycle if the D-state URB is unkillable. Use `pci-reset` (if supported) for D-state, never + `pci-rebind`. +- Running `pci-reset` on a controller without FLR support (Renesas) → ENOTTY; + no software recovery — needs a PVE host power cycle. +- A J-Link reset (`r; go`) does not disconnect a wedged DUT from the host: the + DWC2 soft-connect pullup stays up through a core halt, so stuck URBs stay stuck. diff --git a/.claude/skills/usb-recover/scripts/usb_recover.sh b/.claude/skills/usb-recover/scripts/usb_recover.sh new file mode 100755 index 0000000000..35bd4c7849 --- /dev/null +++ b/.claude/skills/usb-recover/scripts/usb_recover.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# usb_recover.sh — USB recovery helper for the HIL rig; run with sudo. Writes only +# to the specific sysfs control files below; arg regexes block path traversal. +# +# Usage: +# sudo usb_recover.sh authorized # e.g. 3-2 -> deauthorize+reauthorize (re-enumerate, NO VBUS cut) +# sudo usb_recover.sh rebind # e.g. 3-2 -> usb driver unbind+bind (re-probe) +# sudo usb_recover.sh pci-rebind # e.g. 0000:01:00.0 -> HCD unbind+bind (WHOLE controller) +# sudo usb_recover.sh pci-reset # e.g. 0000:01:00.0 -> PCI function-level reset: kills URBs at +# # HW level WITHOUT the device lock; the only cure when a process +# # is stuck in D state (usbfs ioctl) and unbind paths would convoy +# sudo usb_recover.sh pci-bind [driver] # bind a DRIVERLESS controller (e.g. after a pci-rebind +# # whose re-bind hung and left it unbound). Auto-tries the xHCI +# # drivers (xhci-pci-renesas, xhci_hcd) unless one is named. +# sudo usb_recover.sh resolve # e.g. /dev/ttyACM3 -> print its (no privilege needed) +set -euo pipefail + +USBPATH_RE='^[0-9]+-[0-9]+(\.[0-9]+)*$' +PCI_RE='^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9]$' +DRIVER_RE='^[A-Za-z0-9_-]+$' + +die() { echo "usb_recover: $*" >&2; exit 1; } +usage() { grep -E '^# sudo usb_recover' "$0" >&2; exit 2; } + +# Refuse to touch a PCI function that is not a USB controller (class 0x0c03xx), so a stray or +# mistyped BDF can't unbind/reset an unrelated device (storage, NIC) on a shared HIL host. +require_usb_controller() { + local addr=$1 cls + cls=$(cat "/sys/bus/pci/devices/$addr/class" 2>/dev/null) || die "no such pci device: $addr" + [[ "$cls" =~ ^0x0c03 ]] || die "$addr is not a USB controller (class $cls); refusing" +} + +# Resolve a /dev node (ttyACMx, ttyUSBx, sgN, ...) up to its USB device busport. +resolve() { + local node=$1 syspath dev + [ -e "$node" ] || die "no such device node: $node" + syspath=$(udevadm info -q path -n "$node" 2>/dev/null) || die "udevadm failed for $node" + dev="/sys$syspath" + while [ "$dev" != "/sys" ] && [ -n "$dev" ]; do + if [ -e "$dev/busnum" ] && [ -e "$dev/devnum" ] && [ -e "$dev/authorized" ]; then + basename "$dev"; return 0 + fi + dev=$(dirname "$dev") + done + die "could not find parent USB device for $node" +} + +action=${1:-}; target=${2:-} +[ -n "$action" ] && [ -n "$target" ] || usage + +case "$action" in + resolve) + resolve "$target" + ;; + authorized) + [[ "$target" =~ $USBPATH_RE ]] || die "bad usb path: $target" + d="/sys/bus/usb/devices/$target" + [ -e "$d/authorized" ] || die "no such usb device: $target" + echo 0 > "$d/authorized"; sleep 1; echo 1 > "$d/authorized" + echo "re-authorized $target" + ;; + rebind) + [[ "$target" =~ $USBPATH_RE ]] || die "bad usb path: $target" + [ -e "/sys/bus/usb/devices/$target" ] || die "no such usb device: $target" + echo "$target" > /sys/bus/usb/drivers/usb/unbind; sleep 1 + echo "$target" > /sys/bus/usb/drivers/usb/bind + echo "rebound $target" + ;; + pci-rebind) + [[ "$target" =~ $PCI_RE ]] || die "bad pci addr: $target" + require_usb_controller "$target" + [ -e "/sys/bus/pci/devices/$target/driver" ] || die "no driver bound to $target" + drv=$(basename "$(readlink -f "/sys/bus/pci/devices/$target/driver")") + echo "$target" > "/sys/bus/pci/drivers/$drv/unbind"; sleep 1 + echo "$target" > "/sys/bus/pci/drivers/$drv/bind" + echo "rebound pci $target ($drv)" + ;; + pci-bind) + # Re-attach a driver to a controller left DRIVERLESS (e.g. a pci-rebind whose re-bind hung). + [[ "$target" =~ $PCI_RE ]] || die "bad pci addr: $target" + require_usb_controller "$target" + [ -e "/sys/bus/pci/devices/$target" ] || die "no such pci device: $target" + [ -e "/sys/bus/pci/devices/$target/driver" ] && die "$target already has a driver bound" + drv=${3:-} + if [ -n "$drv" ]; then + [[ "$drv" =~ $DRIVER_RE ]] || die "bad driver name: $drv" + [ -e "/sys/bus/pci/drivers/$drv/bind" ] || die "no such pci driver: $drv" + echo "$target" > "/sys/bus/pci/drivers/$drv/bind" + echo "bound pci $target ($drv)" + else + # Auto-try the xHCI drivers (Renesas uPD720201 uses xhci-pci-renesas; others xhci_hcd). + for cand in xhci-pci-renesas xhci_hcd; do + [ -e "/sys/bus/pci/drivers/$cand/bind" ] || continue + if echo "$target" > "/sys/bus/pci/drivers/$cand/bind" 2>/dev/null; then + echo "bound pci $target ($cand)"; exit 0 + fi + done + die "could not bind $target with a known xHCI driver; pass the driver explicitly" + fi + ;; + pci-reset) + [[ "$target" =~ $PCI_RE ]] || die "bad pci addr: $target" + require_usb_controller "$target" + [ -e "/sys/bus/pci/devices/$target/reset" ] || die "no reset support on $target" + echo 1 > "/sys/bus/pci/devices/$target/reset" + echo "flr-reset pci $target" + ;; + *) + usage + ;; +esac diff --git a/.claude/skills/usbtest/SKILL.md b/.claude/skills/usbtest/SKILL.md new file mode 100644 index 0000000000..8146d94e44 --- /dev/null +++ b/.claude/skills/usbtest/SKILL.md @@ -0,0 +1,123 @@ +--- +name: usbtest +description: Use when running, debugging, or porting the Linux usbtest/testusb battery (examples/device/usbtest, cafe:4010) — device "did not bind", SET_CONFIGURATION fails, a case fails with errno 110/32/5/71, toggle-clear/halt/unlink/iso failures, iso packets dropped, or a new MCU/DCD needs the full 30/30 sign-off. +--- + +# usbtest — porting & debugging the Linux kernel USB battery + +## Overview + +`examples/device/usbtest` is the device-side peer of the Linux kernel's `usbtest.ko`/`testusb` +(gadget-zero source/sink protocol): 30 cases over bulk, EP0, interrupt, and isochronous, including +halt, data-toggle, and unlink storms. It is the most adversarial exerciser a DCD gets — every port +so far surfaced at least one real driver bug. Host runner: `test/hil/usbtest.py`; HIL integration +runs it per board and reports `✅ 30/30` cells. + +**Core principle: the battery is a DCD test, not a firmware test.** When a case fails, suspect the +DCD path it exercises (table below), reproduce that one case, and root-cause on hardware before +changing anything (`superpowers:systematic-debugging`). One variable at a time; a fix is proven by +the failing case passing *and* the full battery still at 30/30 across reflash cycles. + +## Run + +```bash +# build (cmake); descriptor sizes auto-adapt per MCU via src/usb_descriptors.h + src/tusb_config.h +cd examples/device/usbtest && cmake -B build -DBOARD= -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel && cmake --build build +# flash, wait ~3-5 s for enumeration to settle, then: +python3 test/hil/usbtest.py --serial --keep-binding # full battery for the advertised tier +python3 test/hil/usbtest.py --serial --keep-binding --tests 29 # one case +``` + +- **Always `--keep-binding`**: the cleanup unbind path has wedged host xHCIs (`usb_hcd_alloc_bandwidth`). +- Always settle a few seconds after flashing — enumeration can bounce once; testusb into the gap sees + the device drop mid-case. +- On a CI rig: stop the actions runner before touching hardware; restart after. Never run two + batteries concurrently (hil_test.py serializes them; concurrent batteries have hard-frozen a rig + via a fatal PCIe error on a VFIO-passed xHCI). + +## Porting ladder — new MCU/DCD to 30/30 + +1. **Tier 1 (bulk)**: set `USBTEST_TIER 1`, get enumeration + cases 0,9,10 (EP0) + 1–8,17–20,27,28 + solid. EP0 correctness first — everything else reports through it. +2. **Tier 2 (ctrl_out 14/21)**, **tier 3 (interrupt 25/26)**, **tier 4 (iso 15/16/22/23)** — raise + the tier only when the layer below is clean; run the *full* battery after each layer. +3. **Fit the endpoints**: tier 4 needs 6 endpoints + EP0. Small parts need per-MCU mps/epbuf + overrides in `src/usb_descriptors.h` (`USBTEST_INT/ISO_EP_MPS_FS`) and `src/tusb_config.h` + (`CFG_TUD_VENDOR_TX_EPSIZE`) — follow the existing CH32/LPC11 patterns. Parts that can't fit go + in `skip.txt`. +4. **Sign-off = reliability, not one pass**: 3–10 full flash→battery cycles. One 30/30 proves + nothing on a flaky bring-up; deterministic partial counts (e.g. exactly 1-in-8 lost) are a + signature, not noise — chase them. +5. Register the board in `test/hil/tinyusb.json` so the HIL suite runs it. + +## Case → DCD subsystem map + +| Failing case(s) | Exercises | First suspect | +|---|---|---| +| 9, 10 | EP0 control storms | EP0 state machine, ZLP/status stage, control starvation under load | +| 1–8, 17–20, 27, 28 | bulk source/sink, sg, perf | FIFO handling, multi-packet, ZLP tolerance | +| 11, 12, 24 | URB unlink mid-transfer | abort/close paths leaving state half-armed | +| 13 | set/clear halt | stall must kill the transfer; halt on armed IN must flush the TX FIFO | +| **29** | clear-halt on an **armed, un-halted** ep | **the classic**: `dcd_edpt_clear_stall` resets toggle but disarms the queued receive → NAKs forever, errno 110. Fix: reset toggle to DATA0 *and* re-arm/preserve the pending transfer. Found independently on rp2040, fsdev, ch32_usbhs, rusb2 | +| 14, 21 | vendor EP0 write/readback | multi-packet control-OUT chunking, DCP flow control | +| 25, 26 | interrupt src/sink | usually free once bulk works | +| 15, 16, 22, 23 | isochronous | see iso rules below | + +## Iso rules (most-violated contract) + +- **DATA0-only in BOTH directions** at FS — never run bulk-style toggle logic on an iso endpoint + (manual-toggle parts: skip the ISR toggle flip for iso IN *and* the toggle-mismatch drop for iso + OUT). Symptom of violating it: exactly every-other packet lost. +- **No handshake** — iso never NAKs/STALLs; parts with response fields use their "no response" + encoding (e.g. NYET on WCH). +- `dcd_edpt_iso_alloc`/`iso_activate` **must not be stubs returning false** — usbd fails the + interface open and the kernel logs "did not bind"/SET_CONFIG times out. If a DCD refuses iso + "because the hardware can't", **verify against the datasheet — the manual outranks the code + comment** (two "no iso support" claims in this tree were false, incl. a per-endpoint exception + the RM documents for one endpoint number only). +- A multi-packet iso IN submit is legal: the DCD streams it one packet per frame, refilling in the + ISR. Slow cores may need double-buffered iso to make the frame deadline. + +## Debug ladder (escalate in order) + +| errno | Meaning | +|---|---| +| 110 | timeout — endpoint NAKing forever / device wedged | +| 32 | EPIPE — unexpected STALL | +| 5 | EIO — iso packet errors (check `dmesg`: "N errors out of M") | +| 71 | EPROTO — device answered wrong / too slow (after HC retries) | + +1. `usbtest.py` per-case output + its captured `dmesg` (`TEST n` markers bracket each case). +2. **usbmon** (`usbmon` skill): URB-level ground truth. **It cannot show data toggles or NAKs** — + a toggle desync and a dead endpoint look identical (Submits without Completes); distinguish + device-side with GDB. +3. **On-device gdb/openocd**: read the EP control registers and DCD structs at the hang. +4. Heisenbugs (vanish under logging): RAM ring-buffer trace dumped over openocd; for silent lockups + JLink PC-sampling (`halt`+`regs` repeatedly — a pinned PC names the spin). +5. **Cross-check the reference manual** (calibre library) before changing any register-level code — + per CLAUDE.md, and because comments/assumptions in DCDs have been wrong about hardware caps. +6. Check the vendor's **silicon errata** early for timing/DMA hangs (an unimplemented erratum + workaround caused a case-10 hang on one port). + +## Traps that pass gcc/desk review but fail elsewhere + +- `TUD_OPT_HIGH_SPEED` is a **compile-time capability, not the live speed**: the FS config + descriptor (and OTHER_SPEED) must use FS-legal sizes (int ≤ 64, iso IN+OUT ≤ 1023 B/frame) even + on HS builds — use separate `_FS`/`_HS` descriptor macros. +- Unused `static inline` helpers: clang `-Wunused-function` and IAR `Pe177` error where gcc stays + quiet → `TU_ATTR_UNUSED`. +- A symbol referenced only inside naked asm is invisible to LTO and gets dropped in `-flto` make + builds → keep a `TU_ATTR_USED` C reference to it. +- Nested USB IRQs on cores with hardware context stacks (QingKe HWSTK): plain + `__attribute__((interrupt))` corrupts the return — use naked handlers relying on the HW stack. +- Dedicated USB RAM budgets (PMA/USB-RAM) differ per part *and* per build system section placement: + check the link map, not just that it builds. + +## Red flags — stop and re-examine + +- "One pass = done" → run reflash cycles. +- "The DCD comment says the hardware can't" → open the datasheet. +- "usbmon shows no toggle problem" → usbmon can't see toggles. +- "It works on gcc" → clang/IAR/LTO/make still pending. +- "Fixed iso IN" → apply the same exemption to iso OUT (toggle logic is symmetric). +- A clean single-board run does not validate concurrent/fleet behavior — batteries serialize. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8c597e503..f24f3ae1f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -333,15 +333,11 @@ jobs: merge-multiple: true - name: Test on actual hardware - run: | - python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS || \ - (if [ -f "${{ env.HIL_JSON }}.skip" ]; then - SKIP_BOARDS=$(cat "${{ env.HIL_JSON }}.skip") - echo "Re-running with SKIP_BOARDS=$SKIP_BOARDS" - python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS - else - exit 1 - fi) + # Single attempt per test (--retry 1), no in-run second pass: a broken fixture + # fails fast instead of holding the runner (and other PRs' HIL jobs) for hours. + # hil_test.py still writes ${HIL_JSON}.skip, so a manual re-run attempt only + # retests what failed (see "Get Skip Boards from previous run"). + run: python3 test/hil/hil_test.py --retry 1 ${{ env.HIL_JSON }} $SKIP_BOARDS - name: Upload HIL report if: always() && github.event_name == 'pull_request' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4a2972898..e87b935dd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,13 @@ repos: - repo: local hooks: + - id: unique-example-pids + name: unique example USB PIDs + files: usb_descriptors\.c$ + entry: python3 tools/check_example_pids.py + pass_filenames: false + language: system + - id: unit-test name: unit-test files: ^(src/|test/unit-test/) diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 1432b36bb3..02fcc0b873 100644 --- a/examples/device/CMakeLists.txt +++ b/examples/device/CMakeLists.txt @@ -35,6 +35,7 @@ set(EXAMPLE_LIST printer_to_cdc uac2_headset uac2_speaker_fb + usbtest usbtmc video_capture video_capture_2ch diff --git a/examples/device/audio_4_channel_mic/src/usb_descriptors.c b/examples/device/audio_4_channel_mic/src/usb_descriptors.c index 6b9a9bbae0..42da9442ca 100644 --- a/examples/device/audio_4_channel_mic/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4001 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c index 216cd062a5..0afb3df0a2 100644 --- a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4002 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test/src/usb_descriptors.c b/examples/device/audio_test/src/usb_descriptors.c index cea4eb8d15..8c25fc2901 100644 --- a/examples/device/audio_test/src/usb_descriptors.c +++ b/examples/device/audio_test/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4003 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test_freertos/src/usb_descriptors.c b/examples/device/audio_test_freertos/src/usb_descriptors.c index 37ebf84d32..709425c49e 100644 --- a/examples/device/audio_test_freertos/src/usb_descriptors.c +++ b/examples/device/audio_test_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4004 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test_multi_rate/src/usb_descriptors.c b/examples/device/audio_test_multi_rate/src/usb_descriptors.c index b1f60dd10b..008c8cd761 100644 --- a/examples/device/audio_test_multi_rate/src/usb_descriptors.c +++ b/examples/device/audio_test_multi_rate/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4005 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/cdc_dual_ports/src/usb_descriptors.c b/examples/device/cdc_dual_ports/src/usb_descriptors.c index adfd8cf9db..779221c0c2 100644 --- a/examples/device/cdc_dual_ports/src/usb_descriptors.c +++ b/examples/device/cdc_dual_ports/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4006 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc/src/usb_descriptors.c b/examples/device/cdc_msc/src/usb_descriptors.c index 5dc80dee37..140ef21405 100644 --- a/examples/device/cdc_msc/src/usb_descriptors.c +++ b/examples/device/cdc_msc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4007 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc_freertos/src/usb_descriptors.c b/examples/device/cdc_msc_freertos/src/usb_descriptors.c index f5b015051c..8398f03651 100644 --- a/examples/device/cdc_msc_freertos/src/usb_descriptors.c +++ b/examples/device/cdc_msc_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4008 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc_throughput/src/usb_descriptors.c b/examples/device/cdc_msc_throughput/src/usb_descriptors.c index 3b0ff6e17a..ba0b0a26f3 100644 --- a/examples/device/cdc_msc_throughput/src/usb_descriptors.c +++ b/examples/device/cdc_msc_throughput/src/usb_descriptors.c @@ -26,7 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -#define USB_PID (0x4000 | ((CFG_TUD_CDC) ? (1 << 0) : 0) | ((CFG_TUD_MSC) ? (1 << 1) : 0)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4009 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_uac2/src/usb_descriptors.c b/examples/device/cdc_uac2/src/usb_descriptors.c index fdffc761e0..9c1bbee479 100644 --- a/examples/device/cdc_uac2/src/usb_descriptors.c +++ b/examples/device/cdc_uac2/src/usb_descriptors.c @@ -29,15 +29,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400a //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dfu/src/usb_descriptors.c b/examples/device/dfu/src/usb_descriptors.c index e4291be2dd..5bfb32b2e4 100644 --- a/examples/device/dfu/src/usb_descriptors.c +++ b/examples/device/dfu/src/usb_descriptors.c @@ -26,14 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400b //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dfu_runtime/src/usb_descriptors.c b/examples/device/dfu_runtime/src/usb_descriptors.c index 8fa078da29..2735664146 100644 --- a/examples/device/dfu_runtime/src/usb_descriptors.c +++ b/examples/device/dfu_runtime/src/usb_descriptors.c @@ -26,14 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400c //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dynamic_configuration/src/usb_descriptors.c b/examples/device/dynamic_configuration/src/usb_descriptors.c index c4049414f7..838052ea11 100644 --- a/examples/device/dynamic_configuration/src/usb_descriptors.c +++ b/examples/device/dynamic_configuration/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400d // Configuration mode // 0 : enumerated as CDC/MIDI. Board button is not pressed when enumerating @@ -79,7 +72,7 @@ tusb_desc_device_t const desc_device_1 = .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = 0xCafe, - .idProduct = USB_PID + 11, // should be different PID than desc0 + .idProduct = USB_PID + 0x0100, // must differ from desc0's PID and stay outside the 0x40xx per-example space .bcdDevice = 0x0100, .iManufacturer = 0x01, diff --git a/examples/device/hid_boot_interface/src/usb_descriptors.c b/examples/device/hid_boot_interface/src/usb_descriptors.c index b5c31a94a9..4d5caa835a 100644 --- a/examples/device/hid_boot_interface/src/usb_descriptors.c +++ b/examples/device/hid_boot_interface/src/usb_descriptors.c @@ -27,14 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400e //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/hid_composite/src/usb_descriptors.c b/examples/device/hid_composite/src/usb_descriptors.c index 46e4b63f96..7f5f74c70b 100644 --- a/examples/device/hid_composite/src/usb_descriptors.c +++ b/examples/device/hid_composite/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400f #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/hid_composite_freertos/src/usb_descriptors.c b/examples/device/hid_composite_freertos/src/usb_descriptors.c index a745c17b52..a0464afaeb 100644 --- a/examples/device/hid_composite_freertos/src/usb_descriptors.c +++ b/examples/device/hid_composite_freertos/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4011 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/hid_generic_inout/src/usb_descriptors.c b/examples/device/hid_generic_inout/src/usb_descriptors.c index 93e7184611..f179b74f7c 100644 --- a/examples/device/hid_generic_inout/src/usb_descriptors.c +++ b/examples/device/hid_generic_inout/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4012 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/hid_multiple_interface/src/usb_descriptors.c b/examples/device/hid_multiple_interface/src/usb_descriptors.c index cd2d93c44e..90aef6dd7f 100644 --- a/examples/device/hid_multiple_interface/src/usb_descriptors.c +++ b/examples/device/hid_multiple_interface/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4013 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/midi_test/src/usb_descriptors.c b/examples/device/midi_test/src/usb_descriptors.c index 99c798ce1d..fc7228c350 100644 --- a/examples/device/midi_test/src/usb_descriptors.c +++ b/examples/device/midi_test/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4014 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/midi_test_freertos/src/usb_descriptors.c b/examples/device/midi_test_freertos/src/usb_descriptors.c index 99c798ce1d..bfdbc555e6 100644 --- a/examples/device/midi_test_freertos/src/usb_descriptors.c +++ b/examples/device/midi_test_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4015 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/msc_dual_lun/src/usb_descriptors.c b/examples/device/msc_dual_lun/src/usb_descriptors.c index b328cf17f2..5e036a8c4e 100644 --- a/examples/device/msc_dual_lun/src/usb_descriptors.c +++ b/examples/device/msc_dual_lun/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4016 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/mtp/src/usb_descriptors.c b/examples/device/mtp/src/usb_descriptors.c index 4c840560e4..fefa8a2391 100644 --- a/examples/device/mtp/src/usb_descriptors.c +++ b/examples/device/mtp/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] MTP | VENDOR | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) | PID_MAP(MTP, 5)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4017 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/net_lwip_webserver/src/usb_descriptors.c b/examples/device/net_lwip_webserver/src/usb_descriptors.c index 09090bb927..85a6a420c8 100644 --- a/examples/device/net_lwip_webserver/src/usb_descriptors.c +++ b/examples/device/net_lwip_webserver/src/usb_descriptors.c @@ -27,16 +27,8 @@ #include "class/net/net_device.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] NET | VENDOR | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID \ - (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) | \ - PID_MAP(ECM_RNDIS, 5) | PID_MAP(NCM, 5)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4018 // String Descriptor Index enum { diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c index db7bfe97a2..b9450c87ed 100644 --- a/examples/device/printer_to_cdc/src/usb_descriptors.c +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -29,7 +29,7 @@ #include "usb_descriptors.h" #define USB_VID 0xCafe -#define USB_PID 0x4005 +#define USB_PID 0x4019 #define USB_BCD 0x0200 //--------------------------------------------------------------------+ diff --git a/examples/device/uac2_headset/src/usb_descriptors.c b/examples/device/uac2_headset/src/usb_descriptors.c index b554e71955..1615b92ec2 100644 --- a/examples/device/uac2_headset/src/usb_descriptors.c +++ b/examples/device/uac2_headset/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401a //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/uac2_speaker_fb/src/usb_descriptors.c b/examples/device/uac2_speaker_fb/src/usb_descriptors.c index f0c780e38d..40a36cbf4c 100644 --- a/examples/device/uac2_speaker_fb/src/usb_descriptors.c +++ b/examples/device/uac2_speaker_fb/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "usb_descriptors.h" #include "common_types.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401b //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/usbtest/CMakeLists.txt b/examples/device/usbtest/CMakeLists.txt new file mode 100644 index 0000000000..10414ede7e --- /dev/null +++ b/examples/device/usbtest/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(usbtest C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_device_example(${PROJECT_NAME} noos) diff --git a/examples/device/usbtest/CMakePresets.json b/examples/device/usbtest/CMakePresets.json new file mode 100644 index 0000000000..5cd8971e9a --- /dev/null +++ b/examples/device/usbtest/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/device/usbtest/Makefile b/examples/device/usbtest/Makefile new file mode 100644 index 0000000000..035e903083 --- /dev/null +++ b/examples/device/usbtest/Makefile @@ -0,0 +1,11 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/device/usbtest/README.md b/examples/device/usbtest/README.md new file mode 100644 index 0000000000..8fcc4ad36e --- /dev/null +++ b/examples/device/usbtest/README.md @@ -0,0 +1,94 @@ +# usbtest + +Device-side peer of the Linux kernel USB test pair: + +- `usbtest.ko` — host kernel module (`drivers/usb/misc/usbtest.c`) containing ~30 numbered + test cases over bulk/control/interrupt/isochronous transfers. +- `testusb` — userspace dispatcher (`tools/usb/testusb.c`) that tells the module which case + to run via usbfs ioctl. + +This example implements the Gadget-Zero style *source/sink* protocol on a vendor-specific +interface so the whole battery can exercise TinyUSB device controller drivers: + +- bulk IN = infinite source (usbtest pattern 0: all zeros) +- bulk OUT = infinite sink (data discarded) + +## Tiers + +The firmware advertises its capability tier in `bcdDevice` (`0x01TT`); the host script picks +the matching test battery automatically. + +| Tier | Capability | usbtest cases | +|------|------------|---------------| +| 1 | bulk source/sink | 0, 9, 10, 1–8, 11, 12, 24, 13, 29, 17–20, 27, 28 | +| 2 | + vendor control `0x5b`/`0x5c` (ctrl_out) | + 14, 21 | +| 3 | + interrupt source/sink | + 25, 26 | +| 4 | + isochronous source/sink | + 15, 16, 22, 23 | + +This example implements all four tiers using the vendor class with the interrupt +(`CFG_TUD_VENDOR_EP_INT_OUT/IN`) and isochronous (`CFG_TUD_VENDOR_EP_ISO_OUT/IN`) +endpoint pairs and altsetting support (`CFG_TUD_VENDOR_ALT_SETTINGS`): alt 0 +carries no endpoints, alt 1 the full source/sink set, per USB 2.0 5.6.3 (the host +usbtest driver selects alt 1 itself). + +## Test cases + +Directions are from the host's point of view: *write* = host→device (OUT endpoint, device +sinks and discards), *read* = device→host (IN endpoint, device sources zeros and the host +verifies every byte). All checking happens host-side in `usbtest.ko`; a case fails on a data +mismatch, an unexpected short packet/STALL, or a timeout. What each case stresses on the +device/DCD side: + +| # | Name | What it does / what it exercises | +|---|------|----------------------------------| +| 0 | NOP | ioctl round-trip sanity, no USB traffic — proves the interface bound with the right capability profile | +| 9 | ch9 subset | chapter-9 standard control requests (GET_DESCRIPTOR, GET_STATUS, SET/CLEAR_FEATURE, SET_INTERFACE, …) — the EP0 state machine, incl. status stages and ZLPs | +| 10 | queued control | many control URBs in flight at once — EP0 under sustained back-to-back SETUPs | +| 1 / 2 | bulk write / read | plain OUT sink / IN source streams of whole max-size packets — FIFO handling, multi-packet transfers | +| 3 / 4 | bulk write / read vary | same with transfer sizes varying per URB — short packets and packet-boundary edge cases | +| 5–8 | bulk sg write/read (+vary) | scatter-gather queued URBs — continuous packet pressure with no inter-URB gap; classic overflow/babble catcher | +| 11 / 12 | unlink reads / writes | URBs submitted then cancelled mid-flight — the device keeps streaming while the host aborts; DCD abort/cleanup paths | +| 24 | unlink queued writes | unlink from a deep OUT queue — same, under queue pressure | +| 13 | ep halt set/clear | SET_FEATURE(ENDPOINT_HALT), verify the endpoint really STALLs, then CLEAR_FEATURE and verify traffic resumes at DATA0 — stall must abort an armed transfer (and flush any loaded FIFO) | +| 29 | toggle clear | CLEAR_FEATURE(HALT) on a **non-halted** endpoint mid-traffic, purely to reset the data toggle — the DCD must reset DATA0 *without* disarming the queued transfer (historically the most common per-DCD bug in this battery) | +| 17 / 18 | bulk write / read unaligned | bulk streams from oddly-offset host buffers — host DMA-alignment path; the device sees normal traffic | +| 19 / 20 | bulk write / read premapped | bulk streams using host pre-mapped DMA buffers — another host memory path | +| 27 / 28 | bulk write / read perf | sustained maximum-throughput streams, reported in MB/s — real-time FIFO servicing under load | +| 14 | ctrl_out write/read | vendor EP0 request `0x5b` stores wLength bytes, `0x5c` reads them back, sizes varying — multi-packet control-OUT data stages and buffer persistence across requests | +| 21 | ctrl_out unaligned | same from odd host buffer offsets | +| 25 / 26 | int write / read | interrupt OUT sink / IN source at the descriptor's polling interval — interrupt endpoint arming and completion | +| 15 / 16 | iso write / read | isochronous OUT sink / IN source, one packet per (micro)frame with per-packet status — no handshake/retry, DATA0-only; the IN source must re-arm fast enough to make every frame deadline | +| 22 / 23 | iso write / read unaligned | same from odd host buffer offsets | + +Per-case iteration counts and sizes are chosen by `test/hil/usbtest.py` for the negotiated +speed (see its `PARAMS` table); the authoritative case implementations live in the kernel's +`drivers/usb/misc/usbtest.c`. + +## Running + +Use the host script (handles driver binding, per-case parameters, result parsing): + +```bash +python3 test/hil/usbtest.py --serial +``` + +Requirements on the host: `usbtest` kernel module (`CONFIG_USB_TEST`, `modprobe usbtest`), +the `testusb` binary built from kernel `tools/usb/testusb.c`, and sudo (usbfs ioctls + +driver bind/unbind). + +Manual runs are possible but beware `testusb` defaults: always pass explicit `-s`/`-v` +values that are multiples of 512 — the device streams whole max-size packets, so a +non-packet-aligned read length overflows (`-EOVERFLOW`), and never run bare `testusb -a` +(the default parameter set includes cases with invalid parameters and hour-long runtimes +at full speed). + +```bash +# bind: MUST use the 5-field form referencing Gadget Zero (0525:a4a0) so the +# dynamic id inherits its capability profile. A plain "cafe 4010" id leaves +# driver_info NULL, which usbtest_probe() dereferences -> kernel oops. +sudo modprobe usbtest +echo "cafe 4010 0 0525 a4a0" | sudo tee /sys/bus/usb/drivers/usbtest/new_id +# example: bulk write/read +sudo testusb -D /dev/bus/usb// -t 1 -c 128 -s 1024 -v 512 +sudo testusb -D /dev/bus/usb// -t 2 -c 128 -s 1024 -v 512 +``` diff --git a/examples/device/usbtest/skip.txt b/examples/device/usbtest/skip.txt new file mode 100644 index 0000000000..b52bdbb141 --- /dev/null +++ b/examples/device/usbtest/skip.txt @@ -0,0 +1,15 @@ +mcu:MSP430x5xx +mcu:NUC121 +mcu:SAMD11 +# DCD has no isochronous support (dcd_edpt_iso_alloc refuses), tier-4 cannot enumerate: +mcu:CXD56 +mcu:FT90X +mcu:LPC175X_6X +mcu:LPC40XX +mcu:NUC100 +mcu:NUC120 +mcu:NUC505 +mcu:PIC32MZ +mcu:SAMG +mcu:SAMX7X +mcu:VALENTYUSB_EPTRI diff --git a/examples/device/usbtest/src/CMakeLists.txt b/examples/device/usbtest/src/CMakeLists.txt new file mode 100644 index 0000000000..cef2b46ee7 --- /dev/null +++ b/examples/device/usbtest/src/CMakeLists.txt @@ -0,0 +1,4 @@ +# This file is for ESP-IDF only +idf_component_register(SRCS "main.c" "usb_descriptors.c" + INCLUDE_DIRS "." + REQUIRES boards tinyusb_src) diff --git a/examples/device/usbtest/src/main.c b/examples/device/usbtest/src/main.c new file mode 100644 index 0000000000..e57a901613 --- /dev/null +++ b/examples/device/usbtest/src/main.c @@ -0,0 +1,344 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/* Device-side peer of the Linux kernel host test driver drivers/usb/misc/usbtest.c + * (driven from userspace by tools/usb/testusb.c). Implements the Gadget-Zero + * style source/sink protocol on a vendor interface (alt 0 = no endpoints, + * alt 1 = full set, selected by the host usbtest driver): + * - bulk/interrupt/isochronous IN = infinite source (pattern 0: all zeros) + * - bulk/interrupt/isochronous OUT = infinite sink (data discarded) + * - EP0 0x5b/0x5c = control write then read-back (ctrl_out tests) + * See examples/device/usbtest/README.md and test/hil/usbtest.py for usage. + */ + +#include +#include +#include + +#include "bsp/board_api.h" +#include "tusb.h" +#include "usb_descriptors.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ + +/* Blink pattern + * - 250 ms : device not mounted + * - 1000 ms : device mounted + */ +enum { + BLINK_NOT_MOUNTED = 250, + BLINK_MOUNTED = 1000, +}; + +static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED; + +// Source data, all zeros = usbtest pattern 0. Sizes are a multiple of the +// endpoint max packet size: transfers are always whole packets, never an +// unintended short packet or ZLP. +static uint8_t const tx_chunk[CFG_TUD_VENDOR_TX_EPSIZE]; +static uint8_t const int_tx_chunk[USBTEST_INT_EP_MPS]; +#if USBTEST_TIER >= 4 +static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS]; +#endif + +// Interrupt/iso submit one packet per (micro)frame, sized to the NEGOTIATED speed's mps — a +// high-speed build enumerated at full speed must submit the FS length, not the HS-capacity buffer +// size (bulk is exempt: it streams multi-packet transfers). See usb_descriptors.h. +static inline uint16_t usbtest_int_len(void) { + return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS; +} +#if USBTEST_TIER >= 4 +static inline uint16_t usbtest_iso_len(void) { + return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS; +} +#endif + +//------------- prototypes -------------// +void led_blinking_task(void* param); +void usbtest_task(void* param); + +#if CFG_TUSB_OS == OPT_OS_FREERTOS +void freertos_init(void); +#endif + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + // If using FreeRTOS: create blinky, tinyusb device, and usbtest source/sink tasks +#if CFG_TUSB_OS == OPT_OS_FREERTOS + freertos_init(); +#else + // init device stack on configured roothub port + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + while (1) { + tud_task(); // tinyusb device task + usbtest_task(NULL); + led_blinking_task(NULL); + } +#endif +} + +//--------------------------------------------------------------------+ +// Source/sink pumps +//--------------------------------------------------------------------+ + +// Polling keeps all four endpoints armed and self-heals after endpoint halt +// (set/clear feature tests): stall marks the endpoint busy so the calls fail +// quietly until the host clears the halt, then the next tick re-arms. +static void usbtest_pump(void) { + if (tud_vendor_mounted()) { + tud_vendor_read_xfer(); // bulk sink: arm/re-arm, quiet fail if armed or halted + if (tud_vendor_write_available()) { // 0 while bulk IN is busy or halted + tud_vendor_write(tx_chunk, sizeof(tx_chunk)); + } + + tud_vendor_int_read_xfer(); // interrupt sink + if (tud_vendor_int_write_available()) { + tud_vendor_int_write(int_tx_chunk, usbtest_int_len()); + } + +#if USBTEST_TIER >= 4 + tud_vendor_iso_read_xfer(); // isochronous sink + if (tud_vendor_iso_write_available()) { + tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); + } +#endif + } +} + +void usbtest_task(void* param) { + (void) param; + #if CFG_TUSB_OS == OPT_OS_FREERTOS + while (1) { + usbtest_pump(); + vTaskDelay(1); // yield; tx/rx completion callbacks keep the pipes saturated between polls + } + #else + usbtest_pump(); // called from the main loop, one tick per call + #endif +} + +// Invoked when received data from host: discard and immediately re-arm +void tud_vendor_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_read_xfer(); +} + +// Invoked when last bulk tx transfer finished: keep the source saturated +void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_write(tx_chunk, sizeof(tx_chunk)); +} + +// Interrupt pair: same discard/refill pumps as bulk +void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_int_read_xfer(); +} + +void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_int_write(int_tx_chunk, usbtest_int_len()); +} + +// Isochronous pair: same discard/refill pumps; a completion may be a missed +// frame, re-arm regardless +#if USBTEST_TIER >= 4 +void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_iso_read_xfer(); +} + +void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); +} +#endif + +//--------------------------------------------------------------------+ +// Vendor control requests (EP0) +//--------------------------------------------------------------------+ + +// Control write/read-back for the ctrl_out tests (14/21), same protocol as +// Gadget Zero: 0x5b stores the host's wLength bytes, 0x5c returns them. +static uint8_t ctrl_buf[1024]; + +// Invoked on vendor control transfers, and by usbd for forwarded standard +// endpoint requests (halt set/clear) whose return value it ignores — return +// false for anything that is not a supported vendor request. +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR) { + return false; + } + + switch (request->bRequest) { + case 0x5b: // control WRITE: receive wLength bytes into ctrl_buf + TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_OUT); + TU_VERIFY(request->wValue == 0 && request->wIndex == 0); + TU_VERIFY(request->wLength <= sizeof(ctrl_buf)); + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, ctrl_buf, request->wLength); + } + return true; // DATA/ACK: payload already landed in ctrl_buf + + case 0x5c: // control READ: send back the previously written bytes + TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_IN); + TU_VERIFY(request->wValue == 0 && request->wIndex == 0); + TU_VERIFY(request->wLength <= sizeof(ctrl_buf)); + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, ctrl_buf, request->wLength); + } + return true; + + default: + return false; + } +} + +//--------------------------------------------------------------------+ +// Device callbacks +//--------------------------------------------------------------------+ + +// Invoked when device is mounted +void tud_mount_cb(void) { + blink_interval_ms = BLINK_MOUNTED; +} + +// Invoked when device is unmounted +void tud_umount_cb(void) { + blink_interval_ms = BLINK_NOT_MOUNTED; +} + +//--------------------------------------------------------------------+ +// BLINKING TASK +//--------------------------------------------------------------------+ +void led_blinking_task(void* param) { + (void) param; + static uint32_t start_ms = 0; + static bool led_state = false; + + while (1) { + #if CFG_TUSB_OS == OPT_OS_FREERTOS + vTaskDelay(blink_interval_ms / portTICK_PERIOD_MS); + #else + // Blink every interval ms + if (tusb_time_millis_api() - start_ms < blink_interval_ms) { + return; // not enough time + } + #endif + + start_ms += blink_interval_ms; + board_led_write(led_state); + led_state = 1 - led_state; // toggle + } +} + +//--------------------------------------------------------------------+ +// FreeRTOS +//--------------------------------------------------------------------+ +#if CFG_TUSB_OS == OPT_OS_FREERTOS + +#define BLINKY_STACK_SIZE configMINIMAL_STACK_SIZE +#define USBTEST_STACK_SIZE (configMINIMAL_STACK_SIZE*2) + +#ifdef ESP_PLATFORM + #define USBD_STACK_SIZE 4096 + int main(void); + void app_main(void) { + main(); + } +#else + // Increase stack size when debug log is enabled + #define USBD_STACK_SIZE (3*configMINIMAL_STACK_SIZE/2) * (CFG_TUSB_DEBUG ? 2 : 1) +#endif + +// static task allocation +#if configSUPPORT_STATIC_ALLOCATION +StackType_t blinky_stack[BLINKY_STACK_SIZE]; +StaticTask_t blinky_taskdef; + +StackType_t usb_device_stack[USBD_STACK_SIZE]; +StaticTask_t usb_device_taskdef; + +StackType_t usbtest_stack[USBTEST_STACK_SIZE]; +StaticTask_t usbtest_taskdef; +#endif + +// USB Device Driver task: processes all usb events and invokes callbacks +void usb_device_task(void* param) { + (void) param; + + // init device stack on configured roothub port. Must be called after the + // scheduler starts: the USB IRQ handler uses RTOS queue APIs. + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + // RTOS forever loop + while (1) { + tud_task(); // put thread to waiting state until there is a new event + } +} + +void freertos_init(void) { + #if configSUPPORT_STATIC_ALLOCATION + xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, blinky_stack, &blinky_taskdef); + xTaskCreateStatic(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_device_stack, &usb_device_taskdef); + xTaskCreateStatic(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, usbtest_stack, &usbtest_taskdef); + #else + xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL); + xTaskCreate(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, NULL); + xTaskCreate(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, NULL); + #endif + + // only start scheduler for non-espressif mcu (espressif starts it in startup code) + #ifndef ESP_PLATFORM + vTaskStartScheduler(); + #endif +} +#endif diff --git a/examples/device/usbtest/src/tusb_config.h b/examples/device/usbtest/src/tusb_config.h new file mode 100644 index 0000000000..dda131ae22 --- /dev/null +++ b/examples/device/usbtest/src/tusb_config.h @@ -0,0 +1,151 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Board Specific Configuration +//--------------------------------------------------------------------+ + +// RHPort number used for device can be defined by board.mk, default to port 0 +#ifndef BOARD_TUD_RHPORT +#define BOARD_TUD_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUD_MAX_SPEED +#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +// Espressif IDF requires "freertos/" prefix in include path +#ifdef ESP_PLATFORM +#define CFG_TUSB_OS_INC_PATH freertos/ +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +// Enable Device stack +#define CFG_TUD_ENABLED 1 + +// Default is max speed that hardware controller could support with on-chip PHY +#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 1 + +// Non-buffered mode: every transfer is submitted with an exact length so the +// host never sees an unexpected short packet or ZLP mid-transfer, which the +// usbtest data-integrity cases treat as failure. +#define CFG_TUD_VENDOR_RX_BUFSIZE 0 +#define CFG_TUD_VENDOR_TX_BUFSIZE 0 + +// App re-arms RX itself: required to recover the sink after halt tests +// (SET_FEATURE/CLEAR_FEATURE endpoint halt) where no completion ever fires. +#define CFG_TUD_VENDOR_RX_MANUAL_XFER 1 + +// Multi-packet IN transfers; must be a multiple of bulk MPS at both speeds (64/512). +// LPC11/13 (ip3511 FS) keep endpoint buffers in a dedicated 2 KB USB RAM: a 2048 B bulk epbuf +// overflows it once the int/iso buffers join, so those parts use 512 (= 8 FS packets). +#if TU_CHECK_MCU(OPT_MCU_LPC11UXX, OPT_MCU_LPC13XX) +#define CFG_TUD_VENDOR_TX_EPSIZE 512 +#else +#define CFG_TUD_VENDOR_TX_EPSIZE 2048 +#endif + +// Interrupt IN/OUT source/sink pair (usbtest cases 25/26). Buffer sizes track the +// per-speed endpoint max packet size (see usb_descriptors.c) so full-speed builds +// don't over-allocate the scarce USB DMA section. +#define CFG_TUD_VENDOR_EP_INT_OUT 1 +#define CFG_TUD_VENDOR_EP_INT_IN 1 +#define CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// Isochronous IN/OUT source/sink pair (usbtest cases 15/16/22/23), placed in +// altsetting 1: alt 0 has no endpoints so no iso bandwidth is claimed by default +#define CFG_TUD_VENDOR_EP_ISO_OUT 1 +#define CFG_TUD_VENDOR_EP_ISO_IN 1 +#define CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128) +#define CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128) +#define CFG_TUD_VENDOR_ALT_SETTINGS 1 + +// CH32V20X fsdev port has only 512 B PMA and single-buffered iso can't keep the iso IN endpoint +// fed under load. Double-buffer iso; the descriptor drops iso mps to 32 there so 2x32 = 64 B/ep +// keeps the same PMA budget (see usb_descriptors.h). Other fsdev parts have room and stay single. +#if CFG_TUSB_MCU == OPT_MCU_CH32V20X +#define CFG_TUD_FSDEV_DOUBLE_BUFFERED_ISO_EP 1 +#endif + +#ifdef __cplusplus + } +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/examples/device/usbtest/src/usb_descriptors.c b/examples/device/usbtest/src/usb_descriptors.c new file mode 100644 index 0000000000..b4f46adb83 --- /dev/null +++ b/examples/device/usbtest/src/usb_descriptors.c @@ -0,0 +1,279 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "bsp/board_api.h" +#include "tusb.h" +#include "usb_descriptors.h" + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +static tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, // per-interface class + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xCafe, + .idProduct = 0x4010, + .bcdDevice = 0x0100 | USBTEST_TIER, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +// Invoked when received GET DEVICE DESCRIPTOR +uint8_t const* tud_descriptor_device_cb(void) { + return (uint8_t const*) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + +// Interface must be number 0: testusb -D issues its ioctls against interface 0 +enum { + ITF_NUM_VENDOR = 0, + ITF_NUM_TOTAL +}; + +// Vendor interface, Gadget-Zero style altsettings: alt 0 carries no endpoints (an +// isochronous endpoint must not claim bandwidth in the default altsetting, USB 2.0 +// 5.6.3), alt 1 carries bulk + interrupt (+ isochronous IN/OUT at tier 4). The host +// usbtest driver skips altsettings without pipes and selects alt 1 itself. No TUD_ +// macro covers this layout, hand-rolled. +#if USBTEST_TIER >= 4 + #define USBTEST_EP_COUNT 6 + #define USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) \ + ,7, TUSB_DESC_ENDPOINT, _isoout, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval,\ + 7, TUSB_DESC_ENDPOINT, _isoin, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval +#else + #define USBTEST_EP_COUNT 4 + #define USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) +#endif +#define USBTEST_DESC_LEN (9 + 9 + USBTEST_EP_COUNT*7) +#define USBTEST_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _bulk_mps, _intout, _intin, _int_mps, _int_interval, _isoout, _isoin, _iso_mps, _iso_interval) \ + /* alt 0: zero bandwidth, no endpoints */\ + 9, TUSB_DESC_INTERFACE, _itfnum, 0, 0, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ + /* alt 1: full source/sink set */\ + 9, TUSB_DESC_INTERFACE, _itfnum, 1, USBTEST_EP_COUNT, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ + 7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ + 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ + 7, TUSB_DESC_ENDPOINT, _intout, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\ + 7, TUSB_DESC_ENDPOINT, _intin, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval\ + USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + USBTEST_DESC_LEN) + +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX + // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number + // 0 control, 1 Interrupt, 2 Bulk, 3 Iso, 4 Interrupt etc ... + #define EPNUM_BULK_OUT 0x02 + #define EPNUM_BULK_IN 0x85 + #define EPNUM_INT_OUT 0x01 + #define EPNUM_INT_IN 0x84 + #define EPNUM_ISO_OUT 0x03 + #define EPNUM_ISO_IN 0x86 + +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + // MCUs that don't support a same endpoint number with different direction IN and OUT + // e.g EP1 OUT & EP1 IN cannot exist together + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_BULK_OUT 0x08 + #define EPNUM_BULK_IN 0x89 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x83 + #define EPNUM_ISO_OUT 0x04 + #define EPNUM_ISO_IN 0x85 + #else + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x82 + #define EPNUM_INT_OUT 0x03 + #define EPNUM_INT_IN 0x84 + #define EPNUM_ISO_OUT 0x05 + #define EPNUM_ISO_IN 0x86 + #endif + +#elif CFG_TUSB_MCU == OPT_MCU_NRF5X + // nRF5x: ISO endpoints are hardware-fixed to EP8 (ISOOUT/ISOIN) + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x81 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x82 + #define EPNUM_ISO_OUT 0x08 + #define EPNUM_ISO_IN 0x88 + +#else + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x81 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x82 + #define EPNUM_ISO_OUT 0x03 + #define EPNUM_ISO_IN 0x83 +#endif + +static uint8_t const desc_fs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval + USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 64, + EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_FS, 1, + EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_FS, 1) +}; + +#if TUD_OPT_HIGH_SPEED +// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration + +static uint8_t const desc_hs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval (1 ms) + USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 512, + EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_HS, 4, + EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_HS, 4) +}; + +// other speed configuration +static uint8_t desc_other_speed_config[CONFIG_TOTAL_LEN]; + +// device qualifier is mostly similar to device descriptor since we don't change configuration based on speed +static tusb_desc_device_qualifier_t const desc_device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0200, + + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0x00 +}; + +// Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request +uint8_t const* tud_descriptor_device_qualifier_cb(void) { + return (uint8_t const*) &desc_device_qualifier; +} + +// Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request +// Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa +uint8_t const* tud_descriptor_other_speed_configuration_cb(uint8_t index) { + (void) index; // for multiple configurations + + // if link speed is high return fullspeed config, and vice versa + // Note: the descriptor type is OTHER_SPEED_CONFIG instead of CONFIG + memcpy(desc_other_speed_config, + (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration, + CONFIG_TOTAL_LEN); + + desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + + return desc_other_speed_config; +} +#endif // highspeed + +// Invoked when received GET CONFIGURATION DESCRIPTOR +uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { + (void) index; // for multiple configurations + +#if TUD_OPT_HIGH_SPEED + // Although we are highspeed, host may be fullspeed. + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// String Descriptor Index +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER, + STRID_PRODUCT, + STRID_SERIAL, +}; + +// array of pointer to string descriptors +static char const* string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "TinyUSB", // 1: Manufacturer + "TinyUSB usbtest", // 2: Product + NULL, // 3: Serials will use unique ID if possible + "TinyUSB usbtest source/sink" // 4: Vendor Interface +}; + +static uint16_t _desc_str[32 + 1]; + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void) langid; + size_t chr_count; + + switch (index) { + case STRID_LANGID: + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + + case STRID_SERIAL: + chr_count = board_usb_get_serial(_desc_str + 1, 32); + break; + + default: + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) return NULL; + + const char* str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type + if (chr_count > max_count) chr_count = max_count; + + // Convert ASCII string into UTF-16 + for (size_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + break; + } + + // first byte is length (including header), second byte is string type + _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); + + return _desc_str; +} diff --git a/examples/device/usbtest/src/usb_descriptors.h b/examples/device/usbtest/src/usb_descriptors.h new file mode 100644 index 0000000000..bcf8b5ec47 --- /dev/null +++ b/examples/device/usbtest/src/usb_descriptors.h @@ -0,0 +1,84 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef USB_DESCRIPTORS_H_ +#define USB_DESCRIPTORS_H_ + +// Device-capability tier advertised in bcdDevice low byte (0x01TT), read by the +// host script to select which usbtest cases to run: +// 1: bulk source/sink +// 2: + vendor control 0x5b/0x5c (ctrl_out) +// 3: + interrupt source/sink +// 4: + isochronous source/sink +// Default is the full tier 4; a board whose DCD cannot serve a tier lowers it here +// (BOARD_ is defined by both build systems) and the host battery follows. +#ifndef USBTEST_TIER + #if defined(BOARD_RA2A1_EK) + // RA2A1's RUSB2 instance has no isochronous pipe (other RA parts have pipes 1-2) + #define USBTEST_TIER 3 + #else + #define USBTEST_TIER 4 + #endif +#endif + +// Interrupt/isochronous endpoint max packet sizes, must match the configuration descriptor. +// TUD_OPT_HIGH_SPEED is a compile-time capability flag, NOT the live bus speed, so the full-speed +// config descriptor (and the OTHER_SPEED descriptor served to a HS host) must use full-speed-legal +// sizes regardless of it: interrupt <= 64 B, isochronous <= 1023 B (and both iso EPs must fit the +// 1023 B/frame FS periodic budget). Hence separate _FS / _HS descriptor sizes; the plain macro +// below is the compile-time capability maximum that sizes the source buffers (runtime write +// lengths follow the negotiated speed via tud_speed_get(), see main.c). +// +// The CH32 USB IPs have tiny per-endpoint buffers so tier-4's six endpoints don't fit at the usual +// FS sizes: usbfs gives 64 B/ep (iso must drop to 64), and the CH32V20X fsdev port shares one 512 B +// PMA across every endpoint (needs iso 32 AND a small interrupt mps to fit alongside EP0+bulk+iso). +#if CFG_TUSB_MCU == OPT_MCU_CH32V20X && defined(CFG_TUD_WCH_USBIP_FSDEV) && CFG_TUD_WCH_USBIP_FSDEV + #define USBTEST_INT_EP_MPS_FS 16 + #define USBTEST_ISO_EP_MPS_FS 32 // double-buffered on fsdev: 2x32=64/ep, same 512 B PMA budget +#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V103, OPT_MCU_CH32F20X, OPT_MCU_CH32V307, OPT_MCU_CH583) + // WCH USBFS parts cap every endpoint (except EP3 IN) at 64 B. For the CH32V307 this applies to its + // full-speed (usbfs) port; its high-speed (usbhs) port uses the _HS sizes below via desc_hs. + #define USBTEST_INT_EP_MPS_FS 64 + #define USBTEST_ISO_EP_MPS_FS 64 +#else + #define USBTEST_INT_EP_MPS_FS 64 + #define USBTEST_ISO_EP_MPS_FS 128 +#endif +// RUSB2 (Renesas RA) interrupt pipes 6-9 have a fixed 64-byte single buffer at any speed +// (RA6M5 UM R01UH0891 sec 29.1: "Pipes 6 to 9: Interrupt transfer with 64-byte single buffer"). +#if TU_CHECK_MCU(OPT_MCU_RAXXX) + #define USBTEST_INT_EP_MPS_HS 64 +#else + #define USBTEST_INT_EP_MPS_HS 512 +#endif +#define USBTEST_ISO_EP_MPS_HS 512 + +// Compile-time capability maximum: sizes the source buffers / vendor epbufs for the largest +// packet the build can negotiate. Runtime write lengths follow tud_speed_get() (see main.c) — +// a high-speed build enumerated at full speed submits the _FS lengths. +#define USBTEST_INT_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS) +#define USBTEST_ISO_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS) + +#endif /* USB_DESCRIPTORS_H_ */ diff --git a/examples/device/usbtmc/src/usb_descriptors.c b/examples/device/usbtmc/src/usb_descriptors.c index ecdcef8340..5ba5d93675 100644 --- a/examples/device/usbtmc/src/usb_descriptors.c +++ b/examples/device/usbtmc/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "class/usbtmc/usbtmc.h" #include "class/usbtmc/usbtmc_device.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401c #define USB_VID 0xcafe #define USB_BCD 0x0200 diff --git a/examples/device/video_capture/src/usb_descriptors.c b/examples/device/video_capture/src/usb_descriptors.c index b3382c82d5..d5d805f0bd 100644 --- a/examples/device/video_capture/src/usb_descriptors.c +++ b/examples/device/video_capture/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] VIDEO | AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VIDEO, 5) | PID_MAP(VENDOR, 6) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401d #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/video_capture_2ch/src/usb_descriptors.c b/examples/device/video_capture_2ch/src/usb_descriptors.c index 8dc986da60..ad65cc0196 100644 --- a/examples/device/video_capture_2ch/src/usb_descriptors.c +++ b/examples/device/video_capture_2ch/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] VIDEO | AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VIDEO, 5) | PID_MAP(VENDOR, 6) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401e #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/webusb_serial/src/usb_descriptors.c b/examples/device/webusb_serial/src/usb_descriptors.c index 5278371617..5986bffdf8 100644 --- a/examples/device/webusb_serial/src/usb_descriptors.c +++ b/examples/device/webusb_serial/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401f //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/dual/dynamic_switch/src/usb_descriptors.c b/examples/dual/dynamic_switch/src/usb_descriptors.c index ef6d795b75..c6d80e2ab7 100644 --- a/examples/dual/dynamic_switch/src/usb_descriptors.c +++ b/examples/dual/dynamic_switch/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4020 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c b/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c index 3efa30e201..3dc32e3f41 100644 --- a/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c +++ b/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4021 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c b/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c index 3efa30e201..4fa3bcbc78 100644 --- a/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c +++ b/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4022 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/hw/bsp/ch32v20x/family.c b/hw/bsp/ch32v20x/family.c index 76024cfdec..8875e4faa6 100644 --- a/hw/bsp/ch32v20x/family.c +++ b/hw/bsp/ch32v20x/family.c @@ -27,25 +27,25 @@ manufacturer: WCH * - CFG_TUD_WCH_USBIP_USBFS */ -// Port0: USBD (fsdev) -__attribute__((interrupt)) __attribute__((used)) void USB_LP_CAN1_RX0_IRQHandler(void) { - #if CFG_TUD_WCH_USBIP_FSDEV - tud_int_handler(0); - #endif -} - -__attribute__((interrupt)) __attribute__((used)) void USB_HP_CAN1_TX_IRQHandler(void) { - #if CFG_TUD_WCH_USBIP_FSDEV - tud_int_handler(0); - #endif - -} - -__attribute__((interrupt)) __attribute__((used)) void USBWakeUp_IRQHandler(void) { - #if CFG_TUD_WCH_USBIP_FSDEV - tud_int_handler(0); - #endif -} +// Port0: USBD (fsdev). The USBD raises three IRQ lines (LP/HP/WakeUp) that all funnel into the +// non-reentrant tud_int_handler and can nest (HP preempts LP) with QingKe HWSTK enabled. The +// mainline toolchain's plain __attribute__((interrupt)) emits a software prologue that fights the +// hardware context stack and corrupts the return on nesting. Emit naked handlers that rely on +// HWSTK for context save/restore (equivalent to WCH's "WCH-Interrupt-fast"), which nests safely. +#if CFG_TUD_ENABLED && CFG_TUD_WCH_USBIP_FSDEV + // The `call dcd_int_handler` below lives inside naked asm where LTO cannot see it; without a + // compiler-visible reference, -flto builds (make) internalize/drop the symbol and the link fails. + TU_ATTR_USED static void (*const fsdev_isr_keep)(uint8_t) = dcd_int_handler; + #define FSDEV_NAKED_ISR(name) \ + __attribute__((naked)) __attribute__((used)) void name(void) { \ + __asm volatile("li a0, 0\n\t call dcd_int_handler\n\t mret"); } +#else + #define FSDEV_NAKED_ISR(name) \ + __attribute__((naked)) __attribute__((used)) void name(void) { __asm volatile("mret"); } +#endif +FSDEV_NAKED_ISR(USB_LP_CAN1_RX0_IRQHandler) +FSDEV_NAKED_ISR(USB_HP_CAN1_TX_IRQHandler) +FSDEV_NAKED_ISR(USBWakeUp_IRQHandler) // Port1: USBFS __attribute__((interrupt)) __attribute__((used)) void USBHD_IRQHandler(void) { diff --git a/hw/bsp/ch32v30x/family.c b/hw/bsp/ch32v30x/family.c index aee4e7d4fe..02c3c7d449 100644 --- a/hw/bsp/ch32v30x/family.c +++ b/hw/bsp/ch32v30x/family.c @@ -29,6 +29,7 @@ */ #include "stdio.h" +#include // https://github.com/openwch/ch32v307/pull/90 // https://github.com/openwch/ch32v20x/pull/12 @@ -166,6 +167,14 @@ uint32_t board_button_read(void) { #endif } +size_t board_get_unique_id(uint8_t id[], size_t max_len) { + volatile uint32_t* ch32_uuid = ((volatile uint32_t*) 0x1FFFF7E8UL); // ESIG unique ID + uint32_t uid[3] = { ch32_uuid[0], ch32_uuid[1], ch32_uuid[2] }; + const size_t len = max_len < sizeof(uid) ? max_len : sizeof(uid); + memcpy(id, uid, len); // byte copy: id[] need not be 4-byte aligned + return len; +} + int board_uart_read(uint8_t* buf, int len) { (void) buf; (void) len; diff --git a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h index f2f1ae0c9c..25638f02b9 100644 --- a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h +++ b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h @@ -51,6 +51,9 @@ #define BSP_CFG_CANFDCLK_DIV (BSP_CLOCKS_CANFD_CLOCK_DIV_8) /* CANFDCLK Div /8 */ #define BSP_CFG_I3CCLK_DIV (BSP_CLOCKS_I3C_CLOCK_DIV_3) /* I3CCLK Div /3 */ #define BSP_CFG_UCK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_5) /* UCK Div /5 */ -#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_8) /* U60CK Div /8 */ +/* U60CK Div /8: PLL1P 480 MHz -> 60 MHz. Hand-fixed: Smart Configurator emitted the USB_ macro + * namespace (BSP_CLOCKS_USB_CLOCK_DIV_8 = 7, rejected by USB60CKDIVCR -> link clock ran at + * 480 MHz); configuration.xml already says u60ck.div.8, so keep the USB60_ macro if regenerating. */ +#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB60_CLOCK_DIV_8) #define BSP_CFG_OCTA_DIV (BSP_CLOCKS_OCTA_CLOCK_DIV_4) /* OCTASPICLK Div /4 */ #endif /* BSP_CLOCK_CFG_H_ */ diff --git a/src/class/audio/audio_device.c b/src/class/audio/audio_device.c index 12e82190e9..94881521a6 100644 --- a/src/class/audio/audio_device.c +++ b/src/class/audio/audio_device.c @@ -1161,9 +1161,6 @@ static bool audiod_set_interface(uint8_t rhport, tusb_control_request_t const *p is_feedback_ep = (desc_ep->bmAttributes.usage == 1); } - //TODO: We need to set EP non busy since this is not taken care of right now in ep_close() - THIS IS A WORKAROUND! - usbd_edpt_clear_stall(rhport, ep_addr); - #if CFG_TUD_AUDIO_ENABLE_EP_IN // For data or data with implicit feedback IN EP if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN && is_data_ep) diff --git a/src/class/vendor/vendor_device.c b/src/class/vendor/vendor_device.c index b3537b665e..24f1405dc6 100644 --- a/src/class/vendor/vendor_device.c +++ b/src/class/vendor/vendor_device.c @@ -21,6 +21,26 @@ typedef struct { uint8_t rhport; uint8_t itf_num; + #if CFG_TUD_VENDOR_EP_INT_OUT + uint8_t ep_int_out; + uint16_t int_rx_xfer_len; + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + uint8_t ep_int_in; + #endif + #if CFG_TUD_VENDOR_EP_ISO_OUT + uint8_t ep_iso_out; + uint16_t iso_rx_xfer_len; + #endif + #if CFG_TUD_VENDOR_EP_ISO_IN + uint8_t ep_iso_in; + #endif + #if CFG_TUD_VENDOR_ALT_SETTINGS // implies non-buffered: fields cleared by bus reset + uint8_t cur_alt; + const uint8_t* p_itf_desc; // whole interface block incl. all altsettings (static app descriptor) + uint16_t itf_desc_len; + #endif + #if CFG_TUD_VENDOR_TXRX_BUFFERED /*------------- From this point, data is not cleared by bus reset -------------*/ tu_edpt_stream_t tx_stream; @@ -35,7 +55,10 @@ typedef struct { } vendord_interface_t; #if CFG_TUD_VENDOR_TXRX_BUFFERED - #define ITF_MEM_RESET_SIZE (offsetof(vendord_interface_t, itf_num) + TU_FIELD_SIZE(vendord_interface_t, itf_num)) + // The reset region is everything before the streams; tx_stream is the first preserved field + // (see the struct comment), so its offset is exactly that boundary regardless of which endpoint + // gates are enabled. + #define ITF_MEM_RESET_SIZE offsetof(vendord_interface_t, tx_stream) #else #define ITF_MEM_RESET_SIZE sizeof(vendord_interface_t) #endif @@ -52,6 +75,32 @@ typedef struct { CFG_TUD_MEM_SECTION static vendord_epbuf_t _vendord_epbuf[CFG_TUD_VENDOR]; #endif +#if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN +typedef struct { + #if CFG_TUD_VENDOR_EP_INT_OUT + TUD_EPBUF_DEF(int_out, CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE); + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + TUD_EPBUF_DEF(int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE); + #endif +} vendord_int_epbuf_t; + +CFG_TUD_MEM_SECTION static vendord_int_epbuf_t _vendord_int_epbuf[CFG_TUD_VENDOR]; +#endif + +#if CFG_TUD_VENDOR_EP_ISO_OUT || CFG_TUD_VENDOR_EP_ISO_IN +typedef struct { + #if CFG_TUD_VENDOR_EP_ISO_OUT + TUD_EPBUF_DEF(iso_out, CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE); + #endif + #if CFG_TUD_VENDOR_EP_ISO_IN + TUD_EPBUF_DEF(iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE); + #endif +} vendord_iso_epbuf_t; + +CFG_TUD_MEM_SECTION static vendord_iso_epbuf_t _vendord_iso_epbuf[CFG_TUD_VENDOR]; +#endif + //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ @@ -66,15 +115,61 @@ TU_ATTR_WEAK void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) { (void) sent_bytes; } +#if CFG_TUD_VENDOR_EP_INT_OUT +TU_ATTR_WEAK void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) { + (void)idx; + (void)buffer; + (void)bufsize; +} +#endif + +#if CFG_TUD_VENDOR_EP_INT_IN +TU_ATTR_WEAK void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void)idx; + (void)sent_bytes; +} +#endif + +#if CFG_TUD_VENDOR_EP_ISO_OUT +TU_ATTR_WEAK void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) { + (void)idx; + (void)buffer; + (void)bufsize; +} +#endif + +#if CFG_TUD_VENDOR_EP_ISO_IN +TU_ATTR_WEAK void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void)idx; + (void)sent_bytes; +} +#endif + bool tud_vendor_n_mounted(uint8_t idx) { TU_VERIFY(idx < CFG_TUD_VENDOR); vendord_interface_t *p_itf = &_vendord_itf[idx]; + // bulk may be absent (interrupt-only vendor interface): count the interrupt endpoints too #if CFG_TUD_VENDOR_TXRX_BUFFERED - return (p_itf->rx_stream.ep_addr != 0) || (p_itf->tx_stream.ep_addr != 0); + bool mounted = (p_itf->rx_stream.ep_addr != 0) || (p_itf->tx_stream.ep_addr != 0); #else - return (p_itf->ep_out != 0) || (p_itf->ep_in != 0); + bool mounted = (p_itf->ep_out != 0) || (p_itf->ep_in != 0); + #endif + #if CFG_TUD_VENDOR_EP_INT_OUT + mounted = mounted || (p_itf->ep_int_out != 0); + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + mounted = mounted || (p_itf->ep_int_in != 0); #endif + // an altsetting may expose only isochronous endpoints; count them so apps that gate an iso + // pump on tud_vendor_mounted() still arm it + #if CFG_TUD_VENDOR_EP_ISO_OUT + mounted = mounted || (p_itf->ep_iso_out != 0); + #endif + #if CFG_TUD_VENDOR_EP_ISO_IN + mounted = mounted || (p_itf->ep_iso_in != 0); + #endif + return mounted; } //--------------------------------------------------------------------+ @@ -107,6 +202,30 @@ void tud_vendor_n_read_flush(uint8_t idx) { } #endif +// Shared non-buffered transfer helpers for the bulk / interrupt / isochronous endpoints, which are +// identical apart from the endpoint, its epbuf and its buffer size. TU_ATTR_UNUSED: in buffered +// mode with the int/iso gates off none is referenced, and clang/IAR error on an unused static. +TU_ATTR_UNUSED static inline uint32_t vendord_ep_write(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf, + uint32_t bufsize, const void *buffer, uint32_t len) { + TU_VERIFY(ep > 0, 0); // must be opened + TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep), 0); + const uint32_t xact_len = tu_min32(len, bufsize); + memcpy(epbuf, buffer, xact_len); + TU_ASSERT(usbd_edpt_xfer(p_itf->rhport, ep, epbuf, (uint16_t) xact_len, false), 0); + return xact_len; +} + +TU_ATTR_UNUSED static inline uint32_t vendord_ep_write_available(vendord_interface_t *p_itf, uint8_t ep, uint32_t bufsize) { + TU_VERIFY(ep > 0, 0); // must be opened + return usbd_edpt_busy(p_itf->rhport, ep) ? 0 : bufsize; +} + +TU_ATTR_UNUSED static inline bool vendord_ep_read_xfer(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf, uint16_t xfer_len) { + TU_VERIFY(ep > 0); // must be opened + TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep)); + return usbd_edpt_xfer(p_itf->rhport, ep, epbuf, xfer_len, false); +} + #if CFG_TUD_VENDOR_RX_MANUAL_XFER bool tud_vendor_n_read_xfer(uint8_t idx) { TU_VERIFY(idx < CFG_TUD_VENDOR); @@ -116,9 +235,8 @@ bool tud_vendor_n_read_xfer(uint8_t idx) { return tu_edpt_stream_read_xfer(&p_itf->rx_stream); #else - // Non-FIFO mode - TU_VERIFY(usbd_edpt_claim(p_itf->rhport, p_itf->ep_out)); - return usbd_edpt_xfer(p_itf->rhport, p_itf->ep_out, _vendord_epbuf[idx].epout, p_itf->rx_xfer_len, false); + // Non-FIFO mode (0 while an altsetting without a bulk OUT ep is active) + return vendord_ep_read_xfer(p_itf, p_itf->ep_out, _vendord_epbuf[idx].epout, p_itf->rx_xfer_len); #endif } #endif @@ -135,12 +253,8 @@ uint32_t tud_vendor_n_write(uint8_t idx, const void *buffer, uint32_t bufsize) { return tu_edpt_stream_write(&p_itf->tx_stream, buffer, (uint16_t)bufsize); #else - // non-fifo mode: direct transfer - TU_VERIFY(usbd_edpt_claim(p_itf->rhport, p_itf->ep_in), 0); - const uint32_t xact_len = tu_min32(bufsize, CFG_TUD_VENDOR_TX_EPSIZE); - memcpy(_vendord_epbuf[idx].epin, buffer, xact_len); - TU_ASSERT(usbd_edpt_xfer(p_itf->rhport, p_itf->ep_in, _vendord_epbuf[idx].epin, (uint16_t)xact_len, false), 0); - return xact_len; + // non-fifo mode: direct transfer (ep_in is 0 while an altsetting without a bulk IN ep is active) + return vendord_ep_write(p_itf, p_itf->ep_in, _vendord_epbuf[idx].epin, CFG_TUD_VENDOR_TX_EPSIZE, buffer, bufsize); #endif } @@ -152,9 +266,7 @@ uint32_t tud_vendor_n_write_available(uint8_t idx) { return tu_edpt_stream_write_available(&p_itf->tx_stream); #else - // Non-FIFO mode - TU_VERIFY(p_itf->ep_in > 0, 0); // must be opened - return usbd_edpt_busy(p_itf->rhport, p_itf->ep_in) ? 0 : CFG_TUD_VENDOR_TX_EPSIZE; + return vendord_ep_write_available(p_itf, p_itf->ep_in, CFG_TUD_VENDOR_TX_EPSIZE); #endif } @@ -173,6 +285,63 @@ bool tud_vendor_n_write_clear(uint8_t idx) { } #endif +//--------------------------------------------------------------------+ +// Interrupt endpoint API +//--------------------------------------------------------------------+ +#if CFG_TUD_VENDOR_EP_INT_OUT +bool tud_vendor_n_int_read_xfer(uint8_t idx) { + TU_VERIFY(idx < CFG_TUD_VENDOR); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_read_xfer(p_itf, p_itf->ep_int_out, _vendord_int_epbuf[idx].int_out, p_itf->int_rx_xfer_len); +} +#endif + +#if CFG_TUD_VENDOR_EP_INT_IN +uint32_t tud_vendor_n_int_write(uint8_t idx, const void *buffer, uint32_t bufsize) { + TU_VERIFY(idx < CFG_TUD_VENDOR, 0); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_write(p_itf, p_itf->ep_int_in, _vendord_int_epbuf[idx].int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, buffer, bufsize); +} + +uint32_t tud_vendor_n_int_write_available(uint8_t idx) { + TU_VERIFY(idx < CFG_TUD_VENDOR, 0); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_write_available(p_itf, p_itf->ep_int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE); +} +#endif + +//--------------------------------------------------------------------+ +// Isochronous endpoint API +//--------------------------------------------------------------------+ +#if CFG_TUD_VENDOR_EP_ISO_OUT +bool tud_vendor_n_iso_read_xfer(uint8_t idx) { + TU_VERIFY(idx < CFG_TUD_VENDOR); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_read_xfer(p_itf, p_itf->ep_iso_out, _vendord_iso_epbuf[idx].iso_out, p_itf->iso_rx_xfer_len); +} +#endif + +#if CFG_TUD_VENDOR_EP_ISO_IN +uint32_t tud_vendor_n_iso_write(uint8_t idx, const void *buffer, uint32_t bufsize) { + TU_VERIFY(idx < CFG_TUD_VENDOR, 0); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_write(p_itf, p_itf->ep_iso_in, _vendord_iso_epbuf[idx].iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, buffer, bufsize); +} + +uint32_t tud_vendor_n_iso_write_available(uint8_t idx) { + TU_VERIFY(idx < CFG_TUD_VENDOR, 0); + vendord_interface_t *p_itf = &_vendord_itf[idx]; + return vendord_ep_write_available(p_itf, p_itf->ep_iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE); +} +#endif + +#if CFG_TUD_VENDOR_ALT_SETTINGS +uint8_t tud_vendor_n_alt(uint8_t idx) { + TU_VERIFY(idx < CFG_TUD_VENDOR, 0); + return _vendord_itf[idx].cur_alt; +} +#endif + //--------------------------------------------------------------------+ // USBD Driver API //--------------------------------------------------------------------+ @@ -232,17 +401,61 @@ static uint8_t find_vendor_itf(uint8_t ep_addr) { for (uint8_t idx = 0; idx < CFG_TUD_VENDOR; idx++) { const vendord_interface_t *p_vendor = &_vendord_itf[idx]; if (ep_addr == 0) { - // find unused: require both ep == 0 - #if CFG_TUD_VENDOR_TXRX_BUFFERED - if (p_vendor->rx_stream.ep_addr == 0 && p_vendor->tx_stream.ep_addr == 0) { + // find unused interface slot + #if CFG_TUD_VENDOR_ALT_SETTINGS + // an opened interface parked in an altsetting without endpoints (the mandatory empty alt 0) + // has all ep fields 0, so the endpoint fields cannot distinguish free from open: use p_itf_desc + if (p_vendor->p_itf_desc == NULL) { + return idx; + } + #elif CFG_TUD_VENDOR_TXRX_BUFFERED + // A slot is free only if none of its endpoints are assigned; bulk may be absent + // (an interrupt-only vendor interface), so check the interrupt endpoints too. + if (p_vendor->rx_stream.ep_addr == 0 && p_vendor->tx_stream.ep_addr == 0 + #if CFG_TUD_VENDOR_EP_INT_OUT + && p_vendor->ep_int_out == 0 + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + && p_vendor->ep_int_in == 0 + #endif + ) { return idx; } #else - if (p_vendor->ep_out == 0 && p_vendor->ep_in == 0) { + // A slot is free only if none of its endpoints are assigned. Bulk may be absent (an + // interrupt-only vendor interface), so the interrupt endpoints must be checked too. + if (p_vendor->ep_out == 0 && p_vendor->ep_in == 0 + #if CFG_TUD_VENDOR_EP_INT_OUT + && p_vendor->ep_int_out == 0 + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + && p_vendor->ep_int_in == 0 + #endif + ) { return idx; } #endif } else { + #if CFG_TUD_VENDOR_EP_INT_OUT + if (ep_addr == p_vendor->ep_int_out) { + return idx; + } + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + if (ep_addr == p_vendor->ep_int_in) { + return idx; + } + #endif + #if CFG_TUD_VENDOR_EP_ISO_OUT + if (ep_addr == p_vendor->ep_iso_out) { + return idx; + } + #endif + #if CFG_TUD_VENDOR_EP_ISO_IN + if (ep_addr == p_vendor->ep_iso_in) { + return idx; + } + #endif #if CFG_TUD_VENDOR_TXRX_BUFFERED if (ep_addr == p_vendor->rx_stream.ep_addr || ep_addr == p_vendor->tx_stream.ep_addr) { return idx; @@ -257,6 +470,266 @@ static uint8_t find_vendor_itf(uint8_t ep_addr) { return 0xff; } +#if CFG_TUD_VENDOR_ALT_SETTINGS + +// Reserve an isochronous endpoint at open time. Ports with a dedicated iso allocator +// (TUP_DCD_EDPT_ISO_ALLOC) reserve the FIFO here and (re)activate on altsetting selection; +// ports with dcd_edpt_close instead open it once here (open == allocate + activate). +static inline bool vendord_iso_ep_alloc(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) { + #ifdef TUP_DCD_EDPT_ISO_ALLOC + return usbd_edpt_iso_alloc(rhport, desc_ep->bEndpointAddress, tu_edpt_packet_size(desc_ep)); + #else + return usbd_edpt_open(rhport, desc_ep); + #endif +} + +// (Re)activate an isochronous endpoint on altsetting selection. +static inline bool vendord_iso_ep_activate(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) { + #ifdef TUP_DCD_EDPT_ISO_ALLOC + return usbd_edpt_iso_activate(rhport, desc_ep); // resets ep_status, aborting any stale transfer + #else + // No iso alloc/activate API: close (which zeros ep_status and frees a stale claim left by a + // prior selection) then re-open, so a re-selected altsetting starts from a clean state. + usbd_edpt_close(rhport, desc_ep->bEndpointAddress); + return usbd_edpt_open(rhport, desc_ep); + #endif +} + +// Abort any in-flight transfer on a tracked endpoint and release its usbd claim; no-op for an +// unset (0) address. stall disables the endpoint in the dcd, clear-stall resets it to DATA0. +static inline void vendord_abort_ep(uint8_t rhport, uint8_t ep_addr) { + if (ep_addr) { + usbd_edpt_stall(rhport, ep_addr); + usbd_edpt_clear_stall(rhport, ep_addr); + } +} + +// Select an altsetting. Endpoints were hardware-opened once at vendord_open (dcds like +// dwc2 allocate FIFO linearly and cannot close/re-open endpoints dynamically): switching +// only re-targets the API to the selected altsetting's endpoints. Bulk/interrupt +// endpoints get a stall/clear-stall cycle, which portably aborts any in-flight transfer +// (stall disables the endpoint in the dcd) and resets the data toggle to DATA0 as +// SET_INTERFACE requires. Isochronous endpoints are (re)activated, which does the same. +// Single pass: the current altsetting's endpoints are dropped only once the target altsetting +// is confirmed present, so a SET_INTERFACE to an unknown alt leaves the interface intact. +static bool vendord_set_alt(uint8_t rhport, uint8_t idx, uint8_t alt) { + vendord_interface_t *p_vendor = &_vendord_itf[idx]; + const uint8_t* p_desc = p_vendor->p_itf_desc; + const uint8_t* desc_end = p_desc + p_vendor->itf_desc_len; + bool in_target_alt = false; + bool alt_found = false; + + while (tu_desc_in_bounds(p_desc, desc_end)) { + const uint8_t desc_type = tu_desc_type(p_desc); + if (desc_type == TUSB_DESC_INTERFACE) { + in_target_alt = (((const tusb_desc_interface_t*)p_desc)->bAlternateSetting == alt); + if (in_target_alt && !alt_found) { + alt_found = true; + // target altsetting confirmed present: abort then drop the previous altsetting's endpoints, + // so a bulk/interrupt endpoint absent from the target altsetting can't stay armed and keep + // its usbd claim in the dcd. (Endpoints the target altsetting reuses are reset again below; + // a double reset is harmless. Iso endpoints are re-activated on reselection.) + vendord_abort_ep(rhport, p_vendor->ep_in); + vendord_abort_ep(rhport, p_vendor->ep_out); + #if CFG_TUD_VENDOR_EP_INT_OUT + vendord_abort_ep(rhport, p_vendor->ep_int_out); + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + vendord_abort_ep(rhport, p_vendor->ep_int_in); + #endif + p_vendor->ep_in = 0; + p_vendor->ep_out = 0; + #if CFG_TUD_VENDOR_EP_INT_OUT + p_vendor->ep_int_out = 0; + #endif + #if CFG_TUD_VENDOR_EP_INT_IN + p_vendor->ep_int_in = 0; + #endif + #if CFG_TUD_VENDOR_EP_ISO_OUT + p_vendor->ep_iso_out = 0; + #endif + #if CFG_TUD_VENDOR_EP_ISO_IN + p_vendor->ep_iso_in = 0; + #endif + } + } else if (in_target_alt && desc_type == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc; + const uint8_t ep_addr = desc_ep->bEndpointAddress; + const bool is_in = tu_edpt_dir(ep_addr) == TUSB_DIR_IN; + (void) is_in; + + switch (desc_ep->bmAttributes.xfer) { + case TUSB_XFER_BULK: + // abort in-flight transfer + reset data toggle + usbd_edpt_stall(rhport, ep_addr); + usbd_edpt_clear_stall(rhport, ep_addr); + if (is_in) { + p_vendor->ep_in = ep_addr; + } else { + p_vendor->ep_out = ep_addr; + p_vendor->rx_xfer_len = + CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep); + #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0 + TU_ASSERT(usbd_edpt_xfer(rhport, p_vendor->ep_out, _vendord_epbuf[idx].epout, + p_vendor->rx_xfer_len, false)); + #endif + } + break; + + #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT + case TUSB_XFER_INTERRUPT: + // stall/clear only for the enabled direction (an endpoint of a disabled + // direction was never opened, so must not be poked in the dcd) + #if CFG_TUD_VENDOR_EP_INT_IN + if (is_in) { + usbd_edpt_stall(rhport, ep_addr); + usbd_edpt_clear_stall(rhport, ep_addr); + p_vendor->ep_int_in = ep_addr; + } + #endif + #if CFG_TUD_VENDOR_EP_INT_OUT + if (!is_in) { + usbd_edpt_stall(rhport, ep_addr); + usbd_edpt_clear_stall(rhport, ep_addr); + p_vendor->ep_int_out = ep_addr; + p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep); + } + #endif + break; + #endif + + #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT + case TUSB_XFER_ISOCHRONOUS: + #if CFG_TUD_VENDOR_EP_ISO_IN + if (is_in) { + TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep)); + p_vendor->ep_iso_in = ep_addr; + } + #endif + #if CFG_TUD_VENDOR_EP_ISO_OUT + if (!is_in) { + TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep)); + p_vendor->ep_iso_out = ep_addr; + p_vendor->iso_rx_xfer_len = tu_edpt_packet_size(desc_ep); + } + #endif + break; + #endif + + default: + break; // unsupported endpoint type / direction gate disabled: ignore + } + } + p_desc = tu_desc_next(p_desc); + } + + TU_VERIFY(alt_found); // unknown alt: endpoints were never cleared, current altsetting intact + p_vendor->cur_alt = alt; + return true; +} + +uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0); + const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len; + + const uint8_t idx = find_vendor_itf(0); + TU_ASSERT(idx < CFG_TUD_VENDOR, 0); + vendord_interface_t *p_vendor = &_vendord_itf[idx]; + p_vendor->rhport = rhport; + p_vendor->itf_num = desc_itf->bInterfaceNumber; + // p_itf_desc is assigned only after the parse succeeds: find_vendor_itf() treats a non-NULL + // p_itf_desc as an occupied slot, so setting it before a mid-parse TU_ASSERT could fail would + // leak the slot (a retry would find no free interface until the next bus reset). + + // Consume every altsetting of this interface and hardware-open each endpoint exactly once + // (bulk/interrupt via usbd_edpt_open, isochronous FIFO-allocated; iso activation and the + // toggle reset happen on altsetting selection). An endpoint address that recurs in another + // altsetting must carry an identical configuration, since it is opened only on first sight; + // reconfiguring the same address per-alt is not supported and is rejected here. + uint8_t seen_type[CFG_TUD_ENDPPOINT_MAX][2]; + uint16_t seen_mps[CFG_TUD_ENDPPOINT_MAX][2]; + tu_memclr(seen_type, sizeof(seen_type)); // 0 == TUSB_XFER_CONTROL, never used as a data ep here + const uint8_t* p_desc = tu_desc_next(desc_itf); + while (tu_desc_in_bounds(p_desc, desc_end)) { + const uint8_t desc_type = tu_desc_type(p_desc); + if (desc_type == TUSB_DESC_INTERFACE_ASSOCIATION) { + break; + } + if (desc_type == TUSB_DESC_INTERFACE) { + if (((const tusb_desc_interface_t*)p_desc)->bInterfaceNumber != p_vendor->itf_num) { + break; // next interface + } + } else if (desc_type == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc; + const uint8_t epnum = tu_edpt_number(desc_ep->bEndpointAddress); + const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress); + const uint8_t xfer = desc_ep->bmAttributes.xfer; + const uint16_t mps = tu_edpt_packet_size(desc_ep); + TU_ASSERT(epnum < CFG_TUD_ENDPPOINT_MAX, 0); + + if (seen_type[epnum][dir] != 0) { + // reused address in a later altsetting: must be an exact match (opened only once) + TU_ASSERT(seen_type[epnum][dir] == xfer && seen_mps[epnum][dir] == mps, 0); + } else { + switch (xfer) { + case TUSB_XFER_BULK: + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + break; + + #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT + case TUSB_XFER_INTERRUPT: + #if CFG_TUD_VENDOR_EP_INT_IN + if (dir == TUSB_DIR_IN) { + TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0); + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + } + #endif + #if CFG_TUD_VENDOR_EP_INT_OUT + if (dir == TUSB_DIR_OUT) { + TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0); + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + } + #endif + break; + #endif + + #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT + case TUSB_XFER_ISOCHRONOUS: + #if CFG_TUD_VENDOR_EP_ISO_IN + if (dir == TUSB_DIR_IN) { + TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, 0); + TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0); + } + #endif + #if CFG_TUD_VENDOR_EP_ISO_OUT + if (dir == TUSB_DIR_OUT) { + TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE, 0); + TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0); + } + #endif + break; + #endif + + default: + break; // unsupported endpoint type / direction gate disabled: ignore + } + seen_type[epnum][dir] = xfer; + seen_mps[epnum][dir] = mps; + } + } + p_desc = tu_desc_next(p_desc); + } + // parse succeeded: commit the descriptor pointer (marks the slot occupied) before selecting alt 0 + p_vendor->p_itf_desc = (const uint8_t*) desc_itf; + p_vendor->itf_desc_len = (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf); + + // default altsetting active until the host selects another + TU_ASSERT(vendord_set_alt(rhport, idx, 0), 0); + return p_vendor->itf_desc_len; +} + +#else // !CFG_TUD_VENDOR_ALT_SETTINGS + uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0); const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len; @@ -275,6 +748,31 @@ uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uin break; // end of this interface } else if (desc_type == TUSB_DESC_ENDPOINT) { const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc; + + #if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN + if (desc_ep->bmAttributes.xfer == TUSB_XFER_INTERRUPT) { + const bool is_int_in = tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN; + (void) is_int_in; + #if CFG_TUD_VENDOR_EP_INT_IN + if (is_int_in) { + TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0); + TU_ASSERT(usbd_edpt_open(rhport, desc_ep)); + p_vendor->ep_int_in = desc_ep->bEndpointAddress; + } + #endif + #if CFG_TUD_VENDOR_EP_INT_OUT + if (!is_int_in) { + TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0); + TU_ASSERT(usbd_edpt_open(rhport, desc_ep)); + p_vendor->ep_int_out = desc_ep->bEndpointAddress; + p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep); + } + #endif + p_desc = tu_desc_next(p_desc); + continue; + } + #endif + TU_ASSERT(usbd_edpt_open(rhport, desc_ep)); uint16_t rx_xfer_len = CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep); @@ -313,6 +811,40 @@ uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uin return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf); } +#endif // CFG_TUD_VENDOR_ALT_SETTINGS + +// Handle interface standard requests (GET/SET_INTERFACE when altsettings are enabled), +// delegate everything else to the application callback as before. +bool vendord_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { +#if CFG_TUD_VENDOR_ALT_SETTINGS + if (request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && + request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_INTERFACE) { + const uint8_t itf_num = tu_u16_low(request->wIndex); + uint8_t idx; + for (idx = 0; idx < CFG_TUD_VENDOR; idx++) { + if (_vendord_itf[idx].itf_num == itf_num && _vendord_itf[idx].p_itf_desc != NULL) { + break; + } + } + if (idx < CFG_TUD_VENDOR) { + if (request->bRequest == TUSB_REQ_SET_INTERFACE) { + if (stage == CONTROL_STAGE_SETUP) { + TU_VERIFY(vendord_set_alt(rhport, idx, tu_u16_low(request->wValue))); + return tud_control_status(rhport, request); + } + return true; + } else if (request->bRequest == TUSB_REQ_GET_INTERFACE) { + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, &_vendord_itf[idx].cur_alt, 1); + } + return true; + } + } + } +#endif + return tud_vendor_control_xfer_cb(rhport, stage, request); +} + bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { (void)rhport; (void)result; @@ -320,6 +852,33 @@ bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint TU_VERIFY(idx < CFG_TUD_VENDOR); vendord_interface_t *p_vendor = &_vendord_itf[idx]; +#if CFG_TUD_VENDOR_EP_INT_OUT + if (ep_addr == p_vendor->ep_int_out) { + // not re-armed automatically: application calls tud_vendor_n_int_read_xfer() + tud_vendor_int_rx_cb(idx, _vendord_int_epbuf[idx].int_out, xferred_bytes); + return true; + } +#endif +#if CFG_TUD_VENDOR_EP_INT_IN + if (ep_addr == p_vendor->ep_int_in) { + tud_vendor_int_tx_cb(idx, xferred_bytes); + return true; + } +#endif +#if CFG_TUD_VENDOR_EP_ISO_OUT + if (ep_addr == p_vendor->ep_iso_out) { + // not re-armed automatically: application calls tud_vendor_n_iso_read_xfer() + tud_vendor_iso_rx_cb(idx, _vendord_iso_epbuf[idx].iso_out, xferred_bytes); + return true; + } +#endif +#if CFG_TUD_VENDOR_EP_ISO_IN + if (ep_addr == p_vendor->ep_iso_in) { + tud_vendor_iso_tx_cb(idx, xferred_bytes); + return true; + } +#endif + #if CFG_TUD_VENDOR_TXRX_BUFFERED if (ep_addr == p_vendor->rx_stream.ep_addr) { // Put received data to FIFO diff --git a/src/class/vendor/vendor_device.h b/src/class/vendor/vendor_device.h index 32216f25fe..ce33e2f62f 100644 --- a/src/class/vendor/vendor_device.h +++ b/src/class/vendor/vendor_device.h @@ -60,6 +60,68 @@ extern "C" { #define CFG_TUD_VENDOR_RX_NEED_ZLP 0 #endif +// Enable support for an optional interrupt OUT / interrupt IN endpoint in the vendor +// interface, each direction gated separately. Interrupt endpoints are non-buffered: +// OUT is armed manually one packet at a time with tud_vendor_n_int_read_xfer() (data +// delivered via tud_vendor_int_rx_cb), IN is a direct transfer via tud_vendor_n_int_write(). +#ifndef CFG_TUD_VENDOR_EP_INT_OUT + #define CFG_TUD_VENDOR_EP_INT_OUT 0 +#endif + +#ifndef CFG_TUD_VENDOR_EP_INT_IN + #define CFG_TUD_VENDOR_EP_INT_IN 0 +#endif + +// Buffer sizes for interrupt endpoint transfers, must be >= the endpoint max packet size +#ifndef CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE + #define CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE 64 +#endif + +#ifndef CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE + #define CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE 64 +#endif + +// Enable support for an optional isochronous OUT / IN endpoint, each direction gated +// separately, with the same non-buffered API shape as the interrupt pair. Isochronous +// endpoints must not claim bandwidth in the default altsetting (USB 2.0 5.6.3): place +// them in a non-zero altsetting and enable CFG_TUD_VENDOR_ALT_SETTINGS. +#ifndef CFG_TUD_VENDOR_EP_ISO_OUT + #define CFG_TUD_VENDOR_EP_ISO_OUT 0 +#endif + +#ifndef CFG_TUD_VENDOR_EP_ISO_IN + #define CFG_TUD_VENDOR_EP_ISO_IN 0 +#endif + +// Buffer sizes for isochronous endpoint transfers, must be >= the endpoint max packet size +#ifndef CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE + #define CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE 64 +#endif + +#ifndef CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE + #define CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE 64 +#endif + +// Enable alternate-setting support: the vendor interface may carry multiple altsettings, +// each with its own endpoint set. GET_INTERFACE is answered and SET_INTERFACE performed by +// closing the current altsetting's endpoints and opening the requested one's (isochronous +// endpoints are FIFO-allocated at open and activated on selection). The configuration +// descriptor must stay valid while mounted (static, the usual TinyUSB pattern). +// Non-buffered mode only. +#ifndef CFG_TUD_VENDOR_ALT_SETTINGS + #define CFG_TUD_VENDOR_ALT_SETTINGS 0 +#endif + +#if CFG_TUD_VENDOR_ALT_SETTINGS && CFG_TUD_VENDOR_TXRX_BUFFERED + #error CFG_TUD_VENDOR_ALT_SETTINGS requires non-buffered mode (CFG_TUD_VENDOR_RX/TX_BUFSIZE = 0) +#endif + +// An isochronous endpoint must not claim bandwidth in the default altsetting (USB 2.0 5.6.3), +// so it can only live in a non-zero altsetting, which requires alternate-setting support. +#if (CFG_TUD_VENDOR_EP_ISO_OUT || CFG_TUD_VENDOR_EP_ISO_IN) && !CFG_TUD_VENDOR_ALT_SETTINGS + #error CFG_TUD_VENDOR_EP_ISO_OUT/IN requires CFG_TUD_VENDOR_ALT_SETTINGS +#endif + //--------------------------------------------------------------------+ // Application API (Multiple Interfaces) i.e CFG_TUD_VENDOR > 1 //--------------------------------------------------------------------+ @@ -107,6 +169,43 @@ TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_n_write_str(uint8_t idx, return tud_vendor_n_write(idx, str, strlen(str)); } +//------------- Interrupt endpoints -------------// +#if CFG_TUD_VENDOR_EP_INT_OUT +// Arm the interrupt OUT endpoint for one packet, return false if a transfer is still ongoing. +// Received data is delivered via tud_vendor_int_rx_cb(); re-arm from the callback or by polling. +bool tud_vendor_n_int_read_xfer(uint8_t idx); +#endif + +#if CFG_TUD_VENDOR_EP_INT_IN +// Send on the interrupt IN endpoint (direct transfer, up to CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE +// bytes). Returns number of bytes queued, 0 if the endpoint is busy or not opened. +uint32_t tud_vendor_n_int_write(uint8_t idx, const void *buffer, uint32_t bufsize); + +// Return available bytes for interrupt IN write: 0 while busy, else the buffer size +uint32_t tud_vendor_n_int_write_available(uint8_t idx); +#endif + +//------------- Isochronous endpoints -------------// +#if CFG_TUD_VENDOR_EP_ISO_OUT +// Arm the isochronous OUT endpoint for one packet, return false if a transfer is still ongoing. +// Received data is delivered via tud_vendor_iso_rx_cb(); re-arm from the callback or by polling. +bool tud_vendor_n_iso_read_xfer(uint8_t idx); +#endif + +#if CFG_TUD_VENDOR_EP_ISO_IN +// Send on the isochronous IN endpoint (direct transfer, up to CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE +// bytes). Returns number of bytes queued, 0 if the endpoint is busy or not opened. +uint32_t tud_vendor_n_iso_write(uint8_t idx, const void *buffer, uint32_t bufsize); + +// Return available bytes for isochronous IN write: 0 while busy, else the buffer size +uint32_t tud_vendor_n_iso_write_available(uint8_t idx); +#endif + +#if CFG_TUD_VENDOR_ALT_SETTINGS +// Return the currently selected alternate setting +uint8_t tud_vendor_n_alt(uint8_t idx); +#endif + // backward compatible #define tud_vendor_n_flush(idx) tud_vendor_n_write_flush(idx) @@ -161,6 +260,44 @@ TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_write_available(void) { return tud_vendor_n_write_available(0); } +#if CFG_TUD_VENDOR_EP_INT_OUT +TU_ATTR_ALWAYS_INLINE static inline bool tud_vendor_int_read_xfer(void) { + return tud_vendor_n_int_read_xfer(0); +} +#endif + +#if CFG_TUD_VENDOR_EP_INT_IN +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_int_write(const void *buffer, uint32_t bufsize) { + return tud_vendor_n_int_write(0, buffer, bufsize); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_int_write_available(void) { + return tud_vendor_n_int_write_available(0); +} +#endif + +#if CFG_TUD_VENDOR_EP_ISO_OUT +TU_ATTR_ALWAYS_INLINE static inline bool tud_vendor_iso_read_xfer(void) { + return tud_vendor_n_iso_read_xfer(0); +} +#endif + +#if CFG_TUD_VENDOR_EP_ISO_IN +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_iso_write(const void *buffer, uint32_t bufsize) { + return tud_vendor_n_iso_write(0, buffer, bufsize); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_iso_write_available(void) { + return tud_vendor_n_iso_write_available(0); +} +#endif + +#if CFG_TUD_VENDOR_ALT_SETTINGS +TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_vendor_alt(void) { + return tud_vendor_n_alt(0); +} +#endif + // backward compatible #define tud_vendor_flush() tud_vendor_write_flush() @@ -176,6 +313,29 @@ void tud_vendor_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize); // Invoked when tx transfer is finished void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes); +#if CFG_TUD_VENDOR_EP_INT_OUT +// Invoked when data is received on the interrupt OUT endpoint. The endpoint is not +// re-armed automatically: call tud_vendor_n_int_read_xfer() to receive more. +void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize); +#endif + +#if CFG_TUD_VENDOR_EP_INT_IN +// Invoked when an interrupt IN transfer is finished +void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes); +#endif + +#if CFG_TUD_VENDOR_EP_ISO_OUT +// Invoked when data is received on the isochronous OUT endpoint. The endpoint is not +// re-armed automatically: call tud_vendor_n_iso_read_xfer() to receive more. +void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize); +#endif + +#if CFG_TUD_VENDOR_EP_ISO_IN +// Invoked when an isochronous IN transfer is finished (result may be a missed frame: +// the data was not necessarily taken by the host, re-arm regardless) +void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes); +#endif + //--------------------------------------------------------------------+ // Internal Class Driver API //--------------------------------------------------------------------+ @@ -183,6 +343,7 @@ void vendord_init(void); bool vendord_deinit(void); void vendord_reset(uint8_t rhport); uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *idx_desc, uint16_t max_len); +bool vendord_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request); bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes); #ifdef __cplusplus diff --git a/src/common/tusb_mcu.h b/src/common/tusb_mcu.h index 1f2afb03a0..0959ac3a91 100644 --- a/src/common/tusb_mcu.h +++ b/src/common/tusb_mcu.h @@ -141,7 +141,6 @@ #elif TU_CHECK_MCU(OPT_MCU_NRF5X) // 8 CBI + 1 ISO #define TUP_DCD_ENDPOINT_MAX 9 - #define TUP_DCD_EDPT_CLOSE_API #elif TU_CHECK_MCU(OPT_MCU_NRF54) #define TUP_USBIP_DWC2 @@ -741,11 +740,9 @@ #define TU_ATTR_FAST_FUNC #endif -#if defined(TUP_USBIP_IP3511) || defined(TUP_USBIP_RUSB2) - #define TUP_DCD_EDPT_CLOSE_API -#endif - -// USBIP implement dcd_edpt_close() and does not support ISO alloc & activate API +// TUP_DCD_EDPT_CLOSE_API is deprecated: these USBIPs implement dcd_edpt_close() and lack the +// ISO alloc & activate API. IP3511, RUSB2 and NRF5X have been migrated to ISO_ALLOC; the remaining +// CLOSE_API MCUs (mm32, pic, da1469x, f1c100s, ch32-usbhs) are pending per-board verification. #ifndef TUP_DCD_EDPT_CLOSE_API #define TUP_DCD_EDPT_ISO_ALLOC #endif diff --git a/src/device/usbd.c b/src/device/usbd.c index 7a6e13f8d4..5471e132db 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -256,7 +256,7 @@ static const usbd_class_driver_t _usbd_driver[] = { .deinit = vendord_deinit, .reset = vendord_reset, .open = vendord_open, - .control_xfer_cb = tud_vendor_control_xfer_cb, + .control_xfer_cb = vendord_control_xfer_cb, .xfer_cb = vendord_xfer_cb, .xfer_isr = NULL, .sof = NULL @@ -418,6 +418,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(dcd_event_t const * event, //--------------------------------------------------------------------+ static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request); +static bool process_get_status(uint8_t rhport, tusb_control_request_t const * request, uint16_t status); static bool process_set_config(uint8_t rhport, uint8_t cfg_num); static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const * p_request); @@ -1041,9 +1042,7 @@ static bool process_std_device_request(uint8_t rhport, tusb_control_request_t co // Device status bit mask // - Bit 0: Self Powered TODO must invoke callback to get actual status // - Bit 1: Remote Wakeup enabled - uint16_t status = (uint16_t) _usbd_dev.dev_state_bm; - tud_control_xfer(rhport, p_request, &status, 2); - return true; + return process_get_status(rhport, p_request, (uint16_t) _usbd_dev.dev_state_bm); } default: @@ -1053,6 +1052,14 @@ static bool process_std_device_request(uint8_t rhport, tusb_control_request_t co } +// Reply to a standard GET_STATUS (device/interface/endpoint) with its 2-byte status word. +// GET_STATUS is Device-to-host only; reject a mis-directed (OUT) request rather than handing +// usbd the address of a stack local to write host data into after this frame has returned. +static bool process_get_status(uint8_t rhport, tusb_control_request_t const * request, uint16_t status) { + TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_IN); + return tud_control_xfer(rhport, request, &status, 2); +} + // This handles the actual request and its response. // Returns false if unable to complete the request, causing caller to stall control endpoints. static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request) { @@ -1150,9 +1157,18 @@ static bool process_setup_received(uint8_t rhport, tusb_control_request_t const } case TUSB_REQ_SET_INTERFACE: + // A class that implements altsettings handles SET_INTERFACE itself and returns true, + // so reaching here means the class does not — where only alt 0 is valid. Any non-zero + // alt (unimplemented, or rejected as invalid by the class) is a Request Error (stall). + TU_VERIFY(tu_u16_low(p_request->wValue) == 0); tud_control_status(rhport, p_request); break; + case TUSB_REQ_GET_STATUS: + // USB 2.0 9.4.5: interface GET_STATUS returns 2 reserved (zero) bytes + TU_VERIFY(process_get_status(rhport, p_request, 0x0000)); + break; + default: return false; } } @@ -1175,35 +1191,34 @@ static bool process_setup_received(uint8_t rhport, tusb_control_request_t const } else { // Handle STD request to endpoint switch (p_request->bRequest) { //-V2520 - case TUSB_REQ_GET_STATUS: { - uint16_t status = usbd_edpt_stalled(rhport, ep_addr) ? 0x0001u : 0x0000u; - tud_control_xfer(rhport, p_request, &status, 2); - } - break; + case TUSB_REQ_GET_STATUS: + // USB 2.0 9.4.5: endpoint GET_STATUS bit 0 = Halt + TU_VERIFY(process_get_status(rhport, p_request, usbd_edpt_stalled(rhport, ep_addr) ? 0x0001u : 0x0000u)); + break; case TUSB_REQ_CLEAR_FEATURE: case TUSB_REQ_SET_FEATURE: { - if ( TUSB_REQ_FEATURE_EDPT_HALT == p_request->wValue ) { - if ( TUSB_REQ_CLEAR_FEATURE == p_request->bRequest ) { - usbd_edpt_clear_stall(rhport, ep_addr); - }else { - usbd_edpt_stall(rhport, ep_addr); - } + // ENDPOINT_HALT is the only endpoint feature; it exists only on a non-control endpoint + // that an interface actually owns. Any other selector, the control endpoint (EP0 has no + // Halt feature, USB 2.0 9.4.9), or an endpoint no driver owns is a Request Error (stall). + TU_VERIFY(TUSB_REQ_FEATURE_EDPT_HALT == p_request->wValue); + TU_VERIFY(ep_num != 0); + TU_VERIFY(driver != NULL); + + if ( TUSB_REQ_CLEAR_FEATURE == p_request->bRequest ) { + usbd_edpt_clear_stall(rhport, ep_addr); + } else { + usbd_edpt_stall(rhport, ep_addr); } - if (driver != NULL) { - // Some classes such as USBTMC needs to clear/re-init its buffer when receiving CLEAR_FEATURE request - // We will also forward std request targeted endpoint to class drivers as well + // Some classes such as USBTMC need to clear/re-init their buffer on CLEAR_FEATURE. + // Clear complete callback if driver set since it can also stall the request. + (void) invoke_class_control(rhport, driver, p_request); + ctrl_xfer->complete_cb = NULL; - // STD request must always be ACKed regardless of driver returned value - // Also clear complete callback if driver set since it can also stall the request. - (void) invoke_class_control(rhport, driver, p_request); - ctrl_xfer->complete_cb = NULL; - - // skip ZLP status if driver already did that - if (!(_usbd_dev.ep_status[0][TUSB_DIR_IN] & TU_EDPT_STATE_BUSY)) { - tud_control_status(rhport, p_request); - } + // STD request must always be ACKed; skip ZLP status if driver already did that. + if (!(_usbd_dev.ep_status[0][TUSB_DIR_IN] & TU_EDPT_STATE_BUSY)) { + tud_control_status(rhport, p_request); } } break; @@ -1648,10 +1663,21 @@ void usbd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - // only clear if currently stalled TU_LOG_USBD(" Clear Stall EP %02X\r\n", ep_addr); + const bool was_stalled = (_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_STALLED) != 0; dcd_edpt_clear_stall(rhport, ep_addr); - _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY); + // Clear STALLED|BUSY unconditionally (long-standing behavior; some classes, e.g. audio's + // set-interface, call this on a non-stalled endpoint solely to drop a leftover BUSY bit). + // Only release the CLAIMED ownership bit when the endpoint was actually stalled: the stall + // aborts the in-flight transfer in the dcd with no completion event to release the claim, so + // clearing it here prevents starvation. On a non-stalled clear (e.g. a data-toggle reset) a + // transfer may still be legitimately claimed by another task, so keep CLAIMED to preserve the + // claim->xfer mutual exclusion. + uint8_t clear_mask = TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY; + if (was_stalled) { + clear_mask |= TU_EDPT_STATE_CLAIMED; + } + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~clear_mask; } bool usbd_edpt_stalled(uint8_t rhport, uint8_t ep_addr) { diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c index 55906e6788..fa98d6882d 100644 --- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c +++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c @@ -393,7 +393,8 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); dcd_reg->ENDPTCTRL[epnum] |= ENDPTCTRL_STALL << (dir ? 16 : 0); - // flush to abort any primed buffer + // flush to abort any primed buffer; the aborted transfer's dQH overlay can be left + // ACTIVE with mid-transfer state - qhd_start_xfer clears it before the next prime dcd_reg->ENDPTFLUSH = TU_BIT(epnum + (dir ? 16 : 0)); } @@ -497,6 +498,7 @@ static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) { dcd_qtd_t *p_qtd = &_dcd_data.qtd[epnum][dir]; p_qhd->qtd_overlay.halted = false; // clear any previous error + p_qhd->qtd_overlay.active = false; // a flushed prime leaves stale ACTIVE state; clear it so the fresh qtd loads p_qhd->qtd_overlay.next = (uint32_t)p_qtd; // link qtd to qhd // flush cache diff --git a/src/portable/mentor/musb/dcd_musb.c b/src/portable/mentor/musb/dcd_musb.c index 1ebd1fe027..17993f23ac 100644 --- a/src/portable/mentor/musb/dcd_musb.c +++ b/src/portable/mentor/musb/dcd_musb.c @@ -292,6 +292,12 @@ static void process_epin_isr(uint8_t rhport, musb_regs_t *musb_regs, uint8_t epn } pipe_state_t* pipe = pipe_get(epnum, TUSB_DIR_IN); + // No active transfer: a halt/abort disarmed the pipe (armed=false) but may leave remaining>0. + // Do not keep loading the aborted transfer — that would re-fill the just-flushed FIFO and the + // next (re-armed) transfer's data would stack on top (host sees an oversized packet -> babble). + if (!pipe->armed) { + return; + } if (pipe->remaining > 0) { pipe_write(musb_regs, pipe, epnum); } else { @@ -910,6 +916,10 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { } else { const tusb_dir_t ep_dir = tu_edpt_dir(ep_addr); const uint8_t is_rx = (ep_dir == TUSB_DIR_OUT ? 1u : 0u); + // A halt aborts the transfer: flush staged FIFO packet(s) before stalling, else leftover TX data + // concatenates with the next transfer after un-halt -> host sees an oversized packet (babble). + // FLUSH must precede SEND_STALL, which clears the TXRDY that hwfifo_flush() gates on. + hwfifo_flush(musb_regs, epn, is_rx, false); ep_csr->maxp_csr[is_rx].csrl = MUSB_CSRL_SEND_STALL(is_rx); pipe_state_t* pipe = pipe_get(epn, ep_dir); pipe->armed = false; diff --git a/src/portable/microchip/samd/dcd_samd.c b/src/portable/microchip/samd/dcd_samd.c index 32ddd3422c..54ef34c8ea 100644 --- a/src/portable/microchip/samd/dcd_samd.c +++ b/src/portable/microchip/samd/dcd_samd.c @@ -229,15 +229,49 @@ bool dcd_edpt_open (uint8_t rhport, tusb_desc_endpoint_t const * desc_edpt) bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { (void) rhport; - (void) ep_addr; - (void)largest_packet_size; - return false; + uint8_t const epnum = tu_edpt_number(ep_addr); + uint8_t const dir = tu_edpt_dir(ep_addr); + + // Reserve the endpoint bank with the largest packet size (persists across altsettings). The + // buffer address/count are filled per-transfer in dcd_edpt_xfer; only the SIZE bucket is fixed. + UsbDeviceDescBank* bank = &sram_registers[epnum][dir]; + uint32_t size_value = 0; + while (size_value < 7) { + if (1 << (size_value + 3) >= largest_packet_size) { + break; + } + size_value++; + } + if ( size_value == 7 && largest_packet_size > 1023 ) return false; + + bank->PCKSIZE.bit.SIZE = size_value; + return true; } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { (void)rhport; - (void)desc_ep; - return false; + uint8_t const epnum = tu_edpt_number(desc_ep->bEndpointAddress); + uint8_t const dir = tu_edpt_dir(desc_ep->bEndpointAddress); + + // Configure and enable the ISO endpoint on altsetting selection (bank SIZE already reserved by + // dcd_edpt_iso_alloc). Mirrors the per-direction setup in dcd_edpt_open(), plus a bank scrub: + // under ISO_ALLOC the EP is never disabled on alt0 (usbd_edpt_close is a no-op), so the bank-ready + // state from the previous streaming session survives into re-activation. Leave the EP un-armed so + // a stale bank can't move a packet before dcd_edpt_xfer re-arms it (a leftover BK1RDY with a stale + // BYTE_COUNT would otherwise babble on the first IN token after re-selecting alt1). + UsbDeviceEndpoint* ep = &USB->DEVICE.DeviceEndpoint[epnum]; + if ( dir == TUSB_DIR_OUT ) { + ep->EPCFG.bit.EPTYPE0 = desc_ep->bmAttributes.xfer + 1; + ep->EPSTATUSCLR.reg = USB_DEVICE_EPSTATUSCLR_STALLRQ0 | USB_DEVICE_EPSTATUSCLR_DTGLOUT; + ep->EPSTATUSSET.reg = USB_DEVICE_EPSTATUSSET_BK0RDY; // OUT: not ready to receive until armed + ep->EPINTENSET.bit.TRCPT0 = true; + } else { + ep->EPCFG.bit.EPTYPE1 = desc_ep->bmAttributes.xfer + 1; + ep->EPSTATUSCLR.reg = USB_DEVICE_EPSTATUSCLR_STALLRQ1 | USB_DEVICE_EPSTATUSCLR_DTGLIN | + USB_DEVICE_EPSTATUSCLR_BK1RDY; // IN: clear stale "loaded" bank + ep->EPINTENSET.bit.TRCPT1 = true; + } + return true; } void dcd_edpt_close_all (uint8_t rhport) diff --git a/src/portable/nordic/nrf5x/dcd_nrf5x.c b/src/portable/nordic/nrf5x/dcd_nrf5x.c index dc85ff78e7..5170e06452 100644 --- a/src/portable/nordic/nrf5x/dcd_nrf5x.c +++ b/src/portable/nordic/nrf5x/dcd_nrf5x.c @@ -122,8 +122,22 @@ TU_ATTR_ALWAYS_INLINE static inline bool is_in_isr(void) { return (SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) ? true : false; } +// Errata 199 "USBD cannot receive tasks during DMA": while an EasyDMA transfer is in progress the +// controller may drop an incoming SETUP/IN/OUT token (lost event -> stuck EP0, esp. under rapid +// back-to-back control transfers). The workaround latches an undocumented "DMA in progress" test +// register (0x40027C1C) so tokens are held instead. Gated on the anomaly being present (all +// nRF52840 revisions; absent on other nRF52 parts). Mirrors nrfx usbd_dma_pending_set/clear(). +#define NRF_USBD_ERRATA_199_REG (*((volatile uint32_t*) 0x40027C1CUL)) + // helper to start DMA static void start_dma(volatile uint32_t* reg_startep) { + // EP0STATUS / EP0RCVOUT take the EasyDMA slot but do not transfer data, so no ERRATA-199 latch. + const bool no_dma = (reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT); + + if (!no_dma && nrf52_errata_199()) { + NRF_USBD_ERRATA_199_REG = 0x00000082UL; + } + (*reg_startep) = 1; __ISB(); __DSB(); @@ -131,7 +145,7 @@ static void start_dma(volatile uint32_t* reg_startep) { // TASKS_EP0STATUS, TASKS_EP0RCVOUT seem to need EasyDMA to be available // However these don't trigger any DMA transfer and got ENDED event subsequently // Therefore dma_pending is corrected right away - if ((reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT)) { + if (no_dma) { atomic_flag_clear(&_dcd.dma_running); } } @@ -146,6 +160,10 @@ static void edpt_dma_start(volatile uint32_t* reg_startep) { // DMA is complete static void edpt_dma_end(void) { + // Clear the ERRATA-199 "DMA in progress" latch set in start_dma(). + if (nrf52_errata_199()) { + NRF_USBD_ERRATA_199_REG = 0x00000000UL; + } atomic_flag_clear(&_dcd.dma_running); } @@ -377,57 +395,51 @@ void dcd_edpt_close_all(uint8_t rhport) { dcd_int_enable(rhport); } -void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; - - uint8_t const epnum = tu_edpt_number(ep_addr); - uint8_t const dir = tu_edpt_dir(ep_addr); - - if (epnum != EP_ISO_NUM) { - // CBI - if (dir == TUSB_DIR_OUT) { - NRF_USBD->INTENCLR = TU_BIT(USBD_INTEN_ENDEPOUT0_Pos + epnum); - NRF_USBD->EPOUTEN &= ~TU_BIT(epnum); - } else { - NRF_USBD->INTENCLR = TU_BIT(USBD_INTEN_ENDEPIN0_Pos + epnum); - NRF_USBD->EPINEN &= ~TU_BIT(epnum); - } - } else { - _dcd.xfer[EP_ISO_NUM][dir].mps = 0; - // ISO - if (dir == TUSB_DIR_OUT) { - NRF_USBD->INTENCLR = USBD_INTENCLR_ENDISOOUT_Msk; - NRF_USBD->EPOUTEN &= ~USBD_EPOUTEN_ISOOUT_Msk; - NRF_USBD->EVENTS_ENDISOOUT = 0; - } else { - NRF_USBD->INTENCLR = USBD_INTENCLR_ENDISOIN_Msk; - NRF_USBD->EPINEN &= ~USBD_EPINEN_ISOIN_Msk; - } - // One of the ISO endpoints closed, no need to split buffers any more. - NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_OneDir; - // When both ISO endpoint are close there is no need for SOF any more. - if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_IN].mps + _dcd.xfer[EP_ISO_NUM][TUSB_DIR_OUT].mps == 0) - NRF_USBD->INTENCLR = USBD_INTENCLR_SOF_Msk; - } - _dcd.xfer[epnum][dir].started = false; - __ISB(); - __DSB(); -} - -#if 0 bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { (void)rhport; - (void)ep_addr; (void)largest_packet_size; - return false; + // nRF ISO endpoints are hardware-fixed to EP8 and use EasyDMA, so there is no packet buffer to + // pre-allocate here; the endpoint is enabled on dcd_edpt_iso_activate(). + TU_ASSERT(tu_edpt_number(ep_addr) == EP_ISO_NUM); + return true; } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { (void)rhport; - (void)desc_ep; - return false; + uint8_t const ep_addr = desc_ep->bEndpointAddress; + uint8_t const epnum = tu_edpt_number(ep_addr); + uint8_t const dir = tu_edpt_dir(ep_addr); + TU_ASSERT(epnum == EP_ISO_NUM); + + // A transfer armed before SET_INTERFACE survives to here (this port has no dcd close); usbd has + // just reset the endpoint's claim/busy state, so drop the stale descriptor too — otherwise the + // class's next arm trips TU_ASSERT(!xfer->started) in dcd_edpt_xfer(). + _dcd.xfer[epnum][dir].started = false; + _dcd.xfer[epnum][dir].data_received = false; + _dcd.xfer[epnum][dir].iso_in_transfer_ready = false; + + _dcd.xfer[epnum][dir].mps = tu_edpt_packet_size(desc_ep); + + if (dir == TUSB_DIR_OUT) { + // SPLIT ISO buffer when the ISO IN endpoint is already active. + if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_IN].mps) NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_HalfIN; + NRF_USBD->EVENTS_ENDISOOUT = 0; + if ((NRF_USBD->INTEN & USBD_INTEN_SOF_Msk) == 0) NRF_USBD->EVENTS_SOF = 0; + NRF_USBD->INTENSET = USBD_INTENSET_ENDISOOUT_Msk | USBD_INTENSET_SOF_Msk; + NRF_USBD->EPOUTEN |= USBD_EPOUTEN_ISOOUT_Msk; + } else { + NRF_USBD->EVENTS_ENDISOIN = 0; + // SPLIT ISO buffer when the ISO OUT endpoint is already active. + if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_OUT].mps) NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_HalfIN; + if ((NRF_USBD->INTEN & USBD_INTEN_SOF_Msk) == 0) NRF_USBD->EVENTS_SOF = 0; + NRF_USBD->INTENSET = USBD_INTENSET_ENDISOIN_Msk | USBD_INTENSET_SOF_Msk; + NRF_USBD->EPINEN |= USBD_EPINEN_ISOIN_Msk; + } + + __ISB(); + __DSB(); + return true; } -#endif bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t total_bytes, bool is_isr) { (void) rhport; diff --git a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c index aa0307d25a..d5b03e4b10 100644 --- a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c +++ b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c @@ -158,6 +158,10 @@ typedef struct // - 55 usb0 (FS) has 5x2 endpoints, usb1 (HS) has 6x2 endpoints #define MAX_EP_PAIRS 6 +// Bounded spin waiting for hardware to clear an EPSKIP bit when retiring a still-armed endpoint on +// reopen (dcd_edpt_open). Hardware clears it within a (micro)frame; the guard only avoids a hang. +#define IP3511_EPSKIP_SPIN 100000u + // NOTE data will be transferred as soon as dcd get request by dcd_pipe(_queue)_xfer using double buffering. // current_td is used to keep track of number of remaining & xferred bytes of the current request. typedef struct @@ -337,13 +341,37 @@ void dcd_sof_enable(uint8_t rhport, bool en) //--------------------------------------------------------------------+ // DCD Endpoint Port //--------------------------------------------------------------------+ +// Retire a still-armed (Active) endpoint before reconfiguring it (reopen across SET_INTERFACE). +// UM11126 §41.7.6/§41.8.3: write EPSKIP and wait for hardware to clear the bit, then Active is +// safe to clear. EPSKIP raises the endpoint interrupt as it clears Active, delivered as a +// (partial) transfer completion. Here that is sanctioned — usbd_edpt_close() documents "in +// progress transfers may be delivered after this call", and that completion is what clears the +// stale usbd busy flag (ISO_ALLOC close is a no-op) so the class can re-arm the reopened +// endpoint. NOT for the stall/iso-activate paths: there the class re-arms from the completion +// callback and the endpoint ends up Active+Stall, which never sends a STALL handshake (usbtest +// case 13 regression on LPC11u37) — those paths must clear Active directly instead. +// Bounded: hardware clears EPSKIP within a (micro)frame. +static void edpt_skip_active(uint8_t rhport, uint8_t ep_id) { + ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id); + if ( ep_cs[0].cmd_sts.active || ep_cs[1].cmd_sts.active ) { + dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; + dcd_reg->EPSKIP |= TU_BIT(ep_id); + uint32_t guard = IP3511_EPSKIP_SPIN; + while ( (dcd_reg->EPSKIP & TU_BIT(ep_id)) && guard-- ) {} + } + ep_cs[0].cmd_sts.active = ep_cs[1].cmd_sts.active = 0; +} + void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { (void) rhport; - // TODO cannot able to STALL Control OUT endpoint !!!!! FIXME try some walk-around uint8_t const ep_id = ep_addr2id(ep_addr); - _dcd.ep[ep_id][0].cmd_sts.stall = 1; + // Clear Active directly before setting Stall (no EPSKIP — see edpt_skip_active): the hardware + // services an armed buffer instead of returning STALL, so a halt requested while a transfer is + // queued would not actually stall the endpoint (usbtest case 13). + _dcd.ep[ep_id][0].cmd_sts.active = 0; + _dcd.ep[ep_id][0].cmd_sts.stall = 1; } void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) @@ -362,9 +390,15 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) //------------- Prepare Queue Head -------------// uint8_t ep_id = ep_addr2id(p_endpoint_desc->bEndpointAddress); ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id); + dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; - // Check if endpoint is available - TU_ASSERT( ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable ); + // usbd_edpt_close() is a no-op on ISO_ALLOC ports, so an endpoint a class closed then reopened + // across SET_INTERFACE (e.g. the video notification or audio streaming endpoint) is still armed + // here rather than disabled. Retire it (edpt_skip_active) before reconfiguring. + if ( !(ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable) ) { + edpt_skip_active(rhport, ep_id); + ep_cs[0].cmd_sts.disable = ep_cs[1].cmd_sts.disable = 1; + } edpt_reset(rhport, ep_id); @@ -389,7 +423,6 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) } // Enable EP interrupt - dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; dcd_reg->INTEN |= TU_BIT(ep_id); return true; @@ -404,29 +437,35 @@ void dcd_edpt_close_all (uint8_t rhport) } } -void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) -{ - (void) rhport; - +bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { + (void) largest_packet_size; + // Reserve the endpoint command/status entry once (persists across altsetting changes); the + // buffer pointer is filled per-transfer, so nothing to pre-allocate. Mirrors the ISO branch of + // dcd_edpt_open(). uint8_t ep_id = ep_addr2id(ep_addr); - _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][0].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual) - _dcd.ep[ep_id][0].cmd_sts.disable = _dcd.ep[ep_id][1].cmd_sts.disable = 1; -} + ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id); + TU_ASSERT( ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable ); -#if 0 -bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { - (void)rhport; - (void)ep_addr; - (void)largest_packet_size; - return false; + edpt_reset(rhport, ep_id); + ep_cs[0].cmd_sts.type = 1; // ISO + + dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs; + dcd_reg->INTEN |= TU_BIT(ep_id); + return true; } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { - (void)rhport; - (void)desc_ep; - return false; + // (Re)activate on altsetting selection: abort a transfer still armed from the previous + // altsetting (the hardware keeps servicing an Active buffer across SET_INTERFACE, fighting the + // fresh transfer the class queues), clear stall and reset the data toggle. Direct Active=0, not + // EPSKIP (see edpt_skip_active). The class re-arms via dcd_edpt_xfer(). + uint8_t ep_id = ep_addr2id(desc_ep->bEndpointAddress); + ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id); + ep_cs[0].cmd_sts.active = 0; + ep_cs[1].cmd_sts.active = 0; + dcd_edpt_clear_stall(rhport, desc_ep->bEndpointAddress); + return true; } -#endif static void prepare_ep_xfer(uint8_t rhport, uint8_t ep_id, uint16_t buf_offset, uint16_t total_bytes) { uint16_t nbytes; diff --git a/src/portable/raspberrypi/rp2040/dcd_rp2040.c b/src/portable/raspberrypi/rp2040/dcd_rp2040.c index 63097cd0a8..a0d312b8f4 100644 --- a/src/portable/raspberrypi/rp2040/dcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/dcd_rp2040.c @@ -550,9 +550,46 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { if (epnum != 0) { struct hw_endpoint* ep = hw_endpoint_get(epnum, dir); - ep->next_pid = 0; // reset data toggle - io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir); - *buf_reg = 0; + + if (ep->state == EPSTATE_ACTIVE) { + // Clear-halt on an endpoint with an in-flight transfer is used as a data-toggle reset + // (e.g. usbtest case 29) rather than to recover from a real stall (a stall aborts the + // transfer, leaving the endpoint IDLE). Abort and re-issue the transfer with the toggle + // reset to DATA0 so it still completes and releases the usbd claim, instead of silently + // dropping it and starving the endpoint. Save the buffer/length before the abort clears them. + uint8_t* user_buf = ep->user_buf; + uint16_t remaining = ep->remaining_len; + const uint16_t xferred = ep->xferred_len; // bytes already moved on this submission + io_rw_32 *ep_reg = get_ep_ctrl(epnum, dir); + io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir); + // bufctrl_prepare16() subtracts each armed buffer's length from remaining_len when arming, + // for BOTH directions, before the host has drained (IN) or filled (OUT) it. The abort below + // discards those still-armed buffers, so rewind remaining_len by their lengths or the re-issue + // is short by 1-2 packets. IN additionally advances user_buf as packets are copied into DPRAM, + // so its pointer must rewind too; OUT copies out only on completion, so its pointer is intact. + const uint32_t bc = *buf_reg; + uint16_t staged = 0; + if (bc & USB_BUF_CTRL_AVAIL) { + staged = (uint16_t)(bc & USB_BUF_CTRL_LEN_MASK); + } + if ((bc >> 16) & USB_BUF_CTRL_AVAIL) { + staged = (uint16_t)(staged + ((bc >> 16) & USB_BUF_CTRL_LEN_MASK)); + } + remaining = (uint16_t)(remaining + staged); + if (dir == TUSB_DIR_IN) { + user_buf -= staged; + } + hw_endpoint_abort_xfer(ep); // safe abort (handles RP2040-E2), resets ep transfer state + ep->next_pid = 0; // DATA0 + rp2usb_xfer_start(ep, ep_reg, buf_reg, user_buf, NULL, remaining); + // rp2usb_xfer_start() zeroes xferred_len; add back what the aborted transfer already moved so + // the eventual completion reports the full length, not just the post-clear-halt remainder. + ep->xferred_len += xferred; + } else { + ep->next_pid = 0; // reset data toggle + io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir); + *buf_reg = 0; // clear the stall response + } } } diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index e4eb0184e1..5421b9b2b9 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -176,10 +176,15 @@ void __tusb_irq_path_func(rp2usb_buffer_start)(hw_endpoint_t *ep, io_rw_32 *ep_r // Note: device EP0 does not have an endpoint control register if (ep_reg != NULL) { uint32_t ep_ctrl = *ep_reg; + // Isochronous endpoints get a single DPRAM buffer (hw_endpoint_open only double-sizes BULK), so + // they must never be double-buffered here even when a transfer spans multiple packets, or buffer + // 1 (at dpram_buf+64) would spill into the next endpoint's DPRAM. (Never true for BULK, so the + // double-buffered bulk path is unaffected.) + const bool is_iso = (((ep_ctrl >> EP_CTRL_BUFFER_TYPE_LSB) & 0x3u) == TUSB_XFER_ISOCHRONOUS); #if CFG_TUH_ENABLED - const bool force_single = (rp2usb_is_host_mode() && ep->interrupt_num > 0); + const bool force_single = is_iso || (rp2usb_is_host_mode() && ep->interrupt_num > 0); #else - const bool force_single = false; + const bool force_single = is_iso; #endif if (ep->remaining_len && !force_single) { diff --git a/src/portable/renesas/rusb2/dcd_rusb2.c b/src/portable/renesas/rusb2/dcd_rusb2.c index f0ef9738b9..c93bab05ec 100644 --- a/src/portable/renesas/rusb2/dcd_rusb2.c +++ b/src/portable/renesas/rusb2/dcd_rusb2.c @@ -28,6 +28,9 @@ typedef struct { uint8_t ep; /* an assigned endpoint address */ uint8_t ff; /* `buf` is TU_FUFO or POD */ + bool queued; /* a transfer is submitted and not yet completed (independent of `buf`, which is + NULL for a zero-length read) -- used to decide clear-stall re-arm */ + bool zlp_pending; /* a zero-length IN packet couldn't be queued at submit (FIFO full); retry on BRDY */ } pipe_state_t; typedef struct @@ -40,6 +43,7 @@ typedef struct static dcd_data_t _dcd; + //--------------------------------------------------------------------+ // INTERNAL OBJECT & FUNCTION DECLARATION //--------------------------------------------------------------------+ @@ -121,9 +125,19 @@ static uint16_t edpt_max_packet_size(rusb2_reg_t *rusb, unsigned num) { return rusb->PIPEMAXP; } -static inline void pipe_wait_for_ready(rusb2_reg_t * rusb, unsigned num) { - while ( rusb->D0FIFOSEL_b.CURPIPE != num ) {} - while ( !rusb->D0FIFOCTR_b.FRDY ) {} +// Select the D0FIFO for `num` and wait until its buffer is ready for CPU access. Both flags +// normally settle within a few cycles (the pipe was just armed, or a BRDY freed a plane). But an +// IN pipe whose double buffer is already full stalls FRDY until the host drains it, and a +// no-handshake iso IN endpoint the host has stopped polling never drains at all — so FRDY would +// hang forever. This runs with the USB IRQ masked, so a naked spin freezes the whole stack; bound +// it and let the caller abort the FIFO access. Returns false on timeout. +#define RUSB2_FIFO_READY_SPIN 100000u +static inline bool pipe_wait_for_ready(rusb2_reg_t *rusb, unsigned num) { + uint32_t spin = RUSB2_FIFO_READY_SPIN; + while ( rusb->D0FIFOSEL_b.CURPIPE != num ) { if (!spin--) return false; } + spin = RUSB2_FIFO_READY_SPIN; + while ( !rusb->D0FIFOCTR_b.FRDY ) { if (!spin--) return false; } + return true; } //--------------------------------------------------------------------+ @@ -177,6 +191,15 @@ static bool pipe0_xfer_out(rusb2_reg_t *rusb) { pipe_state_t *pipe = &_dcd.pipe[0]; const unsigned rem = pipe->remaining; + // BRDY with no armed transfer: a back-to-back data-stage packet beat the PID=NAK below (the + // host has already ACKed it). Park it in the DCP buffer — an unread buffer NAKs further OUTs — + // and let process_pipe0_xfer deliver it when usbd arms the next chunk. BCLR here would silently + // drop the packet and shift every later chunk by one (usbtest ctrl_out corruption at ra4m1). + if (pipe->buf == NULL && rem == 0) { + rusb->DCPCTR = RUSB2_PIPE_CTR_PID_NAK; + return false; + } + const uint16_t mps = edpt0_max_packet_size(rusb); const uint16_t vld = rusb->CFIFOCTR_b.DTLN; const uint16_t len = tu_min16(tu_min16(rem, mps), vld); @@ -201,6 +224,12 @@ static bool pipe0_xfer_out(rusb2_reg_t *rusb) { pipe->remaining = rem - len; if ((len < mps) || (rem == len)) { pipe->buf = NULL; + // Flow-control the single-buffer control pipe: NAK further OUT until usbd arms the next + // data-stage chunk. usbd receives a multi-packet control-OUT one CFG_TUD_ENDPOINT0_SIZE + // packet per submit; without this the DCP auto-accepts the next back-to-back packet into the + // just-emptied buffer and the following BRDY (remaining==0) BCLR-discards it, dropping 64 + // bytes mid-transfer (e.g. usbtest ctrl_out 512B). RA4M1 UM R01UH0887 DCPCTR.PID. + rusb->DCPCTR = RUSB2_PIPE_CTR_PID_NAK; return true; } @@ -226,7 +255,12 @@ static bool pipe_xfer_in(rusb2_reg_t* rusb, unsigned num) } const uint16_t mps = edpt_max_packet_size(rusb, num); - pipe_wait_for_ready(rusb, num); + if (!pipe_wait_for_ready(rusb, num)) { + // Buffer never came ready (double-buffered IN pipe full, host not draining). Drop this load; + // the transfer stays pending and is retried when a BRDY frees a plane or the pipe is re-armed. + rusb->D0FIFOSEL = 0; + return false; + } uint16_t len = tu_min16(rem, mps); void *buf = pipe->buf; @@ -267,7 +301,10 @@ static bool pipe_xfer_out(rusb2_reg_t* rusb, unsigned num) rusb->D0FIFOSEL = fifo_sel; const uint16_t mps = edpt_max_packet_size(rusb, num); - pipe_wait_for_ready(rusb, num); + if (!pipe_wait_for_ready(rusb, num)) { + rusb->D0FIFOSEL = 0; + return false; // FIFO not ready; leave the receive pending (BRDY re-enters when data arrives) + } const uint16_t vld = (uint16_t)rusb->D0FIFOCTR_b.DTLN; const uint16_t len = tu_min16(tu_min16(rem, mps), vld); @@ -333,7 +370,16 @@ static void process_status_completion(uint8_t rhport) dcd_event_xfer_complete(rhport, ep_addr, 0, XFER_RESULT_SUCCESS, true); } -static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer, +// Report a completed transfer on `num` and reset its bookkeeping. Single completion path for the +// BRDY handler and the EP0 parked-packet drain, so they can't diverge (e.g. on clearing `queued`). +static void pipe_xfer_complete(uint8_t rhport, unsigned num, bool in_isr) { + pipe_state_t *pipe = &_dcd.pipe[num]; + pipe->queued = false; + dcd_event_xfer_complete(rhport, pipe->ep, pipe->length - pipe->remaining, + XFER_RESULT_SUCCESS, in_isr); +} + +static bool process_pipe0_xfer(uint8_t rhport, rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer, uint16_t total_bytes) { uint16_t fifo_sel = (rusb2_is_highspeed_reg(rusb) ? RUSB2_FIFOSEL_MBW_32BIT : RUSB2_FIFOSEL_MBW_16BIT) | FIFOSEL_BIGEND; @@ -359,6 +405,15 @@ static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_ad /* IN */ TU_ASSERT(rusb->DCPCTR_b.BSTS && (rusb->USBREQ & 0x80)); pipe0_xfer_in(rusb); + } else if (rusb->CFIFOCTR_b.DTLN > 0) { + /* OUT: a back-to-back packet parked by pipe0_xfer_out already sits in the DCP buffer (its + BRDY has fired and been cleared) — deliver it into this chunk now; no new BRDY will come + for it. Runs with the USB IRQ masked (dcd_edpt_xfer). Detected via the hardware DTLN + rather than a driver flag: the BCLR at SETUP/bus-reset then self-heals any parked state. */ + if (pipe0_xfer_out(rusb)) { + pipe_xfer_complete(rhport, 0, false); + return true; // PID stays NAK (set by pipe0_xfer_out) until the next chunk is armed + } } rusb->DCPCTR = RUSB2_PIPE_CTR_PID_BUF; } else { @@ -370,6 +425,24 @@ static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_ad return true; } +// Queue a zero-length IN packet. Returns false if the FIFO buffer wasn't free (double-buffered pipe +// full, host not draining) so BVAL couldn't be written -- the caller retries on the next BRDY. +static bool pipe_zlp_in(rusb2_reg_t *rusb, unsigned num) { + rusb->D0FIFOSEL = (uint16_t) num; + const bool ready = pipe_wait_for_ready(rusb, num); + if (ready) { + rusb->D0FIFOCTR = RUSB2_CFIFOCTR_BVAL_Msk; + } + rusb->D0FIFOSEL = 0; + // deselect completes within a few bus cycles (not host-dependent), but bound it anyway: this + // runs with the USB IRQ masked, where any stuck spin freezes the whole stack + uint32_t spin = RUSB2_FIFO_READY_SPIN; + while (rusb->D0FIFOSEL_b.CURPIPE) { + if (!spin--) { break; } + } + return ready; +} + static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes) { const unsigned epn = tu_edpt_number(ep_addr); @@ -379,23 +452,20 @@ static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_add TU_ASSERT(num); pipe_state_t *pipe = &_dcd.pipe[num]; - pipe->ff = buffer_type; - pipe->buf = buffer; - pipe->length = total_bytes; - pipe->remaining = total_bytes; + pipe->ff = buffer_type; + pipe->buf = buffer; + pipe->length = total_bytes; + pipe->remaining = total_bytes; + pipe->queued = true; + pipe->zlp_pending = false; if (dir) { /* IN */ if (total_bytes) { pipe_xfer_in(rusb, num); } else { - /* ZLP */ - rusb->D0FIFOSEL = num; - pipe_wait_for_ready(rusb, num); - rusb->D0FIFOCTR = RUSB2_CFIFOCTR_BVAL_Msk; - rusb->D0FIFOSEL = 0; - /* if CURPIPE bits changes, check written value */ - while (rusb->D0FIFOSEL_b.CURPIPE) {} + /* ZLP: if the FIFO buffer isn't free yet, defer the queue to the next BRDY (see process_pipe_brdy) */ + pipe->zlp_pending = !pipe_zlp_in(rusb, num); } } else { // OUT @@ -418,11 +488,11 @@ static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_add return true; } -static bool process_edpt_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes) +static bool process_edpt_xfer(uint8_t rhport, rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes) { const unsigned epn = tu_edpt_number(ep_addr); if (0 == epn) { - return process_pipe0_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes); + return process_pipe0_xfer(rhport, rusb, buffer_type, ep_addr, buffer, total_bytes); } else { return process_pipe_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes); } @@ -448,7 +518,15 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num) if (dir) { /* IN */ - completed = pipe_xfer_in(rusb, num); + if (pipe->zlp_pending) { + // The submit-time ZLP couldn't be queued (FIFO full); a freed buffer plane lets us queue it + // now. Don't report completion until the ZLP is actually queued (and then sent, next BRDY), + // otherwise a spurious BRDY would complete a zero-length IN the host never received. + pipe->zlp_pending = !pipe_zlp_in(rusb, num); + completed = false; + } else { + completed = pipe_xfer_in(rusb, num); + } } else { // OUT if (num) { @@ -458,9 +536,7 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num) } } if (completed) { - dcd_event_xfer_complete(rhport, pipe->ep, - pipe->length - pipe->remaining, - XFER_RESULT_SUCCESS, true); + pipe_xfer_complete(rhport, num, true); // TU_LOG1("C %d %d\r\n", num, pipe->length - pipe->remaining); } } @@ -585,19 +661,8 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { #ifdef RUSB2_SUPPORT_HIGHSPEED if ( rusb2_is_highspeed_rhport(rhport) ) { - rusb->SYSCFG_b.HSE = 1; - - // leave CLKSEL as default (0x11) 24Mhz - - // Power and reset UTMI Phy - uint16_t physet = (rusb->PHYSET | RUSB2_PHYSET_PLLRESET_Msk) & ~RUSB2_PHYSET_DIRPD_Msk; - rusb->PHYSET = physet; - R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); - rusb->PHYSET_b.PLLRESET = 0; - - // set UTMI to operating mode and wait for PLL lock confirmation - rusb->LPSTS_b.SUSPENDM = 1; - while (!rusb->PLLSTA_b.PLLLOCK) {} + rusb->SYSCFG_b.HSE = TUD_OPT_HIGH_SPEED ? 1 : 0; // FS-only build: no HS chirp + rusb2_utmi_phy_powerup(rusb); rusb->SYSCFG_b.DRPD = 0; rusb->SYSCFG_b.USBE = 1; @@ -702,10 +767,20 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) if ( !rusb2_is_highspeed_rhport(rhport) && mps > 256) { return false; } + } else if (xfer == TUSB_XFER_INTERRUPT) { + // Interrupt pipes (6-9) have a fixed 64-byte buffer even in high speed (RA6M5 UM 29.1); + // a larger PIPEMAXP would enumerate, then silently truncate every transfer + TU_ASSERT(mps <= 64); } - const unsigned num = find_pipe(xfer); - TU_ASSERT(num); + // Re-opening an endpoint must reuse its pipe: usbd_edpt_close() is a no-op on ISO_ALLOC ports, + // so a class's close/open across SET_INTERFACE (e.g. video's notification endpoint) would + // otherwise allocate a second pipe with the same EPNUM and leak pipes until exhaustion. + unsigned num = _dcd.ep[dir][epn]; + if (num == 0) { + num = find_pipe(xfer); + TU_ASSERT(num); + } _dcd.pipe[num].ep = ep_addr; _dcd.ep[dir][epn] = num; @@ -713,13 +788,12 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) /* setup pipe */ dcd_int_disable(rhport); + rusb->PIPESEL = num; if ( rusb2_is_highspeed_rhport(rhport) ) { - // FIXME shouldn't be after pipe selection and config, also the BUFNMB should be changed - // depending on the allocation scheme + // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe. + // FIXME BUFNMB is a fixed 0x08 for every pipe; a real per-pipe allocation scheme is needed. rusb->PIPEBUF = 0x7C08; } - - rusb->PIPESEL = num; rusb->PIPEMAXP = mps; volatile uint16_t *ctr = get_pipectr(rusb, num); *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; @@ -748,6 +822,8 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) return true; } +static void edpt_close(uint8_t rhport, uint8_t ep_addr); + void dcd_edpt_close_all(uint8_t rhport) { unsigned i = TU_ARRAY_SIZE(_dcd.pipe); @@ -757,12 +833,14 @@ void dcd_edpt_close_all(uint8_t rhport) if (!ep_addr) { continue; } - dcd_edpt_close(rhport, (uint8_t)ep_addr); + edpt_close(rhport, (uint8_t)ep_addr); } dcd_int_enable(rhport); } -void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) +// Internal helper: on this (ISO_ALLOC) IP the stack no longer calls dcd_edpt_close(); only +// dcd_edpt_close_all() uses it to tear down each pipe. +static void edpt_close(uint8_t rhport, uint8_t ep_addr) { rusb2_reg_t * rusb = RUSB2_REG(rhport); const unsigned epn = tu_edpt_number(ep_addr); @@ -774,24 +852,73 @@ void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) *ctr = 0; rusb->PIPESEL = (uint16_t)num; rusb->PIPECFG = 0; - _dcd.pipe[num].ep = 0; + _dcd.pipe[num].ep = 0; + _dcd.pipe[num].queued = false; + _dcd.pipe[num].zlp_pending = false; _dcd.ep[dir][epn] = 0; } -#if 0 bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { - (void)rhport; - (void)ep_addr; - (void)largest_packet_size; - return false; + rusb2_reg_t * rusb = RUSB2_REG(rhport); + const unsigned epn = tu_edpt_number(ep_addr); + const unsigned dir = tu_edpt_dir(ep_addr); + + // Fullspeed ISO is limited to 256 bytes + if (!rusb2_is_highspeed_rhport(rhport) && largest_packet_size > 256) { + return false; + } + + // Reserve an ISO-capable pipe (1 or 2) once; it persists across altsetting changes so + // dcd_edpt_iso_activate() only has to re-arm it in place (no pipe free/realloc, which on this + // shared-register IP would churn PIPESEL/PIPECFG and disturb the other pipes). + const unsigned num = find_pipe(TUSB_XFER_ISOCHRONOUS); + TU_ASSERT(num); + _dcd.pipe[num].ep = ep_addr; + _dcd.ep[dir][epn] = num; + + dcd_int_disable(rhport); + rusb->PIPESEL = (uint16_t) num; + if (rusb2_is_highspeed_rhport(rhport)) { + // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe. + // FIXME (as in dcd_edpt_open): BUFNMB is a fixed 0x08 for every pipe; a real allocator is needed. + rusb->PIPEBUF = 0x7C08; + } + rusb->PIPEMAXP = largest_packet_size; + volatile uint16_t *ctr = get_pipectr(rusb, num); + *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; + *ctr = 0; // leave the pipe NAKing until activated + rusb->PIPECFG = (uint16_t) ((dir << 4) | epn | RUSB2_PIPECFG_TYPE_ISO | RUSB2_PIPECFG_DBLB_Msk); + rusb->BRDYSTS = (uint16_t) (0x3FFu ^ TU_BIT(num)); + rusb->BRDYENB |= TU_BIT(num); + dcd_int_enable(rhport); + return true; } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { - (void)rhport; - (void)desc_ep; - return false; + rusb2_reg_t * rusb = RUSB2_REG(rhport); + const uint8_t ep_addr = desc_ep->bEndpointAddress; + const unsigned epn = tu_edpt_number(ep_addr); + const unsigned dir = tu_edpt_dir(ep_addr); + const unsigned num = _dcd.ep[dir][epn]; + TU_ASSERT(num); // must have been iso-alloc'd + + dcd_int_disable(rhport); + rusb->PIPESEL = (uint16_t) num; + rusb->PIPEMAXP = tu_edpt_packet_size(desc_ep); + volatile uint16_t *ctr = get_pipectr(rusb, num); + *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; // abort in-flight + reset data toggle + *ctr = 0; + // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the + // stale bookkeeping so a BRDY firing before the class re-arms can't replay it + pipe_state_t *pipe = &_dcd.pipe[num]; + pipe->buf = NULL; + pipe->remaining = 0; + pipe->queued = false; + pipe->zlp_pending = false; + *ctr = RUSB2_PIPE_CTR_PID_BUF; // enable + dcd_int_enable(rhport); + return true; } -#endif bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t total_bytes, bool is_isr) { @@ -799,7 +926,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t rusb2_reg_t* rusb = RUSB2_REG(rhport); dcd_int_disable(rhport); - bool r = process_edpt_xfer(rusb, 0, ep_addr, buffer, total_bytes); + bool r = process_edpt_xfer(rhport, rusb, 0, ep_addr, buffer, total_bytes); dcd_int_enable(rhport); return r; @@ -812,7 +939,7 @@ bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t * ff, uint16_ rusb2_reg_t* rusb = RUSB2_REG(rhport); dcd_int_disable(rhport); - bool r = process_edpt_xfer(rusb, 1, ep_addr, ff, total_bytes); + bool r = process_edpt_xfer(rhport, rusb, 1, ep_addr, ff, total_bytes); dcd_int_enable(rhport); return r; @@ -847,7 +974,19 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) } else { const unsigned num = _dcd.ep[0][tu_edpt_number(ep_addr)]; rusb->PIPESEL = (uint16_t)num; - if (rusb->PIPECFG_b.TYPE != 1) { + // Drop any packet parked in the buffer while halted: a data-OUT packet the host sent before + // aborting its transfer would otherwise be delivered into the next read after recovery + // (BOT reset + clear-halt re-arms a 31-byte CBW read which then receives stale WRITE data, + // "SCSI CBW is not valid" -> stall -> reset loop; ra6m5 msc write wedge). + *ctr = RUSB2_PIPE_CTR_ACLRM_Msk; + *ctr = 0; + // Non-bulk OUT re-enables straight away. Bulk OUT is normally armed together with its transaction + // counter (TRE) by process_pipe_xfer(), so we don't blindly re-enable it here — but if a receive + // was already armed (still queued), SQCLR above just left it NAKing. Re-assert BUF so it keeps + // receiving; the class driver still considers that read submitted and never re-arms it, so + // otherwise the endpoint NAKs forever (usbtest toggle test 29 clears the halt on an armed pipe). + // `queued` (not `buf`) is the armed test: a zero-length OUT read has buf==NULL yet is armed. + if (rusb->PIPECFG_b.TYPE != 1 || _dcd.pipe[num].queued) { *ctr = RUSB2_PIPE_CTR_PID_BUF; } } diff --git a/src/portable/renesas/rusb2/hcd_rusb2.c b/src/portable/renesas/rusb2/hcd_rusb2.c index 849551d278..489162e542 100644 --- a/src/portable/renesas/rusb2/hcd_rusb2.c +++ b/src/portable/renesas/rusb2/hcd_rusb2.c @@ -454,11 +454,9 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { if (rusb2_is_highspeed_rhport(rhport) ) { rusb->SYSCFG_b.HSE = 1; rusb->PHYSET_b.HSEB = 0; - rusb->PHYSET_b.DIRPD = 0; - R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); - rusb->PHYSET_b.PLLRESET = 0; - rusb->LPSTS_b.SUSPENDM = 1; - while ( !rusb->PLLSTA_b.PLLLOCK ); + // same PHY reference-clock + power-up requirements as dcd_init: without CLKSEL matching the + // board XTAL the PLL never locks and the wait below would spin forever (e.g. EK-RA8M1, 20 MHz) + rusb2_utmi_phy_powerup(rusb); rusb->SYSCFG_b.DRPD = 1; rusb->SYSCFG_b.DCFM = 1; rusb->SYSCFG_b.DPRPU = 0; diff --git a/src/portable/renesas/rusb2/rusb2_ra.h b/src/portable/renesas/rusb2/rusb2_ra.h index e5945ffe21..0954d0d25a 100644 --- a/src/portable/renesas/rusb2/rusb2_ra.h +++ b/src/portable/renesas/rusb2/rusb2_ra.h @@ -49,6 +49,23 @@ typedef struct { #define rusb2_is_highspeed_rhport(_p) (_p == 1) #define rusb2_is_highspeed_reg(_reg) (_reg == RUSB2_REG(1)) + + // UTMI PHY reference clock is the main oscillator: PHYSET.CLKSEL must match the board XTAL + // before the PHY PLL is released (RA6M5 UM R01UH0891 29.2.17: 00=12 MHz, 10=20 MHz, + // 11=24 MHz reset default). EK-RA6M5 runs 24 MHz (default works); EK-RA8M1 runs 20 MHz and + // never locks/chirps on the default. A board with a non-standard USB clocking scheme can + // pre-define RUSB2_PHYSET_CLKSEL_VALUE to override this selection. + #ifndef RUSB2_PHYSET_CLKSEL_VALUE + #if BSP_CFG_XTAL_HZ == 12000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 0u + #elif BSP_CFG_XTAL_HZ == 20000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 2u + #elif BSP_CFG_XTAL_HZ == 24000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 3u + #else + #error "USBHS UTMI PHY: no PHYSET.CLKSEL encoding for this BSP_CFG_XTAL_HZ; define RUSB2_PHYSET_CLKSEL_VALUE" + #endif + #endif #else #define RUSB2_CONTROLLER_COUNT 1 @@ -84,6 +101,31 @@ TU_ATTR_ALWAYS_INLINE static inline void rusb2_int_disable(uint8_t rhport) { TU_ATTR_ALWAYS_INLINE static inline void rusb2_phy_init(void) { } +#ifdef RUSB2_SUPPORT_HIGHSPEED +// UTMI PHY power-up per the FSP reference sequence (r_usb_preg_access.c), shared by dcd_init and +// hcd_init: program CLKSEL to the board XTAL while the PHY is powered down (DIRPD=1), 1 us, +// release DIRPD, 1 ms, release PLLRESET, then wait for PLL lock. Changing CLKSEL as the PHY +// powers up gets mis-sampled (EK-RA8M1, 20 MHz). +static inline void rusb2_utmi_phy_powerup(rusb2_reg_t* rusb) { + uint16_t physet = rusb->PHYSET | RUSB2_PHYSET_DIRPD_Msk; + rusb->PHYSET = physet; + #ifdef RUSB2_PHYSET_CLKSEL_VALUE + physet = (uint16_t) ((physet & ~RUSB2_PHYSET_CLKSEL_Msk) | + (RUSB2_PHYSET_CLKSEL_VALUE << RUSB2_PHYSET_CLKSEL_Pos)); + rusb->PHYSET = physet; + #endif + R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MICROSECONDS); + physet &= (uint16_t) ~RUSB2_PHYSET_DIRPD_Msk; + rusb->PHYSET = physet; + R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); + rusb->PHYSET_b.PLLRESET = 0; + + // set UTMI to operating mode and wait for PLL lock confirmation + rusb->LPSTS_b.SUSPENDM = 1; + while (!rusb->PLLSTA_b.PLLLOCK) {} +} +#endif + #ifdef __cplusplus } #endif diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c index aecd689b3a..ba05818b6f 100644 --- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c +++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c @@ -835,7 +835,17 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { ep_reg &= U_EPREG_MASK | EP_STAT_MASK(dir) | EP_DTOG_MASK(dir); if (!ep_is_iso(ep_reg)) { - ep_change_status(&ep_reg, dir, EP_STAT_NAK); + // Only knock a genuinely STALLED endpoint down to NAK (the class then re-arms it). If the + // endpoint is armed (VALID) - e.g. a clear-halt used purely to reset the data toggle, as in + // usbtest case 29 - leave STAT untouched so the in-flight transfer isn't disarmed with no + // completion, which would leak the usbd claim and starve the endpoint. Masking the STAT bits + // to 0 writes no toggle, so an armed/idle endpoint keeps its current status. + const uint8_t stat_pos = (uint8_t) (U_EPTX_STAT_Pos + (dir == TUSB_DIR_IN ? 0u : 8u)); + if (((ep_reg >> stat_pos) & 0x3u) == EP_STAT_STALL) { + ep_change_status(&ep_reg, dir, EP_STAT_NAK); + } else { + ep_reg &= ~EP_STAT_MASK(dir); + } } ep_change_dtog(&ep_reg, dir, 0); // Reset to DATA0 ep_write(ep_idx, ep_reg, true); diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 6c88b4f271..86aa545104 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -393,10 +393,6 @@ static void edpt_schedule_packets(uint8_t rhport, const uint8_t epnum, const uin } dep->diepdma = (uintptr_t) xfer->buffer; dep->diepctl = depctl.value; // enable endpoint - // Advance buffer pointer for EP0 - if (epnum == 0) { - xfer->buffer += total_bytes; - } } else #endif { @@ -732,6 +728,8 @@ static void handle_bus_reset(uint8_t rhport) { tu_memclr(xfer_status, sizeof(xfer_status)); + _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; + _dcd_data.ep0_pending[TUSB_DIR_IN] = 0; _dcd_data.sof_en = false; _dcd_data.allocated_epin_count = 0; @@ -1009,6 +1007,10 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi if (edpt_is_enabled(epin0)) { edpt_disable(rhport, 0x80, false); } + // a new SETUP aborts any in-progress control transfer: drop leftover EP0 chunking state so a + // stale latched completion cannot re-arm from it + _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0; + _dcd_data.ep0_pending[TUSB_DIR_IN] = 0; dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, sizeof(_dcd_usbbuf.setup_buffer)); @@ -1029,24 +1031,37 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi // only handle data skip if it is setup or status related // Normal OUT transfer complete if (!doepint_bm.status_phase_rx && !doepint_bm.setup_packet_rx) { + xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_OUT]) { - // EP0 can only handle one packet Schedule another packet to be received. + // EP0 can only handle one packet: invalidate and advance past the received bytes, then + // schedule the next. + if (xfer->buffer != NULL) { + dcd_dcache_invalidate(xfer->buffer, CFG_TUD_ENDPOINT0_SIZE); + xfer->buffer += CFG_TUD_ENDPOINT0_SIZE; + } edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT); } else { dwc2_dep_t* epout = &dwc2->epout[epnum]; - xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT); // determine actual received bytes const dwc2_ep_tsize_t tsiz = {.value = epout->tsiz}; const uint16_t remain = tsiz.xfer_size; xfer->total_len -= remain; + // EP0 invalidates only this (final) chunk's DMA-written bytes: DOEPDMA "is incremented on + // every AHB transaction" (databook 7.1.83), i.e. it points past the last word written. + // Read it before dma_setup_prepare() re-targets it at the setup buffer + uint16_t inval_len = xfer->total_len; + if (epnum == 0) { + inval_len = (uint16_t)(epout->doepdma - (uintptr_t)xfer->buffer); + } + // prepare EP0 for next setup if(epnum == 0) { dma_setup_prepare(rhport); } - dcd_dcache_invalidate(xfer->buffer, xfer->total_len); + dcd_dcache_invalidate(xfer->buffer, inval_len); dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true); } } @@ -1058,7 +1073,10 @@ static void handle_epin_dma(uint8_t rhport, uint8_t epnum, dwc2_diepint_t diepin if (diepint_bm.xfer_complete) { if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_IN]) { - // EP0 can only handle one packet. Schedule another packet to be transmitted. + // EP0 can only handle one packet: advance past the sent bytes, then schedule the next. + if (xfer->buffer != NULL) { + xfer->buffer += CFG_TUD_ENDPOINT0_SIZE; + } edpt_schedule_packets(rhport, epnum, TUSB_DIR_IN); } else { dcd_event_xfer_complete(rhport, epnum | TUSB_DIR_IN_MASK, xfer->total_len, XFER_RESULT_SUCCESS, true); diff --git a/src/portable/wch/ch32_usbfs_reg.h b/src/portable/wch/ch32_usbfs_reg.h index 8bac103fee..5b037281fc 100644 --- a/src/portable/wch/ch32_usbfs_reg.h +++ b/src/portable/wch/ch32_usbfs_reg.h @@ -179,6 +179,14 @@ #endif #endif +// CH32V20x/V30x/F20x USBFS gives endpoint 3 a 1023-byte isochronous packet (CH32FV2x_V3xRM ch23: +// every endpoint is 64 B except EP3 = 1023 B, from EP3's 10-bit R16_UEP3_T_LEN field plus a single +// contiguous >=1023 B DMA buffer — NOT double-buffering, which only yields 2x64 B). +// CH32V103/X035/CH58x cap every endpoint at 64 B. dcd_ch32_usbfs.c reads this to size EP3's buffer. +#if CFG_TUSB_MCU == OPT_MCU_CH32V20X || CFG_TUSB_MCU == OPT_MCU_CH32V307 || CFG_TUSB_MCU == OPT_MCU_CH32F20X + #define CH32_USBFS_EP3_1023_BUFSIZE 1 +#endif + #ifdef __GNUC__ #pragma GCC diagnostic pop #endif diff --git a/src/portable/wch/dcd_ch32_usbfs.c b/src/portable/wch/dcd_ch32_usbfs.c index a6458748a9..ec521224e5 100644 --- a/src/portable/wch/dcd_ch32_usbfs.c +++ b/src/portable/wch/dcd_ch32_usbfs.c @@ -16,6 +16,17 @@ /* private defines */ #define EP_MAX (8) + // EP3 IN buffer size. CH32V20x/V30x/F20x USBFS support full-speed iso packets up to 1023 B on + // endpoint 3 (every other endpoint is 64 B); those parts set CH32_USBFS_EP3_1023_BUFSIZE in + // ch32_usbfs_reg.h. V103/X035/CH58x cap every endpoint at 64 B. Overridable per project. + #ifndef CFG_TUD_WCH_USBFS_EP3_BUFSIZE + #ifdef CH32_USBFS_EP3_1023_BUFSIZE + #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 1023 + #else + #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 64 + #endif + #endif + // Struct-based EP register access (uniform layout). CH58X has a different register map and // defines EP_DMA/EP_TX_LEN/EP_CTRL itself in ch32_usbfs_reg.h. #if CFG_TUSB_MCU == OPT_MCU_CH583 @@ -107,7 +118,7 @@ struct usb_xfer { static struct { bool ep0_tog; - bool isochronous[EP_MAX]; + bool isochronous[EP_MAX][2]; // per [ep][dir]: an ep number may be iso in one direction struct usb_xfer xfer[EP_MAX][2]; #ifdef CH32_USBFS_EP4_SHARES_EP0 // CH58X buffers laid out by hand so EP0/EP4 don't burn two unused buffer[] slots. EP0 and EP4 @@ -123,21 +134,23 @@ static struct { TU_ATTR_ALIGNED(4) uint8_t ep6_buffer[2][64]; TU_ATTR_ALIGNED(4) uint8_t ep7_buffer[2][64]; #else + // Every endpoint gets a 64-byte OUT + 64-byte IN buffer. TU_ATTR_ALIGNED(4) uint8_t buffer[EP_MAX][2][64]; - // EP3 IN gets an enlarged buffer for full-speed isochronous (packets up to 1023 B). + #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64 + // ...except EP3, which supports full-speed iso packets up to 1023 B on CH32V20x/V30x/F20x, so its + // IN buffer is enlarged (OUT stays 64 B; an OUT transfer >64 B on EP3 would overwrite queued IN). TU_ATTR_ALIGNED(4) struct { - // OUT transfers >64 bytes will overwrite queued IN data! uint8_t out[64]; - uint8_t in[1023]; + uint8_t in[CFG_TUD_WCH_USBFS_EP3_BUFSIZE]; uint8_t pad; } ep3_buffer; + #endif #endif } data; // DMA / copy buffer pointers per endpoint. The WCH USBFS buffer holds OUT (RX) at offset 0 and -// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN; EP3 has an enlarged IN -// buffer for throughput. On CH58X, EP0/EP4 share ep0_ep4_buffer and the regular endpoints use -// their own named buffer (see the struct above). +// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN. On CH58X, EP0/EP4 share +// ep0_ep4_buffer and the regular endpoints use their own named buffer (see the struct above). #ifdef CH32_USBFS_EP4_SHARES_EP0 // OUT base of the regular CH58X endpoints (EP1/2/3/5/6/7; EP0/EP4 share ep0_ep4_buffer). static inline uint8_t* ch58x_ep_buffer(uint8_t ep) { @@ -157,7 +170,9 @@ static inline uint32_t ep_dma_addr(uint8_t ep) { if (ep == 0 || ep == 4) { return (uint32_t) &data.ep0_ep4_buffer[0]; } // EP4 shares EP0's DMA return (uint32_t) ch58x_ep_buffer(ep); #else - if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; } + #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64 + if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; } // EP3 has an enlarged IN buffer + #endif return (uint32_t) &data.buffer[ep][0]; #endif } @@ -168,7 +183,9 @@ static inline uint8_t* ep_out_buf(uint8_t ep) { if (ep == 4) { return &data.ep0_ep4_buffer[64]; } return ch58x_ep_buffer(ep); #else + #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64 if (ep == 3) { return data.ep3_buffer.out; } + #endif return data.buffer[ep][TUSB_DIR_OUT]; #endif } @@ -180,7 +197,9 @@ static inline uint8_t* ep_in_buf(uint8_t ep) { return ch58x_ep_buffer(ep) + 64; // IN at +64 within the endpoint's 128-byte buffer #else if (ep == 0) { return data.buffer[0][TUSB_DIR_OUT]; } // EP0 half-duplex: IN reuses OUT chunk - if (ep == 3) { return data.ep3_buffer.in; } + #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64 + if (ep == 3) { return data.ep3_buffer.in; } // enlarged IN buffer for full-speed iso + #endif return data.buffer[ep][TUSB_DIR_IN]; #endif } @@ -202,9 +221,8 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) { if (force || xfer->len) { size_t len = TU_MIN(xfer->max_size, xfer->len); #if CFG_TUSB_MCU == OPT_MCU_CH583 - // Every CH58x endpoint buffer is 64 bytes. Isochronous (which would push max_size up to 1023) - // is refused in dcd_edpt_iso_alloc(), but some classes (e.g. video) ignore that result, so cap - // the copy here to guarantee we never write past the buffer into a neighbouring endpoint's. + // Every CH58x endpoint buffer is 64 bytes; cap the copy so an iso mps a class mistakenly set + // larger can't write past the buffer into a neighbouring endpoint's. len = TU_MIN(len, 64u); #endif memcpy(ep_in_buf(ep), xfer->buffer, len); @@ -216,7 +234,7 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) { if (ep == 0) { ep_tx_ctrl_set(0, USBFS_EP_T_RES_ACK | (data.ep0_tog ? USBFS_EP_T_TOG : 0)); data.ep0_tog = !data.ep0_tog; - } else if (data.isochronous[ep]) { + } else if (data.isochronous[ep][TUSB_DIR_IN]) { ep_tx_set_response(ep, USBFS_EP_T_RES_NYET); } else { ep_tx_set_response(ep, USBFS_EP_T_RES_ACK); @@ -225,7 +243,7 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) { xfer->valid = false; if (ep == 0) { ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK | (data.ep0_tog ? USBFS_EP_T_TOG : 0)); - } else if (!data.isochronous[ep]) { + } else if (!data.isochronous[ep][TUSB_DIR_IN]) { ep_tx_set_response(ep, USBFS_EP_T_RES_NAK); } dcd_event_xfer_complete(rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, XFER_RESULT_SUCCESS, true); @@ -254,7 +272,7 @@ static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) { ep_rx_set_response(0, USBFS_EP_R_RES_NAK); } else { uint8_t rx_res = - data.isochronous[ep] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK); + data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK); ep_rx_set_response(ep, rx_res); } } @@ -319,12 +337,14 @@ void dcd_int_handler(uint8_t rhport) { // Drop an OUT packet whose data toggle doesn't match what we expect -- a host retransmit // after a lost ACK, or a host that doesn't alternate DATA0/DATA1. The hardware auto-toggle // does not reject these on its own, so the check is needed on every variant. EP0 keeps its - // own toggle via the SETUP/status flow and is exempt. - if (ep != 0 && !(int_st & USBFS_INT_ST_TOG_OK)) { break; } + // own toggle via the SETUP/status flow and is exempt; isochronous is DATA0-only (no toggle), + // so its packets must not be toggle-checked. + if (ep != 0 && !data.isochronous[ep][TUSB_DIR_OUT] && !(int_st & USBFS_INT_ST_TOG_OK)) { break; } #ifdef CH32_USBFS_EP_MANUAL_TOG // CH58x has no hardware auto-toggle: advance the expected RX toggle after each accepted packet // (EP0 included -- it also has no auto-toggle and a control-OUT data stage can span packets). - EP_CTRL(ep) ^= USBFS_EPC_R_TOG; + // Iso endpoints are DATA0-only, so leave them alone (matches the PID_IN path). + if (!data.isochronous[ep][TUSB_DIR_OUT]) { EP_CTRL(ep) ^= USBFS_EPC_R_TOG; } #endif update_out(rhport, ep, rx_len); break; @@ -333,7 +353,8 @@ void dcd_int_handler(uint8_t rhport) { case PID_IN: #ifdef CH32_USBFS_EP_MANUAL_TOG // Manual toggle: flip the TX toggle after each ACK'd IN packet (EP0 manages its own). - if (ep != 0) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; } + // Isochronous transfers are DATA0-only (no toggle), so leave iso endpoints alone. + if (ep != 0 && !data.isochronous[ep][TUSB_DIR_IN]) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; } #endif update_in(rhport, ep, false); break; @@ -443,6 +464,7 @@ bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress); TU_ASSERT(ep < EP_MAX); + data.isochronous[ep][dir] = false; // (re)opening as a non-iso endpoint clears any stale iso flag data.xfer[ep][dir].max_size = tu_edpt_packet_size(desc_ep); if (ep != 0) { @@ -464,31 +486,38 @@ void dcd_edpt_close_all(uint8_t rhport) { bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { (void)rhport; - (void)ep_addr; - (void)largest_packet_size; -#if CFG_TUSB_MCU == OPT_MCU_CH583 - // No isochronous support on CH58x: its 8-bit T_LEN caps a packet at 255B and the endpoints use - // plain 64-byte buffers, so accepting an iso max_size (up to 1023) would let update_in()/ - // update_out() run off the end of the buffer into neighbouring ones. Refuse it outright. - return false; -#else uint8_t ep = tu_edpt_number(ep_addr); uint8_t dir = tu_edpt_dir(ep_addr); + TU_ASSERT(ep < EP_MAX); - data.isochronous[ep] = true; + // Endpoint buffers are 64 B, except EP3 IN which is enlarged for full-speed iso on the parts that + // support 1023-byte EP3 packets (CH32V20x/V30x/F20x; CFG_TUD_WCH_USBFS_EP3_BUFSIZE). Reject a + // larger mps rather than running off the end into the neighbouring endpoint's memory. + uint16_t max_packet = 64; +#if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64 + if (ep == 3 && dir == TUSB_DIR_IN) { max_packet = CFG_TUD_WCH_USBFS_EP3_BUFSIZE; } +#endif + TU_VERIFY(largest_packet_size <= max_packet); + + data.isochronous[ep][dir] = true; data.xfer[ep][dir].max_size = largest_packet_size; return true; -#endif } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { (void)rhport; - (void)desc_ep; -#if CFG_TUSB_MCU == OPT_MCU_CH583 - return false; // CH58x has no isochronous support (see dcd_edpt_iso_alloc) -#else + const uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress); + const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress); + + // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the + // stale descriptor and NAK the endpoint so the ISR can't complete it against the old buffer + data.xfer[ep][dir].valid = false; + if (dir == TUSB_DIR_IN) { + ep_tx_set_response(ep, USBFS_EP_T_RES_NAK); + } else { + ep_rx_set_response(ep, USBFS_EP_R_RES_NAK); + } return true; -#endif } bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) { @@ -510,7 +539,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t to if (dir == TUSB_DIR_IN) { update_in(rhport, ep, true); } else { - uint8_t rx_res = data.isochronous[ep] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK; + uint8_t rx_res = data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK; ep_rx_set_response(ep, rx_res); } dcd_int_enable(rhport); @@ -546,9 +575,15 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK); } } else { - // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR + // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR. + // Preserve an in-flight receive: if a read is still armed (the class driver considers it + // submitted and won't re-arm), fall back to ACK, not NAK, or the endpoint NAKs forever and the + // host times out (usbtest toggle test 29 clears the halt between bulk writes on an armed EP). if (dir == TUSB_DIR_OUT) { - ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK); + uint8_t res = data.xfer[ep][TUSB_DIR_OUT].valid + ? (data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK) + : USBFS_EP_R_RES_NAK; + ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | res); } else { ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK); } diff --git a/src/portable/wch/dcd_ch32_usbhs.c b/src/portable/wch/dcd_ch32_usbhs.c index 0c154f5cee..577f86582a 100644 --- a/src/portable/wch/dcd_ch32_usbhs.c +++ b/src/portable/wch/dcd_ch32_usbhs.c @@ -348,8 +348,16 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { const tusb_dir_t dir = tu_edpt_dir(ep_addr); if (dir == TUSB_DIR_OUT) { - EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; - ep_data_tog[ep_num][TUSB_DIR_OUT] = false; + ep_data_tog[ep_num][TUSB_DIR_OUT] = false; // clear-halt resets the toggle to DATA0 + xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_OUT); + if (xfer->valid) { + // A receive is still armed (the class driver considers it submitted and won't re-arm it); + // re-queue it (ACK/NYET) instead of leaving it NAKing, or the endpoint NAKs forever after + // clear-halt (usbtest toggle test 29 clears the halt on an armed bulk-OUT pipe). + queue_out_packet(ep_num, xfer); + } else { + EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; + } } else { EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; ep_data_tog[ep_num][TUSB_DIR_IN] = false; diff --git a/src/tusb_option.h b/src/tusb_option.h index e19ee1629d..65cf747e26 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -376,7 +376,7 @@ //------------ RUSB2 --------------// #if defined(TUP_USBIP_RUSB2) #define CFG_TUD_EDPT_DEDICATED_HWFIFO 1 - #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | (TUD_OPT_HIGH_SPEED ? 4 : 0)) // 16 bit and 32 bit if highspeed + #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | 4) // HS module uses 32-bit access at any link speed (e.g. FS-forced build) #define CFG_TUSB_FIFO_HWFIFO_ADDR_STRIDE 0 #define CFG_TUSB_FIFO_HWFIFO_CUSTOM_WRITE // custom write since rusb2 can change access width 32 -> 16 and can write // odd byte with byte access diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 4b87154b68..bfb73234f2 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -28,6 +28,8 @@ # libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64 # alsa-utils - arecord (device/audio_test_freertos) # iperf - throughput tests (device/net_lwip_*) +# - device/usbtest: usbtest kernel module + testusb binary (kernel tools/usb/testusb.c) on PATH, +# plus sudo for modprobe / sysfs writes # - Python packages: pip install -r requirements.txt # # udev rules : @@ -51,8 +53,14 @@ import subprocess import json import glob -from multiprocessing import Pool, Lock +import multiprocessing from multiprocessing import TimeoutError as MpTimeoutError + +# Raw Lock/Semaphore objects passed via Pool initargs are inheritable only under the fork +# start method (spawn/forkserver pickle them and fail at Pool creation) — pin it so a +# future interpreter default change cannot break the run at startup. +_mp = multiprocessing.get_context('fork') +Pool, Lock, Semaphore, Manager = _mp.Pool, _mp.Lock, _mp.Semaphore, _mp.Manager import hashlib import ctypes from pymtp import MTP @@ -103,7 +111,33 @@ def acquire_board_lock(board_name): return fh -ENUM_TIMEOUT = 15 +# Enumeration wait budget. The first attempt gets ENUM_TIMEOUT; retry attempts get the +# shorter ENUM_TIMEOUT_RETRY - the board was just re-flashed again, and a device that is +# going to enumerate shows up within a few seconds, so a failing test costs ~3-5x a +# passing one instead of 10-30x. Per-attempt value is set by test_example(); each pool +# worker is its own process, so a module global is safe. +ENUM_TIMEOUT = 8 +ENUM_TIMEOUT_RETRY = 4 +_enum_timeout = ENUM_TIMEOUT + + +def enum_timeout_s() -> int: + """Enumeration wait budget for the current test attempt.""" + return _enum_timeout + + +def wait_until(predicate, step: float = 1.0): + """Poll predicate under the per-attempt enum budget. Deadline-based so a slow predicate + body (subprocess, libmtp scan) counts against the budget. Returns the first truthy + predicate value, or None on timeout.""" + deadline = time.monotonic() + enum_timeout_s() + while True: + r = predicate() + if r: + return r + if time.monotonic() >= deadline: + return None + time.sleep(step) STATUS_OK = "\033[32mOK\033[0m" STATUS_FAILED = "\033[31mFailed\033[0m" @@ -113,17 +147,49 @@ def acquire_board_lock(board_name): # A missing binary is reported as skipped too. REPORT_CELL = {'pass': '✅', 'fail': '❌', 'skip': '⚪'} + +class TestFail(AssertionError): + """Fail a test but still surface a metric string in its report cell (e.g. usbtest's '❌ 29/30' + instead of a bare ❌). The cell metric is icon-prefixed so render/tally treat it as a failure.""" + def __init__(self, msg: str, metric: str | None = None): + super().__init__(msg) + self.metric = metric + + verbose = False test_only = [] board_test = {} build_dir = 'cmake-build' skip_flash = False print_lock = None - - -def init_worker(lock): - global print_lock +shuffle_seed = None # per-run seed for the per-board test-order shuffle (HIL_SHUFFLE_SEED to replay) + +# Per-host-controller concurrency (see controller_of/ctrl_slot below): a usbtest battery +# saturates its DUT's host controller, so batteries and flashes are budgeted per controller. +# NOTE: a Renesas uPD720201 host card must run its latest firmware (>= 2.0.2.6; RAM-uploaded, +# so it must be re-loaded every power cycle) - its ROM firmware dies under battery + +# flash/re-enumeration churn, and usbtest.py refuses the unlink-stress cases on old firmware. +# Widths profiled 2026-07-13/14 on fw 2.0.2.6 (8/1 through 12/8): wall time falls +# 22.2/14.3/12.5/10.8 min at usbtest width 1/2/3/4 and plateaus there; flash width beyond 8 +# buys nothing and only amplifies flasher-hub contention; the first battery case failures +# (bandwidth stretch on shared leaf-hub uplinks) appear at 12/8. Hence the 8/4 defaults. +FLASH_PARALLEL = max(1, int(os.getenv('HIL_FLASH_PARALLEL', '8'))) +USBTEST_PARALLEL = max(1, int(os.getenv('HIL_USBTEST_PARALLEL', '4'))) +CTRL_SLOTS = 12 # lock slots; controllers are assigned to slots on first sight +usbtest_sems = None # CTRL_SLOTS semaphores: up to USBTEST_PARALLEL batteries per controller +flash_sems = None # CTRL_SLOTS semaphores(FLASH_PARALLEL): flash permits per controller +ctrl_map = None # shared dict: 'pci:' -> slot, 'uid:' -> pci addr cache +ctrl_meta = None # guards slot assignment in ctrl_map + + +def init_worker(lock, seed, b_mutexes, f_sems, cmap, cmeta): + global print_lock, shuffle_seed, usbtest_sems, flash_sems, ctrl_map, ctrl_meta print_lock = lock + shuffle_seed = seed + usbtest_sems = b_mutexes + flash_sems = f_sems + ctrl_map = cmap + ctrl_meta = cmeta def log_line(msg: str) -> None: @@ -135,6 +201,95 @@ def log_line(msg: str) -> None: print(msg, file=out, flush=True) +# ------------------------------------------------------------- +# Per-controller scheduling +# ------------------------------------------------------------- +def controller_of(uid: str): + """Resolve a DUT uid to its root host controller's PCI address, or None if the device + is not enumerated (e.g. parked in board_test firmware with USB off). Successful + resolutions are cached — cabling does not change mid-run. Dual-port parts (e.g. + CH32V307 usbhs/usbfs variants) share one uid and one cache entry: budgeting is only + exact when both ports sit on the same controller (true on this rig).""" + if ctrl_map is None: + return None + cached = ctrl_map.get(f'uid:{uid}') + if cached: + return cached + for f in glob.glob('/sys/bus/usb/devices/*/serial'): + d = os.path.dirname(f) + try: + if open(f).read().strip().lower() != uid.lower(): + continue + bus = int(open(os.path.join(d, 'busnum')).read()) + root = os.path.realpath(f'/sys/bus/usb/devices/usb{bus}') + m = re.findall(r'[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]', root) + if m: + ctrl_map[f'uid:{uid}'] = m[-1] + return m[-1] + except (OSError, ValueError): + continue + return None + + +def ctrl_slot(pci: str) -> int: + """Map a controller PCI address to a lock slot (assigned on first sight).""" + key = f'pci:{pci}' + with ctrl_meta: + slot = ctrl_map.get(key) + if slot is None: + slot = ctrl_map.get('nslots', 0) + if slot >= CTRL_SLOTS: + slot = 0 # more controllers than slots: overflow shares slot 0 (safe, over-serialized) + else: + ctrl_map['nslots'] = slot + 1 + ctrl_map[key] = slot + return slot + + +class ctrl_permit: + """Context manager: one permit from `sems` on the board's controller slot. If the + controller is unknown, fail closed: take one permit from EVERY slot, in order, so the + operation respects the budget wherever it might land. `warn_unknown` logs that fallback + (used by usbtest, where the device is expected to be enumerated by the caller).""" + def __init__(self, sems, uid: str, warn_unknown: bool = False): + self.sems = sems + self.slots = None + if sems is None: + return + pci = controller_of(uid) + if pci is None and warn_unknown: + log_line(f'warning: cannot resolve {uid} to a host controller; ' + 'taking a permit on every slot (over-serialized)') + self.slots = [ctrl_slot(pci)] if pci else list(range(CTRL_SLOTS)) + + def __enter__(self): + if self.slots: + taken = [] + try: + for s in self.slots: + self.sems[s].acquire() + taken.append(s) + except BaseException: + for s in reversed(taken): + self.sems[s].release() + raise + return self + + def __exit__(self, *exc): + if self.slots: + for s in reversed(self.slots): + self.sems[s].release() + return False + + +def flash_permit(uid: str) -> ctrl_permit: + return ctrl_permit(flash_sems, uid) + + +def usbtest_permit(uid: str) -> ctrl_permit: + return ctrl_permit(usbtest_sems, uid, warn_unknown=True) + + def compact_output(raw: str) -> str: if not raw: return '' @@ -189,7 +344,7 @@ class HilConfig(TypedDict): boards: list[Board] CMD_TIMEOUT = int(os.getenv('HIL_CMD_TIMEOUT', '180')) -POOL_TIMEOUT = int(os.getenv('HIL_POOL_TIMEOUT', '3000')) +POOL_TIMEOUT = int(os.getenv('HIL_POOL_TIMEOUT', '4200')) # usbtest batteries are serialized fleet-wide, lengthening the tail SERIAL_READ_TIMEOUT = float(os.getenv('HIL_SERIAL_READ_TIMEOUT', '5')) SERIAL_WRITE_TIMEOUT = float(os.getenv('HIL_SERIAL_WRITE_TIMEOUT', '10')) @@ -271,7 +426,7 @@ def get_alsa_capture_dev(id): def open_serial_dev(port: str): - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() ser = None while timeout > 0: if os.path.exists(port): @@ -306,27 +461,31 @@ def read_disk_file(uid: str, lun: int, fname: str) -> bytes: # Reads a file from a FAT volume on a block device without mounting it. # Requires mtools: `apt install mtools` (no pip dependency). dev = get_disk_dev(uid, 'TinyUSB', lun) - timeout = ENUM_TIMEOUT last_err = None - while timeout > 0: - if os.path.exists(dev): - try: - data = subprocess.check_output( - ['mtype', '-i', dev, f'::/{fname}'], stderr=subprocess.PIPE) - assert data, f'Cannot read file {fname} from {dev}' - return data - except subprocess.CalledProcessError as e: - last_err = e.stderr.decode(errors='replace').strip() - time.sleep(1) - timeout -= 1 - raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed') + def try_read(): + nonlocal last_err + if not os.path.exists(dev): + return None + try: + data = subprocess.check_output( + ['mtype', '-i', dev, f'::/{fname}'], stderr=subprocess.PIPE) + assert data, f'Cannot read file {fname} from {dev}' + return data + except subprocess.CalledProcessError as e: + last_err = e.stderr.decode(errors='replace').strip() + return None + + data = wait_until(try_read) + if data is None: + raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed') + return data def open_mtp_dev(uid): mtp = MTP() - timeout = ENUM_TIMEOUT - while timeout > 0: + + def try_open(): # unmount gio/gvfs MTP mount which blocks libmtp from accessing the device subprocess.run(f"gio mount -u mtp://TinyUsb_TinyUsb_Device_{uid}/", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -337,9 +496,9 @@ def open_mtp_dev(uid): if sn == uid: return mtp mtp.disconnect() - time.sleep(1) - timeout -= 1 - return None + return None + + return wait_until(try_open) def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): @@ -358,14 +517,13 @@ def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): def open_printer_dev(id: str, vendor_str, product_str, ifnum: int) -> str: """Wait for printer device to enumerate and return its path""" - timeout = ENUM_TIMEOUT - while timeout > 0: + def try_find(): lp_dev = get_printer_dev(id, vendor_str, product_str, ifnum) - if lp_dev and os.path.exists(lp_dev): - return lp_dev - time.sleep(1) - timeout -= 1 - assert False, f'Printer device not found for {id} if{ifnum:02d}' + return lp_dev if lp_dev and os.path.exists(lp_dev) else None + + lp_dev = wait_until(try_find) + assert lp_dev, f'Printer device not found for {id} if{ifnum:02d}' + return lp_dev # ------------------------------------------------------------- @@ -591,7 +749,7 @@ def test_dual_host_info_to_device_cdc(board): # read until all expected devices are enumerated data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -643,7 +801,7 @@ def test_host_device_info(board): # read until all expected devices are enumerated data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -722,7 +880,7 @@ def test_host_cdc_msc_hid(board): # Wait for all expected mount messages data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() wait_cdc = len(cdc_devs) > 0 wait_msc = len(msc_devs) > 0 while timeout > 0: @@ -815,7 +973,7 @@ def test_host_msc_file_explorer(board): # Wait for MSC mount (Disk Size message) data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -880,6 +1038,7 @@ def test_host_msc_file_explorer(board): break ser.close() + assert speed is not None, 'MSC read produced no speed report (dd stalled or failed)' return speed @@ -979,7 +1138,7 @@ def parse_speed(dd_output): # Wait for MSC disk enumeration dev = get_disk_dev(uid, 'TinyUSB', 0) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(dev): break @@ -988,7 +1147,7 @@ def parse_speed(dd_output): # Wait for CDC tty enumeration tty = get_serial_dev(uid, 'TinyUSB', 'Throughput', 0) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(tty): break @@ -999,7 +1158,7 @@ def parse_speed(dd_output): is_fs = False for f in glob.glob('/sys/bus/usb/devices/*/serial'): try: - if open(f).read().strip() == uid: + if open(f).read().strip().lower() == uid.lower(): is_fs = (open(os.path.join(os.path.dirname(f), 'speed')).read().strip() == '12') break except (OSError, ValueError): @@ -1045,17 +1204,19 @@ def parse_speed(dd_output): def test_device_dfu(board): uid = board['uid'] - # Wait device enum - timeout = ENUM_TIMEOUT - while timeout > 0: + # Wait device enum. Deadline-based: dfu-util -l itself takes ~1 s per call, which a + # per-iteration countdown would not charge against the budget. + deadline = time.monotonic() + enum_timeout_s() + found = False + while time.monotonic() < deadline: ret = run_cmd(f'dfu-util -l') stdout = cmd_stdout_text(ret.stdout) - if f'serial="{uid}"' in stdout and 'Found DFU: [cafe:4000]' in stdout: + if f'serial="{uid}"' in stdout and 'Found DFU: [cafe:400b]' in stdout: + found = True break time.sleep(1) - timeout = timeout - 1 - assert timeout > 0, 'Device not available' + assert found, 'Device not available' f_dfu0 = f'dfu0_{uid}' f_dfu1 = f'dfu1_{uid}' @@ -1085,17 +1246,18 @@ def test_device_dfu(board): def test_device_dfu_runtime(board): uid = board['uid'] - # Wait device enum - timeout = ENUM_TIMEOUT - while timeout > 0: + # Wait device enum (deadline-based, see test_device_dfu) + deadline = time.monotonic() + enum_timeout_s() + found = False + while time.monotonic() < deadline: ret = run_cmd(f'dfu-util -l') stdout = cmd_stdout_text(ret.stdout) - if f'serial="{uid}"' in stdout and 'Found Runtime: [cafe:4000]' in stdout: + if f'serial="{uid}"' in stdout and 'Found Runtime: [cafe:400c]' in stdout: + found = True break time.sleep(1) - timeout = timeout - 1 - assert timeout > 0, 'Device not available' + assert found, 'Device not available' def test_device_hid_boot_interface(board): @@ -1104,7 +1266,7 @@ def test_device_hid_boot_interface(board): mouse1 = get_hid_dev(uid, 'TinyUSB', 'TinyUSB_Device', 'if01-event-mouse') mouse2 = get_hid_dev(uid, 'TinyUSB', 'TinyUSB_Device', 'if01-mouse') # Wait device enum - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(kbd) and os.path.exists(mouse1) and os.path.exists(mouse2): break @@ -1302,9 +1464,9 @@ def test_device_net_lwip_webserver(board): # Wait for the host to get an IPv4 address in the device's subnet (DHCP served by the device). # USB enum + DHCP serve can take longer on the CI HIL hardware than on local — give it 30s. iface_timeout = 30 - deadline = time.time() + iface_timeout + deadline = time.monotonic() + iface_timeout host_ip = None - while time.time() < deadline: + while time.monotonic() < deadline: ret = subprocess.run(['ip', '-o', '-4', 'addr', 'show', iface], capture_output=True, text=True, timeout=2) m = re.search(r'inet (192\.168\.7\.\d+)/', ret.stdout) if ret.returncode == 0 else None @@ -1316,9 +1478,9 @@ def test_device_net_lwip_webserver(board): # Poll the iperf TCP port until the device is accepting. The net stack comes up a bit # after DHCP completes; iperf server binding isn't instantaneous after reflash. - deadline = time.time() + ENUM_TIMEOUT + deadline = time.monotonic() + enum_timeout_s() last_err = None - while time.time() < deadline: + while time.monotonic() < deadline: try: with socket.create_connection((device_ip, iperf_port), timeout=1): last_err = None @@ -1326,7 +1488,7 @@ def test_device_net_lwip_webserver(board): except OSError as e: last_err = e time.sleep(0.3) - assert last_err is None, f'iperf TCP {device_ip}:{iperf_port} not accepting within {ENUM_TIMEOUT}s: {last_err}' + assert last_err is None, f'iperf TCP {device_ip}:{iperf_port} not accepting within {enum_timeout_s()}s: {last_err}' # Throughput: 5-second iperf2 TCP test, CSV output for stable parsing. # iperf2 CSV final summary line: timestamp,src_ip,src_port,dst_ip,dst_port,id,interval,bytes,bps @@ -1366,7 +1528,7 @@ def test_device_midi_test(board): uid = board['uid'] # Find MIDI device via /dev/snd/by-id using board UID - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() midi_port = None while timeout > 0: pattern = f'/dev/snd/by-id/usb-*_{uid}-*' @@ -1388,8 +1550,8 @@ def test_device_midi_test(board): with open(midi_port, 'rb') as f: notes = [] # Read for up to 3 seconds to capture a few notes (286ms interval) - end_time = time.time() + 3 - while time.time() < end_time: + end_time = time.monotonic() + 3 + while time.monotonic() < end_time: ready, _, _ = select.select([f], [], [], 0.5) if ready: data = f.read(64) @@ -1425,7 +1587,7 @@ def test_device_audio_test_freertos(board): return 'skipped' pcm = None - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: pcm = get_alsa_capture_dev(uid) if pcm: @@ -1492,7 +1654,7 @@ def test_device_hid_generic_inout(board): import hid # cython-hidapi (pip: hidapi, apt: python3-hid) # Find HID device by UID (VID=0xCafe) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() dev = None while timeout > 0: for d in hid.enumerate(0xCafe): @@ -1524,16 +1686,74 @@ def test_device_hid_generic_inout(board): h.close() +def test_device_usbtest(board): + # Run the Linux testusb tier-4 battery (test/hil/usbtest.py) against the enumerated cafe:4010 + # device; surface the pass count in the report cell ("✅ 30/30", or "❌ 29/30" on a partial). + uid = board['uid'] + + def usbtest_enumerated(): + # match VID:PID too, not just the serial: right after flashing, the previous example's + # enumeration (same serial, different PID) can linger and would fail usbtest.py's lookup + for f in glob.glob('/sys/bus/usb/devices/*/serial'): + d = os.path.dirname(f) + try: + if (open(f).read().strip().lower() == uid.lower() + and open(os.path.join(d, 'idVendor')).read().strip() == 'cafe' + and open(os.path.join(d, 'idProduct')).read().strip() == '4010'): + return True + except OSError: + pass + return False + + end = time.monotonic() + enum_timeout_s() + while time.monotonic() < end and not usbtest_enumerated(): + time.sleep(0.2) + # fail before usbtest_permit: an absent device would otherwise queue on the battery + # mutex for minutes behind real batteries just to have usbtest.py report "no device" + assert usbtest_enumerated(), f'no cafe:4010 device with serial {uid}' + # settle: right after flashing the enumeration can bounce once (and on dual-port parts like + # CH32V307 the other port's stale usbtest node — same serial and PID — lingers a moment); + # running testusb into that gap sees the device drop mid-case + time.sleep(3) + + # --keep-binding is required for concurrent batteries: usbtest.py's cleanup unbinds + # EVERY usbtest-bound interface (releasing stale same-PID grabs), which would kill a + # peer battery mid-run under USBTEST_PARALLEL > 1; the unbind path has also wedged a + # host xHCI (usb_hcd_alloc_bandwidth) on this rig. Leaving bindings is harmless with + # unique example PIDs - the next example re-enumerates under a different PID and binds + # its normal driver. usbtest_permit budgets USBTEST_PARALLEL batteries per controller. + script = Path(__file__).resolve().parent / 'usbtest.py' + cmd = f'python3 "{script}" --serial "{uid}" --json --keep-binding --timeout 60' + with usbtest_permit(uid): + r = run_cmd(cmd, timeout=200) + out = cmd_stdout_text(r.stdout) + brace = out.find('{') + try: + data = json.loads(out[brace:]) + passed, failed = int(data['passed']), int(data['failed']) + except (ValueError, KeyError, json.JSONDecodeError): + raise AssertionError(f'usbtest did not run: {compact_output(out) or cmd_stdout_text(r.stderr)}') + + skipped = int(data.get('skipped', 0)) # host-controller limitation (see usbtest.py host_broken_cases) + total = passed + failed + if total == 0 and skipped > 0: + return 'skipped' # every case host-skipped: a skip, not a 0/0 failure + if failed == 0 and total > 0: + return f'{REPORT_CELL["pass"]} {passed}/{total}' + (f' +{skipped}skip' if skipped else '') + bad = [c.get('num') for c in data.get('cases', []) if c.get('status') not in ('PASS', 'SKIP')] + raise TestFail(f'usbtest {passed}/{total} (cases failed: {bad})', + metric=f'{REPORT_CELL["fail"]} {passed}/{total}') + + # ------------------------------------------------------------- # Main # ------------------------------------------------------------- # device tests -# note don't test 2 examples with cdc or 2 msc next to each other device_tests = [ - # Order matters: cdc_msc and cdc_msc_throughput share the same VID:PID (cafe:4003), so keep a - # differently-PID'd example (dfu, cafe:4000) between them. Boards whose CPU-reset does not drop - # D+ (e.g. WCH CH58x via openocd) only re-enumerate when the PID changes; back-to-back same-PID - # firmware would otherwise leave the host on the previous example's cached descriptors. + # The per-board run order is shuffled (see test_board). Every example carries a unique + # hardcoded idProduct (see its usb_descriptors.c), so any two different examples always + # re-enumerate back-to-back — even on boards whose CPU-reset does not drop D+ (e.g. WCH + # CH58x via openocd), which only re-enumerate when the PID changes. 'device/cdc_dual_ports', 'device/cdc_msc', 'device/dfu', @@ -1547,6 +1767,7 @@ def test_device_hid_generic_inout(board): 'device/printer_to_cdc', 'device/midi_test', 'device/mtp', + 'device/usbtest', # cafe:4010, unique PID; runs the Linux testusb tier-4 battery via usbtest.py # 'device/net_lwip_webserver', # disabled for PR #3605: USB net iface enum is flaky on the CI HIL host ] @@ -1577,7 +1798,7 @@ def find_firmware(variant: str, example: str): return None -def test_example(board: Board, variant: str, example: str) -> tuple[int, str]: +def test_example(board: Board, variant: str, example: str) -> tuple[int, str, str | None]: """ Test example firmware :param board: board dict @@ -1604,15 +1825,18 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str]: # flash firmware (unless --skip-flash), then run the test. Both may fail randomly, # retry a few times. + global _enum_timeout start_s = time.time() flash_ok = True last_err = '' last_detail = '' for i in range(max_retry): + _enum_timeout = ENUM_TIMEOUT if i == 0 else ENUM_TIMEOUT_RETRY attempt_out = io.StringIO() with redirect_stdout(attempt_out): if not skip_flash: - ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) + with flash_permit(board['uid']): + ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) flash_ok = (ret.returncode == 0) if flash_ok: try: @@ -1637,6 +1861,8 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str]: last_detail = compact_output(attempt_out.getvalue()) if i == max_retry - 1: err_count += 1 + # a failing test may still carry a metric to show in its cell (e.g. "❌ 29/30") + metric = getattr(e, 'metric', None) msg = f'{test_name} {STATUS_FAILED}: {e}' if last_detail: msg += f' {last_detail}' @@ -1752,10 +1978,24 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: rows = [] # list of (row_label, {example: status}) — one row per build variant variants = board.get('variant') or [{'name': name, 'flags': ''}] + prev_last = None # last test of the previous variant: the variant boundary is an adjacency too for v in variants: vname = v['name'] + # Shuffle each (board, variant)'s run order — de-synchronizes the worker pool so + # usbtest batteries and flash churn spread across the timeline instead of convoying, + # and surfaces order-dependent bugs. Seeded for replay (HIL_SHUFFLE_SEED, logged by + # main). Unique per-example PIDs make any two different examples re-enumerate; only + # the variant boundary can repeat the same example (same PID) — swap it away. + run_list = list(test_list) + if shuffle_seed is not None and len(run_list) > 1: + random.Random(f'{shuffle_seed}:{name}:{vname}').shuffle(run_list) + if run_list[0] == prev_last: + run_list[0], run_list[-1] = run_list[-1], run_list[0] + log_line(f'{vname:40} test order: {", ".join(t.rsplit("/", 1)[-1] for t in run_list)}') + if run_list: + prev_last = run_list[-1] cells = {} - for test in test_list: + for test in run_list: ec, status, metric = test_example(board, vname, test) err_count += ec cells[test] = metric if metric else status @@ -1789,16 +2029,21 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: def render_matrix(rows_all: list) -> str: """Render rows (list of (row_label, {example: status})) as an aligned markdown matrix: columns = tests (bare names) centered, boards left-aligned.""" - canonical = device_tests + dual_tests + host_test seen = set() for _, cells in rows_all: seen.update(cells) if not seen: return 'No tests were run.' - # columns: canonical order first, then any extras (e.g. from -t) alphabetically - columns = [t for t in canonical if t in seen] - columns += [t for t in sorted(seen) if t not in canonical] + # metric-bearing columns pinned first (usbtest score, throughput, explorer read speed), + # the rest alphabetical by bare test name: stable regardless of the (shuffled) execution order + pinned = ['usbtest', 'cdc_msc_throughput', 'msc_file_explorer', 'msc_file_explorer_freertos'] + + def col_key(t): + name = t.rsplit('/', 1)[-1] + return (pinned.index(name) if name in pinned else len(pinned), name, t) + + columns = sorted(seen, key=col_key) headers = [c.rsplit('/', 1)[-1] for c in columns] # bare example name def cell(cells, col): @@ -1820,10 +2065,19 @@ def line(label, values): sep = '| ' + '-' * board_w + ' | ' + ' | '.join(':' + '-' * (w - 2) + ':' for w in col_w) + ' |' body = [line(lbl, [cell(cells, c) for c in columns]) for lbl, cells in rows_all] - # tally run cells (blank/not-run cells are absent from the dicts); a metric string counts as pass - failed = sum(v == 'fail' for _, cells in rows_all for v in cells.values()) - skipped = sum(v == 'skip' for _, cells in rows_all for v in cells.values()) - passed = sum(v not in ('fail', 'skip') for _, cells in rows_all for v in cells.values()) + # tally run cells (blank/not-run cells are absent from the dicts). A cell is a bare status + # ('pass'/'fail'/'skip') or a metric string that carries its own icon (e.g. "❌ 29/30" is a + # fail, "✅ 30/30" / "✅ CDC …" a pass), so classify by the leading icon. + def cell_kind(v): + if v == 'fail' or (isinstance(v, str) and v.startswith(REPORT_CELL['fail'])): + return 'fail' + if v == 'skip' or (isinstance(v, str) and v.startswith(REPORT_CELL['skip'])): + return 'skip' + return 'pass' + kinds = [cell_kind(v) for _, cells in rows_all for v in cells.values()] + failed = kinds.count('fail') + skipped = kinds.count('skip') + passed = kinds.count('pass') summary = (f'**{REPORT_CELL["pass"]} {passed} passed · {REPORT_CELL["fail"]} {failed} failed · ' f'{REPORT_CELL["skip"]} {skipped} skipped · blank not run**') @@ -1951,7 +2205,16 @@ def main() -> None: for f in (REPORT_JSON, REPORT_MD): (report_dir / f).unlink(missing_ok=True) - with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=(Lock(),)) as pool: + seed = os.getenv('HIL_SHUFFLE_SEED') or str(int(time.time())) + log_line(f'test-order shuffle seed: {seed} (HIL_SHUFFLE_SEED={seed} to replay); ' + f'flash/usbtest parallel per controller: {FLASH_PARALLEL}/{USBTEST_PARALLEL}; ' + f'enum timeout first/retry: {ENUM_TIMEOUT}/{ENUM_TIMEOUT_RETRY}s') + mgr = Manager() + initargs = (Lock(), seed, + [Semaphore(USBTEST_PARALLEL) for _ in range(CTRL_SLOTS)], + [Semaphore(FLASH_PARALLEL) for _ in range(CTRL_SLOTS)], + mgr.dict(), Lock()) + with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=initargs) as pool: async_ret = pool.map_async(test_board, config_boards) try: mret = async_ret.get(timeout=POOL_TIMEOUT) diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 59ab57a062..8ed33c8a2a 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -22,10 +22,12 @@ { "name": "espressif_p4_function_ev-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } ], "tests": { + "comment": "espressif fleet build = IDF/FreeRTOS examples plus the IDF-buildable bare-metal-style ones tools/build.py allowlists (board_test, usbtest, video_capture)", "only": [ "device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", + "device/usbtest", "host/device_info", "host/msc_file_explorer_freertos" ], @@ -65,6 +67,7 @@ "device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", + "device/usbtest", "host/device_info", "host/msc_file_explorer_freertos" ], @@ -166,19 +169,20 @@ "name": "mimxrt1064_evk", "uid": "BAE96FB95AFA6DBB8F00005002001200", "tests": { + "skip": ["host/cdc_msc_hid"], + "comment-cdc-echo": "CH9102+Lexar bundle (moved here from stm32f723disco) mounts fine but echo returns nothing - TX-RX loopback jumper likely lost in the move; re-check wiring then re-enable", "device": true, "host": true, "dual": true, "dev_attached": [ { - "vid_pid": "10c4_ea60", - "serial": "0001", - "is_cdc": true, - "comment": "cp2102" + "vid_pid": "1a86_55d4", + "serial": "52D2003414", + "is_cdc": true }, { "vid_pid": "21c4_0cc7", - "serial": "900058874D871F66", + "serial": "90005893730A1A63", "is_msc": true, "block_size": 512, "block_count": 60620800, @@ -230,6 +234,8 @@ "device": true, "host": true, "dual": true, + "skip": ["host/cdc_msc_hid", "host/device_info", "host/msc_file_explorer", "host/msc_file_explorer_freertos", "dual/host_info_to_device_cdc"], + "comment-skip": "PIO-USB host port enumerates nothing since the board moves (CH340+UDisk bundle unplugged or unpowered) - re-attach the bundle then drop these skips", "dev_attached": [ { "vid_pid": "1a86_7523", @@ -379,13 +385,14 @@ "dual": false, "dev_attached": [ { - "vid_pid": "1a86_55d4", - "serial": "52D2003414", - "is_cdc": true + "vid_pid": "10c4_ea60", + "serial": "0001", + "is_cdc": true, + "comment": "cp2102" }, { "vid_pid": "21c4_0cc7", - "serial": "90005893730A1A63", + "serial": "900058874D871F66", "is_msc": true, "block_size": 512, "block_count": 60620800, @@ -494,6 +501,25 @@ "args": "" } }, + { + "name": "ch32v307v_r1_1v0", + "uid": "DE6B3E263B3857CAFFFFFFFF", + "toolchain": "riscv-gcc", + "variant": [ + {"name": "ch32v307v_r1_1v0-usbhs", "defines": ["SPEED=high"]}, + {"name": "ch32v307v_r1_1v0-usbfs", "defines": ["SPEED=full"]} + ], + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "openocd_wch", + "uid": "BC5DA47360D0", + "args": "" + } + }, { "name": "ch582m_evt", "uid": "D443627B5450", @@ -508,9 +534,72 @@ "uid": "7FD88F0604B5", "args": "" } + }, + { + "name": "nrf5340dk", + "uid": "78E60E166B5F88BE", + "tests": { + "device": true, + "host": false, + "dual": false, + "skip": ["device/cdc_msc_freertos", "device/audio_test_freertos"], + "comment": "board new to HIL: FreeRTOS examples hardfault (UFSR=INVPC) at first task launch on the CM33_NTZ port - pre-existing upstream issue, non-FreeRTOS examples and usbtest pass; fix separately" + }, + "flasher": { + "name": "jlink", + "uid": "001050076405", + "args": "-device NRF5340_XXAA_APP" + } + }, + { + "name": "nrf54lm20dk", + "uid": "899C3DE5B0F4D5CA", + "tests": { + "device": true, + "host": false, + "dual": false, + "skip": ["device/audio_test_freertos"], + "comment": "board new to HIL: audio_test_freertos never reaches dcd_init (FreeRTOS itself runs; cdc_msc_freertos and usbtest pass) - example-level issue on nRF54L, fix separately" + }, + "flasher": { + "name": "jlink", + "uid": "1051856258", + "args": "-device NRF54LM20A_M33" + } } ], "boards-skip": [ + { + "name": "ra6m5_ek", + "uid": "8419032D32363657364EF4622D294B4E", + "tests": { + "device": true, + "host": false, + "dual": false, + "skip": ["device/cdc_msc_throughput", "device/msc_dual_lun"], + "comment": "MSC writes wedge the uPD720201 host (URBs queued, zero wire activity, bus-15 ctrl xfers time out until the device's URBs are killed); reproduced identically with master firmware - device side armed+BUF and exonerated. MSC reads and usbtest bulk (15.8 MB/s) are fine" + }, + "flasher": { + "name": "jlink", + "uid": "000831915224", + "args": "-device R7FA6M5BH" + } + }, + { + "name": "ra8m1_ek", + "uid": "797D142D36345030364E1737922E4B4E", + "comment": "USBHS bring-up pending (HS chirp completes digitally but terminations never switch; FS-forced build passed usbtest 30/30). Parked until fixed", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "jlink", + "uid": "001083115236", + "args": "-device R7FA8M1AH" + } + }, { "name": "stm32f769disco", "uid": "21002F000F51363531383437", diff --git a/test/hil/usbtest.py b/test/hil/usbtest.py new file mode 100755 index 0000000000..9aec8ac0a9 --- /dev/null +++ b/test/hil/usbtest.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +"""Run the Linux kernel usbtest/testusb battery against a TinyUSB usbtest device. + +Device firmware: examples/device/usbtest (VID:PID cafe:4010). The firmware +advertises its capability tier in bcdDevice low byte; the battery is selected +accordingly (see examples/device/usbtest/README.md). + +Requires: usbtest kernel module (CONFIG_USB_TEST), testusb binary (built from +kernel tools/usb/testusb.c), sudo for driver binding + usbfs ioctls. + +testusb reporting quirks this script works around: +- its exit code is always 0 when the device exists: results are parsed from stdout +- a case gated off by the driver's capability profile (or an in-kernel parameter + check) returns -EOPNOTSUPP, which testusb silently skips: a missing result line + means NOT RUN, and is reported as a failure since every case in the selected + battery is expected to run. + +Binding uses the 5-field new_id form referencing Gadget Zero (0525:a4a0) so the +dynamic id inherits its capability profile (autoconf + ctrl_out + iso + intr). +Never register a plain "vid pid" dynamic id with usbtest: the dynid then has +driver_info == 0 and usbtest_probe() dereferences it without a NULL check +(kernel oops). autoconf is also what enables bulk endpoint discovery; the +capability flags only unlock cases, they don't require the endpoints to exist. +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import time +from pathlib import Path + +VID = 'cafe' +PID = '4010' +GZ_REF = '0525 a4a0' # copy Gadget Zero's capability profile (ctrl_out+iso+intr) +SYS_USB = Path('/sys/bus/usb/devices') +DRIVER = Path('/sys/bus/usb/drivers/usbtest') +USB_RECOVER = Path(__file__).resolve().parents[2] / '.claude/skills/usb-recover/scripts/usb_recover.sh' +PATTERN_PARAM = Path('/sys/module/usbtest/parameters/pattern') + +# Battery per tier, in run order: control sanity first, then simple bulk, +# queued, unaligned, unlink, halt/toggle, throughput last. +TIER_CASES = { + 1: [0, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 11, 12, 24, 13, 29, 27, 28], + 2: [14, 21], + 3: [25, 26], + 4: [15, 16, 22, 23], +} + +# Per-case testusb parameters (full speed / high speed). All -s/-v values are +# multiples of 512 so transfers stay packet-aligned at both speeds: the device +# streams whole max-size packets and a non-aligned IN length would babble. +# 14/21 must never run with defaults (vary >= length is -EINVAL in the kernel). +PARAMS = { + 0: ('-c 1', '-c 1'), + 9: ('-c 256', '-c 1000'), + 10: ('-c 64 -g 16', '-c 256 -g 16'), + **{n: ('-c 128 -s 1024 -v 512', '-c 512 -s 1024 -v 512') for n in (1, 2, 3, 4, 17, 18, 19, 20)}, + **{n: ('-c 8 -s 1024 -g 8', '-c 32 -s 1024 -g 16') for n in (5, 6, 7, 8)}, + **{n: ('-c 64 -s 1024 -g 8', '-c 256 -s 1024 -g 8') for n in (11, 12, 24)}, + 13: ('-c 16 -s 512', '-c 64 -s 512'), + 29: ('-c 16 -s 512', '-c 64 -s 512'), + 27: ('-c 16 -s 1024 -g 32', '-c 128 -s 1024 -g 32'), + 28: ('-c 16 -s 1024 -g 32', '-c 128 -s 1024 -g 32'), + 14: ('-c 64 -s 512 -v 61', '-c 256 -s 512 -v 61'), + 21: ('-c 64 -s 512 -v 61', '-c 256 -s 512 -v 61'), + 25: ('-c 32 -s 512', '-c 256 -s 1024'), + 26: ('-c 32 -s 512', '-c 256 -s 1024'), + **{n: ('-c 16 -s 512 -g 8', '-c 64 -s 1024 -g 8') for n in (15, 16, 22, 23)}, +} + +CASE_NAMES = { + 0: 'NOP', 1: 'bulk write', 2: 'bulk read', 3: 'bulk write vary', 4: 'bulk read vary', + 5: 'bulk sg write', 6: 'bulk sg read', 7: 'bulk sg write vary', 8: 'bulk sg read vary', + 9: 'ch9 subset', 10: 'queued control', 11: 'unlink reads', 12: 'unlink writes', + 13: 'ep halt set/clear', 14: 'ctrl_out write/read', 15: 'iso write', 16: 'iso read', + 17: 'bulk write unaligned', 18: 'bulk read unaligned', 19: 'bulk write premapped', + 20: 'bulk read premapped', 21: 'ctrl_out unaligned', 22: 'iso write unaligned', + 23: 'iso read unaligned', 24: 'unlink queued writes', 25: 'int write', 26: 'int read', + 27: 'bulk write perf', 28: 'bulk read perf', 29: 'toggle clear', +} + +RE_PASS = re.compile(r'test (\d+),\s*(\d+)\.(\d+) secs') +RE_FAIL = re.compile(r'test (\d+) --> (\d+) \((.*)\)') + + +def run(cmd, **kw): + kw.setdefault('capture_output', True) + kw.setdefault('text', True) + return subprocess.run(cmd, **kw) + + +def sudo(cmd, **kw): + if os.geteuid() != 0: + cmd = ['sudo', '-n'] + cmd + r = run(cmd, **kw) + if r.returncode != 0 and 'password is required' in (r.stderr or ''): + sys.exit(f'sudo needs a password for: {" ".join(cmd)}\n' + 'Run as root, or grant this user NOPASSWD sudo.') + return r + + +def sysfs_write(path, data, check=True): + # A driver-registry write (new_id/remove_id/bind) blocks in D state when a wedged device + # holds its lock (driver_attach walks the bus): fail fast and loud instead of piling up + # unkillable writers and hanging the whole run -- the rig needs USB recovery first. + try: + r = sudo(['tee', str(path)], input=data, timeout=15) + except subprocess.TimeoutExpired: + sys.exit(f'write "{data}" > {path} blocked >15s: USB subsystem is wedged ' + '(a D-state device lock exists). Recover the rig (usb_recover.sh) ' + 'before running batteries.') + if check and r.returncode != 0: + sys.exit(f'write "{data}" > {path} failed: {r.stderr.strip()}') + return r.returncode == 0 + + +def find_device(serial, first=False): + """Locate the usbtest device in sysfs, return info dict or None.""" + matches = [] + for dev in SYS_USB.iterdir(): + try: + if (dev / 'idVendor').read_text().strip() != VID or \ + (dev / 'idProduct').read_text().strip() != PID: + continue + dev_serial = (dev / 'serial').read_text().strip() + if serial and dev_serial.lower() != serial.lower(): + continue + matches.append({ + 'sysname': dev.name, + 'serial': dev_serial, + 'node': '/dev/bus/usb/%03d/%03d' % (int((dev / 'busnum').read_text()), + int((dev / 'devnum').read_text())), + 'speed': (dev / 'speed').read_text().strip(), + 'tier': int((dev / 'bcdDevice').read_text().strip()[-2:], 16), + }) + except (OSError, ValueError): + continue + if not matches: + return None + if len(matches) > 1 and not first: + if serial: + # Dual-port parts (nanoch32v203 fsdev/usbfs, ch32v307 usbhs/usbfs) briefly enumerate + # BOTH ports with the same serial around a variant reflash; picking one arbitrarily + # could bind the stale port. Report ambiguity so the caller retries until it drops. + return {'ambiguous': sorted(m['sysname'] for m in matches)} + sys.exit(f'multiple {VID}:{PID} devices found, use --serial: ' + + ', '.join(m["serial"] for m in matches)) + return matches[0] + + +def host_broken_cases(dev): + """Cases the DUT's upstream host controller cannot run: {case: reason}. Exits the + whole run instead if the host is a uPD720201 on pre-2.0.2.6 firmware (see below). + The MosChip MCS9990 (9710:9990) EHCI cannot run interrupt-OUT: its FRINDEX + register is buggy silicon (the kernel probes it with "applying MosChip + frame-index workaround") and ehci-hcd never keeps the int-OUT QH in the + hardware periodic schedule, so every int-OUT URB times out regardless of + bInterval/mps/size while the device sits armed. Verified A/B 2026-07-09, + same board+hub: EHCI FAIL (QH absent from the debugfs periodic schedule the + whole hang), OHCI companion PASS, xHCI fine; int-IN unaffected. Skip with a + visible SKIP so the battery self-heals once the DUT tree is back on an xHCI.""" + for attempt in range(3): + try: + root = Path(f"/sys/bus/usb/devices/usb{int(dev['node'].split('/')[-2])}") + drv = (root / '../driver').resolve().name + pci = (root / '..').resolve() + vid_did = ((pci / 'vendor').read_text().strip(), (pci / 'device').read_text().strip()) + break + except (OSError, ValueError): + # transient sysfs error (e.g. racing a re-enumeration): retry so a blip doesn't + # silently run known-broken cases; if the probe truly fails, fail open but say so + if attempt == 2: + print('warning: cannot probe the upstream host controller; ' + 'known-broken-host cases will run instead of being skipped', file=sys.stderr) + return {} + time.sleep(1) + if drv.startswith('ehci') and vid_did == ('0x9710', '0x9990'): + return { + 25: 'host EHCI (MosChip MCS9990) loses interrupt-OUT completions', + # Unlinking an in-progress read intermittently completes it as a short transfer + # (EREMOTEIO) instead of -ECONNRESET; device-side exonerated by TX counters (only + # full-mps loads, no ZLP). Passes on xHCI. Some boards dodge it by timing. + 11: 'host EHCI (MosChip MCS9990) completes unlinked reads as short (EREMOTEIO)', + } + if drv.startswith('xhci') and vid_did in (('0x1912', '0x0014'), ('0x1912', '0x0015')): + # The Renesas uPD720201/uPD720202 must run its latest firmware (>= 2.0.2.6, + # K2026090.mem; RAM-uploaded, so it reverts to ROM on every power cycle unless + # re-loaded). On the ROM firmware its command ring intermittently dies under unlink + # stress: a Configure Endpoint command stops completing, the hub worker deadlocks + # holding the device lock (needs a host power cycle). Three separate boards killed + # it this way (ch32v307 2026-07-10; ra6m5 test 24, mimxrt1015 2026-07-11). Both + # parts expose the FW version register at PCI config offset 0x6c. NOTE this check + # is necessary, not sufficient: board-specific batteries have killed the controller + # on current firmware too (mimxrt1015, stop-endpoint timeout) - those are handled + # by per-board skips in the rig config. + fw = None + try: + r = sudo(['setpci', '-s', pci.name, '0x6c.l'], capture_output=True, text=True) + if r.returncode == 0: + fw = int(r.stdout.strip(), 16) + except (OSError, ValueError): + pass + if fw is None: + sys.exit(f'REFUSING to run: cannot read host xHCI Renesas ({pci.name}) firmware ' + 'version (setpci missing or not permitted) - usbtest requires verified ' + 'firmware >= 0x00202609 (2.0.2.6); on older firmware the command ring ' + 'dies under unlink stress. Install pciutils / fix sudo, or load the ' + 'firmware and re-check.') + if fw < 0x00202609: + sys.exit(f'REFUSING to run: host xHCI Renesas ({pci.name}) firmware 0x{fw:08x} ' + '< 0x00202609 (2.0.2.6) - its command ring dies under usbtest unlink ' + 'stress. Load the latest firmware (K2026090.mem; it is RAM-uploaded and ' + 'reverts to ROM on every power cycle).') + return {} + + +def bind_usbtest(dev): + """Bind the device's interface 0 to the usbtest driver.""" + if not DRIVER.exists(): + r = sudo(['modprobe', 'usbtest']) + if r.returncode != 0 or not DRIVER.exists(): + sys.exit(f'cannot load usbtest module: {r.stderr.strip()}') + + # always re-register in case a stale dynamic id carries a different profile + intf = f'{dev["sysname"]}:1.0' + drv = SYS_USB / intf / 'driver' + stale_binding = drv.is_symlink() and drv.resolve().name == 'usbtest' + sysfs_write(DRIVER / 'remove_id', f'{VID} {PID}', check=False) + sysfs_write(DRIVER / 'new_id', f'{VID} {PID} 0 {GZ_REF}') + if stale_binding: + # bound before the re-registration: that probe captured the OLD dynamic id's capability + # profile; unbind once (device is idle here) so the loop below reprobes the fresh one + sysfs_write(drv / 'unbind', intf, check=False) + + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + drv = SYS_USB / intf / 'driver' + if drv.is_symlink(): + if drv.resolve().name == 'usbtest': + return + # claimed by a foreign driver: steal the interface + sysfs_write(drv / 'unbind', intf) + sysfs_write(DRIVER / 'bind', intf, check=False) + time.sleep(0.2) + sys.exit(f'interface {intf} did not bind to usbtest') + + +def set_pattern(value): + try: + if PATTERN_PARAM.read_text().strip() != str(value): + sysfs_write(PATTERN_PARAM, str(value)) + except OSError as e: # FileNotFoundError (no param), PermissionError (root-only), ... + sys.exit(f'{PATTERN_PARAM} not usable ({e.strerror}): this usbtest module build may lack ' + 'the "pattern" param, or it is not readable') + + +def dmesg_tail(): + r = sudo(['dmesg']) + lines = [l for l in r.stdout.splitlines() if 'usbtest' in l] + return '\n'.join(lines[-8:]) + + +def pci_addr_of_bus(busnum): + """Return the PCI B:D.F backing a USB bus, or None for a non-PCI (SoC/platform) controller.""" + m = re.search(r'([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9])/usb\d+$', + os.path.realpath(f'/sys/bus/usb/devices/usb{int(busnum)}')) + return m.group(1) if m else None + + +def run_case(num, dev, testusb, quick, timeout): + fs_hs = PARAMS[num][0 if dev['speed'] == '12' else 1] + if quick: + fs_hs = re.sub(r'-c (\d+)', lambda m: f'-c {max(1, int(m.group(1)) // 8)}', fs_hs) + cmd = [testusb, '-D', dev['node'], '-t', str(num)] + fs_hs.split() + # device nodes are usually opened directly (udev rule); sudo only if not + if not os.access(dev['node'], os.W_OK) and os.geteuid() != 0: + cmd = ['sudo', '-n'] + cmd + result = {'num': num, 'name': CASE_NAMES[num], 'params': fs_hs} + + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + try: + out, _ = p.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + p.kill() + try: + out, _ = p.communicate(timeout=5) + except subprocess.TimeoutExpired: + # SIGKILL had no effect: the child is in uninterruptible sleep on an + # in-kernel usbfs ioctl (device stopped responding mid-transfer). + # Abandon it — waiting or re-signalling can never succeed. + result.update(status='HUNG', detail=f'testusb stuck in D state after {timeout}s', + dmesg=dmesg_tail()) + return result + result.update(status='FAIL', detail=f'timeout after {timeout}s', dmesg=dmesg_tail()) + return result + + m = RE_PASS.search(out) + if m and int(m.group(1)) == num: + secs = float(f'{m.group(2)}.{m.group(3)}') + result.update(status='PASS', secs=secs) + if num in (27, 28) and secs > 0: + opts = dict(zip(fs_hs.split()[::2], fs_hs.split()[1::2])) + total = int(opts['-c']) * int(opts['-s']) * int(opts['-g']) + result['mbps'] = round(total / secs / 1e6, 2) + return result + + m = RE_FAIL.search(out) + if m and int(m.group(1)) == num: + result.update(status='FAIL', detail=f'errno {m.group(2)} ({m.group(3)})', + dmesg=dmesg_tail()) + return result + + if cmd[0] == 'sudo' and ('password is required' in out or 'a terminal is required' in out): + result.update(status='FAIL', detail='sudo needs a password to run testusb: the device node ' + 'is not writable') + return result + + # no result line: the kernel returned -EOPNOTSUPP (capability profile or + # in-kernel parameter gate) and testusb skipped silently + result.update(status='NOTRUN', detail='case gated off: check binding profile/pattern', + stderr=out.strip()) + return result + + +def main(): + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument('--serial', help='board uid (USB serial string) to select the device') + p.add_argument('--tier', type=int, choices=sorted(TIER_CASES), + help='override tier (default: from device bcdDevice)') + p.add_argument('--tests', help='comma-separated case numbers, overrides tier battery') + p.add_argument('--quick', action='store_true', help='divide iteration counts by 8') + p.add_argument('--json', action='store_true', help='machine-readable output on stdout') + p.add_argument('--keep-binding', action='store_true', help='leave usbtest dynamic id registered') + p.add_argument('--testusb', default=None, help='path to testusb binary') + p.add_argument('--timeout', type=int, default=120, help='per-case timeout in seconds') + args = p.parse_args() + sys.stdout.reconfigure(line_buffering=True) # per-case results visible when piped/logged + + testusb = args.testusb or shutil.which('testusb') or os.path.expanduser('~/testusb') + if not os.access(testusb, os.X_OK): + sys.exit('testusb binary not found: build kernel tools/usb/testusb.c ' + 'and install it, or pass --testusb') + + # retry briefly: right after a flash the enumeration may still be settling, and on dual-port + # parts the other port's stale same-serial node takes a moment to drop off (see find_device) + deadline = time.monotonic() + 8 + while True: + dev = find_device(args.serial) + if dev and 'ambiguous' not in dev: + break + if time.monotonic() > deadline: + if dev: + sys.exit(f"multiple devices with serial {args.serial}: {', '.join(dev['ambiguous'])} " + '— stale enumeration from another port? replug or retry') + sys.exit(f'no {VID}:{PID} device' + (f' with serial {args.serial}' if args.serial else '')) + time.sleep(0.5) + + # tier drives which cases run; a stale/foreign device advertising an out-of-range tier + # must not silently run an empty battery ('0/0 passed' would read as green in CI) + tier = args.tier or dev['tier'] + if not 1 <= tier <= max(TIER_CASES): + sys.exit(f"device advertises tier {tier} (bcdDevice ...{tier:02x}); reflash a usbtest build " + f"or pass --tier 1..{max(TIER_CASES)} — refusing to run an unknown/empty battery") + if args.tests: + cases = [] + for tok in args.tests.split(','): + tok = tok.strip() + if not tok.isdecimal() or int(tok) not in PARAMS: # isdecimal rejects unicode digits + sys.exit(f'--tests: {tok!r} is not a known case number (valid 0..{max(PARAMS)})') + cases.append(int(tok)) + else: + cases = [n for t in range(1, tier + 1) for n in TIER_CASES[t]] + + info = f"device {dev['serial']} {dev['node']} speed={dev['speed']} tier={tier}" + if not args.json: + print(info) + + # probe the upstream controller before touching the device: an unsupported host + # (uPD720201 on pre-2.0.2.6 firmware) exits here, before any bind + broken = host_broken_cases(dev) + + results = [] + unrecovered_hang = False + try: + bind_usbtest(dev) + set_pattern(0) # tier 1 firmware sources zeros; also required by perf cases 27/28 + + for num in cases: + if num in broken: + results.append({'num': num, 'name': CASE_NAMES[num], 'status': 'SKIP', + 'detail': broken[num]}) + if not args.json: + print(f"test {num:2d} {CASE_NAMES[num]:22s} SKIP {broken[num]}") + continue + results.append(run_case(num, dev, testusb, args.quick, args.timeout)) + r = results[-1] + if not args.json: + extra = f" {r.get('secs', '')}s" if r['status'] == 'PASS' else f" {r.get('detail', '')}" + extra += f" {r['mbps']} MB/s" if 'mbps' in r else '' + print(f"test {num:2d} {r['name']:22s} {r['status']:6s}{extra}") + if r['status'] == 'HUNG': + pci = pci_addr_of_bus(dev['node'].split('/')[-2]) + if pci: + print(f'aborting battery: kernel-side hang, device wedged mid-transfer.\n' + f'auto-recovering: sudo {USB_RECOVER} pci-reset {pci} ' + f'(see .claude/skills/usb-recover)', file=sys.stderr) + # FLR frees the D-state ioctl without the device lock; must run BEFORE + # any unbind/remove_id, which would deadlock the bus otherwise + if sudo([str(USB_RECOVER), 'pci-reset', pci]).returncode != 0: + unrecovered_hang = True + time.sleep(5) # let the bus re-enumerate before cleanup touches sysfs + else: + unrecovered_hang = True + print('aborting battery: kernel-side hang, and the controller has no PCI address ' + 'for FLR recovery — manual intervention (reboot) required', file=sys.stderr) + break + # re-resolve: after a mid-battery re-enumeration the devnum (and thus the node + # path) changes; keep testing the live node instead of the stale one. Match on the + # concrete serial (not args.serial, which may be None) so this can never retarget to + # a different device that happens to share the VID:PID. + live = find_device(dev['serial'], first=True) + if not live: + results.append({'num': num, 'status': 'FAIL', + 'detail': f'device dropped off the bus after case {num}'}) + break + dev = live + finally: + # best-effort cleanup: a sudo/sysfs failure here (sudo() may sys.exit) must not replace + # an exception propagating out of the try body with a less useful one + try: + if unrecovered_hang: + # testusb is still stuck in a usbfs ioctl holding the device lock; remove_id/unbind + # would join the convoy and deadlock the bus (see usb-recover skill) — leave it be + print('skipping cleanup after unrecovered hang: reboot required to release the bus', + file=sys.stderr) + elif not args.keep_binding: + sysfs_write(DRIVER / 'remove_id', f'{VID} {PID}', check=False) + # release every claimed interface: other devices sharing the VID:PID (stale example + # firmware on a test rig) may have been grabbed on probe and would otherwise stay + # bound to usbtest until re-plugged, hijacking the next test's device + for intf in DRIVER.glob('*:*'): + sysfs_write(DRIVER / 'unbind', intf.name, check=False) + except SystemExit: + pass + + failed = [r for r in results if r['status'] not in ('PASS', 'SKIP')] + skipped = [r for r in results if r['status'] == 'SKIP'] + ran = len(results) - len(skipped) + if args.json: + print(json.dumps({'serial': dev['serial'], 'speed': dev['speed'], 'tier': tier, + 'passed': ran - len(failed), 'failed': len(failed), + 'skipped': len(skipped), 'cases': results}, indent=2)) + else: + note = f", {len(skipped)} skipped (host limitation)" if skipped else '' + print(f"{ran - len(failed)}/{ran} passed{note}") + for r in failed: + print(f" FAILED test {r['num']}: {r.get('detail', '')}") + if r.get('dmesg'): + print(' ' + r['dmesg'].replace('\n', '\n ')) + return len(failed) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/build.py b/tools/build.py index 5eaaeb5134..51d3d0f70c 100755 --- a/tools/build.py +++ b/tools/build.py @@ -92,6 +92,7 @@ def get_examples(family): if family == 'espressif': all_examples.append('device/board_test') + all_examples.append('device/usbtest') all_examples.append('device/video_capture') all_examples.append('host/device_info') all_examples.sort() diff --git a/tools/check_example_pids.py b/tools/check_example_pids.py new file mode 100644 index 0000000000..d8795af345 --- /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 + ` +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()) diff --git a/tools/usb_drivers/tinyusb_win_usbser.inf b/tools/usb_drivers/tinyusb_win_usbser.inf index 659f048aee..e3875a4787 100644 --- a/tools/usb_drivers/tinyusb_win_usbser.inf +++ b/tools/usb_drivers/tinyusb_win_usbser.inf @@ -88,11 +88,11 @@ ServiceBinary=%12%\%DRIVERFILENAME%.sys [SourceDisksNames] [DeviceList] -%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00 +%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00, USB\VID_CAFE&PID_4006&MI_00, USB\VID_CAFE&PID_4008&MI_00, USB\VID_CAFE&PID_400a&MI_00, USB\VID_CAFE&PID_4020&MI_00, USB\VID_CAFE&PID_4022&MI_00 [DeviceList.NTamd64] -%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00 +%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00, USB\VID_CAFE&PID_4006&MI_00, USB\VID_CAFE&PID_4008&MI_00, USB\VID_CAFE&PID_400a&MI_00, USB\VID_CAFE&PID_4020&MI_00, USB\VID_CAFE&PID_4022&MI_00 ;------------------------------------------------------------------------------ ; String Definitions