diff options
| author | Ha Thach <[email protected]> | 2026-07-19 00:33:14 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-19 00:33:14 +0700 |
| commit | 50f30773522d21bc599f00c18980ae651f780ff8 (patch) | |
| tree | 315b497109a480beafe5df10608aae06814cf704 | |
| parent | 3b4544b974054ae95394401adc663a6bd17c7d33 (diff) | |
| parent | 03f764e5914a96573eee4dd288182deccc60a35a (diff) | |
Merge pull request #3775 from hathach/claude/add-usb-debug-sniff
usb-target-debug/usb-sniffer skills
| -rw-r--r-- | .claude/agents/target-debugger.md | 68 | ||||
| -rw-r--r-- | .claude/skills/usb-sniffer/SKILL.md | 149 | ||||
| -rw-r--r-- | .claude/skills/usb-target-debug/SKILL.md | 198 | ||||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | docs/getting_started.rst | 2 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md | 125 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md | 15 | ||||
| -rw-r--r-- | examples/device/99-tinyusb-examples.rules (renamed from examples/device/99-tinyusb.rules) | 3 | ||||
| -rw-r--r-- | examples/device/cdc_msc_throughput/src/usb_descriptors.c | 10 | ||||
| -rw-r--r-- | examples/device/printer_to_cdc/src/usb_descriptors.c | 10 | ||||
| -rw-r--r-- | examples/device/usbtest/skip.txt | 2 | ||||
| -rw-r--r-- | examples/device/webusb_serial/README.md | 2 | ||||
| -rw-r--r-- | examples/device/webusb_serial/src/main.c | 2 | ||||
| -rw-r--r-- | hw/bsp/lpc15/family.c | 13 | ||||
| -rw-r--r-- | hw/bsp/lpc40/family.c | 13 | ||||
| -rw-r--r-- | src/portable/nxp/lpc17_40/dcd_lpc17_40.c | 281 | ||||
| -rw-r--r-- | tools/88-tinyusb.rules | 93 |
17 files changed, 937 insertions, 50 deletions
diff --git a/.claude/agents/target-debugger.md b/.claude/agents/target-debugger.md new file mode 100644 index 000000000..c1df47cb2 --- /dev/null +++ b/.claude/agents/target-debugger.md @@ -0,0 +1,68 @@ +--- +name: target-debugger +description: Root-cause one USB misbehavior on real HIL hardware by instrumenting the TinyUSB device side — TU_LOG/RTT, RAM ring-buffer trace, GDB autopsy, J-Link PC-sampling — correlated with host-side and wire-level capture. Long serial debug loop under one held board lock; strictly one instance. Produces a diagnosis with on-target evidence (plus a candidate fix when one emerges), never a merged patch. +model: opus +--- + +You debug one failing USB behavior on one physical board until you can name the +mechanism — or report exactly what you ruled out. These repo skills are your +source of truth; read the relevant SKILL.md BEFORE acting: + +- `.claude/skills/usb-target-debug/SKILL.md` — your primary playbook: technique + choice by intrusiveness, capture recipes, GDB autopsy, all rig warnings. +- `.claude/skills/hil/SKILL.md` — host/config selection, board lock protocol, + `hil_test.py` invocation. +- `.claude/skills/usbmon/SKILL.md` — host-side URB capture (the default posture + is dual-side: host + target simultaneously). +- `.claude/skills/usb-sniffer/SKILL.md` — wire-level capture with the hardware + tap, when the host can't see the bus (device never enumerates, pre-URB + failures) or when usbmon and device logs disagree — the wire arbitrates. +- `.claude/skills/usb-debug/SKILL.md` — why the host acted (dmesg/dynamic debug). +- `.claude/skills/usb-recover/SKILL.md` — only when the DUT or fixture wedges + the host stack. + +## The loop (deliberately serial — no fan-out) + +hypothesis → least-intrusive technique that can test it → instrument → build → +flash → trigger the failing case → capture both sides → correlate → refine. +One hypothesis per cycle. A disproven hypothesis is progress — record it and +what disproved it. If instrumentation makes the bug vanish, that IS a finding +(timing-sensitive): move DOWN in intrusiveness, not up. + +## Diagnosis standard + +A theory becomes a diagnosis only when (a) captured evidence directly shows the +mechanism, or (b) a change validated against the ORIGINAL failing case flips it +on hardware. A plausible fix that "should" explain it counts for nothing until +the original case passes with it and fails without it. Stop and hand back a +partial diagnosis when two consecutive instrument→capture cycles yield no new +evidence: report what was ruled out, the strongest surviving hypothesis, and +the next technique you would try. + +## Lock discipline + +- Hold the board lock for the WHOLE session (`board_lock.py hold <board> + --reason "target debug: <bug>"`). Multi-hour holds are fine; never stop the + actions-runner. Locks held by others: report holder/reason, never force + unless your prompt states the user authorized it. +- `hil_test.py` self-locks: release your hold before any `hil_test.py` run, + re-hold immediately after. +- You cannot ask the user anything mid-session. + +## Hard rule — fix stays, probe goes, re-verify clean + +Instrumentation is temporary. Before releasing the lock at session end: +1. Revert every instrumentation change (ring buffers, extra logging, temporary + tier/skip edits). The candidate fix, if one emerged, stays in the working + tree — uncommitted. +2. Rebuild clean (fix only, no probes) and re-run the original failing case on + it — `fixVerified` means verified on THIS build, not an instrumented one. +3. Reflash pristine firmware so the next CI run inherits nothing. +Anything you could not revert or verify goes in `notes`, explicitly. + +## Output contract + +Your final message is parsed by a program. Return ONLY this JSON — no prose, +no code fences: + +{"board": "...", "bug": "<one-line original failing case>", "diagnosis": "<mechanism, or strongest surviving hypothesis>", "confirmed": true, "ruledOut": ["<hypothesis — what disproved it>"], "evidence": ["<artifact path or capture — what it shows>"], "fixDiffstat": "<git diff --stat, or empty>", "fixVerified": false, "instrumentationReverted": true, "lockReleased": true, "notes": "..."} diff --git a/.claude/skills/usb-sniffer/SKILL.md b/.claude/skills/usb-sniffer/SKILL.md new file mode 100644 index 000000000..7c2cd2644 --- /dev/null +++ b/.claude/skills/usb-sniffer/SKILL.md @@ -0,0 +1,149 @@ +--- +name: usb-sniffer +description: Use when you need wire-level USB evidence that host-side capture can't provide — a device that never enumerates (usbmon shows nothing or only Submits), suspected NAK storms/STALL/babble/bad handshakes, bus-reset or enumeration timing, split-transaction issues, or a usbmon-vs-device-log disagreement the wire must arbitrate. Captures LS/FS/HS packets (PIDs, tokens, handshakes, SE0/line states) with the ataradov usb-sniffer hardware into Wireshark pcapng. +--- + +# usb-sniffer — wire-level capture with the ataradov hardware analyzer + +Extends the debugging trio with the layer below URBs: + +| Skill | Answers | +|---|---| +| `usbmon` | what the host software exchanged (URBs) | +| `usb-debug` | why the host acted (dmesg / dynamic debug) | +| `usb-target-debug` | what the device firmware did | +| **`usb-sniffer`** | **what actually crossed D+/D-** (PIDs, handshakes, resets, timing) | + +Reach for it when usbmon can't see (device never binds, pre-enumeration +failures) or can't be trusted (URB completed but did the wire really ACK?). +For everything visible in URBs, usbmon is cheaper — no hardware, no locks. + +## Rig inventory — find the sniffer and what it taps + +```bash +lsusb -d 6666:6620 # sniffer present? (github.com/ataradov/usb-sniffer) +``` + +The sniffer is a passive tap: host-side and device-side connectors pass +through, the capture port is a separate USB device. What it taps is a cabling +fact you must confirm every session, not assume: start a capture (below), +provoke known control traffic to a candidate (`lsusb -v -s <bus>:<dev> +>/dev/null`), and see whether those requests appear on the wire. The DUT's +link speed (`cat /sys/bus/usb/devices/<port>/speed`) picks `--speed`. + +The tapped board is rig hardware: hold its board lock for any session that +resets or reflashes it (`hil` skill). The sniffer itself is not lockable and +capture alone perturbs nothing. + +## Capture + +The tool is `usb_sniffer` (installed in `~/.local/bin`, extcap-symlinked so +Wireshark's GUI also shows a "USB Sniffer" interface). Headless recipe: + +```bash +timeout 15s usb_sniffer --capture --fifo /tmp/cap.pcapng --speed hs # or fs / ls +``` + +- `--speed` MUST match the DUT's link speed (default is fs!). Wrong speed = + no USB packets, only Syslog pseudo-packets ("Line state: SE0", "VBUS ON"). + If you see only those, fix `--speed` before doubting the hardware. +- ALWAYS bound the capture: `timeout` and/or `--limit N` (packets). HS runs + 15–20 MB/s even with `--fold` when any device on the bus is busy (`--fold` + only collapses truly empty frames). Unbounded HS captures reach GB fast. +- The output is valid pcapng the moment the process dies; a plain file path + works (no FIFO needed). `--trigger low|high|falling|rising` arms capture + on the external trigger pin instead of starting immediately. +- Tool diagnostics: `USB_SNIFFER_LOG=/tmp/sniffer.log usb_sniffer ...` + +Start the capture FIRST, then trigger the event you care about. The proven +one-pass enumeration recipe (`--limit` makes the tool exit by itself; on a +busy HS bus ~470k packets/s ≈ 20 MB/s, so 3M packets ≈ 6–7 s ≈ 120 MB — do +NOT capture for 20+ s "to be safe", the raw balloons and every later tshark +pass pays for it; but do NOT go below ~3M either: J-Link connect latency +varies run-to-run (0.5–4 s) and a 3 s window has provably missed the ladder): + +```bash +usb_sniffer --capture --fifo raw.pcapng --speed hs --fold --limit 3000000 & +sleep 1 +# trigger: full ladder incl. SET_ADDRESS (needs board lock; J-Link resets the MCU): +printf 'r\ng\nqc\n' | JLinkExe -device $JLINK_DEVICE -SelectEmuBySN <probe-uid> \ + -if swd -speed 4000 -autoconnect 1 -nogui 1 +wait # tool prints "Capture limit reached" and exits +``` + +No-probe trigger alternative — kernel-side re-enumeration (may reuse the +xHCI address and skip parts of the ladder; fine for descriptor reads, weak +for reset timing): +`echo 0 | sudo tee /sys/bus/usb/devices/<port>/authorized; sleep 1; echo 1 | sudo tee ...` + +## Reading the capture + +```bash +tshark -r cap.pcapng -Y 'usb.bmRequestType' # the control ladder +tshark -r cap.pcapng -Y 'usb.bDescriptorType == 1' \ + -T fields -e usb.idVendor -e usb.idProduct # VID:PID off the wire +tshark -r cap.pcapng -Y 'usbll.pid' # raw token/handshake level +editcap -r cap.pcapng slice.pcapng <first>-<last> # trim huge captures +``` + +On a capture >100 MB, make exactly ONE filtered pass (the ladder filter +above) to find the frame numbers of your event window, `editcap -r` to that +window, and do all further analysis on the slice — repeated broad tshark +passes over a 300 MB raw are what turn a 5-minute job into 15. + +Find the DUT's wire address from the capture, not from lsusb: the +SET ADDRESS request payload carries it (`00 05 <addr> 00 ...`), and all +subsequent traffic goes to `<addr>.<ep>` (`usbll.addr`). **On xHCI hosts the +lsusb device number is NOT the wire address** — they diverge routinely. +Filter analysis to the DUT: `-Y 'usbll.addr contains "4."'`. + +## What the wire really shows (read before concluding anything) + +- **Downstream is broadcast.** Tokens, SETUP and OUT data addressed to EVERY + device on the tapped bus segment appear in the capture; upstream (DATA in + response to IN) appears only from devices on the tapped branch. Lone + IN→ACK pairs without DATA to some other address are normal, not corruption. +- **The sniffer can capture its own upload.** If its capture port shares the + host controller bus with the tap, its bulk-IN polling floods the capture + (easily >90% of packets) — filter it out by address; for surgically clean + captures move the capture cable to a different host controller. +- **Port-reset visibility depends on the tap point.** Tapping the DUT's own + cable: a reset reaches the sniffer PHY and you get explicit + `--- Bus Reset ---` / `Detected speed:` Syslog records. Tapping a hub + upstream: the hub isolates downstream port resets — no marker appears. Anchor reset timing on the hub choreography instead: + SetPortFeature(PORT_RESET) to the hub's address = reset start, + ClearPortFeature(C_PORT_RESET) = reset end (start the capture before + triggering, or the initiating SetPortFeature is missing from the file). + The DUT's silence gap corroborates, but do not read every gap as a + reset — idle captures contain benign multi-ms gaps. +- **FS device behind an HS hub**: the upstream tap shows SPLIT transactions, + not native FS packets. Tap the DUT's own cable and capture at `fs` for + clean full-speed traffic. + +## Setup (one-time) + +```bash +# udev: tools/88-tinyusb.rules covers 6666:6620 + blank FX2LP 04b4:8613 +sudo cp tools/88-tinyusb.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger -s usb + +# binary (ataradov repo bin/usb_sniffer_linux) + Wireshark extcap symlink (needs Wireshark >= 4.x) +cp usb_sniffer_linux ~/.local/bin/usb_sniffer && chmod +x ~/.local/bin/usb_sniffer +mkdir -p ~/.local/lib/wireshark/extcap +ln -sf ~/.local/bin/usb_sniffer ~/.local/lib/wireshark/extcap/usb_sniffer +``` + +Never run `--mcu-eeprom` / `--fpga-flash` / `--fpga-erase` against a working sniffer — those program NEW hardware. + +## Warnings + +- **Bound every capture** (`timeout` / `--limit`) and delete or `editcap`-trim + multi-hundred-MB raws before handing off; a forgotten capture process fills + the disk at HS rates. +- The tap is passive — capturing, or unplugging the capture port, does not + disturb the DUT's link. Unplugging the pass-through DOES. +- Answers must come from packet payloads (SETUP/DATA hex), not from host-side + logs — that is the whole point of being on the wire; if an answer isn't in + the capture, say so rather than approximating from sysfs/dmesg. +- Release the board lock and leave no capture processes running at session + end (`pgrep -a usb_sniffer`). diff --git a/.claude/skills/usb-target-debug/SKILL.md b/.claude/skills/usb-target-debug/SKILL.md new file mode 100644 index 000000000..c664bf8fc --- /dev/null +++ b/.claude/skills/usb-target-debug/SKILL.md @@ -0,0 +1,198 @@ +--- +name: usb-target-debug +description: Use when a TinyUSB device misbehaves on real hardware and host-side capture can't explain it — a HIL test fails but usbmon shows only Submits with no Completes, the device silently NAKs, wedges, STALLs, babbles, or drops data, EP0 starves, an ISR or DCD/HCD state bug is suspected — and you need device-side evidence: TU_LOG/RTT logs, GDB state dumps, a RAM ring-buffer event trace, or PC-sampling of where the core spins. +--- + +# usb-target-debug — device-side capture & debugging on the HIL rig + +Completes the debugging trio (the `usb-sniffer` skill adds a fourth, +wire-level view when hardware tapping is available): + +| Skill | Answers | +|---|---| +| `usbmon` | what the host actually exchanged (URBs) | +| `usb-debug` | why the host acted (dmesg / dynamic debug) | +| **`usb-target-debug`** | **what the device did** (logs, driver state, PC) | +| `usb-sniffer` | what crossed the wire (PIDs, handshakes, resets — hardware tap) | + +For enumeration/transfer bugs the default posture is **dual-side capture** — +usbmon on the host *and* a target-side channel, simultaneously — not +host-first-then-escalate. + +## Rig discipline — lock first, always + +Hold the board lock for the WHOLE manual session; never stop the +actions-runner (see the `hil` skill for the full lock protocol): + +```bash +python3 test/hil/board_lock.py hold <board> --reason "target debug: <bug>" +# ... instrument / build / flash / capture / GDB ... +python3 test/hil/board_lock.py release <board> +``` + +Board → probe mapping: `test/hil/tinyusb.json` — `flasher.name` is the probe +family, `flasher.uid` the **probe serial** (many identical probes on the rig: +J-Link needs `-SelectEmuBySN <uid>` / GDB server `-select usb=<uid>`; OpenOCD +`-c 'adapter serial <uid>'`). `JLINK_DEVICE` / `OPENOCD_OPTION` come from +`hw/bsp/<family>/boards/<board>/board.cmake` (or `board.mk`); find the family +with `ls -d hw/bsp/*/boards/<board>`. Run on the host that owns the probe — +config is `test/hil/tinyusb.json` on ci, `local.json` on htpc (`hil` skill). + +## Pick the least intrusive technique that can answer the question + +Observation can mask the bug — the ch32v307 Heisenbug changed behavior under +logging *and* under the debugger. If the bug disappears when instrumented, +that IS a finding (timing-sensitive): move down in intrusiveness, not up. + +| Technique | Intrusiveness | Reach for it when | +|---|---|---| +| PC-sampling | none — no halt, no code change | core wedged/spinning somewhere unknown (rusb2 FRDY) | +| RAM ring-buffer | ~tens of cycles per event | ISR ordering/timing bugs (musb babble) | +| TU_LOG (RTT) | µs per line | logic bugs that survive logging | +| TU_LOG (UART) | ms per line — blocking write | same, when no J-Link on the board | +| GDB halt / breakpoints | stops USB service entirely | post-mortem state autopsy once wedged | + +## TU_LOG capture + +Build with `LOG=2` (`LOG=3` adds per-transfer noise and much more timing skew). +`LOGGER=rtt` routes it over the debug probe (J-Link only) — no UART wiring: + +```bash +# RTT: JLinkGDBServer from AGENTS.md "GDB Debugging" + -RTTTelnetPort, then: +timeout 20s JLinkRTTClient > /tmp/rtt.log # non-interactive capture +# UART (board's debug serial, if wired): +stty -F /dev/ttyACM<N> 115200 raw && timeout 20s cat /dev/ttyACM<N> | tee /tmp/uart.log +``` + +An RTT-built firmware that has since wedged still holds a log tail in RAM — +but ONLY what fits the drain model: the default SEGGER mode (NO_BLOCK_SKIP) +**drops** writes once the ring fills with no reader, so an undrained target +holds the first KB after boot, not the wedge tail. There is no overwrite mode +in stock SEGGER RTT (only SKIP/TRIM/BLOCK): post-mortem RTT is evidence only +if a live drain was running — otherwise instrument with the RAM ring below. +Use `JLinkGDBServer -RTTTelnetPort 19021` + `JLinkRTTClient` for the drain +(proven; note the server briefly halts the core on connect). `JLinkRTTLogger` +fails to find the control block on some parts (LPC4088) even when it exists +and even given `-RTTAddress`; don't fight it — `nm` the ELF for `_SEGGER_RTT`, +read the aUp[0] descriptor (`mem32`), `savebin` the buffer — debug-AP RAM +reads don't halt the target. + +## GDB — state autopsy and watchpoints + +Connect/load recipes per probe family (J-Link, OpenOCD for ST-Link / +CMSIS-DAP / WCH-Link) are in AGENTS.md "GDB Debugging". Release builds keep +DWARF (`MinSizeRel`), so `p`/struct access works on HIL firmware. + +**Autopsy of a wedged board: attach and halt ONLY** — skip AGENTS.md's +`monitor reset halt` + `load` (those are for fresh starts; a reset destroys +the evidence). Symbolize with the ELF that is actually flashed — +`<build root>/cmake-build-<board>/<example>/<example>.elf` from the run that +wedged; do not rebuild while the wedge is still on the board. The debug-loop +specifics: + +```gdb +p/x _usbd_dev.ep_status # usbd core [epnum][dir] (1=IN): busy/stalled/claimed +p/x <port's private state> # per-port names — read the board's dcd_*.c first +x/32wx <USB peripheral base> # raw EP/FIFO regs; base = the macro the dcd uses +watch xfer_status[2][1].total_len # HW watchpoint (Cortex-M: ~4); dwc2 names shown +break dcd_int_handler # works, but see warning below +``` + +While halted the device answers **nothing**: host control transfers time out +in ~5 s and the OS may reset/re-enumerate — after `continue`, the bus traffic +shows recovery, not the original bug. Prefer one halt for a post-mortem dump +over stepping through live USB traffic. + +## RAM ring-buffer trace + +The zero-print instrument (cracked the musb babble): a small event ring in the +dcd/hcd, dumped over GDB after the failure. Single-writer (ISR) — no locking: + +```c +typedef struct { uint16_t ev; uint16_t a; uint32_t b; } dbg_ev_t; +#define DBG_N 512 // power of two +static volatile dbg_ev_t dbg_ring[DBG_N]; // volatile REQUIRED: -Os dead-store- +static volatile uint32_t dbg_wr; // eliminates a write-only static array +static inline void DBG_EV(uint16_t ev, uint16_t a, uint32_t b) { + uint32_t i = dbg_wr++; + dbg_ring[i & (DBG_N - 1)] = (dbg_ev_t){ ev, a, b }; +} +// call sites: DBG_EV(__LINE__, ep_addr, count); — __LINE__ as event id +``` + +After building, `nm` the ELF for `dbg_ring`/`dbg_wr` — if they're missing the +compiler deleted your instrument and the run will "reproduce" with an empty ring. + +Order is the index; if durations matter add a `uint32_t t = DWT->CYCCNT` field +(enable once: `CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= 1;` +RISC-V: read `mcycle`). Let the failure happen, halt, then: + +```gdb +p dbg_wr # total events; oldest slot = dbg_wr & (DBG_N-1) once wrapped +p dbg_ring +dump binary memory /tmp/ring.bin &dbg_ring[0] &dbg_ring[512] +``` + +## PC-sampling (J-Link) — find where the core spins, without halting + +`DWT_PCSR` (0xE000101C) returns the current PC on every read, target running +(Cortex-M3+; optional on M0+, reads 0 if absent; 0xFFFFFFFF = core halted or +WFI-asleep — `mem32 E000EDF0, 1`, DHCSR bit 17 S_HALT, tells which). One +probe serves one client: quit JLinkExe before starting JLinkGDBServer on the +same probe. Nailed the rusb2 FRDY wedge: + +```bash +for i in $(seq 300); do echo 'mem32 E000101C, 1'; done \ + | JLinkExe -device $JLINK_DEVICE -SelectEmuBySN <uid> -if swd -speed 4000 -autoconnect 1 -nogui 1 \ + | awk '/E000101C = /{print $3}' | sort | uniq -c | sort -rn | head +arm-none-eabi-addr2line -e <firmware.elf> -f -a 0x<hot-pc> ... # PCs → functions +``` + +OpenOCD variant: repeat `mdw 0xE000101C` over telnet :4444. The histogram's +top entries are the spin site; a flat histogram = core is servicing normally. + +## Dual-side capture — the default for enumeration/transfer bugs + +Start both channels, then trigger the failing test: + +```bash +.claude/skills/usbmon/scripts/usbcap.sh cafe: 30 /tmp/host.pcapng & # host URBs (usbmon skill) +timeout 30s JLinkRTTClient > /tmp/target.rtt & # target (or ring dump after) +wait +``` + +RTT lines and ring events carry no wall-clock: correlate on unambiguous +anchors — bus reset, SET_ADDRESS, the first transfer on the failing EP — then +lay device events between anchors in host-URB order. Logging the SOF/frame +number on the target gives a shared clock when you need finer alignment. +When host and target evidence disagree, or the host sees nothing at all, add +the wire itself: `usb-sniffer` skill (hardware tap, PID-level). + +## Warnings + +- **Halting/resetting via the probe does NOT disconnect the device**: a DWC2 + soft-connect pullup stays up through core halt *and* reset, so the host's + stuck URBs stay stuck and a wedged DUT stays wedged — recover the host side + with the `usb-recover` skill. +- **A bug that vanishes under LOG=2 is a timing bug**, not fixed: switch to + the ring buffer; if it vanishes under GDB too, PC-sampling only. +- **UART TU_LOG blocks in the write path** (worst perturbation, including + inside the ISR); RTT is much cheaper but not free; `LOG=3` multiplies both. +- Flash/GDB only with the board lock held; a `hold` refused with reason + `hil_test.py` means CI is mid-test on that board — wait, don't force. +- **Instrumentation is temporary**: before `release`, reflash pristine + firmware (the next CI run must not inherit a debug build) and revert the + instrumentation diff — or hand it over explicitly with the diagnosis. +- **A register snapshot without a validity anchor lies**: J-Link tool sessions + can reset or briefly halt the DUT as a side effect, and a snapshot of a + freshly-reset chip (e.g. NVIC ISER = 0) reads like a smoking gun. Read DHCSR + (0xE000EDF0: bit 17 S_HALT, bit 25 S_RESET_ST) with every snapshot, and + cross-check against something the device demonstrably still does. +- **A marginal link can fake a deterministic firmware bug** — down to failing + the same test at the same iteration twice. "USB disconnect" in dmesg on a + freshly re-cabled port (high devnum = churn) means the plug, not the code: + first sustained bulk traffic is when a bad contact drops. Before declaring a + regression, re-run the OLD build on the SAME link state — and if a bisect + exonerates every hunk, believe it: re-test the exact failing binary. +- **Release your manual lock before `hil_test.py`** — it self-locks each board + and fails immediately on your own hold (`hil` skill). diff --git a/.gitignore b/.gitignore index 61358d118..145069e72 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ BrowseInfo README_processed.rst docs/examples/ .worktrees +.claude/worktrees/ cmake-metrics/ # Directories fetched by tools/get_deps.py - not to be committed lib/CMSIS_5/ diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 7fcc2f5d1..bce028ccb 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -181,7 +181,7 @@ Some examples require udev permissions to access USB devices: .. code-block:: bash - $ cp `examples/device/99-tinyusb.rules <https://github.com/hathach/tinyusb/tree/master/examples/device/99-tinyusb.rules>`_ /etc/udev/rules.d/ + $ sudo cp examples/device/99-tinyusb-examples.rules /etc/udev/rules.d/ $ sudo udevadm control --reload-rules && sudo udevadm trigger Next Steps diff --git a/docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md b/docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md new file mode 100644 index 000000000..b56d035d9 --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md @@ -0,0 +1,125 @@ +# Hand-off: `usb-target-debug` skill + `target-debugger` agent + +**Status: agreed but NOT started.** Design discussion happened 2026-07-13 in session +`c31a4617-43b1-491d-9865-3e35f393996b` (post-merge of the agents/workflows harness, +PR #3762 / `ac595bc5c`). This document is the implementation brief for a fresh session. + +**Agreed sequencing: skill first → dogfood on 1-2 real HIL failures → then the agent +as its own small PR.** Do not build both at once — the agent charter's hard parts are +exactly what dogfooding the skill answers. + +## The gap being filled + +When HIL fails today, *what failed* is covered (hil-validate workflow, hil-operator +agent) but the deep *why* loop — instrument the target, capture on both sides, +correlate — has no skill and no agent. Every hard case so far (musb babble, rusb2 +FRDY wedge, ch32v307 Heisenbug) fell back to interactive main-session work. + +Why no existing agent can do it: + +- **hil-operator** (sonnet) is deliberately mechanical: lock → flash → `hil_test.py` + → recover. It never edits source, so it cannot inject instrumentation. +- **port-dev** can edit source but its charter is scoped changes verified by a + *build*; it has no hardware mandate. +- The host-side capture knowledge lives in skills (`usbmon`, `usb-debug`); the + device-side half exists only as CLAUDE.md recipes plus session memory. + +The skill completes the debugging trio: + +| Skill | Answers | Status | +|---|---|---| +| `usbmon` | what the host actually exchanged (URBs) | on master | +| `usb-debug` | why the host acted (dmesg / dynamic debug) | ships in PR #3758 (untracked copy in tree) | +| `usb-target-debug` | what the device did | **this hand-off** | + +## Part 1 — `usb-target-debug` skill (do this first) + +Create `.claude/skills/usb-target-debug/SKILL.md`. Match the style of +`.claude/skills/usbmon/SKILL.md` and `usb-debug/SKILL.md`: frontmatter `name` + +`description` where the description states concretely *when* to reach for it +(HIL test fails and host-side capture can't explain it; device silently NAKs, +wedges, or misbehaves; need TU_LOG/device-state evidence from real hardware). + +Playbook to codify — all techniques already proven on this rig: + +1. **TU_LOG capture** — build with `LOG=2` (add `LOGGER=rtt` for RTT); UART capture + from the board's debug serial; RTT via `JLinkGDBServer -RTTTelnetPort 19021` + + `JLinkRTTClient` (non-interactive: `timeout 20s JLinkRTTClient > rtt.log`). + Note which log level perturbs timing (see warning #6). +2. **GDB recipes per probe family** — J-Link, OpenOCD (ST-Link / CMSIS-DAP / + WCH-Link). Base connect/load recipes already exist in CLAUDE.md "GDB Debugging"; + the skill adds the debug-loop specifics: breakpoints in ISR context, dumping + endpoint/FIFO registers, watchpoints on driver state variables. +3. **RAM ring-buffer trace pattern** (used to crack the musb babble): instrument + the dcd/hcd with a small RAM ring of event records instead of TU_LOG when + printing perturbs timing; let the failure happen; halt and dump the ring via + GDB. Include a minimal C snippet (fixed-size struct ring, no allocation, + ISR-safe single-writer). +4. **J-Link PC-sampling** (nailed the rusb2 FRDY wedge): statistically sample PC + without halting to find where the core spins — the non-intrusive option when + halting or logging masks the bug. +5. **Dual-side capture**: usbmon on the host + RTT/ring-buffer on the target, + simultaneously; correlate host URBs against device events on one timeline. + This is the default posture for enumeration/transfer bugs, not an escalation. +6. **Warnings**: observation can mask the bug (the ch32v307 case changed behavior + under logging/debug — prefer ring-buffer over TU_LOG, PC-sampling over halting, + and say so explicitly); a J-Link core reset does NOT drop a DWC2 soft-connect + pullup, so a wedged DUT stays wedged on the host side (cross-ref + `usb-recover/SKILL.md`). +7. **Rig discipline**: hold the board lock for the whole manual session — + `python3 test/hil/board_lock.py hold <board> --reason "target debug: <bug>"` + … work … `release <board>`. Never stop the actions-runner. Board → probe + mapping via `test/hil/tinyusb.json`; `JLINK_DEVICE`/`OPENOCD_OPTION` via + `hw/bsp/*/boards/*/board.cmake` or `board.mk`. + +**Where to ship**: its own small PR (usb-recover/usb-debug already belong to +PR #3758 — don't grow that one), or fold into #3758 if it is still open and being +rebased anyway. User's call at the time. + +## Part 2 — `target-debugger` agent (later, after dogfooding) + +Create `.claude/agents/target-debugger.md` as its own PR once the skill has been +through at least one real debug session. + +Agreed charter outline: + +- **Frontmatter**: `model: opus`; omit `tools:` (= all tools — it must edit source + AND drive hardware). Note the registry supports no `effort` field — the agreed + opus/**xhigh** tier is requested per `agent()` call by whichever workflow or + session spawns it. +- **Loop**: instrument → build → flash under one held board lock → dual-side + capture (host usbmon + target RTT/ring-buffer/GDB) → correlate → refine + hypothesis → repeat. Deliberately serial: no fan-out win; the value is + backgrounding a long debug session and the codified playbook. +- **Strictly one instance**, holds the board lock for the entire session — its work + is exactly the "hardware work outside hil_test.py" case in the lock protocol. +- **Skills are its source of truth** (mirror hil-operator's pattern): read + `usb-target-debug`, `usbmon`, `usb-debug`, `usb-recover`, `hil` SKILL.md files + before acting. +- **Hard rule — instrumentation is temporary**: the instrumentation diff must be + reverted (or explicitly listed in the hand-back report) at session end; the *fix* + itself goes to port-dev. Keeps charters clean: this agent produces a diagnosis + and evidence, not a merged patch. + +Questions dogfooding must answer before the charter is written (do NOT guess these +now — that was the whole reason for skill-first): + +1. When to stop instrumenting and report a partial diagnosis vs keep digging. +2. Maximum board-lock hold time / check-in cadence for a backgrounded session. +3. What "revert instrumentation" means when a partial fix emerged mid-debug + (revert + attach diff? keep on a branch?). + +## Conventions and references for the implementing session + +- Skill style exemplars: `.claude/skills/usbmon/SKILL.md`, `usb-debug/SKILL.md`, + `usb-recover/SKILL.md` (the latter two are #3758's copies, present untracked). +- Agent style exemplars: `.claude/agents/hil-operator.md` (lock discipline, + skills-as-source-of-truth), `port-dev.md` (source-edit + verify charter). +- When the agent lands, update the harness spec's agent roster: + `docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md` + (convention: spec evolves in-repo; plans like this file are per-effort records). +- Agents register from `.claude/agents/*.md` at session start — a new agent file + is only visible to sessions launched after it exists. +- Past cases to mine for the skill's examples: musb babble (ring-buffer trace), + rusb2 FRDY wedge (J-Link PC-sampling), ch32v307 Heisenbug (observation + sensitivity) — details in session memory and the referenced session transcript. diff --git a/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md b/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md index 63788720c..3035723c4 100644 --- a/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md +++ b/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md @@ -29,10 +29,12 @@ Layered: **agents** (who does the work, with baked-in domain knowledge) × ### Worker agents — `.claude/agents/*.md` -Tiered models (owner revision 2026-07-09; originally all-opus): `port-dev` -and `driver-reviewer` on **opus** at **xhigh**; `hil-operator`, `pr-monitor` -and `static-analyzer` on **sonnet**; `builder` on **haiku** (mechanical, -log-heavy). +Tiered models (owner revision 2026-07-09; originally all-opus): `port-dev`, +`driver-reviewer` and `target-debugger` on **opus** at **xhigh**; +`hil-operator`, `pr-monitor` and `static-analyzer` on **sonnet**; `builder` +on **haiku** (mechanical, log-heavy). The registry has no effort field — +xhigh is requested per `agent()` call by whichever workflow or session spawns +the agent. | Agent | Effort | Role | |---|---|---| @@ -40,6 +42,7 @@ log-heavy). | `port-dev` | xhigh | Implement one well-scoped change in one port / file set. Follows repo rules: C99, 2-space indent, snake_case, `TU_ASSERT`, no dynamic allocation, ISR work deferred to task context. Runs `clang-format` (repo `.clang-format`) on touched files before finishing. Cross-checks the MCU datasheet in `$HOME/Documents/calibre-library` when changing dcd/hcd register logic. Verifies with a targeted build of one board using the port. Returns `{item, diffstat, buildOk, notes}`. | | `driver-reviewer` | xhigh | Review one dcd/hcd directory against dimensions: correctness, ISR safety, register use vs. datasheet AND MCU errata (calibre library; missing erratum workarounds are findings), style. Returns structured findings `{file, line, snippet, why, severity, confidence}` — coverage-first (report everything; filtering happens downstream). | | `hil-operator` | default | All rig interaction — the actions-runner service is NEVER stopped; per-board flock locks arbitrate with concurrent CI. `hil_test.py` runs rely on its per-board self-locking; manual hardware work (JLink/GDB, usbtest, serial) is wrapped in `test/hil/board_lock.py hold/release`; rig-wide ops (uhubctl, pci-rebind) require `hold --all`; on wedge `usb_recover.sh` + dmesg. Used strictly serially — never two instances concurrently. | +| `target-debugger` | xhigh | Root-cause one USB misbehavior on one board by instrumenting the device side (TU_LOG/RTT, RAM ring-buffer trace, GDB autopsy, J-Link PC-sampling) with dual-side host+target capture, per `.claude/skills/usb-target-debug/SKILL.md`, plus wire-level capture via the ataradov hardware tap (`.claude/skills/usb-sniffer/SKILL.md`) when the host side can't see or is disputed. Deliberately serial loop under one held board lock (released around `hil_test.py` runs, which self-lock); strictly one instance. Diagnosis standard: evidence shows the mechanism, or a fix flips the ORIGINAL failing case on hardware; stops after two evidence-free cycles with a partial report. Hard rule "fix stays, probe goes, re-verify clean": instrumentation reverted, candidate fix left uncommitted and re-verified on a clean build, pristine firmware reflashed before lock release. Returns `{board, bug, diagnosis, confirmed, ruledOut[], evidence[], fixDiffstat, fixVerified, instrumentationReverted, lockReleased, notes}`. | | `pr-monitor` | default | Triage one GitHub PR via `gh`: check CI status (`gh pr checks`), read failing run logs and classify each failure infra/flake vs real; re-run infra failures (`gh run rerun --failed`); harvest automated review comments (Codex/Copilot/Claude bots — knows their signals: Codex posts a "Didn't find any major issues" issue comment when clean; Copilot drops out of `requested_reviewers` when done; bot logins differ across APIs); adversarially validate each finding against the actual code. Returns structured triage `{ci: {status, infraRerun[], realFailures[]}, findings: [{source, file, line, claim, verdict, fixHint}]}`. Read/triage/re-run/reply only — never edits code. | | `static-analyzer` | low | Run PVS-Studio (SAST + MISRA C:2023/C++:2008) for one board: build with exported `compile_commands.json` (via `run_pvs.sh` solo, or a dedicated `cmake-build-pvs` dir when parallel builders run), analyze against `.PVS-Studio/.pvsconfig`, gate on diagnostics in files changed vs a base ref. Returns `{pass, ga1, ga2, changedFindings[], detail}`; `pass=false` only on GA:1 in changed files or tool failure. Read-only. | @@ -120,8 +123,8 @@ carries the judgment; JS carries the orchestration. ## Model & effort policy -- Tiered worker models: `port-dev`/`driver-reviewer` **opus** `xhigh`; - `hil-operator`/`pr-monitor` **sonnet**; `builder` **haiku**. +- Tiered worker models: `port-dev`/`driver-reviewer`/`target-debugger` **opus** + `xhigh`; `hil-operator`/`pr-monitor` **sonnet**; `builder` **haiku**. - Inline workflow stages: unit/size **haiku**; pvs **sonnet** (low effort); pr-babysit push/replies **sonnet**. - Agent frontmatter `model:` is canonical for `agentType` calls; it is read diff --git a/examples/device/99-tinyusb.rules b/examples/device/99-tinyusb-examples.rules index d306bada5..e7a399345 100644 --- a/examples/device/99-tinyusb.rules +++ b/examples/device/99-tinyusb-examples.rules @@ -1,5 +1,6 @@ +# udev rules for running the TinyUSB device examples as a non-root user. # Copy this file to the location of your distribution's udev rules, for example on Ubuntu: -# sudo cp 99-tinyusb.rules /etc/udev/rules.d/ +# sudo cp 99-tinyusb-examples.rules /etc/udev/rules.d/ # Then reload udev configuration by executing: # sudo udevadm control --reload-rules # sudo udevadm trigger diff --git a/examples/device/cdc_msc_throughput/src/usb_descriptors.c b/examples/device/cdc_msc_throughput/src/usb_descriptors.c index ba0b0a26f..dca5a65cf 100644 --- a/examples/device/cdc_msc_throughput/src/usb_descriptors.c +++ b/examples/device/cdc_msc_throughput/src/usb_descriptors.c @@ -65,7 +65,15 @@ enum { }; // Place bulk endpoints on EP>=8 for MAX32690 class parts (bigger FIFO, DPB-capable). -#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY +#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 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ... + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_MSC_OUT 0x05 + #define EPNUM_MSC_IN 0x85 +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY #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_CDC_NOTIF 0x81 diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c index b9450c87e..92cd2b6be 100644 --- a/examples/device/printer_to_cdc/src/usb_descriptors.c +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -67,7 +67,15 @@ uint8_t const *tud_descriptor_device_cb(void) { //--------------------------------------------------------------------+ // Endpoint numbers -#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY +#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 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ... + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_PRINTER_OUT 0x05 + #define EPNUM_PRINTER_IN 0x85 +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY #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_CDC_NOTIF 0x81 diff --git a/examples/device/usbtest/skip.txt b/examples/device/usbtest/skip.txt index b52bdbb14..792404fe4 100644 --- a/examples/device/usbtest/skip.txt +++ b/examples/device/usbtest/skip.txt @@ -4,8 +4,6 @@ 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 diff --git a/examples/device/webusb_serial/README.md b/examples/device/webusb_serial/README.md index 5ca70f909..15837e59e 100644 --- a/examples/device/webusb_serial/README.md +++ b/examples/device/webusb_serial/README.md @@ -51,4 +51,4 @@ make BOARD=raspberry_pi_pico all After flashing, open the landing page (`https://example.tinyusb.org/webusb-serial/index.html`) in a WebUSB-capable browser such as Chrome, click **Connect**, and select the device — the on-board LED lights solid once connected. Characters typed in the web page are echoed back, and are also mirrored to the CDC serial port (e.g. `/dev/ttyACM0`) and vice versa. -On Linux/macOS you may need to install the udev rules from `examples/device/99-tinyusb.rules` for the browser to access the device. +On Linux/macOS you may need to install the udev rules from `examples/device/99-tinyusb-examples.rules` for the browser to access the device. diff --git a/examples/device/webusb_serial/src/main.c b/examples/device/webusb_serial/src/main.c index 4be5e4db4..e200c334c 100644 --- a/examples/device/webusb_serial/src/main.c +++ b/examples/device/webusb_serial/src/main.c @@ -39,7 +39,7 @@ * is done automatically by firmware. * * - On Linux/macOS, udev permission may need to be updated by - * - copying '/examples/device/99-tinyusb.rules' file to /etc/udev/rules.d/ then + * - copying 'examples/device/99-tinyusb-examples.rules' file to /etc/udev/rules.d/ then * - run 'sudo udevadm control --reload-rules && sudo udevadm trigger' */ diff --git a/hw/bsp/lpc15/family.c b/hw/bsp/lpc15/family.c index bbfee1b51..5178ad68b 100644 --- a/hw/bsp/lpc15/family.c +++ b/hw/bsp/lpc15/family.c @@ -122,6 +122,19 @@ uint32_t board_button_read(void) return Chip_GPIO_GetPinState(LPC_GPIO, BUTTON_PORT, BUTTON_PIN) ? 0 : 1; } +size_t board_get_unique_id(uint8_t id[], size_t max_len) +{ + // IAP ReadUID (cmd 58) returns status + 4 words = full 128-bit UID + unsigned int command[5] = { IAP_READ_UID_CMD, 0, 0, 0, 0 }; + unsigned int result[5]; + iap_entry(command, result); + TU_ASSERT(result[0] == IAP_CMD_SUCCESS, 0); + + size_t const len = tu_min32(max_len, 16); + memcpy(id, &result[1], len); + return len; +} + int board_uart_read(uint8_t* buf, int len) { (void) buf; (void) len; diff --git a/hw/bsp/lpc40/family.c b/hw/bsp/lpc40/family.c index d8a63576b..750bd2660 100644 --- a/hw/bsp/lpc40/family.c +++ b/hw/bsp/lpc40/family.c @@ -135,6 +135,19 @@ uint32_t board_button_read(void) { return BUTTON_ACTIV_STATE == Chip_GPIO_GetPinState(LPC_GPIO, BUTTON_PORT, BUTTON_PIN); } +size_t board_get_unique_id(uint8_t id[], size_t max_len) { + // IAP ReadUID (cmd 58) returns status + 4 words = full 128-bit UID + // (lpcopen's Chip_IAP_ReadUID() only returns the first word) + unsigned int command[5] = { IAP_READ_UID_CMD, 0, 0, 0, 0 }; + unsigned int result[5]; + iap_entry(command, result); + TU_ASSERT(result[0] == IAP_CMD_SUCCESS, 0); + + size_t const len = tu_min32(max_len, 16); + memcpy(id, &result[1], len); + return len; +} + int board_uart_read(uint8_t *buf, int len) { //return UART_ReceiveByte(BOARD_UART_PORT); (void) buf; diff --git a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c index 182710016..b577d0e9f 100644 --- a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c +++ b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c @@ -19,6 +19,12 @@ //--------------------------------------------------------------------+ #define DCD_ENDPOINT_MAX 32 +// The iso machinery (5th DD word + packet-size memory) costs USB RAM on every build; +// compile it only when a class that can open an iso endpoint is enabled. Keep this in +// sync with the classes that actually arm an iso endpoint: audio, video, BTH (voice), +// and vendor (its optional CFG_TUD_VENDOR_EP_ISO_* endpoints, exercised by usbtest). +#define DCD_ISO_ENABLED (CFG_TUD_AUDIO || CFG_TUD_VIDEO || CFG_TUD_VENDOR || CFG_TUD_BTH) + typedef struct TU_ATTR_ALIGNED(4) { //------------- Word 0 -------------// @@ -48,11 +54,37 @@ typedef struct TU_ATTR_ALIGNED(4) volatile uint16_t present_count; // For non-iso : The number of bytes transferred by the DMA engine // For iso : number of packets +#if DCD_ISO_ENABLED //------------- Word 4 -------------// - // uint32_t iso_packet_size_addr; // iso only, can be omitted for non-iso + volatile uint32_t iso_packet_size_addr; // iso only: pointer into iso packet-size memory, + // advanced by hardware after each packet +#endif }dma_desc_t; -TU_VERIFY_STATIC( sizeof(dma_desc_t) == 16, "size is not correct"); // TODO not support ISO for now +TU_VERIFY_STATIC( sizeof(dma_desc_t) == (DCD_ISO_ENABLED ? 20 : 16), "size is not correct"); + +// Hardware fixes endpoint type by number: 3, 6, 9, 12 are the iso-capable ones. +// Constant per ep_id (= 2*epnum + dir) — unlike dd->isochronous, which dcd_edpt_xfer +// transiently zeroes while rebuilding the DD, this is safe to dispatch on from the ISR. +// TU_ATTR_UNUSED: every caller is under #if DCD_ISO_ENABLED, so non-iso builds don't +// reference it and clang -Wunused-function (fatal) would otherwise reject the build. +TU_ATTR_UNUSED TU_ATTR_ALWAYS_INLINE static inline bool ep_id_is_iso(uint8_t ep_id) { + uint8_t const epnum = (uint8_t)(ep_id >> 1); + return (epnum % 3) == 0 && (epnum != 0) && (epnum != 15); +} + +#if DCD_ISO_ENABLED +// Isochronous packet-size memory (UM10562 12.15.6.3): one word per packet. +// IN : software fills Packet_length (bits 15:0), 0 = ZLP +// OUT: hardware writes Frame_number (31:17) | Packet_valid (16) | Packet_length (15:0) +// Iso-capable endpoint numbers are 3, 6, 9, 12 -> 8 slots (x2 directions). +// One packet moves per FRAME, so a deep queue only adds latency: 8 frames is plenty. +#define ISO_MAX_PACKETS 8 +#define ISO_SLOT_COUNT 8 +TU_ATTR_ALWAYS_INLINE static inline uint8_t iso_slot(uint8_t ep_id) { + return (uint8_t)(((ep_id / 6) - 1) * 2 + (ep_id & 1)); // ep_id = 2*epnum + dir, epnum in {3,6,9,12} +} +#endif typedef struct { @@ -66,11 +98,17 @@ typedef struct { uint8_t* out_buffer; uint8_t out_bytes; + volatile bool out_queued; // an OUT xfer is queued; out_buffer may legitimately be NULL (status ZLP) volatile bool out_received; // indicate if data is already received in endpoint uint8_t in_bytes; } control; +#if DCD_ISO_ENABLED + // iso packet-size memory, must be DMA-reachable like the DDs + volatile uint32_t iso_psize[ISO_SLOT_COUNT][ISO_MAX_PACKETS]; +#endif + } dcd_data_t; CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd; @@ -79,6 +117,29 @@ CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd; //--------------------------------------------------------------------+ // SIE Command //--------------------------------------------------------------------+ + +// The SIE command protocol (CmdCode + CCEMPTY/CDFULL handshake) and the +// slave-mode Ctrl/RxData/TxData registers are shared between thread-mode API +// calls and dcd_int_handler, and are not reentrant: an ISR preempting a +// thread-mode SIE sequence consumes its handshake flags and overwrites +// CmdCode (symptom: EP0 wedges/answers stale data right after SET_INTERFACE +// stall/clear-stall bursts overlapping bulk EOT interrupts). Mask only the +// USB interrupt around those sequences; safe to nest, including from the ISR. +static inline bool usb_irq_lock(void) +{ + bool const enabled = NVIC_GetEnableIRQ(USB_IRQn) != 0; + if (enabled) + { + NVIC_DisableIRQ(USB_IRQn); // CMSIS already ends this with DSB+ISB + } + return enabled; +} + +static inline void usb_irq_unlock(bool enabled) +{ + if (enabled) NVIC_EnableIRQ(USB_IRQn); +} + static void sie_cmd_code (sie_cmdphase_t phase, uint8_t code_data) { LPC_USB->DevIntClr = (DEV_INT_COMMAND_CODE_EMPTY_MASK | DEV_INT_COMMAND_DATA_FULL_MASK); @@ -92,19 +153,28 @@ static void sie_cmd_code (sie_cmdphase_t phase, uint8_t code_data) static void sie_write (uint8_t cmd_code, uint8_t data_len, uint8_t data) { + bool const lock = usb_irq_lock(); + sie_cmd_code(SIE_CMDPHASE_COMMAND, cmd_code); if (data_len) { sie_cmd_code(SIE_CMDPHASE_WRITE, data); } + + usb_irq_unlock(lock); } static uint8_t sie_read (uint8_t cmd_code) { + bool const lock = usb_irq_lock(); + sie_cmd_code(SIE_CMDPHASE_COMMAND , cmd_code); sie_cmd_code(SIE_CMDPHASE_READ , cmd_code); - return (uint8_t) LPC_USB->CmdData; + uint8_t const data = (uint8_t) LPC_USB->CmdData; + + usb_irq_unlock(lock); + return data; } //--------------------------------------------------------------------+ @@ -117,6 +187,11 @@ static inline uint8_t ep_addr2idx(uint8_t ep_addr) static void set_ep_size(uint8_t ep_id, uint16_t max_packet_size) { + // ReEp RMW + the EP_RLZED handshake share DevIntSt with the ISR: a bus reset + // from dcd_int_handler writes DevIntClr = 0xFFFFFFFF and would consume the + // flag this spin waits on, hanging it forever -> same lock as the SIE paths. + bool const lock = usb_irq_lock(); + // follows example in 11.10.4.2 LPC_USB->ReEp |= TU_BIT(ep_id); LPC_USB->EpInd = ep_id; // select index before setting packet size @@ -124,6 +199,8 @@ static void set_ep_size(uint8_t ep_id, uint16_t max_packet_size) while ((LPC_USB->DevIntSt & DEV_INT_ENDPOINT_REALIZED_MASK) == 0) {} LPC_USB->DevIntClr = DEV_INT_ENDPOINT_REALIZED_MASK; + + usb_irq_unlock(lock); } @@ -230,6 +307,7 @@ static inline uint8_t byte2dword(uint8_t bytes) static void control_ep_write(void const * buffer, uint8_t len) { uint32_t const * buf32 = (uint32_t const *) buffer; + bool const lock = usb_irq_lock(); // Ctrl/TxData + SIE sequence must not interleave with the ISR LPC_USB->Ctrl = USBCTRL_WRITE_ENABLE_MASK; // logical endpoint = 0 LPC_USB->TxPLen = (uint32_t) len; @@ -245,10 +323,14 @@ static void control_ep_write(void const * buffer, uint8_t len) // select control IN & validate the endpoint sie_write(SIE_CMDCODE_ENDPOINT_SELECT+1, 0, 0); sie_write(SIE_CMDCODE_BUFFER_VALIDATE , 0, 0); + + usb_irq_unlock(lock); } static uint8_t control_ep_read(void * buffer, uint8_t len) { + bool const lock = usb_irq_lock(); // Ctrl/RxData + SIE sequence must not interleave with the ISR + LPC_USB->Ctrl = USBCTRL_READ_ENABLE_MASK; // logical endpoint = 0 while ((LPC_USB->RxPLen & USBRXPLEN_PACKET_READY_MASK) == 0) {} // TODO blocking, should have timeout @@ -267,6 +349,7 @@ static uint8_t control_ep_read(void * buffer, uint8_t len) sie_write(SIE_CMDCODE_ENDPOINT_SELECT+0, 0, 0); sie_write(SIE_CMDCODE_BUFFER_CLEAR , 0, 0); + usb_irq_unlock(lock); return len; } @@ -281,8 +364,9 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) uint8_t const epnum = tu_edpt_number(p_endpoint_desc->bEndpointAddress); uint8_t const ep_id = ep_addr2idx(p_endpoint_desc->bEndpointAddress); - // Endpoint type is fixed to endpoint number - // 1: interrupt, 2: Bulk, 3: Iso and so on + // Endpoint type is fixed to endpoint number (1 interrupt, 2 bulk, 3 iso, ...). + // Iso endpoints are armed via dcd_edpt_iso_alloc/activate, never through here + // (TUP_DCD_EDPT_ISO_ALLOC is defined for this IP), so only bulk/interrupt land here. switch ( p_endpoint_desc->bmAttributes.xfer ) { case TUSB_XFER_INTERRUPT: @@ -293,10 +377,6 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) TU_ASSERT((epnum % 3) == 2 || (epnum == 15)); break; - case TUSB_XFER_ISOCHRONOUS: - TU_ASSERT((epnum % 3) == 0 && (epnum != 0) && (epnum != 15)); - break; - default: break; } @@ -307,9 +387,7 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) //------------- first DD prepare -------------// dma_desc_t* const dd = &_dcd.dd[ep_id]; - tu_memclr(dd, sizeof(dma_desc_t)); - - dd->isochronous = (p_endpoint_desc->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) ? 1 : 0; + tu_memclr(dd, sizeof(dma_desc_t)); // non-iso: isochronous stays 0 dd->max_packet_size = ep_size; dd->retired = 1; // invalid at first @@ -319,16 +397,54 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) } bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { +#if DCD_ISO_ENABLED (void)rhport; - (void)ep_addr; - (void)largest_packet_size; + uint8_t const ep_id = ep_addr2idx(ep_addr); + + // hardware fixes iso to endpoint numbers 3, 6, 9, 12 + TU_ASSERT(ep_id_is_iso(ep_id)); + TU_ASSERT(largest_packet_size > 0); + + set_ep_size(ep_id, largest_packet_size); + + dma_desc_t* const dd = &_dcd.dd[ep_id]; + tu_memclr(dd, sizeof(dma_desc_t)); + dd->isochronous = 1; + dd->max_packet_size = largest_packet_size; + dd->retired = 1; // invalid at first + + sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0); + return true; +#else + (void)rhport; (void)ep_addr; (void)largest_packet_size; return false; +#endif } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { +#if DCD_ISO_ENABLED (void)rhport; - (void)desc_ep; + uint8_t const ep_id = ep_addr2idx(desc_ep->bEndpointAddress); + dma_desc_t* const dd = &_dcd.dd[ep_id]; + + // same fixed-number rule as alloc: without it a rejected-but-ignored alloc (classes + // discard that return) would set isochronous on a non-iso ep_id and underflow iso_slot() + TU_ASSERT(ep_id_is_iso(ep_id)); + + // kill any armed transfer from a previous alternate setting + LPC_USB->EpDMADis = TU_BIT(ep_id); + _dcd.udca[ep_id] = NULL; + + dd->isochronous = 1; + dd->max_packet_size = tu_edpt_packet_size(desc_ep); + dd->retired = 1; + + sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0); + return true; +#else + (void)rhport; (void)desc_ep; return false; +#endif } void dcd_edpt_close_all (uint8_t rhport) @@ -369,19 +485,28 @@ static bool control_xact(uint8_t rhport, uint8_t dir, uint8_t * buffer, uint8_t control_ep_write(buffer, len); }else { + // guard the out_received/out_buffer handshake against the EP0 OUT ISR + bool const lock = usb_irq_lock(); + if ( _dcd.control.out_received ) { // Already received the DATA OUT packet _dcd.control.out_received = false; - _dcd.control.out_buffer = NULL; - _dcd.control.out_bytes = 0; uint8_t received = control_ep_read(buffer, len); + // event queued with in_isr=true, which skips the queue's own locking: keep the + // USB IRQ masked across it, or a real ISR completion could interleave the write dcd_event_xfer_complete(0, 0, received, XFER_RESULT_SUCCESS, true); + usb_irq_unlock(lock); }else { + // buffer is NULL for a status-stage ZLP: signal the pending xfer explicitly, + // NOT via out_buffer != NULL — a NULL-buffer queue mistaken for "nothing queued" + // leaves out_received stale and poisons the next control OUT data stage. _dcd.control.out_buffer = buffer; _dcd.control.out_bytes = len; + _dcd.control.out_queued = true; + usb_irq_unlock(lock); } } @@ -406,26 +531,68 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t uint16_t const ep_size = dd->max_packet_size; uint8_t is_iso = dd->isochronous; - tu_memclr(dd, sizeof(dma_desc_t)); - dd->isochronous = is_iso; - dd->max_packet_size = ep_size; - dd->buffer = (uint32_t) buffer; - dd->buflen = total_bytes; +#if DCD_ISO_ENABLED + if ( is_iso ) + { + // iso: buflen counts packets; per-packet sizes live in the packet-size memory. + // One packet moves per frame (UM10562 12.15.6: DMA request is raised for + // DMA-enabled iso endpoints on every FRAME interrupt, both directions). + // Validate BEFORE touching the DD: bailing out mid-rebuild would leave a + // zeroed (retired=0 -> serviceable) descriptor armed for the frame engine. + TU_ASSERT(ep_size > 0); + uint16_t const packets = (total_bytes > 0) ? (uint16_t) tu_div_ceil(total_bytes, ep_size) : 1; + TU_ASSERT(packets <= ISO_MAX_PACKETS); + + uint8_t const slot = iso_slot(ep_id); + uint16_t remain = total_bytes; + for ( uint16_t i = 0; i < packets; i++ ) + { + uint16_t const pkt_len = tu_min16(remain, ep_size); + // IN: length to send (0 = ZLP). OUT: hardware writes back + // Frame_number|Packet_valid|Packet_length -- prefill 0 so a frame the + // hardware never wrote (missed/invalid) cannot read back as data. + _dcd.iso_psize[slot][i] = (ep_id & 1) ? pkt_len : 0; + remain = (uint16_t)(remain - pkt_len); + } - _dcd.udca[ep_id] = dd; + tu_memclr(dd, sizeof(dma_desc_t)); + dd->isochronous = 1; + dd->max_packet_size = ep_size; + dd->buffer = (uint32_t) buffer; + dd->buflen = packets; + dd->iso_packet_size_addr = (uint32_t) &_dcd.iso_psize[slot][0]; - if ( ep_id % 2 ) + _dcd.udca[ep_id] = dd; + LPC_USB->EpDMAEn = TU_BIT(ep_id); // frame-triggered: no DMARSet, no EpIntEn + } + else +#else + (void) is_iso; +#endif { - // Clear EP interrupt before Enable DMA - LPC_USB->EpIntEn &= ~TU_BIT(ep_id); - LPC_USB->EpDMAEn = TU_BIT(ep_id); + tu_memclr(dd, sizeof(dma_desc_t)); + dd->max_packet_size = ep_size; + dd->buffer = (uint32_t) buffer; + dd->buflen = total_bytes; - // endpoint IN need to actively raise DMA request - LPC_USB->DMARSet = TU_BIT(ep_id); - }else - { - // Enable DMA - LPC_USB->EpDMAEn = TU_BIT(ep_id); + _dcd.udca[ep_id] = dd; + + if ( ep_id % 2 ) + { + // Clear EP interrupt before Enable DMA + // EpIntEn read-modify-write races the ISR's own RMWs -> lock + bool const lock = usb_irq_lock(); + LPC_USB->EpIntEn &= ~TU_BIT(ep_id); + LPC_USB->EpDMAEn = TU_BIT(ep_id); + usb_irq_unlock(lock); + + // endpoint IN need to actively raise DMA request + LPC_USB->DMARSet = TU_BIT(ep_id); + }else + { + // Enable DMA + LPC_USB->EpDMAEn = TU_BIT(ep_id); + } } return true; @@ -451,13 +618,20 @@ static void control_xfer_isr(uint8_t rhport, uint32_t ep_int_status) uint8_t setup_packet[8]; control_ep_read(setup_packet, 8); // TODO read before clear setup above + // a new SETUP voids any half-finished control state + _dcd.control.out_queued = false; + _dcd.control.out_received = false; + _dcd.control.out_buffer = NULL; + _dcd.control.out_bytes = 0; + dcd_event_setup_received(rhport, setup_packet, true); } - else if ( _dcd.control.out_buffer ) + else if ( _dcd.control.out_queued ) { - // software queued transfer previously + // software queued transfer previously (out_buffer NULL = status ZLP) uint8_t received = control_ep_read(_dcd.control.out_buffer, _dcd.control.out_bytes); + _dcd.control.out_queued = false; _dcd.control.out_buffer = NULL; _dcd.control.out_bytes = 0; @@ -513,7 +687,32 @@ static void dd_complete_isr(uint8_t rhport, uint8_t ep_id) uint8_t result = (dd->status == DD_STATUS_NORMAL || dd->status == DD_STATUS_DATA_UNDERUN) ? XFER_RESULT_SUCCESS : XFER_RESULT_FAILED; uint8_t const ep_addr = (ep_id / 2) | ((ep_id & 0x01) ? TUSB_DIR_IN_MASK : 0); - dcd_event_xfer_complete(rhport, ep_addr, dd->present_count, result, true); + uint32_t xferred_bytes; +#if DCD_ISO_ENABLED + if ( ep_id_is_iso(ep_id) ) + { + // present_count is in packets; actual byte counts are in the packet-size memory + // (IN: as programmed by us, OUT: Packet_length written back by hardware, + // guarded by Packet_valid -- a frame with no packet must count as 0) + uint8_t const slot = iso_slot(ep_id); + uint16_t const packets = tu_min16(dd->present_count, ISO_MAX_PACKETS); + xferred_bytes = 0; + for (uint16_t i = 0; i < packets; i++) + { + uint32_t const psize = _dcd.iso_psize[slot][i]; + if ( (ep_id & 1) || (psize & TU_BIT(16)) ) + { + xferred_bytes += (psize & 0xFFFFu); + } + } + } + else +#endif + { + xferred_bytes = dd->present_count; + } + + dcd_event_xfer_complete(rhport, ep_addr, (uint16_t) xferred_bytes, result, true); } // main USB IRQ handler @@ -569,6 +768,16 @@ void dcd_int_handler(uint8_t rhport) { if ( tu_bit_test(eot, ep_id) ) { + // dispatch on the hardware's fixed ep-number/type map, NOT dd->isochronous: + // thread-mode dcd_edpt_xfer transiently zeroes the DD while rebuilding it +#if DCD_ISO_ENABLED + if ( ep_id_is_iso(ep_id) ) + { + // iso: last packet already left with its frame; complete both directions here + dd_complete_isr(rhport, ep_id); + } + else +#endif if ( ep_id & 0x01 ) { // IN enable EpInt for end of usb transfer diff --git a/tools/88-tinyusb.rules b/tools/88-tinyusb.rules new file mode 100644 index 000000000..e0c5215b0 --- /dev/null +++ b/tools/88-tinyusb.rules @@ -0,0 +1,93 @@ +# Copy this file to the location of your distribution's udev rules: +# Then reload udev configuration by executing: +# sudo cp 88-tinyusb.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules && sudo udevadm trigger + +# Check SUBSYSTEM +SUBSYSTEMS=="hidraw", KERNEL=="hidraw*", MODE="0666", GROUP="dialout" +SUBSYSTEM=="usbmon", MODE="0640", GROUP="wireshark" + +# Rule applies to all TinyUSB example +ATTRS{idVendor}=="cafe", MODE="0666", GROUP="dialout" + +# Rule to make Trinket/Pro Trinket/Gemma/Flora programmable without running Arduino as root. +# Tested with Ubuntu 14.04 and 12.04. Other distributions might need to update GROUP="dialout" +# to another group value like "users". +SUBSYSTEM=="usb", ATTRS{idProduct}=="0c9f", ATTRS{idVendor}=="1781", MODE="0660", GROUP="dialout" + +# Rule to blacklist Adafruit USB CDC boards from being manipulated by ModemManager. +# Fixes issue with hanging references to /dev/ttyACM* devices on Ubuntu 15.04. +ATTRS{idVendor}=="239a", ENV{ID_MM_DEVICE_IGNORE}="1" + +# All Adafruit boards +ATTRS{idVendor}=="239a", MODE="0660", GROUP="adm" + +# All Espressif boards +ATTRS{idVendor}=="303a", MODE="0660", GROUP="adm" + +# All RaspberryPi boards +ATTRS{idVendor}=="2e8a", MODE="0660", GROUP="adm" + +# All NXP Boards +ATTRS{idVendor}=="1fc9", MODE="0660", GROUP="adm" + +# All ST +SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", GROUP="adm" + +# Rule to blacklist TinyUSB example from being manipulated by ModemManager. +SUBSYSTEMS=="usb", ATTRS{idVendor}=="cafe", ENV{ID_MM_DEVICE_IGNORE}="1" + +# Xplained Pro SamG55 Device +SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2111", MODE="0666", GROUP="users", ENV{ID_MM_DEVICE_IGNORE}="1" +SUBSYSTEMS=="tty", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2111", MODE="0666", GROUP="users", ENV{ID_MM_DEVICE_IGNORE}="1" + +# TI Stellaris/Tiva-C Launchpad ICDI +SUBSYSTEM=="usb", ATTRS{idVendor}=="1cbe", ATTRS{idProduct}=="00fd", MODE="0666" + +# CMSIS-DAP, vendor = ARM +SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", MODE="0666" + +# wch-link +SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="8010", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e0", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="8012", GROUP="plugdev" + +# Pxlogic +SUBSYSTEM=="usb", ATTRS{idVendor}=="2a0e", MODE="0666" +SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", MODE="0666" + +# Arduino Renesas +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2341", MODE="0666" + +# E2/E2 Lite/E1/E20/IE850A emulator +ATTR{idProduct}=="82a1", ATTR{idVendor}=="045b", MODE="0666" +ATTR{idProduct}=="82a0", ATTR{idVendor}=="045b", MODE="0666" +ATTR{idProduct}=="823b", ATTR{idVendor}=="045b", MODE="0666" +ATTR{idProduct}=="823c", ATTR{idVendor}=="045b", MODE="0666" +ATTR{idProduct}=="0250", ATTR{idVendor}=="045b", MODE="0666" +# Prevent E2/E2Lite/E1/E20/IE850A from being captured by modem manager service as E2/E2 Lite/E1/E20/IE850A is not a modem +ATTR{idProduct}=="82a1", ATTR{idVendor}=="045b", ENV{ID_MM_DEVICE_IGNORE}="1" +ATTR{idProduct}=="82a0", ATTR{idVendor}=="045b", ENV{ID_MM_DEVICE_IGNORE}="1" +ATTR{idProduct}=="823b", ATTR{idVendor}=="045b", ENV{ID_MM_DEVICE_IGNORE}="1" +ATTR{idProduct}=="823c", ATTR{idVendor}=="045b", ENV{ID_MM_DEVICE_IGNORE}="1" +ATTR{idProduct}=="0250", ATTR{idVendor}=="045b", ENV{ID_MM_DEVICE_IGNORE}="1" + +#TI MSP430UIF +ATTRS{idVendor}=="2047",ATTRS{idProduct}=="0010",MODE="0666" +ATTRS{idVendor}=="2047",ATTRS{idProduct}=="0013",MODE="0666" +ATTRS{idVendor}=="2047",ATTRS{idProduct}=="0014",MODE="0666" +ATTRS{idVendor}=="2047",ATTRS{idProduct}=="0203",MODE="0666" +ATTRS{idVendor}=="2047",ATTRS{idProduct}=="0204",MODE="0666" +ATTRS{idVendor}=="0451",ATTRS{idProduct}=="f432",MODE="0666" + +# fomu +ATTRS{idVendor}=="1209",ATTRS{idProduct}=="5bf0",MODE="0666" + +# FTDI +ATTRS{idVendor}=="0403", MODE="0660", GROUP="adm" + +# Sipeed Slogic16 +SUBSYSTEM=="usb", ATTRS{idVendor}=="359f", MODE="0666", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" + +# ataradov usb-sniffer (github.com/ataradov/usb-sniffer): programmed unit + blank FX2LP +ATTRS{idVendor}=="6666", ATTRS{idProduct}=="6620", MODE="0666" +ATTRS{idVendor}=="04b4", ATTRS{idProduct}=="8613", MODE="0666" |
