mt7612u: fix the receive collapse (1 Hz PHY tick) and recover the USB wedge - #414
mt7612u: fix the receive collapse (1 Hz PHY tick) and recover the USB wedge#414snokvist wants to merge 14 commits into
Conversation
A run that dies mid transfer (a SIGKILL during a flood is enough) leaves the adapter unable to load firmware ever again: every attempt ends in `fw chunk bulk out: LIBUSB_ERROR_TIMEOUT`, while register reads AND writes still round-trip, the MAC and RF are fine and GATE A passes. libusb_reset_device(), already called on every open, does not reach it, and the kernel mt76x2u driver cannot bind the adapter either - which is what shows the fault is device state rather than this loader. There are two severities, and MT_USB_U3DMA_CFG tells them apart. Soft, TX_BUSY clear (reads 0x00c00020). Isolated one step at a time against a freshly wedged adapter, each tried alone: libusb_clear_halt on all four endpoints, stopping the MAC and the USB DMA, taking WLAN_EN/WLAN_CLK_EN down, and the PBF block reset (MCU|DMA|MAC|PBF|ASY) all left it wedged. Pulsing MT_USB_DMA_CFG_TX_CLR clears it on its own - a bit mt76 declares and writes nowhere. mt_open() now pulses it, so every entry point self-heals: verified over three SIGKILL-mid-flood cycles, a plain `bringup fw` passes afterwards with no recovery step. Hard, TX_BUSY stuck set (reads 0x80c00020). Twenty TX_CLR pulses with TX_BULK_EN dropped do not shift it, nor does anything else above, nor the kernel driver. This one still needs a replug, so mt_open() now says so plainly instead of letting the caller debug an opaque firmware-load timeout. mt_open() also silences the receiver and drains the RX pipe, because mt_rx_flush() only ran from mt_mac_stop() and a killed process never gets there. Separately: mt7612u_start() has always derived enable_rx from rx_active, because the receiver coming up with nothing draining EP 4 is what wedges this part. Three gates called the internal mt_mac_start() with a literal 1 and bypassed that. enable_rx now names its drain source and mt_mac_start() refuses MT_RX_DRAIN_RING when no ring is running, so the internal entry point is as safe as the public one. gate_arx and gate_duplex start the ring first and share mt7612u_set_monitor_rx() instead of open-coding the filter write, and gate_arx reports the ring's own frame count next to the callback count so "nothing arrived" is distinguishable from "arrived, not delivered". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Device-verified.
The fault: against a peer 20 cm away airing 1400-byte HT-MCS7 frames at
3037 fps, a receiver took 3 frames in 10 s. Not the adapter (symmetric in
both directions, and both adapters hear 400-1000 fps of ambient traffic on
ch1/36/40/44/48), not the ring (ring counter agrees with the callback at 3),
and not the air - an RTL8812AU witness counted 103805 frames from that same
transmitter.
The cause: mt76 runs cal_work every second (MT_CALIBRATE_INTERVAL == HZ,
mt76x2/usb_phy.c:42) - channel_calibrate, tssi_compensate (which issues an MCU
command each second) and update_channel_gain. This port ran none of it.
Bisected rather than guessed, one verified peer per arm:
nothing 3 frames (reproduced 8 times)
MT_RX_STAT_* reads only 3 frames
a periodic MCU calibration 43902 frames
the full tick 49002 frames (4867 fps)
So the MCU calibration is the mechanism. mt76x2_phy_update_channel_gain,
mt76x02_phy_adjust_vga_gain and mt76x2_phy_set_gain_val are ported too, with
the low-gain class switch and an average-RSSI feed from the RX path (mt76
takes that from associated stations; a monitor consumer has none) - but the
gain half alone does NOT fix this (4 frames), so it is here for fidelity to
mt76's 1 Hz work, not as the cure.
The tick runs on the CALLER's thread deliberately: MCU commands from two
threads share one 4-bit sequence number and one response endpoint ("mcu resp
mismatch ... want 1"). io_lock is initialised in mt_open() because consumers
may allocate the device struct themselves - a zeroed pthread_mutex_t is a
valid NON-recursive lock, and the tick nests mt_vendor_req inside its own
lock.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Record the vendor-derived MT76x2U reset sequence, exact EP0 encodings, and diagnostic registers missing from the hard-wedge experiment matrix. Distinguish source-backed candidates from device-verified recovery, and explain the limits of FCE index writes and standard USB resets. Validation: source cross-check and git diff --check. Documentation only; no hardware exercised.
From Codex's docs/mt7612u-usb-wedge.md. MT76x2U exposes SEPARATE UDMA TX/RX (CFG 0x9014) and IFDMA/FCE (CFG 0x0064[22:21]) resets that neither mt76 nor this port ever wrote - every earlier failed recovery attempt only reached CFG 0x9018 and MAC 0x0400, which is why they could not cover every block. `bringup swreset 0` observes and repairs nothing (the failing control); `swreset 1` runs the seven-step sequence. The verdict either way is a real firmware load, not an idle status register. Also dumps the diagnostics that doc identifies: per-EP empty bits (CFG 0x2240..0x2290 bit17), UDMA TX state (CFG 0x9100), FCE TX1/TX2 (MAC 0x0a30/0x0a34) and the FCE descriptor index. MT7612U_NO_AUTORECOVER disables the open-path TX_CLR recovery, so a wedge experiment cannot be repaired inside open() and pass for the wrong reason. NOT yet validated against a wedge: the fault has stopped reproducing. The SIGKILL-in-the-sync-arm recipe that wedged 3/3 earlier today now yields a healthy 0x00c40020 at every kill point tried (2, 4, 8 s, and three attempts in the async arm). The gate and the control are in place for the next time a wedge appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…t cause The tick's comments claimed a second thread doing synchronous libusb transfers beside the RX ring's event thread "hangs". libusb's source says otherwise: the sync API waits on its own completed flag and libusb_unlock_events broadcasts to waiters every round, so that shape is supported. The one threaded hang seen was later traced to io_lock being non-recursive on a caller-allocated device. Also states the measured mechanism correctly - the periodic MCU calibration, not the gain tracking - and the 9/9 verification. Comment-only. make check unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…() comments Review of everything since f206354 for dead and invalid code: - The receive fix was unreachable: mt_phy_tick was internal-only. It is now mt7612u_phy_tick() in the public header, with the contract (once a second while receiving; it reads and clears MT_RX_STAT_1's false-CCA field, which a caller also sampling mt7612u_link_stats() will notice). - arx shipped with the tick OFF behind a bisect knob, so the shipped receive gate still went deaf. The tick is now the default in every receiving gate (arx, duplex, linkrx, rxbytes, ack, rx) via wait_ticking(); `arx <ch> <s> 1` is the negative control. The two bisect arms that are now known to be a no-op and a superset are gone. - MT7612U_NO_AUTORECOVER promised "observe only" but open() still silenced the MAC and flushed RX before the guard. The guard now comes first. - cal.false_cca was written and never read. Removed. - mt_vendor_req's comment described a tick thread that no longer exists, and the TX_BUSY loop's comment promised a recovery it has never been seen to perform. Both now say what was measured, and point at docs/mt7612u-usb-wedge.md and `bringup swreset` for the untested candidate. - swreset names MT_TX_CPU_FROM_FCE_CPU_DESC_IDX instead of a raw 0x09a8. - docs/mt7612u.md gains the RX tick contract with the measurements behind it. make check green; strict -Wall -Wextra -Wshadow -Wmissing-prototypes build clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Measured, not assumed. Twelve cycles of killing a receiver under a flood, both adapters as victim, then retrying the RAW firmware upload at +3, +8 and +15 s with no repair applied: every one of the twelve failed at +3 s and passed by itself at +3 s (5-1) or +8 s (2-1). That is a settling window after a process dies mid transfer, not a wedge, and an open that lands inside it must not fail. Separately, on a healthy idle adapter one raw upload in twenty fails and the next succeeds. One bounded retry after a 3 s settle absorbs both. A fault that survives it is reported exactly as before, which is the case the open-path recovery and docs/mt7612u-usb-wedge.md are for. The three "heal failures" in the earlier 24-kill stress run were healing opens that ran inside this window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
A reply that lands after mcu_wait_resp() has given up stays queued on EP 5. The next command then reads its predecessor's reply, mismatches, times out (~1.5 s) and leaves one more stale reply behind, so the channel never gets back in step. Observed as mcu resp mismatch: evt=0 seq=10 (want 15) ... seq=14 (want 15) mcu command timed out waiting for response mcu resp mismatch: evt=0 seq=15 (want 1) cascading through every 1 Hz PHY tick until the receiving gate outlived its 90 s wrapper; the same binary completed the same run in the other direction at 4861 fps, so it depends only on whether a stale reply is queued when the periodic traffic starts. It surfaced once the tick made MCU traffic periodic; before that, one stale reply at most sat in the queue and the five-read loop absorbed it. mt_mcu_send() now drains EP 5 before sending, bounded at 16 replies, and warns when it had to. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…d not finish Measured on 2-1 with a peer 20 cm away saturating the channel: during channel setup seven MCU calibrations in a row timed out at 1.5 s, and once the RX ring was up the new drain pulled nine of their replies out of EP 5 at once. The MCU answers - seconds late. Two consequences, two changes: - mcu_wait_resp() reads ten times, not five (3 s, was 1.5 s). mt76's own budget is five; under RF load it is not enough for this part. - channel_calibrate() marks the channel calibrated only when every command was answered. mt76 sets the flag regardless; a burst that timed out left a receiver at 153 fps where 4850 is normal, and with the flag set nothing ever redid it. Now the 1 Hz tick retries the burst until it completes. The one-time MAC configuration is applied either way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…burst retry An earlier commit withheld channel_cal_done unless every calibration command in the burst was answered, so the 1 Hz tick would re-run the whole burst. That was wrong, and measured wrong: it regressed a receiver to 153 fps. The individual mt_mcu_calibrate() commands DO time out when the MCU answers slowly under RF load - nine per bring-up on this bench, with a peer 20 cm away saturating the channel - and their replies arrive late (mt_mcu_send drains them). But the calibration takes regardless. With the flag set once, as mt76 does, both adapters receive at a flat 5415-5470 fps across eight runs, each of which logged those nine timeouts. Withholding it turned that into a per-second re-calibration thrash. The timeouts were never the discriminator: the runs that received at 48986 frames earlier carried the identical timeout count. I chased the message instead of checking whether the good runs showed it too. They did. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Per the maintainer's ask, docs/mt7612u.md now reflects what the wedge fix actually changed: - mt_mac_start() refuses RX with no drainer (MT_RX_DRAIN_*), replacing the old "two things changed at once" note with the prevention that shipped. - mt_open() now recovers the soft wedge (TX_CLR pulse + RX-pipe drain) instead of needing a replug, and mt_fw_init() retries the post-kill transient; the hard TX_BUSY-stuck tier is still replug-only and its swreset candidate is unverified. - The new recursive io_lock and the EP 5 drain in mt_mcu_send() are described, since the 1 Hz tick made MCU traffic concurrent with the RX event thread. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
PR Summary by Qodomt7612u: restore RX with PHY ticks and recover USB wedges
AI Description
Diagram
High-Level Assessment
Files changed (12)
|
Code Review by Qodo
1.
|
From the qodo review of OpenIPC#414 (findings 3, 4, 6, plus 5 and a leak both found adjacent). The adopt path (mt7612u_open_handle -> mt_adopt) reaches mt_vendor_req() during identification, which locks io_lock, but io_lock and the calibration sentinels were initialised only in mt_open(). A zeroed pthread_mutex_t is a valid NON-recursive lock, so the first mt7612u_phy_tick() - which locks io_lock then nests mt_vendor_req/mt_mcu_send - would self-deadlock a device opened by a libusb-owning consumer, which is exactly the integration path. And mt7612u_close() destroyed a never-initialised mutex. - One shared mt_dev_state_init() (recursive io_lock, idempotent) runs first on BOTH open paths; a guarded mt_dev_state_destroy() folded into mt_close() releases it exactly once. This also closes a mutex leak the review did not name: every mt_open() failure path and bring_up()'s fail path freed the device without destroying io_lock. - The gain-tracking sentinels (low_gain=-1, avg_rssi_all=0, agc_gain_adjust=0) are now reset per-tune in mt_set_channel_ex() instead of once in mt_open(). That fixes two things: an adopted receiver no longer starts with low_gain=0 (which could take the no-class-change branch and program still-zero agc_gain_cur, wrapping the unsigned gain), and a retune no longer classifies the new channel with the previous channel's cached RSSI/VGA offset. - cal.avg_rssi_all is written on the libusb RX thread and read by the PHY tick on the caller's thread; it is now _Atomic with relaxed load/store. Not a lock: taking io_lock on the RX path would block receive behind the tick's multi-second MCU calibration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
… deepen the MCU drain From the qodo review of OpenIPC#414 (findings 9, 7, 8). - Reordering the RX gates to start the ring before the receiver created a failure path: on an mt_mac_start() failure after mt7612u_rx_start(), the gate returned without stopping the ring, so main()'s mt_close() ran with RX transfers still owned by libusb. gate_arx/gate_duplex/gate_rxbytes/ gate_linkrx and gate_linkstat's mac_start now call mt7612u_rx_stop() first, and gate_linkstat's mid-loop mt7612u_link_stats() failure does too (the review named the mac_start pattern; this second return is the same bug). - gate_duplex floods TX with RX enabled but did not tick during the flood, so the concurrent-RX figure decayed and was confounded. It now ticks once a second inside the loop. (gate_linkstat already calibrates each second via mt7612u_link_stats(), and gate_caps measures registers not RX rate, so neither needs a change.) - The stale-MCU-reply drain capped at 16; raised to 64 (observed depth ~5) so a deeper transient drains fully instead of leaving a straggler, with a distinct warning if the cap is hit, which would mean a genuinely desynced channel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Thanks — several of these are real and are now fixed in two commits on the branch. Point by point: #3 (adopted devices use an invalid mutex) — fixed, and it was the important one. #4 (adopted receivers use invalid gain) — fixed, consolidated with #6. The gain sentinels ( #6 (retunes reuse the previous gain state) — fixed by that same per-tune reset. #9 (failed tests leave USB transfers running) — fixed. The reordered gates now call #5 (RX parsing races gain tracking) — fixed as an atomic, not a lock. #7 (some receive tools miss ticks) — fixed where it was real. #8 (full reply queue stays desynced) — hardened. The stale-reply drain cap is raised from 16 to 64 (observed depth is ~5) so a deeper transient drains fully, with a distinct warning if the cap is actually hit (which would mean a genuinely desynced channel). #1 (env var) and #2 (doc duplication) — no change, and here's why. Verification: |
josephnef
left a comment
There was a problem hiding this comment.
Read every commit and the head diff against daabab7, walked the vendor gain code and the lock paths, checked the nine open Qodo threads against the head. The RX-collapse work is measured and the bisect table is convincing; the lock/adopt fixes from the Qodo round are in. Two things block for me, both because the PR's own contract ("every entry point self-heals", "the receiver must never come up with nothing draining EP 4") is not yet met by the code. The rest are inline.
Blocking
-
The wedge recovery is unreachable from
mt7612u_open_handle()(inline atusb.cmt_adopt). The whole TX_CLR + MAC-off + RX-flush block sits inmt_open()only. TheIRtlDevicewrapper — the consumer this subtree exists for — owns libusb and will come in throughopen_handle/mt_adopt, and gets none of it; after a killed run it pays bothmt_fw_initattempts and fails with exactly the message this PR fixes. Hoist the recovery into a shared post-identify step both paths run (likemt_dev_state_initalready is). -
Teardown still runs the receiver undrained (inline at
init.cmt7612u_close).mt_async_stop()cancels and reaps the EP 4 ring first; MAC RX stays enabled untilmt_mac_stop()clearsMT_MAC_SYS_CTRL, which happens after its ownmt_rx_flushand the TX-idle wait. Everyrx_stop(); mac_stop();pair in bringup has the same shape. That is the condition the newmt_mac_startguard exists to prevent, on the path that runs at the end of every session. Pre-existing, but this PR is the one that names the invariant. ClearingENABLE_RXbefore cancelling the ring closes it.
Non-blocking, worth doing in this PR
- Fast retune leaves
agc_gain_initstale while forcing a reprogram (inlinephy.c:717). avg_rssi_allis fed by every decoded frame, neighbours included; mt76 in monitor mode has no stations and sits at the -75 class forever (inlinerx.c:114). Your own bisect says gain tracking is not what fixes RX, so this is fidelity-only code that is currently less faithful than doing nothing.- The 5 ms stale-reply drain runs before every MCU command, including the 3-6 inside every tune (inline
mcu.c:91). - Two
mt_rr()results are consumed without the~0usentinel check on the new paths (inlineusb.c:585). mt_fw_initretry fires on any failure, so a missing blob now costs 3 s and blames a crashed run (inlinefw.c:242).gate_rxticks on the thread that is the only EP 4 drainer (inlinebringup.c:508).
Process
- The
qodo-gatecheck is red only because four threads are unresolved: #1getenv(usb.c:556), #2 docs duplication, #6 retunes (fixed on head — resolve it), #8 drain cap (head warns distinctly and sends anyway — reply with that and resolve). On #1: this repo's rule is that the library reads no environment;MT7612U_DEVwas already deferred to integration in #412, so this is the second one accumulating. A field onmt7612u_open()/abringup-only flag would end the debt; if you defer again, say so in the thread and resolve it. - The PR body says 8 runs at 5415–5470 fps, the header says ~4850 (nine runs),
phy.csays 4362/s,docs/mt7612u.mdsays ~4850. Pick one figure and one home (the header) — Qodo #2 is really about this drift. docs/mt7612u-usb-wedge.mdopens with a research date, a branch name and two commit hashes. Docs here are current-state; git is the changelog. Keep the register tables and the untested-status verdict, drop the provenance header.
No MT7612U on my bench, so nothing above is hardware-verified from this side; the blocking items are code-path facts, not measurements.
| d->io_lock_ready = 0; | ||
| } | ||
|
|
||
| int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, |
There was a problem hiding this comment.
Blocking. mt_adopt() returns straight from mt_identify(). The recovery block (MAC_SYS_CTRL=0, mt_rx_flush, the TX_CLR pulse loop, the TX_BUSY verdict) lives only in mt_open() below. mt7612u_open_handle() is the path a libusb-owning consumer — the IRtlDevice wrapper — uses, so after a killed run it sees U3DMA_CFG=0x00c00020, nothing pulses TX_CLR, and mt_fw_init times out twice (now +3 s) with the exact failure this PR fixes for mt7612u_open(). Factor the recovery into a helper both paths run after identify, the way mt_dev_state_init already is.
| @@ -466,7 +479,7 @@ void mt7612u_close(struct mt7612u_dev *d) | |||
| if (!d) return; | |||
| mt_async_stop(d); | |||
There was a problem hiding this comment.
Blocking. Ordering inverts the invariant mt_mac_start() now enforces: mt_async_stop() cancels and reaps the EP 4 ring here, and MAC RX stays enabled until mt_mac_stop() clears MT_MAC_SYS_CTRL — after its mt_rx_flush and the up-to-150 ms TX-idle wait (longer if a transfer is stuck and the ring strands). On a busy channel that window is the undrained-EP4 overflow you describe as stopping RX DMA for good, and it runs at the end of every session. Same shape in every bringup mt7612u_rx_stop(); mt_mac_stop(); pair. Clear MT_MAC_SYS_CTRL_ENABLE_RX (or run mt_mac_stop) before cancelling the ring.
| * gain class and fine VGA offset all belong to the old channel/band. | ||
| * low_gain=-1 forces the first tick to program a class (this is also | ||
| * what makes an adopted handle's first tick valid - see mt_dev_state_init). | ||
| * avg_rssi_all is written on the RX thread, so store it atomically. */ |
There was a problem hiding this comment.
low_gain = -1 here forces the next tick to reprogram AGC 8/9 from agc_gain_init — but the fast path (if (fast) return 0; at ~line 717) returns before the agc_gain_init snapshot is re-read, while mt_mcu_init_gain (both paths) has just programmed AGC 8/9 for the new channel. So the first tick after a fast retune writes the previous full tune's gain base. mt76 re-reads agc_gain_init on every set_channel. Move the two mt_rr(AGC 8/9) reads above the fast return.
| { | ||
| int8_t avg = atomic_load_explicit(&d->cal.avg_rssi_all, | ||
| memory_order_relaxed); | ||
| avg = avg ? (int8_t)((avg * 7 + info->rssi[0]) / 8) : info->rssi[0]; |
There was a problem hiding this comment.
This EMA takes rssi[0] of every frame the monitor receiver parses — third-party transmitters included (and CRC-error frames, if the filter admits them). mt76's mt76_get_min_avg_rssi averages associated stations only; in monitor mode there are none, so mt76 sits at the -75 fallback and never leaves the middle gain class. Here a -40 dBm neighbour AP drives low_gain=2, AGC 35/37=0x08080808 and gain_cur -= 10..14 against a wanted peer at -80 dBm. The bisect says gain tracking is not what restores RX, so this is fidelity-only code that is currently less faithful than a fixed -75. Either pin it to mt76's monitor behaviour, or feed it only from frames that pass the consumer's SA filter. (Minor: the int8 divide truncates toward zero, and an average that lands on exactly 0 reseeds to -75.)
| uint8_t stale[MCU_RESP_URB_SIZE]; | ||
| int n = 0, got; | ||
|
|
||
| while (n < 64 && |
There was a problem hiding this comment.
This 5 ms bulk-IN timeout is paid before every command, including the 3-6 inside mt_set_channel_ex (fast retune included) and no-wait commands that never produce a reply — a guaranteed LIBUSB_ERROR_TIMEOUT each, so every hop is +15-30 ms with nothing ever drained in the healthy case. Drain only when the previous mcu_wait_resp actually timed out (one d->mcu_stale_pending flag set on that path): zero cost on the hot path, same protection.
| mt_wr(d, MT_MAC_SYS_CTRL, 0); | ||
| mt_rx_flush(d); | ||
|
|
||
| if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) { |
There was a problem hiding this comment.
mt_rr() returns ~0u on a failed control transfer, which has TX_BUSY set: one EP0 hiccup here sends a healthy adapter through TX_BULK_EN off, 20 TX_CLR pulses (400 ms) and a false "still busy" WARN when the re-read fails too. Same in phy_adjust_vga_gain: a failed MT_RX_STAT_1 read is 65535 false CCAs and a 2 dB gain step down. Use mt_rr_chk on these two.
| * an open that lands inside it must not fail. One bounded retry after | ||
| * a settle covers it; a fault that survives the retry is reported as | ||
| * before, and that is the case the open-path recovery is for. */ | ||
| for (int attempt = 0; attempt < 2; attempt++) { |
There was a problem hiding this comment.
The retry keys on any non-zero return, so a missing or short blob (slurp fails, "rom patch too short") now also WARNs that a previous run died mid-transfer and sleeps 3 s before failing identically — sending the user to the wedge doc instead of to the missing file. Retry only on the upload-transfer failure (a distinct return from fw_send_data), which is the case you measured.
|
|
||
| while (got < want && empty < 200) { | ||
| if (now_ms() - last_tick >= 1000.0) { | ||
| mt7612u_phy_tick(&dev); |
There was a problem hiding this comment.
gate_rx runs MT_RX_DRAIN_SYNC — this thread is the only EP 4 drainer — and the tick now blocks it for up to ~3.3 s when the MCU answers late (your own measurement under RF load). That is the undrained-receiver window on the sync gate. Bringup-only, but it is the negative control for the very fault, so either stop RX around the tick here or note that gate_rx is not a valid tick witness under load.
| * under investigation, so it must not become an infinite wait. */ | ||
| mt_wr(&dev, MT_MAC_SYS_CTRL, 0); | ||
| for (int i = 0; i < 50; i++) { | ||
| if (!(mt_rr(&dev, MT_MAC_STATUS) & BIT(0))) break; |
There was a problem hiding this comment.
regs.h has MT_MAC_STATUS_TX = BIT(1) and MT_MAC_STATUS_RX = BIT(0). This loop (and the "TX idle=%d" print two lines down) tests BIT(0), so swreset waits for RX idle and then pulses the UDMA TX reset with TX possibly still in flight — the precondition the vendor sequence states. A FAIL from this gate then cannot separate "sequence does not work" from "run too early". Use mt_poll(&dev, MT_MAC_STATUS, MT_MAC_STATUS_TX, 0, ...) as init.c does, or the MT_USB_DMA_CFG_TX_BUSY test mt_mac_stop uses.
| * an open that lands inside it must not fail. One bounded retry after | ||
| * a settle covers it; a fault that survives the retry is reported as | ||
| * before, and that is the case the open-path recovery is for. */ | ||
| for (int attempt = 0; attempt < 2; attempt++) { |
There was a problem hiding this comment.
Second point on the retry: d->io_err is not cleared between attempts, and mt_init_hardware refuses to report success while it is non-zero (init.c ~417). An attempt 1 that also tripped any EP0 accessor (mt_single_wr FCE addr/len, the CPU_DESC_IDX read-modify-write, mt_poll) before the bulk chunk timed out poisons a clean attempt 2: firmware loads, open still fails with "failed register transfers ... refusing to report success". Add mt_io_clear(d) at the top of each attempt.
josephnef
left a comment
There was a problem hiding this comment.
Review of #414 (8 findings, ranked most severe first). Each was independently verified against the PR head; line-anchored comments below.
The four I would fix before merge: the wedge recovery is reachable only via mt7612u_open() and not mt7612u_open_handle(); the RSSI EMA that drives the gain class is fed by every frame on the channel rather than mt76's associated-station average; mt7612u_phy_tick() can block the caller thread ~3 s, which for a sync-drain consumer is the undrained-EP4 state the PR itself refuses at mt_mac_start; and teardown still disables RX only after the ring is cancelled, the mirror image of the new start-side invariant.
Duplicate of review 5144973019 (same findings, posted twice by tooling); its inline comments were removed. The earlier review stands.
…oints Blocking - The wedge recovery is factored into mt_recover_usb() and runs after identify on BOTH open paths, so a libusb-owning consumer arriving through mt7612u_open_handle()/mt_adopt() gets it too. It lived in mt_open() alone, which left that consumer paying both mt_fw_init() attempts and failing with the exact error the recovery removes. - Teardown no longer runs the receiver undrained. New mt_mac_rx_disable() clears ENABLE_RX before the EP 4 ring is cancelled - in mt7612u_close() and at every ring-cancelling site in bringup, via one rx_teardown() helper rather than the pair open-coded twenty times. Inline - phy.c: the AGC 8/9 snapshot moved above the fast return, so a fast retune no longer programs the previous channel's gain base. - rx.c/phy.c: the ambient-RSSI EMA is gone. mt76_get_min_avg_rssi() averages ASSOCIATED stations and returns 0 for a monitor consumer, so mt76 sits at the -75 fallback; feeding it from every frame let a -40 dBm neighbour step the gain down against a wanted peer at -80 dBm. phy_update_channel_gain() uses -75 directly and the RX hot path does no cross-thread write at all. - mcu.c: the EP 5 drain is armed by mcu_stale_pending instead of running before every command, where it cost a guaranteed 5 ms timeout each. - usb.c/phy.c: mt_rr_chk() on the U3DMA and MT_RX_STAT_1 reads - ~0u has TX_BUSY set, and reads as 65535 false CCAs. - fw.c: the retry keys on a distinct upload/device failure, so a missing blob fails immediately naming the file; mt_io_errors is restored (not zeroed) around the retry. - bringup: gate_swreset polls MT_MAC_STATUS_TX, and gate_rx says in its own output that it is not a valid tick witness under load. Process - MT7612U_NO_AUTORECOVER is now d->no_autorecover, set by bringup. MT7612U_DEV stays deferred as agreed in OpenIPC#412: there is no public way to pass a selector, so removing it would leave a multi-adapter consumer unable to choose at all. - One figure, one home: the mt7612u_phy_tick() comment. phy.c, bringup.c and docs point at it. The 4362/s figure is deleted rather than re-homed - nothing says which build produced it. - docs/mt7612u-usb-wedge.md loses its provenance header. Also fixed, from an adversarial pass over the merged result - both were integration defects no single-file change could see: - Hoisting the AGC reads above the fast return also moved them outside the only mt_io_errors() bracket guarding them. A failed read is ~0u, and FIELD_GET(MT_BBP_AGC_GAIN, ~0u) is 0x7f, so one EP0 hiccup during a fast retune would have programmed a gain base of 127 and gone deaf until the next full tune, silently. Now checked, keeping the previous snapshot. - mt_io_clear() in the fw retry would have wiped the caller's whole bracket: mt_init_hardware() opens it before mt_power_cycle(), and there is only one accumulator, so a partly powered WLAN core could have reported success. - Also: the bulk-OUT failure path now arms the drain (a failed OUT can still have been delivered), the drain only disarms on a clean timeout, gate_rx got the teardown invariant it was missing, and gate_adopt takes the interface claim as a guard before resetting and releases/reattaches on every path. Device-verified on MT7612U 2-1 (peer: a second MT7612U flooding ch149): RX 4902-4913 fps over 5 runs, rx_err/invalid/dropped all 0 adopt `bringup adopt` brings the device up; with NO_AUTORECOVER=1 the recovery announces itself ON THAT PATH, which is the code-path fact the review asked for drain 9 MCU timeouts under RF load produced ONE drain of 9 replies hop 530 ms full / 50.6 ms fast retune Strict build (-Wall -Wextra -Wshadow -Wmissing-prototypes) and make check clean. Not reproduced this session: the soft USB wedge (5 kill cycles, U3DMA_CFG stayed healthy), so "adopt recovers a WEDGED adapter" remains a code-path fact - the same shared function on both paths - not a measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Both blocking items were right, and both were this PR's own contract not being met by the code. All eight inline points are fixed, plus the three process items. Head is Blocking1. Recovery unreachable from Nothing in — the "auto-recovery disabled" line is emitted from inside Honest limit: I could not reproduce the soft wedge this session (5 kill cycles with TX+RX up; 2. Teardown runs the receiver undrained. Fixed. New Inline
Process
Two more, found reviewing the merged resultWorth calling out because neither was visible in any single change:
Verification on this headPeer is a second MT7612U flooding ch149 — a different stimulus from §1's, so quoted separately rather than folded into that figure:
No MT7612U-side claim here is inferred: each row is from a run on |
Summary
Follow-up to #412 (merged as
daabab7). Library-internal — noIRtlDevicesurface. Two faults found by exercising the subtree the way a ground station
would, both device-verified, plus a hardware review of the whole delta.
Public header (for the wrapper)
One addition, no changed signatures:
int mt7612u_phy_tick(struct mt7612u_dev *dev)ininclude/mt7612u/mt7612u.h. It is required forreceive — a consumer that receives must call it about once a second or the
front end goes deaf against a strong transmitter (§1). The
IRtlDevicewrappershould call it on a 1 Hz timer once it drives RX; everything else in the header
is unchanged, so the rename and integration PRs that only consume this header
are unaffected apart from adding that one call.
docs/mt7612u.mdis updated inthis PR for the lock and the RX-undrained recovery story, as requested.
1. A receiver went deaf against a strong transmitter
Against a peer ~20 cm away airing 1400-byte HT-MCS7 at 3037 fps,
arxtook3 frames in 10 s. Not the adapter (symmetric both directions; both hear
400–1000 fps of ambient on ch1/36/40/44/48), not the ring (its own counter
agreed at 3), not the air (an RTL8812AU witness counted 103805 frames from the
same transmitter). mt76 runs
cal_workevery second(
MT_CALIBRATE_INTERVAL == HZ,mt76x2/usb_phy.c:42); this port ran none ofit. Bisected with one verified peer per arm:
MT_RX_STAT_*reads onlyNew public
mt7612u_phy_tick(), caller-driven (MCU commands share one 4-bitsequence number and one response endpoint), called once a second from every
receiving path. Final reliability, both adapters as receiver, single verified
peer: eight consecutive runs at 5415–5470 fps. The MCU answers slowly under
RF load — nine calibration timeouts per bring-up on this bench — so
mcu_wait_resp()waits 10×300 ms andmt_mcu_send()drains any late reply offEP 5 before the next command (without which the reply channel desynced and
cascaded
mcu resp mismatch … (want 1)). The calibration takes regardless ofthose timeouts; the channel is marked calibrated once, as mt76 does.
2. A run killed mid-transfer left the adapter unable to load firmware
Register reads and writes still round-trip, MAC and RF are fine, and the
kernel
mt76x2udriver cannot bind it either — every bulk OUT NAKs.libusb_reset_device(already on every open) does not reach it. Isolated onestep at a time against a freshly wedged adapter:
clear_halton all fourendpoints, MAC + USB DMA stop, WLAN_EN/WLAN_CLK_EN down, and the PBF block reset
each left it wedged; pulsing
MT_USB_DMA_CFG_TX_CLR— a bit mt76 declares andnever writes — clears it alone. That plus an RX-pipe drain is
mt_recover_usb(),which runs after identify on both open paths —
mt_open()andmt_adopt()—so a libusb-owning consumer arriving through
mt7612u_open_handle()self-healstoo. (Review fix: it lived in
mt_open()alone, which was precisely theconsumer this subtree exists for getting none of it.
bringup adoptnowexercises that path.)
A separately-observed transient: a post-kill firmware upload fails for 3–8 s
then succeeds by itself (12/12 kills, no repair applied) — a settling window,
not a wedge.
mt_fw_init()absorbs it with one bounded retry after a 3 ssettle. On a healthy idle adapter the recovery is a no-op: 0/40 healing-open
failures.
Known issue, called out honestly: a harder tier —
TX_BUSYstuck at0x80c00020— was seen once and is not cleared by any of the above; itneeded a physical replug, and it has not reproduced since (36+ kills).
docs/mt7612u-usb-wedge.mdtraces the vendor tree's separate UDMA and IFDMA/FCEresets that none of the attempts reached, implemented as
bringup swresetandstill awaiting a reproduction to test against.
mt_open()warns clearly when ithits this state rather than failing with an opaque timeout.
Verification
shipping binary 0/12 across the scenarios that actually wedge.
mt_mac_start()now refuses to enable the receiver with no ring draining(
MT_RX_DRAIN_*; the publicmt7612u_start()always derived this, threegates bypassed it).
make checkgreen; strict-Wall -Wextra -Wshadow -Wmissing-prototypesbuild clean.
Retracted in the course of this
Two claims made and then falsified: that the inverted ring/receiver order in
arxexplained the 3 frames (it didn't), and that a synchronous MCU command"hangs forever" beside the RX ring's event thread — it never reproduced in 8
stack-capture attempts, and libusb's own source shows that shape is supported.
The comments say so where they used to say otherwise.
Not in scope
The vendor-neutral rename,
IRtlDevicewiring, and a proven recovery for thestuck-
TX_BUSYtier (needs a reproduction first).🤖 Generated with Claude Code
https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Review round (head
4a6a14c)Both blocking items and all eight inline points are addressed; see the review
reply for the point-by-point. Headlines: the recovery is shared by both open
paths, teardown clears
ENABLE_RXbefore the EP 4 ring is cancelled, the EP 5drain is armed rather than unconditional, the ambient-RSSI EMA is gone in favour
of mt76's monitor behaviour, and the library no longer reads
MT7612U_NO_AUTORECOVER(MT7612U_DEVstays deferred per #412).Re-verified on this head — a different stimulus from §1's, so quoted
separately rather than folded into that figure. Peer: a second MT7612U flooding
ch149; DUT: MT7612U at
2-1.rx_err/rx_invalid/rx_droppedall 0, peer verified airing before and after every runbringup adoptbrings the device up; withMT7612U_NO_AUTORECOVER=1the recovery announces itself on that path-Wall -Wextra -Wshadow -Wmissing-prototypesclean;make checkgreenNot reproduced this session: the soft USB wedge (5 kill cycles with TX+RX up,
U3DMA_CFGstayed healthy each time). So "the adopt path recovers a wedgedadapter" is a code-path fact — one shared function, two call sites — not a
measurement from this round.