USB FS device packet ownership and integration boundaries

Public interface: nexus/io/usb_device.h. This slice provides internal-PHY full-speed device PIO and actual SDK bindings for Native, STM32F407 and GD32F470. Maintained scope is EP0 control, explicitly selected EP1–EP3 bulk IN/OUT, SETUP/reset/suspend/resume facts, STALL, SET_ADDRESS, cancellation and exit. No Host, HS, DMA, ISO, remote wake, USB class or product stack is included. A request is one packet no larger than MPS; external applications own multipacket control/class transactions, terminating ZLP, descriptors and workers.

Objects, resources and clocks

Controller and endpoint retain separate identities; public faces contain only read-only ops and context. Assembly owns one controller, exact endpoint array, exact event ring and an explicit 48 MHz clock owner. Instances share read-only tables without global mutable registries, maximum endpoint pools, hidden workers or mandatory OS. Unselected instances create no state, and MCU hot paths use direct volatile MMIO. Model builds replace only external MMIO/NVIC leaves; MCUs carry no model-function tables or runtime-simulation branches.

Both SoCs use USBFS base 0x50000000, four IN / four OUT directions, 320-word FIFO and IRQ 67. Bindings validate actual offsets, strides, capacity and W1C/read-only bits against locked SDKs. EP0 MPS is fixed at 64 bytes; bulk MPS may only be 8/16/32/64. RX FIFO requires at least 64 words and each selected IN FIFO at least 16 words. Partition validation rejects overlap or totals exceeding 320 words.

SoC

Actual 48 MHz source

Integration behavior

STM32F407

HSE 8 MHz / M8 × N336 / Q7

Validate existing PLL/ready; manage only the USBFS gate

GD32F470

HXTAL 25 MHz / M25 × PLLSAI N192 / P4

Bounded startup after exclusively owning idle PLLSAI and CK48 mux

GD32’s original 200 MHz main-PLL Q9 output is not 48 MHz and cannot directly clock USB. Its clock owner rejects newly active USBHS/TRNG/SDIO/TLI consumers, preserves neighboring fields and never changes SYSCLK. IDLE/CLEANUP/READY and exact consumer counts track responsibility; failed rollback without readback proof retains CLEANUP. DCD validates/retains first, releases consumers after completed exit, and external owners stop clocks last. External resource owners must authorize and quiesce pins/electrical/VBUS and hidden clock-gated consumers; zero gates alone do not prove system-wide exclusivity.

Actual ARM GCC 14.3 sizeof probes: controller 144 B, each endpoint 40 B, each event 40 B, request 56 B, STM/GD clock state 8/16 B. Production ARM objects have no global .data/.bss; actual assembly builds separately prove unselected GC/resource changes in complete ELFs.

Packet completion and control epochs

Prepare never accesses payloads; rejected admission retains no new references. ACCEPTED borrows requests and payloads until acquire-observed SETTLED. Requests, payloads, outputs, counts, immutable clock ops and wake targets are checked against controller/endpoint/ring/clock state and existing loans for overlap. Rejected state/read_events output aliases preserve outputs.

Writing IN FIFO is not completion. SUCCESS requires XFRC, cleared EP enable, restored FIFO free capacity and successful observed-W1C-completion clear readback. OUT requires both packet data and XFRC; short packets complete normally. Oversized packets consume every FIFO word, copy at most caller capacity, then report OVERFLOW and drain, never writing outside caller buffers.

IN TXFE is read-only and contributes neither W1C clearing nor zero-value completion proof. ITTXFE means a retryable host IN token arrived while FIFO was empty; OUT STUP is normal SETUP phase. Neither incorrectly terminates packets. IN control timeout and OUT FIFO overrun use real direction-specific event bits. SETUP/core owns EP0 DATA PID: bits 29:28/16 are reserved on EP0, so software uses DATA0 commands only on bulk endpoints.

SETUP strictly decodes eight little-endian bytes and increments epochs monotonically. New SETUP/reset drains old control loans; changing epochs alone never revokes borrows. Reset and queued SETUP in one snapshot have no proven order, so previous queued SETUP is discarded; new admission waits for reset drain. SET_ADDRESS validates current standard SETUP fields and stages the address, writing DCFG only after corresponding status-IN actual XFRC. Failed address readback closes the controller.

The event ring retains value facts without overwriting old events when full; exact software-loss markers follow existing queue contents. Latest SETUP/epoch/closing state does not depend on ring space. Timestamps are IRQ acquisition times, not USB bus sampling times; unordered old SETUP in reset batches is not counted as newly received control.

Cancellation, failure and complete stop

Task entries require one serialized privileged executor outside Handler mode with clear incoming IRQ masks. IRQs save/restore masks, consume at most eight RX status items, 128 payload words and eight selected directions per call. Cold/hot paths are separate: cold reset/flush has finite poll budgets; IRQ flush issues one command and readback, never cold polling in IRQ. Timeout uses independent monotonic deadlines; USB suspend/unplug still needs external executor service, not SOF or hidden timers.

IN drain requests SNAK, observes actual NAK, then requests disable. EN clear, EPDISD, FIFO flush and W1C clear must all hold. Nonzero OUT first confirms global OUT NAK, then disables and drains RX. Drain timeout retains QUARANTINED and never revokes buffer loans.

Control OUT0 EPD is permanently zero and SETUP may bypass NAK. Cancellation cannot be completed by modeling a nonexistent disable acknowledgement. Without actual completion proof, individual OUT0 cancellation remains DRAINING then QUARANTINED; users may select full device stop. Full stop reads back soft disconnect, global OUT NAK and DMAEN=0, consumes RX FIFO, disables global/endpoint/NVIC sources and joins all notification publishers, flushes FIFO, then rechecks disconnect/NAK/FIFO/IRQ state before releasing OUT0 PIO loans and finally publishing SETTLED. OUT0 EP enable may still be one because hardware owns only internal FIFO and withdrawn PIO executors no longer own caller buffers; next cold core reset reestablishes hardware state. Missing proof at any step returns BUSY with caller storage and clock consumer retained.

After wake-target capture, publisher counts still retain that borrow; stop/detach returns BUSY until notify returns. Wake must support both service/cancel task context and true IRQ context. Binding reads actual priority/grouping to validate syscall ceilings without changing global grouping.

Evidence and qualification not yet established

Development evidence: tools/testing/evidence/usb-device-iteration/manifest.json. Complete execution covered Native 43, STM32 7 and GD32 7 Google cases; ASan/UBSan ran the same 57 cases with zero skips. Six production ARM units using both actual SDKs passed strict C11 compilation. The clock owner’s separate 27 cases remain in usb-clock-iteration and are not counted again in DCD’s 57. Historical incorrect TXFE/EP0/PID model assumptions and failure logs remain retained; current-source and old-report identities are not mixed.

Sources are FS register facts in locked ST CMSIS headers, GD32F4xx User Manual Rev3.3 §29.3 (especially printed p986–995 for EP0/W1C/TXFE), and §4.3.24/26 for PLLSAI/CK48. GD manual SHA256: c8e1d20d7cde338892d08dcb233345e03f7960256a60a36c755c576d9cd65c13. Complete provenance is bound in the manifest.

This is not a clean software candidate, product release or physical qualification. USB role, VBUS sensing/power/backfeed, pull-up/ESD, electrical jumpers, pin leases and shared-pin conflicts on all three PCBs need separately reviewed Board assemblies; this batch configures none of those pins. Enumeration, host retries, reset/unplug, transfer errors, PHY three-cycle flush timing and actual ISR/WCET/HIL remain unexecuted and inherit no historical example results.