Skip to content

usb: dwc2: host: No ISOC OUT start-split allowed in uframe 7 - #7554

Open
eggfly wants to merge 1 commit into
raspberrypi:rpi-6.18.yfrom
TypixNode:fix/dwc2-isoc-out-ssplit-uframe7
Open

usb: dwc2: host: No ISOC OUT start-split allowed in uframe 7#7554
eggfly wants to merge 1 commit into
raspberrypi:rpi-6.18.yfrom
TypixNode:fix/dwc2-isoc-out-ssplit-uframe7

Conversation

@eggfly

@eggfly eggfly commented Aug 14, 2026

Copy link
Copy Markdown

I'm building a carrier board for the Raspberry Pi CM5 (BCM2712) — a handheld cyberdeck called TypixNode. On the board, a Terminus FE2.1 multi-TT USB 2.0 hub hangs off the CM5's dwc2 controller (CM_USB), and behind the hub sit a full-speed composite device we built ourselves on an ESP32-S3 (TinyUSB, UAC1 48 kHz 16-bit stereo audio plus a CDC-ACM debug port), a full-speed STM32 keyboard, and a low-speed mouse. The kernel is a self-built rpi-6.18.y (6.18.44). Topology, roughly:

dwc2 root hub (BCM2712 CM_USB), 480M
 └─ Terminus FE2.1 hub, 6 ports, multi-TT, 480M
     ├─ ESP32-S3 composite: UAC1 48kHz + CDC-ACM (TinyUSB), 12M
     ├─ STM32 keyboard (HID), 12M
     ├─ mouse (HID), 1.5M
     └─ C-Media CM108 (0d8c:0014), 12M   (added later for cross-checking)

48 kHz playback through the UAC device was completely silent. Not distorted, not choppy — silent, while everything on the host side reported success: URBs completed, hcint read XFERCOMPL|CHHLTD|ACK (0x23), and the ISO IN feedback endpoint kept delivering plausible rate data. What made this debuggable is that the audio device runs our own firmware: I added lock-free packet counters on the device side, and they showed exactly zero ISO OUT packets arriving. The host was discarding every audio packet without noticing.

The clue that eventually cracked it was strange: opening the CDC-ACM port made audio work, 100% deterministically. Close the tty, silence; open it, sound. Even with DTR cleared so the firmware never writes a byte, merely having the host poll the notify interrupt endpoint was enough. That ruled out the firmware and pointed at host-side scheduling.

So I rebuilt the dwc2 module with the scheduler trace points turned into trace_printk (plus a couple of extra prints at channel arm/completion time) and compared the two placements. Condensed from the traces:

bad placement (CDC closed, TT low-speed schedule otherwise empty):
  scheduler picks rel_uframe 0
  begin-SSPLIT on the wire in uframe 7 of the PREVIOUS frame (xact_pos=BEGIN, 188 bytes)
  end-SSPLIT   on the wire in uframe 0 of the next frame     (xact_pos=END, remainder)
  completion hcint=0x23 on every channel — "success"
  device-side counter: 0 packets received

good placement (CDC open — its notify interrupt endpoint claims the head
of the TT schedule, pushing the ISO OUT split one microframe later):
  begin-SSPLIT in uframe 0, end-SSPLIT in uframe 1, same frame
  register-level identical otherwise (same xact_pos, lengths, odd/even bits)
  device-side counter: 1000 packets/s, no gaps

When the TT's low-speed schedule is empty, dwc2_uframe_schedule_split() picks rel_uframe 0 for the ISO OUT split, and by the driver's own convention the data-carrying start-split goes on the wire one microframe earlier — in uframe 7 of the previous HS frame. For transfers larger than 188 bytes the remaining start-splits land in the following frame, so the begin/end start-splits of a single FS transaction straddle the HS frame boundary and carry different frame numbers. The FE2.1's TT silently discards the whole transaction when its frame advances.

This is not an FE2.1 quirk being papered over: ehci-hcd fixed exactly this in 2013 (commit 8c05dc5, "USB: EHCI: No SSPLIT allowed in uframe 7", Alan Stern), and the EHCI reference implementation never produces such a placement at all — iso_stream_schedule() rejects (start % 8) >= 6 and its smask_out[] tables only cover microframes 0-5. dwc2 generates a placement that the reference host implementation forbids. The discard behaviour does vary by TT (see the #7475 note below), which is why the commit message says "at least some hub TTs".

The fix mirrors the existing "avoiding broken 1st xfer" retry in the same function: for ISO OUT, skip rel_uframe 0 and retry from the next microframe. All start-splits of a transaction then stay inside one frame and none is transmitted in uframe 7.

Verification on the board (CM5, 6.18.44 + this patch, FE2.1, CDC closed the entire time): three consecutive playback runs, device counters show 12028 packets received with zero inter-packet gaps above 1.5 ms, stable audio. To rule out our own device I also retested with an off-the-shelf C-Media CM108 (0d8c:0014, plain UAC1) — 48 kHz and 44.1 kHz both play correctly. Keyboard (FS interrupt splits), mouse (LS via PRE), and CDC are unaffected — the interrupt split path never reaches the new branch. We were previously forced to run the whole bus at full speed via a maximum-speed = "full-speed" overlay to get audio on CM5 (which broke the low-speed mouse behind the hub, among other things); that workaround is no longer needed.

One side effect, disclosed in the commit message: ISO OUT endpoints lose one microframe's worth of schedulable low-speed slices, so an endpoint with a wMaxPacketSize that previously only just fit may now be rejected with -ENOSPC. I think failing enumeration loudly beats completing transfers that never reach the bus.

Related issues, and why I believe this doesn't trade one bug for another:

Per the contribution guidelines, hcd_queue.c carries no downstream changes relative to mainline, so this fix belongs on linux-usb, and I'm preparing to send it to Minas Harutyunyan / Greg KH (it applies cleanly to mainline; Fixes: 9f9f09b, Cc: stable). I'm opening this PR so the Pi USB folks can see it early, poke holes in it, and verify on their own hardware — and carry it downstream first if that's useful. Happy to run more experiments on this setup; the device-side packet counters make bad placements directly observable.

The microframe scheduler can place a full-speed isochronous OUT
split so that its first start-split transaction is transmitted in
microframe 7 of the previous HS frame: when the TT's low-speed
schedule is empty, dwc2_uframe_schedule_split() picks rel_uframe 0,
and by the driver's own convention the data-carrying SSPLIT goes on
the wire one microframe earlier, i.e. in uframe 7.  For transfers
larger than 188 bytes the remaining start-splits then land in the
following frame, so the begin/end start-splits of a single FS
transaction straddle the HS frame boundary.

This placement is not allowed for isochronous OUT.  ehci-hcd
enforces the same rule in commit 8c05dc5 ("USB: EHCI: No SSPLIT
allowed in uframe 7"), and in fact never starts an FS ISO
transaction later than microframe 5 (iso_stream_schedule() rejects
(start % 8) >= 6 and its smask_out[] tables only cover microframes
0-5), so the placement dwc2 produces here is never generated by the
reference EHCI implementation at all.

At least some hub TTs silently discard such transactions.  Observed
with a Terminus FE2.1 multi-TT hub and a full-speed UAC1 audio
device (48 kHz stereo S16, 192-byte packets) behind a BCM2712
(Raspberry Pi CM5) dwc2 host: every ISO OUT URB completes
successfully (XferCompl, no error bits set in hcint), but nothing
ever appears on the downstream full-speed bus and playback stays
completely silent.  The failure disappears as soon as any other
periodic endpoint on the same TT claims the head of the schedule -
for example, opening the cdc_acm notify endpoint of a composite
device pushes the ISO OUT split to rel_uframe 1 and audio starts
working - which is what made the broken placement visible in the
first place.

Skip rel_uframe 0 for isochronous OUT and retry from the next
microframe, mirroring the existing "avoiding broken 1st xfer" check
above.  All start-splits of a transaction then stay inside one
frame and none of them is transmitted in microframe 7.

One microframe's worth of low-speed schedule slices is no longer
usable by ISO OUT endpoints, so an endpoint with a wMaxPacketSize
that previously only just fit may now be rejected with -ENOSPC
instead of silently losing data on the bus.

Fixes: 9f9f09b ("usb: dwc2: host: Totally redo the microframe scheduler")
Cc: stable@vger.kernel.org
Signed-off-by: Haohua Li <lihaohua90@gmail.com>
@P33M

P33M commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

I won't comment on the AI generated wall of text, but note that the dwc OTG core is not in any way related to EHCI. It's perfectly permissible to schedule a periodic start-split transaction in microframe 7 because the hardware will issue the start-split token in the next microframe (well, generally - there's an oddframe register bit that's used to mediate this behaviour but mis-programming as a result of hardIRQ latency will occasionally result in instant transmission).

Counterpoint: why has nobody else reported that 16-bit 48khz audio playback is "completely silent" with dwc2, which is now the default on 64-bit OSes for Pi 3 / Zero2W?

@eggfly

eggfly commented Aug 14, 2026

Copy link
Copy Markdown
Author

Fair enough on EHCI - different core entirely, I brought it up for the TT-facing pattern, not the core mechanics. Let me stick to what I can measure.

On the IRQ-latency theory: the u7 placement here is deliberate and happens on every packet, not occasionally. In the failing case the scheduler hands the begin-SSPLIT next_active_frame = 0x2d0e, so wire_frame = 0x2d0f (u7), and dwc2_hc_set_even_odd_frame() programs HCCHAR.ODDFRM = odd to match; the end-SSPLIT gets 0x2d10 (u0). No EO MISS path taken, parity matches the intended microframe on both channels, hcint = 0x23 on both. So the begin/end pair of one FS transaction is issued under different HS frame numbers by the current scheduler's design, ~1000 times a second - and the device receives 7 packets out of ~18000 over 18 s. Same boot, swap in the patched module: 17994/17994. Swap back: single digits again. I can't see the bus without an analyzer, so I can't tell whether the core defers the u7 token to the next microframe as you describe - but if it did, I'd expect the TT to reassemble this fine, and it demonstrably delivers nothing downstream.

On the counterpoint: Zero2W has no onboard hub, so a FS audio device sits directly on the root port and never uses split transactions at all. Pi 3's onboard LAN9514/LAN7515 TT forwards this exact placement fine - the reporter in #7475 (Pi 3B, FS DAC behind the onboard hub, same scheduler) has clean 48 kHz playback and only 44.1 kHz broken, which is the alignment regression, not this. You need a hub whose TT drops the cross-frame pair to see this bug; the FE2.1 here does, 100% of the time.

If the right model is "a u7 begin-SSPLIT is fine because the core defers it", then the bug would be in the ODDFRM / frame-boundary handling rather than in the schedule, and I'd genuinely rather fix that than burn a microframe of ISO OUT budget. I'm happy to test that variant, or any instrumentation you'd like to see - the device-side per-packet counters on this rig verify either way within minutes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants