Spec (brainstormed): own-skill backend decision, PHY-conflict map, six
verification gates, external-JTAG TODO. Plan executed same-day: all gates
run on the rig; apptrace resolved per its own gate rule as (untested).
- DWT data trace: FUNCTION 0b0011 (ARMv7-M ARM Table C1-21) streams value +
accessor-PC packets per access, no halt, no code. Verified on
stm32f407disco (J-Link SWORead: 451 KB, value = uptime ms, PC =
tusb_time_millis_api) and stm32h743nucleo (OpenOCD/ST-Link tpiu capture:
607 KB, same decode). Caveat recorded: R/W-only trace floods on polled
variables.
- SWO enable chain documented: tools own TRCENA/ITM/TPIU; vendor part bites
— H7 needs DBGMCU trace clocks, PB3 manually muxed to AF0, native
stlink-dap (hla tpiu silently no-ops), the cfg's stm32h7x.swo object (the
.tpiu object is the parallel port), traceclk = c_ck 400 MHz (wrong guesses:
ratio-garbage or silence).
- Sections reordered to match the intrusiveness table (least->most
intrusive); cross-references fixed; table gains the data-trace row.
- target-debugger: skill list is now a table referencing skills by name only
(path pattern stated once).
- target-debug: probe-mapping run-on paragraph split into bullets; drop the
GDB Ninth-Edition caveat (calibre now holds the Tenth-Edition PDF, id 2264).
- Align markdown table columns across target-debug, usb-sniffer, usbmon, hil,
usbtest and the agent (7 tables); tables with paragraph-length cells left
unpadded (usbmon symptom map, usbtest case map).
- Vector catch + Cortex-M fault autopsy, verified with a deliberate bad-load
on stm32f407disco: CFSR=0x8200 (BFARVALID|PRECISERR), BFAR = exact bad
address, stacked pc addr2lined to the faulting line; gotchas recorded
(stale FPB comparators fire phantom SIGTRAPs — scrub first; arm DEMCR
after reset; loads precise / stores imprecise; ARMv6-M has no CFSR/BFAR)
- SWO exception trace + hw PC sampling gate PASSED on F407: 680 KB of
packets in 3 s (0x17 PC samples in flash range, 0x0E SysTick enter/exit);
JLinkSWOViewerCL decodes stimulus only — raw SWORead is the recipe;
SWOStart needs an explicit speed headless
- verifybin 'Verify successful.'; FreeRTOS -rtos plugin lists all 6
cdc_msc_freertos tasks after a run->stop cycle (plain attach = 0xDEAD
placeholder); semihosting anti-note; monitor-mode pointer (untested)
- Intrusiveness table gains the new rows; agent playbook bullet updated;
retrieval gate 5/5 with a fresh reader; executed plan committed
LOGGER=rtt builds of any rp2040 example fail with -Werror=missing-prototypes
(stdio_rtt_init has no prototype and is only called from family.c).
Found by building cdc_msc -DLOG=2 -DLOGGER=rtt for raspberry_pi_pico.
Rename usb-target-debug -> target-debug, usb-debug -> usb-kernel-debug,
usb-recover -> usb-kernel-recover (script filenames unchanged), and make all
debug skills/agents decide tool applicability by which end of the link runs
Linux: TinyUSB may run the device or host stack, and its peer may be a Linux
PC, another TinyUSB board, or a Linux gadget (e.g. Raspberry Pi UDC).
- usbmon: exists only when a Linux PC is the link's host
- usb-kernel-debug: either Linux end; allowlist gains dwc3/libcomposite/udc_core
for the gadget side of a Linux peer
- usb-sniffer: the only full-visibility capture when TinyUSB is the host
- target-debug: covers dcd_* and hcd_*/tuh_ debugging; channel choice by topology
- update target-debugger/hil-operator agents, pre-pr, hil-validate.js, and the
USB_RECOVER path constant in test/hil/usbtest.py
- CLAUDE.md: fold the dcd/hcd datasheet cross-check rule into the read-doc line
* docs: add read-doc skill for on-demand datasheet lookup
Search and read MCU datasheets, reference manuals, errata, and the USB
spec from a local Calibre library ($HOME/Documents/calibre-library)
instead of answering register/bitfield/pinout questions from model
memory. Gated on the library's existence, so it no-ops for contributors
who don't have it.
* docs: reference read-doc skill and tighten CLAUDE.md
Point the datasheet/reference entry at the new read-doc skill, and trim
sections that only duplicate a skill already owning the detail:
PVS-Studio and Code Size collapse to pointers; GDB/Build/Flash command
blocks condensed to essentials. 213 -> 129 lines; behavioral guidelines
and the validation checklist unchanged.
* docs: tighten skill redundancy; rename AGENTS.md refs to CLAUDE.md
code-size: fold the step list into a sentence and drop invocation
examples the argument tables already cover. hil: merge the duplicated
self-lock bullets and compress the hifiphile note. usbmon: compress the
group-membership setup paragraph. All commands, flags, lock rules, and
report paths preserved. usb-target-debug and the pvs script only get
stale AGENTS.md references renamed to CLAUDE.md (now the real file);
run_pvs.sh no longer cites a --dump-files mention that CLAUDE.md dropped.
* docs: fix review findings — restore Espressif cd step, ELF placeholder, code-size comment
Codex/Copilot/Claude review of #3778: the condensed Espressif bullet
lost its cd (idf.py resolves the project from CWD, so the command failed
from repo root); the GDB example now uses the build/your_app.elf
placeholder that docs/troubleshooting.rst established; the code-size
invocation comment no longer references --combined, which the shown
command doesn't use.
- Drop the dated/host-specific tap topology; confirm the cabling each
session instead (the tap gets re-cabled often), and read the DUT link
speed from sysfs to pick --speed.
- Genericize the hub-upstream reset-visibility note.
- Rewrite "one-time setup" as a copy-paste shell block (udev + binary +
Wireshark extcap symlink), keeping only the firmware-command caution.
Host (hcd_ci_fs.c):
- Release the speculatively-armed sibling BDT on the NAK path (IN only) as
well as on completion, so a NAKed multi-packet IN no longer leaks a BDT
that stays own=1 and blocks every same-direction pipe. Both paths now go
through a single release_sibling_bd() helper (was a copy-pasted disarm).
- Clear the ENTIRE shared BDT (both directions) on bus reset; clearing only
the IN half left a stale OUT/SETUP descriptor after a disconnect mid-OUT,
blocking the first control transfer on re-enumeration.
- Size bda[] to span the whole BDT (2*2*4) so STAT-indexed access is within
the declared array bounds (was out-of-declared-bounds, benign via union).
Shared (ci_fs_type.h):
- Hoist buffer_descriptor_t and the TOK_PID enum out of the device and host
drivers into the shared header so the identical definitions cannot drift.
Board (kinetis_k):
- Drop a redundant local in board_get_unique_id.
Build-verified: host + kinetis k/kl/k32l + MCX. HIL: frdm_k64f host 2/2
(cdc_msc_hid + device_info); frdm_kl25z device core suite green with the
relocated definitions.
- getting_started.rst: the reST inline-link markup rendered literally
inside the code-block (not a runnable command) and lacked sudo — use a
plain `sudo cp examples/device/99-tinyusb-examples.rules ...`.
- tools/88-tinyusb.rules: normalize the six MODE="666" entries to the
4-digit octal MODE="0666" used everywhere else in the file.
From a second max-effort review of the branch:
- Drop the dead TUSB_XFER_ISOCHRONOUS case in dcd_edpt_open: iso endpoints
are armed via dcd_edpt_iso_alloc/activate (TUP_DCD_EDPT_ISO_ALLOC is
defined for this IP), never through dcd_edpt_open, so the case and its
dd->isochronous assignment were unreachable and asserted a false
invariant. Only bulk/interrupt reach the switch now.
- Extend the iso compile gate to the classes that actually arm an iso
endpoint: DCD_ISO_ENABLED now includes CFG_TUD_BTH (bth_device.c opens
an iso voice endpoint). Without it a BTH build would compile the iso
machinery out and fail SET_INTERFACE at runtime.
- Un-skip LPC175X_6X in the usbtest example: it shares dcd_lpc17_40.c with
LPC40XX verbatim, so the "DCD has no isochronous support" skip reason no
longer holds. Build-verified for lpcxpresso1769 (previously blocked by
the skip).
- TU_ATTR_UNUSED on the ep_id_is_iso helper: every caller is under
#if DCD_ISO_ENABLED, so non-iso builds don't reference it and clang's
-Wunused-function (fatal in CI) rejected the build — gcc stays quiet.
Verified with the full lpc17 and lpc40 example sets under arm-clang.
A fifth finding — bounding control_ep_read's PACKET_READY spin with a
timeout — was implemented and REVERTED: a naive 100k-iteration bound fires
on legitimately-slow control reads and intermittently drops the device
(hardware-proven by interleaved A/B testing against the pre-fix binary).
The infinite wait is retained; the read is only reached once out_received/
out_queued signal data is present, so the theoretical IRQ-off hang is not
reachable in practice.
Re-verified on ea4088_quickstart: usbtest 30/30 (repeated) + HIL 14/14.
Fourth view in the USB debugging toolset (usbmon = host URBs, usb-debug =
host reasoning, usb-target-debug = device firmware, usb-sniffer = what
actually crossed D+/D-). Covers the ataradov/usb-sniffer analyzer:
headless pcapng capture (--speed ls/fs/hs, --fold, --limit self-exit),
Wireshark/tshark analysis recipes, and the wire realities that bite:
downstream broadcast, sniffer self-capture noise, xHCI devnum != wire
address, tap-point-dependent reset visibility (hub choreography anchors),
FS-behind-HS-hub splits. Every recipe hardware-validated on the rig,
including the capture-window floor (a 3 s window provably misses the
enumeration ladder; 3M packets minimum).
Two udev files with distinct audiences, not one:
- examples/device/99-tinyusb-examples.rules (renamed from 99-tinyusb.rules):
the user-facing rules the examples need — cafe VID access, hidraw, the
ModemManager blacklist, a couple of board probes. getting_started.rst,
the webusb_serial README and its source comment point here.
- tools/88-tinyusb.rules: the HIL rig's private probe/analyzer allowlist,
now with the sniffer (6666:6620 + blank FX2LP 04b4:8613). Installed on
the rig only; the usb-sniffer skill references it.
hcd_ci_fs shares a single BDT set across all pipes. prepare_packets()
speculatively arms the sibling (odd^1) BDT of a multi-packet transfer so it
can ping-pong without NAKs. When such a transfer ends early (a short IN
packet) or fails, the still-owned sibling was never released, permanently
blocking the shared BDT for every other pipe.
This deadlocked a 2nd device enumerating behind a hub while another device
issued descriptor reads (host/device_info with CDC+MSC): the MSC's control
transfers could never acquire the BDT, so it never got Set Address.
Release the sibling in process_tokdne()'s completion path, but ONLY for a
multi-packet transfer (length > max_packet_size): a single-packet transfer
never arms a sibling, so that BDT slot may legitimately belong to another
pipe's in-flight transfer and must not be disturbed (doing so unconditionally
corrupts concurrent transfers, e.g. the CDC bulk-IN vs MSC enum in
host/cdc_msc_hid).
Mirrors the equivalent device-side fix in dcd_ci_fs.c; the host needs the
multi-packet guard because its BDT set is shared across pipes.
Verified on frdm_k64f (HIL): host/device_info now enumerates both CDC+MSC
behind a hub, host/cdc_msc_hid still mounts the MSC (no regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExGPLP5eU43LR7o6yYLpNi
frdm_k64f as a USB host with a CH9102 CDC (TX-RX loopback) and a Lexar MSC
drive behind a hub; flasher = onboard OpenSDA J-Link. host/cdc_msc_hid passes
(CDC mount+echo, MSC mount + disk-size check). device_info remains a known
device_info/usbh limitation (its synchronous descriptor dump starves a 2nd
device's enumeration) and is not ci_fs-specific.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExGPLP5eU43LR7o6yYLpNi
- board_uart_read was a stub returning 0, so host examples that bridge the
UART console to a CDC device (echo test) received nothing. Implement it via
an RDRF-interrupt-fed tu_fifo, matching the stm32 family (non-blocking, no
RX overrun). board_uart_write is already non-blocking.
- implement board_get_unique_id() from the SIM 128-bit UID registers so
frdm_k64f/teensy_35 report a real USB serial instead of the fixed default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExGPLP5eU43LR7o6yYLpNi
A multi-packet OUT transfer speculatively arms both even/odd BDTs to avoid
NAK. When the host ends the transfer early with a short packet, the sibling
BDT was left armed (own=1), desyncing the even/odd ping-pong so the next OUT
packet landed at buffer+max_packet_size instead of buffer and the stack read
stale data. Disarm the sibling on completion.
Fixes device/mtp on Kinetis (GetDeviceInfo command was received into the wrong
buffer half -> hang). Pre-existing (MSC only arms single-packet command
receives so it never hit the double-buffer path). HIL: frdm_kl25z & frdm_k64f
device 13/13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExGPLP5eU43LR7o6yYLpNi
Complete the khci -> chipidea ci_fs migration that was started for device
(commit d70403f1f "host is not yet"):
- device: switch kinetis_k/kl/k32l (Makefiles + k32l CMake) to dcd_ci_fs.c
- host: add hcd_ci_fs.c (port of hcd_khci.c onto ci_fs_regs_t) and switch all
Kinetis families to it; remove src/portable/nxp/khci entirely
- enable host examples (device_info, cdc_msc_hid) for mcu:KINETIS_K
- README: merge the KL and K32L2 rows into a single "KL, K32L" ci_fs row
hcd_ci_fs.c also fixes two pre-existing host bugs found via HIL on frdm_k64f
(present in the old hcd_khci.c too):
- data toggle was flipped on a NAK in suspend_transfer; a NAK transfers no
data so the toggle must be preserved, else the retried bulk packet is
silently discarded by the device (MSC CBW/CSW hang). See comment in file.
- prepare_packets asserted and dropped a transfer when the single shared BDT
was still owned by an in-flight transfer under concurrent activity; now it
returns busy and resume_transfer defers/retries on the next SOF.
HIL verified on frdm_k64f: device 13/13, host cdc_msc_hid (CDC mount + echo +
MSC mount, through a hub).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExGPLP5eU43LR7o6yYLpNi
Opus-tier agent charter for backgrounding a long hardware debug session:
instrument -> build -> flash under one held board lock -> dual-side
capture -> correlate -> refine, strictly one instance, skills as source
of truth (usb-target-debug, usbmon, usb-debug, usb-sniffer, usb-recover,
hil). The charter encodes what dogfooding established:
- diagnosis standard: evidence must show the mechanism, or a fix must
flip the ORIGINAL failing case on hardware; stop after two
evidence-free cycles and hand back a partial diagnosis
- lock cadence: hold for the whole session, release around hil_test.py
runs (it self-locks per board)
- revert semantics: "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 a machine-parseable diagnosis report including ruledOut[] —
disproven hypotheses are deliverables. Spec roster updated (opus/xhigh,
effort requested per agent() call).
Real 128-bit chip UID as the board serial (IAP cmd 58, status checked
against IAP_CMD_SUCCESS), replacing the shared placeholder — required for
HIL board identification by serial. lpc40's lpcopen Chip_IAP_ReadUID()
returns only the first UID word, hence the direct iap_entry() call.
Verified on ea4088_quickstart and lpcxpresso1549: both enumerate with
their chip UID and are selected by it in the HIL configs.
The SIE command protocol (CmdCode + CCEMPTY/CDFULL handshake), the
slave-mode Ctrl/RxData/TxData window, the EpIntEn read-modify-writes, and
set_ep_size's ReEp/EP_RLZED handshake are all shared between thread-mode
API calls and dcd_int_handler, and none are reentrant: an ISR preempting a
thread-mode sequence consumes its handshake flags or, in set_ep_size's
case, a bus reset's DevIntClr = 0xFFFFFFFF eats the EP_RLZED flag the
spin waits on, hanging it forever. Guard them by masking only the USB IRQ
(nestable, ISR-safe; CMSIS NVIC_DisableIRQ already ends with DSB+ISB).
control_xact keeps the mask across its in_isr=true event push, since
osal_none skips queue locking for in_isr.
Hardening, not a fix for an observed failure: the ea4088 usbtest 30/30 +
HIL 14/14 results were reproduced with and without it. The windows are a
few instructions wide and most exposed on RTOS builds where class drivers
queue transfers from tasks concurrent with the USB IRQ.
EP0 control-OUT fix (usbtest 14/21, errno 110/-74): usbd queues the
status-stage OUT ZLP of every control read with buffer=NULL, so the ISR's
`if (out_buffer)` check missed it and marked the arriving ZLP as
out_received instead. The stale flag poisoned the next control-OUT with
data: its first chunk "completed" instantly from an empty EP0 buffer and
the host's real DATA NAKed forever. Track queued transfers with an
explicit out_queued flag and void half-finished control state on a new
SETUP.
Isochronous support (UM10562 12.15.6): 5-word DMA descriptors with
per-packet size memory, buflen/present_count in packets, one packet per
FRAME (no DMARSet/EpIntEn involvement), completion at EOT for both
directions. Details that matter:
- the iso machinery (5th DD word + packet-size memory) is compiled only
when an iso-capable class is enabled (CFG_TUD_AUDIO/VIDEO/VENDOR), so
non-iso builds pay nothing: _dcd stays 648 B vs 1032 B with iso
- ISR dispatch keys on the hardware's fixed ep-number/type map
(ep_id_is_iso), never on dd fields that thread mode rebuilds
- iso OUT honors Packet_valid (bit 16) and prefills the hardware
writeback slots with 0, so a missed frame counts as 0 bytes instead of
reading back stale buffer contents as data
- packet count is validated (tu_div_ceil <= ISO_MAX_PACKETS) before the
DD is touched, so an oversized transfer is refused without leaving a
serviceable half-built descriptor armed for the frame engine
- dcd_edpt_iso_alloc and iso_activate both enforce the fixed iso endpoint
numbers (3/6/9/12); classes ignore alloc's return value, so activate
must not trust it
Un-skip LPC40XX in the usbtest example; tier 4 now enumerates and passes
iso cases 15/16/22/23. cdc_msc_throughput and printer_to_cdc had bulk on
iso-only EP3 (SET_CONFIGURATION failed with -32); add the LPC17/40 EPNUM
block (bulk on EP2/EP5) like other fixed-EP examples.
Verified on ea4088_quickstart: usbtest tier-4 battery 30/30 repeatedly
and the full device HIL suite 14/14 (incl. audio_test iso).
Completes the debugging toolset (usbmon = what the host exchanged,
usb-debug = why the host acted, usb-sniffer = what crossed the wire):
TU_LOG/RTT capture, per-probe GDB autopsy without reset, RAM ring-buffer
event trace, J-Link DWT_PCSR PC-sampling, dual-side capture posture, and
board-lock rig discipline. Includes the implementation plan it executes.
Hard-won warnings baked in from real bring-up sessions: volatile ring
buffers vs -Os dead-store elimination, RTT NO_BLOCK_SKIP post-mortem
limits (no overwrite mode exists), DHCSR validity anchors for register
snapshots, release-lock-before-hil_test, and that a marginal just-recabled
link can fake a deterministic firmware bug.
Also ignore .claude/worktrees/.