Repository navigation
Conversation
… bring-up On a channel with traffic, an MT7612U bring-up took ~27.5 s instead of ~0.87 s: from the RXDCOC calibration of the channel set on, every MCU command ran out its 3 s reply budget, and all the replies then appeared within ~7 ms of the RX ring's first EP 4 transfer. A register probe at the first timeout read MT_MAC_SYS_CTRL = 0 (MAC RX and TX off) with the USB RX bulk enable set, and the EP 4 read decoded there was a data frame from the peer's address. So received data reaches EP 4 during RXDCOC before anything reads it, and while it sits there the EP 5 replies are held back too. What in the calibration lets it through is not established. mcu_wait_resp() now drains EP 4 once per command, on its first 300 ms timeout, and only while nobody else reads EP 4. Ownership is a new mt7612u_dev::ep4_owner (NONE / RING / SYNC) rather than d->a->rx_active: it lives on the device, so the PHY tick can read it while StopRxLoop frees the ring (which it does without _mu). The ring claims it under io_lock before its first RX submit and releases it once no RX transfer is left in flight; an MT_RX_DRAIN_SYNC caller holds it until the receiver is switched off; every transition is a compare-exchange from the expected owner. A reply on time costs nothing; a late one costs one mt_rx_flush(), which now returns the bytes drained (partial reads included). The drain logs the command, length and first word. tests/mt7612u_busy_bringup.sh reproduces it with rxdemo + a txdemo peer (build with -DDEVOURER_MT7612U=ON; it runs under mt7612u_sta_lib.sh's root-run rules). One MT7612U, ch 100, an RTL8812EU peer 50 cm away at zero gap: master 09c2ff3 stalled 6 of 6 at 20 MHz and 6 of 6 at HT40 (27.5 s, 9 timeouts each); with this change 10 of 10 and 10 of 10 reached the RX ring in 1.18 s, 0.31 s over the quiet control. The odds rise with the peer's load (a 6 Mb/s peer at a 500 us gap: 1 of 6 on master). Not measured: a ring-less (TX-only) channel set under traffic, and whether the ten-read reply budget could go back to five. docs/mt7612u.md has the table and limits; src/mt7612u/CLAUDE.md replaces "bring this one up before the interferer" with the mechanism; the busy-window probe's notes that relied on that rule are updated (its loaded MediaTek arms stay skipped: not re-run with the sensor started second); tests/README.md lists the reproducer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge
PR Summary by Qodomt7612u: drain EP 4 to prevent busy-channel bring-up stalls
AI Description
Diagram
High-Level Assessment
Files changed (10)
|
Code Review by Qodo
1.
|
…efused claims From the review on OpenIPC#510: - mt_rx_flush() ended on a read that timed out after moving bytes, though more may be queued behind them; it now ends on an empty read, a hard error or its read budget. mcu_wait_resp() drained once per command; it drains again on a later timeout if the previous drain found data, all of a command's drains sharing one 64-read budget, so the per-command bound stays 3 s of reply reads + 0.64 s. - mt_async_start() submitted RX transfers, and mt_mac_start(MT_RX_DRAIN_SYNC) enabled RX, even when their ep4_owner claim failed. Both now refuse. The SYNC claim is taken under io_lock (the lock the drain's owner check holds) and before any register write, so a refusal leaves a running ring's receiver and filter untouched; a SYNC claimed by a call that then fails is released. - tests/mt7612u_busy_bringup.sh: the peer is checked before every loaded bring-up and every bring-up that comes up must hear it, else INCONCLUSIVE; a slow bring-up is FAIL ahead of that. - CLAUDE.md scoped: the MCU-wait drain never shares EP 4 with a ring or a synchronous drainer; the teardown/recovery flushes do not consult the owner. Hardware, same rig, master d0a9e10 as control: master 3/3 slow at each width (27.5 s); this 10/10 + 10/10 fast (1.18-1.20 s; one at 0.87 s needed no drain), and 20/20 with an RTL8812BU peer. docs/mt7612u.md has the table. ctest 86/86 with -DDEVOURER_MT7612U=ON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge
|
Qodo's three findings are addressed in 6c84a49. There's a reply in each thread. Before pushing, two independent reviewers went over the diff. They found two more things, both fixed in the same commit:
Bound. All of one command's drains share one 64-read budget, so the per-command worst case is unchanged: 3 s of reply reads plus 0.64 s. Hardware. Same rig as before: one MT7612U, ch 100, the peer 50 cm away,
Limits.
🤖 Generated with Claude Code |
josephnef
left a comment
There was a problem hiding this comment.
Review: PR 510 (mt7612u: drain EP 4 when an MCU reply is late)
Verdict: request changes. The drain fixes the stall it targets, and I reproduced both the stall and the fix on this bench. But the revised mt_rx_flush exit rule (commit 6c84a49, the Qodo "partial reads" fix) adds ~2 s to EVERY MT7612U bring-up on this host, quiet channel included.
Reproduced (this bench)
One MT7612U (0e8d:7612, 4-2.3.2, USB 3 hub), peer RTL8812EU (0bda:a81a, USB 2 hub), ch 100, 20 MHz, tests/mt7612u_busy_bringup.sh from the PR branch. The 8812EU on this USB 2 hub aborts txdemo at zero gap (8 consecutive bulk-OUT timeouts), so the peer ran at PEER_GAP_US=300, MCS3/20/LDPC. Firmware had to be decompressed by hand (see nit 1).
| build | control (quiet) | loaded bring-ups |
|---|---|---|
| master d0a9e10 (fresh worktree, same configure) | 1.79 s | 3 of 3 + 1 of 1 slow: 28.3 s, 9 MCU timeouts each |
| PR 6c84a49 | 3.79–3.84 s (2 runs) | 6 of 6 + 1 of 1 fast: 4.62–4.72 s, 0 timeouts, one drain each of 243–249 kB |
| PR + candidate fix below | 1.79 s | 4 of 4 fast: 2.04–2.11 s, 0 timeouts, one drain each of 11–14 kB |
The regression
strace of a quiet-channel PR bring-up showed 320 intervals of exactly 10 ms. Instrumenting mt_rx_flush (scratch build): every call on the bring-up path reads 64 × 4096 B, 651 ms, rc = LIBUSB_ERROR_TIMEOUT, n = 4096 on every read, with the buffer returned all zeros over a 0xA5 sentinel fill. So on this host (libusb 1.0.30, kernel 6.18, xhci) a timed-out bulk-IN read of an idle EP 4 reports actual_length == len with no data. The new rule
if (n == 0 || (rc && rc != LIBUSB_ERROR_TIMEOUT))
break;treats that as "bytes moved, keep going" and runs the whole 64-read budget. Three flushes sit on bring-up (mt_init_hardware, mt_mac_stop on the previous session's state, …) → ~2 s. The loaded drains are the same thing: 243–249 kB "drained" is ~10 kB of real data plus ~50 phantom 10 ms reads, which is why the loaded bring-up sits 0.8 s above the control here instead of your 0.31 s. On your rig the timed-out read evidently reports 0, which is why your control did not move.
Candidate fix (verified above)
Keep the PR's re-drain logic and go back to "any error ends the drain":
/* A timed-out read on an idle EP 4 reports n == len with no data on
* some hosts, so n is not a safe continue condition. A drain that ended
* on a timeout with bytes is re-run by mcu_wait_resp() on its next reply
* timeout (ep4_last > 0), which covers the genuinely partial read. */
if (n > 0)
total += (size_t)n;
if (rc || n == 0)
break;The Qodo concern (partial read, more queued) is already handled by the caller's re-drain path; it does not need the loop to continue past a timeout. Please re-run your 20/40 MHz cells with this — on this bench control 1.79 s / loaded 2.04–2.11 s with 11–14 kB drains, which matches your 8.6–10.1 kB. docs/mt7612u.md and src/mt7612u/CLAUDE.md describe the "not on a timed-out read that moved bytes" rule, so they need the same edit.
Smaller points
tests/mt7612u_busy_bringup.shdoes not callsta_fw_readable/sta_fw_linklike its siblings (mt7612u_sta_identity.shetc.). On a stock linux-firmware host (*.bin.zstonly) the control dies with "firmware not found" and the script printsINCONCLUSIVE: the DUT did not come up on a quiet channel (DIED)— the sibling scripts name the real cause.docs/mt7612u.md"Bring-up on a busy channel": the "After review (…)" paragraph and "across the drain's revisions" narrate PR revision history and give two master baselines (09c2ff3,d0a9e10). Current-state docs only: one table against one master, numbers from the final code.tests/busy_window_probe.sh: "(its bring-up stall is fixed: …)" is the same historical phrasing. Say what is true now ("the MediaTek sensor can start second; these arms have not been re-run that way").- Ownership logic itself reads correctly: RING claimed under
io_lockbefore the first submit, released only withrx_inflight == 0; SYNC claimed before any register write and released bymt_mac_rx_disable/mt_mac_stop;mt_async_start's failed claim goes throughfail→mt_async_stop, whose RING→NONE CAS is a harmless no-op there.mt7612u_devisnew-constructed so thestd::atomicinitialiser runs. No findings.
…ot data 6c84a49 kept draining past a timed-out read that reported bytes. On one host (libusb 1.0.30, kernel 6.18, xhci) a timed-out read of an idle EP 4 reports n == len with no data, so every flush ran its whole 64-read budget: ~2 s on every bring-up, quiet channel included (review 5708821119). Any error now ends the drain, a timeout included; a drain that ended on a timeout with real data is re-run by mcu_wait_resp() on its next reply timeout. A timed-out read reporting a full buffer is also not counted: a read that really fills the buffer completes, and counting the artefact would make every MCU-wait drain look like data, re-drain on each later reply timeout and log bytes that were never there. docs/mt7612u.md: one table against master d0a9e10, numbers from this code. Same rig (MT7612U 8-1, RTL8812EU peer at MCS3 zero gap, ch 100): master 3 of 3 slow at 20 and at 40 MHz (27.5-27.6 s, 9 timeouts each); this code 10 of 10 at each width, 1.18-1.20 s, one drain each (8.6-10.1 kB at 20 MHz, 15.8-17.3 kB at 40 MHz), control 0.86-0.87 s. Not run on the host that shows the artefact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge
The script never checked the MT7612U blobs, so a host with compressed
firmware only died in the control and printed a misleading INCONCLUSIVE
('did not come up on a quiet channel'). It now picks the directory the
library will load from (FW_DIR, /lib/firmware/mediatek, ./firmware, as
resolve_fw_dir and sta_client_onair.sh do) and refuses through
sta_fw_readable, which names the compressed files. No ./firmware link is
needed: rxdemo takes the directory from DEVOURER_MT7612U_FW_DIR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge
busy_window_probe.sh said the stall 'is fixed'; it now says what is true: the MediaTek sensor can start second, and these arms have not been re-run that way. tests/README.md said 'on master', which stops being true when this merges: 'without the drain'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge
|
Thanks. That's a real regression and a precise diagnosis. Addressed in Exit rule: your rule as given. Any error ends the drain, a timeout included, and a drain that ended on a timeout with data is re-run by One deliberate addition: a timed-out read reporting a full buffer is not counted toward the drained bytes. A read that really fills the buffer completes with rc 0, so Re-run, same rig as before (MT7612U 8-1, RTL8812EU peer at MCS3 with zero gap, ch 100, near field), against master
No bring-up needed a second drain, so that path is still unexercised on hardware. The 0.31–0.33 s over the control is the first 300 ms reply timeout. Docs: Nits:
|
|
Re-reviewed Code: the exit rule is the one I verified here, plus the full-buffer-timeout exclusion. That exclusion reads correctly to me: a read that really fills the buffer completes with Docs and nits: all three addressed as asked. The "Bring-up on a busy channel" section is current-state only now, and it says plainly that this exact code has not run on the host that shows the artefact. Hardware re-run on this host: not done yet. The MT7612U that showed the |
|
Converted to draft until the busy-bringup re-run on the host that shows the full-buffer timeout exists (its MT7612U is off the bus at the moment). Mark it ready again once that run is posted, or once you are happy to ship without it. |
What
On a channel that is already in use, an MT7612U bring-up takes ~27.5 s instead of ~0.87 s. The RXDCOC calibration of the channel set runs out its 3 s MCU reply budget, and so does every command after it. All the replies then turn up within ~7 ms of the RX ring's first EP 4 transfer.
A register probe at the first timeout read
MT_MAC_SYS_CTRL = 0(MAC RX and TX off) with the USB RX bulk enable on (MT_USB_U3DMA_CFG = 0x00c40020). The EP 4 read decoded there was an RX descriptor and a data frame from the peer's address. So received data reaches EP 4 during RXDCOC before anything reads it, and while it sits there the EP 5 replies are held back too. What in the calibration lets it through is not established.This is the "bring this one up BEFORE the interferer" note in
src/mt7612u/CLAUDE.md, and probably the same thing the "ten reads, not five" comment inmcu_wait_resp()describes ("the MCU answered every command, but seconds late … their nine replies then arrived together").Change
mcu_wait_resp()drains EP 4 (mt_rx_flush()) once per command, on its first 300 ms timeout, and only while nobody else reads EP 4. A reply that arrives in time costs nothing. A late one costs one flush, and its log line gives the command, length and first word.mt7612u_dev::ep4_owner(NONE/RING/SYNC), notd->a->rx_active:StopRxLoopfrees the ring (StopRxLoopdoes that without_mu).io_lockbefore its first RX submit. That is the lockmcu_wait_respholds from the check through the drain.MT_RX_DRAIN_SYNCcaller (bringupgate F) holds it until the receiver is switched off.mt_rx_flush()returns the bytes it drained, partial reads included. Existing callers ignore the return value.src/mt7612u/CLAUDE.md: a current-state "MCU replies and EP 4" section replaces the "bring it up first" rule.docs/mt7612u.md: a "Bring-up on a busy channel" section with the table and its limits.tests/busy_window_probe.*: the notes that relied on the old rule are updated. Its loaded MediaTek arms stay skipped, because they have not been re-run with the sensor started second.tests/README.mdlists the reproducer.tests/mt7612u_busy_bringup.sh, built onmt7612u_sta_lib.shand its root-run rules:txdemoloads the channel at zero gap;REPStimes under it withrxdemo, each run timed to its RX ring starting;-DDEVOURER_MT7612U=ONand says INCONCLUSIVE otherwise.Measured
One MT7612U (
0e8d:7612, USB 3) on ch 100. The peer is an RTL8812EU 50 cm away atDEVOURER_TX_PWR_OFFSET_QDB=-40(−36 dBm at the DUT). The peer was heard after every bring-up.09c2ff3ctest with
-DDEVOURER_MT7612U=ON: 86/86 pass; the two that need external reference data are skipped. No new warnings insrc/mt7612u/.Limits
Possible follow-ups (not in this PR)
mt_mac_start's "refuse rather than wedge".busy_window_probe.sh's loaded MediaTek arms with the sensor started second, and drop their skip.Found while bringing up an RTL8812EU → MT7612U video link. Before opening, this was reviewed by three independent agents over two rounds. The first revision read
d->a->rx_activeunlocked from the tick thread; that use-after-free window was caught in review and is gone.🤖 Generated with Claude Code
https://claude.ai/code/session_011dReg1L6xtBhf95NpAeuge