mirror of
https://github.com/hathach/tinyusb.git
synced 2026-08-18 11:02:16 +00:00
Merge pull request #3775 from hathach/claude/add-usb-debug-sniff
usb-target-debug/usb-sniffer skills
This commit is contained in:
68
.claude/agents/target-debugger.md
Normal file
68
.claude/agents/target-debugger.md
Normal file
@ -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": "..."}
|
||||
149
.claude/skills/usb-sniffer/SKILL.md
Normal file
149
.claude/skills/usb-sniffer/SKILL.md
Normal file
@ -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`).
|
||||
198
.claude/skills/usb-target-debug/SKILL.md
Normal file
198
.claude/skills/usb-target-debug/SKILL.md
Normal file
@ -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).
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
125
docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md
Normal file
125
docs/superpowers/plans/2026-07-14-usb-target-debug-handoff.md
Normal file
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'
|
||||
*/
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
_dcd.udca[ep_id] = dd;
|
||||
|
||||
if ( ep_id % 2 )
|
||||
#if DCD_ISO_ENABLED
|
||||
if ( is_iso )
|
||||
{
|
||||
// Clear EP interrupt before Enable DMA
|
||||
LPC_USB->EpIntEn &= ~TU_BIT(ep_id);
|
||||
LPC_USB->EpDMAEn = TU_BIT(ep_id);
|
||||
// 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);
|
||||
|
||||
// endpoint IN need to actively raise DMA request
|
||||
LPC_USB->DMARSet = TU_BIT(ep_id);
|
||||
}else
|
||||
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);
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
_dcd.udca[ep_id] = dd;
|
||||
LPC_USB->EpDMAEn = TU_BIT(ep_id); // frame-triggered: no DMARSet, no EpIntEn
|
||||
}
|
||||
else
|
||||
#else
|
||||
(void) is_iso;
|
||||
#endif
|
||||
{
|
||||
// Enable DMA
|
||||
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;
|
||||
|
||||
_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
|
||||
|
||||
93
tools/88-tinyusb.rules
Normal file
93
tools/88-tinyusb.rules
Normal file
@ -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"
|
||||
Reference in New Issue
Block a user