diff --git a/board/mister/de10nano/linux-patches/0062-dwc2-host-sof-holdoff-in-hardirq.patch b/board/mister/de10nano/linux-patches/0062-dwc2-host-sof-holdoff-in-hardirq.patch index d7b7894..d8dfd99 100644 --- a/board/mister/de10nano/linux-patches/0062-dwc2-host-sof-holdoff-in-hardirq.patch +++ b/board/mister/de10nano/linux-patches/0062-dwc2-host-sof-holdoff-in-hardirq.patch @@ -1,4 +1,4 @@ -From 62ef2098bc6b0776cfd83fda4c44c482b4d4425c Mon Sep 17 00:00:00 2001 +From cbc0164b35559447f19841c7e2924e0ba89217bf Mon Sep 17 00:00:00 2001 From: "Michael C. Ferguson" Date: Fri, 25 Sep 2026 20:31:45 -0500 Subject: [PATCH 2/2] usb: dwc2: host: acknowledge a SOF with nothing due in @@ -44,10 +44,17 @@ suspended or hibernated port and device mode are passed through. While the hold-off is on, held SOFs are not reported as missed ones by dwc2_track_missed_sofs(). -The hold-off is on by default and can be switched off per controller at +The hold-off is off by default and can be switched on per controller at run time through a "sof_holdoff" debugfs file. Its decisions are counted in "hcd_stats". +On a PREEMPT_RT 7.2 kernel with a hub and a full-speed pad idle, it cut +the IRQ thread's CPU time from 13.2% to 7.7%. On a non-RT 6.18 kernel it +gained nothing when idle, and with about 195 Mbit/s of receive traffic +through a USB WiFi adapter on the same host port, a fixed CPU0 workload +ran slower in six of six runs and complete-split misses rose by about +10-15%. It stays off until that is understood. + This needs "usb: dwc2: host: serve the HCD from the common interrupt handler", which leaves one action on the line: with the HCD's own usb_hcd_irq() action also there, its forced thread would still wake on @@ -58,18 +65,17 @@ Signed-off-by: Michael C. Ferguson drivers/usb/dwc2/core.h | 40 +++++++++++++++++- drivers/usb/dwc2/core_intr.c | 50 ++++++++++++++++++++++ drivers/usb/dwc2/debugfs.c | 19 +++++++-- - drivers/usb/dwc2/hcd.c | 1 + drivers/usb/dwc2/hcd.h | 1 + drivers/usb/dwc2/hcd_intr.c | 80 +++++++++++++++++++++++++++++++++++- drivers/usb/dwc2/hcd_queue.c | 42 +++++++++++++++++++ drivers/usb/dwc2/platform.c | 18 ++++++-- - 8 files changed, 243 insertions(+), 8 deletions(-) + 7 files changed, 242 insertions(+), 8 deletions(-) diff --git a/drivers/usb/dwc2/core.h b/drivers/usb/dwc2/core.h -index eabdf12..92d1c7d 100644 +index df6c49c..5f4a2b4 100644 --- a/drivers/usb/dwc2/core.h +++ b/drivers/usb/dwc2/core.h -@@ -859,8 +859,18 @@ struct dwc2_hregs_backup { +@@ -855,8 +855,18 @@ struct dwc2_hregs_backup { * @sel_np: QHs given a channel from the non-periodic schedule * @halts: Channel interrupts, per host channel * @halts_by: Channel interrupts by endpoint type, split and direction @@ -89,7 +95,7 @@ index eabdf12..92d1c7d 100644 */ struct dwc2_hcd_stats { u32 hcd_passes; -@@ -872,6 +882,15 @@ struct dwc2_hcd_stats { +@@ -868,6 +878,15 @@ struct dwc2_hcd_stats { u32 sel_np; u32 halts[MAX_EPS_CHANNELS]; u32 halts_by[4][2][2]; @@ -105,7 +111,7 @@ index eabdf12..92d1c7d 100644 }; /** -@@ -1004,6 +1023,11 @@ struct dwc2_hcd_stats { +@@ -1000,6 +1019,11 @@ struct dwc2_hcd_stats { * SOF enable/disable. * @hcd_stats: Host interrupt counters, see struct dwc2_hcd_stats * @hcd_stats_base: Counter values at the last debugfs reset @@ -117,7 +123,7 @@ index eabdf12..92d1c7d 100644 * @free_hc_list: Free host channels in the controller. This is a list of * struct dwc2_host_chan items. * @periodic_channels: Number of host channels assigned to periodic transfers. -@@ -1198,6 +1222,14 @@ struct dwc2_hsotg { +@@ -1192,6 +1216,14 @@ struct dwc2_hsotg { bool new_connection; struct dwc2_hcd_stats hcd_stats; struct dwc2_hcd_stats hcd_stats_base; @@ -132,7 +138,7 @@ index eabdf12..92d1c7d 100644 u16 last_frame_num; -@@ -1393,6 +1425,9 @@ void dwc2_wakeup_from_lpm_l1(struct dwc2_hsotg *hsotg, bool remotewakeup); +@@ -1386,6 +1418,9 @@ void dwc2_wakeup_from_lpm_l1(struct dwc2_hsotg *hsotg, bool remotewakeup); /* This function should be called on every hardware interrupt. */ irqreturn_t dwc2_handle_common_intr(int irq, void *dev); @@ -142,7 +148,7 @@ index eabdf12..92d1c7d 100644 /* The device ID match table */ extern const struct of_device_id dwc2_of_match_table[]; -@@ -1545,6 +1580,7 @@ void dwc2_hcd_connect(struct dwc2_hsotg *hsotg); +@@ -1538,6 +1573,7 @@ void dwc2_hcd_connect(struct dwc2_hsotg *hsotg); void dwc2_hcd_disconnect(struct dwc2_hsotg *hsotg, bool force); void dwc2_hcd_start(struct dwc2_hsotg *hsotg); irqreturn_t dwc2_hcd_handle_irq(struct dwc2_hsotg *hsotg); @@ -150,7 +156,7 @@ index eabdf12..92d1c7d 100644 int dwc2_core_init(struct dwc2_hsotg *hsotg, bool initial_setup); int dwc2_port_suspend(struct dwc2_hsotg *hsotg, u16 windex); int dwc2_port_resume(struct dwc2_hsotg *hsotg); -@@ -1574,6 +1610,8 @@ static inline void dwc2_hcd_disconnect(struct dwc2_hsotg *hsotg, bool force) {} +@@ -1567,6 +1603,8 @@ static inline void dwc2_hcd_disconnect(struct dwc2_hsotg *hsotg, bool force) {} static inline void dwc2_hcd_start(struct dwc2_hsotg *hsotg) {} static inline irqreturn_t dwc2_hcd_handle_irq(struct dwc2_hsotg *hsotg) { return IRQ_NONE; } @@ -163,7 +169,7 @@ diff --git a/drivers/usb/dwc2/core_intr.c b/drivers/usb/dwc2/core_intr.c index f9f8f08..5112329 100644 --- a/drivers/usb/dwc2/core_intr.c +++ b/drivers/usb/dwc2/core_intr.c -@@ -914,3 +914,53 @@ out: +@@ -914,3 +914,53 @@ irqreturn_t dwc2_handle_common_intr(int irq, void *dev) return retval; } @@ -218,10 +224,10 @@ index f9f8f08..5112329 100644 + return retval; +} diff --git a/drivers/usb/dwc2/debugfs.c b/drivers/usb/dwc2/debugfs.c -index faadb48..20c833e 100644 +index 8396a27..43c6bde 100644 --- a/drivers/usb/dwc2/debugfs.c +++ b/drivers/usb/dwc2/debugfs.c -@@ -799,6 +799,16 @@ static int hcd_stats_show(struct seq_file *seq, void *v) +@@ -798,6 +798,16 @@ static int hcd_stats_show(struct seq_file *seq, void *v) seq_printf(seq, "split_nak %u\n", st.split_nak); seq_printf(seq, "sel_periodic %u\n", st.sel_periodic); seq_printf(seq, "sel_np %u\n", st.sel_np); @@ -238,7 +244,7 @@ index faadb48..20c833e 100644 seq_puts(seq, "halts"); for (i = 0; i < hsotg->params.host_channels; i++) seq_printf(seq, " %u", st.halts[i]); -@@ -842,9 +852,12 @@ static const struct file_operations hcd_stats_fops = { +@@ -841,9 +851,12 @@ static const struct file_operations hcd_stats_fops = { static void dwc2_hcd_create_debug(struct dwc2_hsotg *hsotg) { @@ -254,23 +260,11 @@ index faadb48..20c833e 100644 } #else static inline void dwc2_hcd_create_debug(struct dwc2_hsotg *hsotg) {} -diff --git a/drivers/usb/dwc2/hcd.c b/drivers/usb/dwc2/hcd.c -index e2e81fb..56b3381 100644 ---- a/drivers/usb/dwc2/hcd.c -+++ b/drivers/usb/dwc2/hcd.c -@@ -5158,6 +5158,7 @@ int dwc2_hcd_init(struct dwc2_hsotg *hsotg) - return -ENODEV; - - dev_dbg(hsotg->dev, "DWC OTG HCD INIT\n"); -+ hsotg->sof_holdoff = true; - - retval = -ENOMEM; - diff --git a/drivers/usb/dwc2/hcd.h b/drivers/usb/dwc2/hcd.h -index eb2ab64..0b832e4 100644 +index b7254d9..36e7f27 100644 --- a/drivers/usb/dwc2/hcd.h +++ b/drivers/usb/dwc2/hcd.h -@@ -549,6 +549,7 @@ struct dwc2_qh *dwc2_hcd_qh_create(struct dwc2_hsotg *hsotg, +@@ -538,6 +538,7 @@ struct dwc2_qh *dwc2_hcd_qh_create(struct dwc2_hsotg *hsotg, gfp_t mem_flags); void dwc2_hcd_qh_free(struct dwc2_hsotg *hsotg, struct dwc2_qh *qh); int dwc2_hcd_qh_add(struct dwc2_hsotg *hsotg, struct dwc2_qh *qh); @@ -279,7 +273,7 @@ index eb2ab64..0b832e4 100644 void dwc2_hcd_qh_deactivate(struct dwc2_hsotg *hsotg, struct dwc2_qh *qh, int sched_csplit); diff --git a/drivers/usb/dwc2/hcd_intr.c b/drivers/usb/dwc2/hcd_intr.c -index 5bd81d4..97ada27 100644 +index d172dfa..60e62df 100644 --- a/drivers/usb/dwc2/hcd_intr.c +++ b/drivers/usb/dwc2/hcd_intr.c @@ -124,7 +124,11 @@ static void dwc2_sof_intr(struct dwc2_hsotg *hsotg) @@ -369,7 +363,7 @@ index 5bd81d4..97ada27 100644 } /* -@@ -2265,6 +2336,12 @@ irqreturn_t dwc2_handle_hcd_intr(struct dwc2_hsotg *hsotg) +@@ -2249,6 +2320,12 @@ irqreturn_t dwc2_handle_hcd_intr(struct dwc2_hsotg *hsotg) /* Check if HOST Mode */ if (dwc2_is_host_mode(hsotg)) { gintsts = dwc2_read_core_intr(hsotg); @@ -382,7 +376,7 @@ index 5bd81d4..97ada27 100644 if (!gintsts) { spin_unlock(&hsotg->lock); return retval; -@@ -2299,6 +2376,7 @@ irqreturn_t dwc2_handle_hcd_intr(struct dwc2_hsotg *hsotg) +@@ -2283,6 +2360,7 @@ irqreturn_t dwc2_handle_hcd_intr(struct dwc2_hsotg *hsotg) dwc2_hc_intr(hsotg); if (gintsts & GINTSTS_PTXFEMP) dwc2_perio_tx_fifo_empty_intr(hsotg); @@ -391,7 +385,7 @@ index 5bd81d4..97ada27 100644 if (dbg_gintsts) { dev_vdbg(hsotg->dev, diff --git a/drivers/usb/dwc2/hcd_queue.c b/drivers/usb/dwc2/hcd_queue.c -index ad5137c..cf873b4 100644 +index 5aec13c..17d3af1 100644 --- a/drivers/usb/dwc2/hcd_queue.c +++ b/drivers/usb/dwc2/hcd_queue.c @@ -1427,6 +1427,7 @@ static enum hrtimer_restart dwc2_wait_timer_fn(struct hrtimer *t) diff --git a/docs/dwc2-usb-irq.md b/docs/dwc2-usb-irq.md index 1bfdcb3..598e153 100644 --- a/docs/dwc2-usb-irq.md +++ b/docs/dwc2-usb-irq.md @@ -1,6 +1,6 @@ # dwc2 host: interrupt load, descriptor DMA and split transactions -Tracking issue: #205. Status as of 2026-09-25: patches `0054`–`0066` carried, M1 and the `fs_ddma` fixes verified on hardware (RT 7.2.7), +Tracking issue: #205. Status as of 2026-09-28: patches `0054`–`0066` carried (the `0062` SOF hold-off ships **off** by default), M1 and the `fs_ddma` fixes verified on hardware (RT 7.2.7), the `fs_ddma` interrupt-channel chaining feature parked. Research record, parked patches and lab tools: [`dwc2-usb-irq/`](dwc2-usb-irq/README.md). @@ -37,7 +37,7 @@ exists: 8,000 interrupts/s on the high-speed root port, whatever is plugged in. | `0059-dwc2-fs-ddma-param` | MiSTer-local | `dwc2.fs_ddma=1` opt-in: caps the port to full speed and enables descriptor DMA. High speed stays the default | | `0060-dwc2-host-keep-periodic-qh-cadence` | yes (Fixes: fb616e3f837e) | a 1 kHz FS interrupt endpoint was polled every 2 ms in buffer mode; rig-measured 500 → 984 reports/s | | `0061-dwc2-host-debugfs-hcd-stats` | with 0062 | M0: `hcd_stats` debugfs counters (SOF passes with and without work, complete-split window misses, split NAKs, halts per channel and type). Behaviour unchanged | -| `0062-dwc2-host-sof-holdoff-in-hardirq` | yes (needs 0058) | M1: the primary handler acks a SOF with nothing due and does not wake the IRQ thread. Buffer DMA only. Off switch: `echo 0 > /sys/kernel/debug/usb/ffb40000.usb/sof_holdoff` | +| `0062-dwc2-host-sof-holdoff-in-hardirq` | yes (needs 0058) | M1: the primary handler acks a SOF with nothing due and does not wake the IRQ thread. Buffer DMA only. **Off by default** since 2026-09-28 (regresses under bulk USB receive on 6.18, see Plan 3). On switch: `echo 1 > /sys/kernel/debug/usb/ffb40000.usb/sof_holdoff` | | `0063-dwc2-ddma-desc-list-bidirectional` | yes (Fixes: 95105a998dff) | the descriptor list was mapped `DMA_TO_DEVICE` but unmapped and synced as `DMA_FROM_DEVICE` | | `0064-dwc2-ddma-giveback-on-dequeue-halt` | yes (Fixes: dc4c76e7b22c) | a dequeue halt dropped descriptors the core had already completed, and the data toggle went stale, with more than one URB queued | | `0065-dwc2-ddma-halt-before-freeing-desc-list` | yes (Fixes: dc4c76e7b22c) | a QH's descriptor list was freed while its channel could still be enabled; now halted and waited for (bounded) first | @@ -119,6 +119,15 @@ Reopen only with new evidence (for example the licensed databook describing an S - Report rates and gaps for the pad and mouse are the same with M1 on and off. `cs_miss`, safety-net and race counters stayed 0; no warnings. - An HS mouse with five interrupt endpoints keeps a channel busy almost every microframe, so M1 cannot help there (M4b in the ledger). - The rig module also carried the in-progress `fs_ddma` chain patches (inert in buffer mode). `0061`/`0062` apply at `-F0` on `0054`–`0060` for 6.18.53 and 7.2.7. + - **Non-RT 6.18.54, 2026-09-28** (production image with the series; hold-off switched live through debugfs; hub + wired pad + BT dongle + an `rtw88_8822bu` USB WiFi adapter; iperf3 from a PC over WiFi; CPU0 cost read as the median time of a fixed CPU0 workload, since that kernel has no IRQ time accounting): + + | Load | CPU0 workload, hold-off on vs off | `cs_miss`, on vs off | + |---|---|---| + | idle | 1,380 vs 1,384 ms | 0 vs 0 | + | WiFi send, 126 Mbit/s | 4.7–5.0 vs 5.2 s | same | + | WiFi receive, ~195 Mbit/s | 8.6–13.8 vs 5.1–9.5 s (worse in 6 of 6 pairs) | 673–730 vs 553–660 per 40 s | + + - So off RT the hold-off gains nothing idle and **regresses under heavy USB receive**, and RT was never tested under bulk load. It now ships **off by default**; the debugfs switch stays for testing. Test notes: with Ethernet and WiFi on one subnet, traffic for the WiFi address arrives on `eth0` (ARP flux) unless `arp_ignore=1`/`arp_announce=2` and a gratuitous ARP on `wlan0`; replies leave by the default route unless the rig's iperf3 server uses `--bind-dev wlan0`. Check `/sys/class/net/*/statistics` before trusting a number. 4. **Gate for anything bigger:** on the M1 kernel with several devices on the hub, compare user-visible work (CD-streaming stutter, CHD/ROM load time, `update_all` duration) against "no USB load" (`fs_ddma=1` or devices unplugged). No visible difference → stop. @@ -141,7 +150,8 @@ Parked work, with the trigger that would reopen each. Update this table instead | **usbhid: two interrupt-IN URBs in flight** | small usbhid patch; upstream-sensitive | makes the `fs_ddma` channel chaining pay off for every HID device: 500 → ~1,000/s | owner wants 1 kHz HID under `fs_ddma`; needs the chaining patches carried first | | **`fs_ddma` interrupt-channel chaining** (parked patches in `dwc2-usb-irq/parked/`) | 3 patches, ~1,000 lines; medium | rig: 2 URBs in flight 500 → ~964/s, 4 URBs 666 → 1,000/s; 1 URB unchanged (hardware prefetch) | a driver with several interrupt-IN URBs in flight matters under `fs_ddma`: USB MIDI on interrupt endpoints (`snd-usb-midi` keeps 7) or the usbhid option below. Retest on the rig before carrying | | **Automatic `fs_ddma`** (userspace: udev + unbind/rebind) | ~100 lines + testing; low–medium | ~7 points of CPU0 over M1 for all-full-speed setups (7.7% → ~0.7%) | CPU0 still matters after M1. Switch only at boot or in the menu: every switch drops all USB for 1–2 s. Detect high-speed-capable devices plugged in while capped with a `DEVICE_QUALIFIER` request (full-speed-only devices stall it) or by class (storage, network) | -| **Measure on 6.18 (non-RT)** | one lab-kernel boot | tells whether `0058`/`0062` matter off RT | before offering them to the MiSTer 6.18 kernel | +| **Hold-off under bulk receive** (why `0062` costs CPU0 and split completions under WiFi receive) | investigation, then a fix | makes `0062` default-on material again | before re-enabling it or offering it upstream. First suspect: the rule that wakes the thread for pending non-periodic work, which constantly re-triggers under bulk traffic. Then retest RT and non-RT under WiFi receive with a Bluetooth pad paired | +| **Measure `0058` on 6.18 (non-RT)** | one boot of a kernel without `0058` | whether the single IRQ action pays off off RT | before offering it to the MiSTer 6.18 kernel. About a day of normal use on 6.18.54 with the series showed no problems, but no gain was measured | | FS cap by default | — | — | **declined**: high speed stays the default, `fs_ddma` is opt-in | ## Side issues found along the way @@ -160,4 +170,5 @@ Nothing has been sent; every submission needs the owner's approval. - **MiSTer 6.18 kernel (Linux-Kernel_MiSTer)**, which is non-RT and runs high-speed buffer DMA by default: - `0060`: small, generic, and helps any 1 kHz full-speed device. Measured only on a full-speed root port so far; confirm behind the high-speed hub on 6.18 first. - The `fs_ddma` bundle (`0054`, `0055`, `0057`, `0059`, `0063`–`0066`): an opt-in that removes the SOF storm on any kernel. The DDMA fixes are only reachable through `0059`, so they go together or not at all. Offer it after a 6.18 measurement. - - Not `0058`, `0061`, `0062` until 6.18 numbers show they matter off RT. Not `0056` on its own. + - Not `0062`/`0061`: measured on 6.18.54 (2026-09-28), no gain idle and a regression under bulk USB receive. Not `0058` until a 6.18 A/B shows a gain. Not `0056` on its own. + - Nothing is ready to offer yet (owner decision 2026-09-28: fix our own tree first).