summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-07-19 00:33:14 +0700
committerGitHub <[email protected]>2026-07-19 00:33:14 +0700
commit50f30773522d21bc599f00c18980ae651f780ff8 (patch)
tree315b497109a480beafe5df10608aae06814cf704
parent3b4544b974054ae95394401adc663a6bd17c7d33 (diff)
parent03f764e5914a96573eee4dd288182deccc60a35a (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.md68
-rw-r--r--.claude/skills/usb-sniffer/SKILL.md149
-rw-r--r--.claude/skills/usb-target-debug/SKILL.md198
-rw-r--r--.gitignore1
-rw-r--r--docs/getting_started.rst2
-rw-r--r--docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md125
-rw-r--r--docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md15
-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.c10
-rw-r--r--examples/device/printer_to_cdc/src/usb_descriptors.c10
-rw-r--r--examples/device/usbtest/skip.txt2
-rw-r--r--examples/device/webusb_serial/README.md2
-rw-r--r--examples/device/webusb_serial/src/main.c2
-rw-r--r--hw/bsp/lpc15/family.c13
-rw-r--r--hw/bsp/lpc40/family.c13
-rw-r--r--src/portable/nxp/lpc17_40/dcd_lpc17_40.c281
-rw-r--r--tools/88-tinyusb.rules93
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"