From af34382dc415cb2cf281222e47e2b5b68dbacf7e Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 18:36:08 +0200 Subject: [PATCH 01/14] mt7612u: recover the soft USB wedge, and name the hard one 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) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/init.c | 12 +++++++++ src/mt7612u/internal.h | 8 ++++++ src/mt7612u/regs.h | 1 + src/mt7612u/tools/bringup.c | 50 +++++++++++++++++++------------------ src/mt7612u/usb.c | 47 ++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index 6880eaf2..8c41b799 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -268,6 +268,18 @@ void mt_rx_flush(struct mt7612u_dev *d) int mt_mac_start(struct mt7612u_dev *d, int enable_rx) { + /* Refuse rather than wedge. The receiver running with nothing draining + * EP 4 puts this part below the USB level; on a quiet channel the gap + * is survivable, which is why it went unnoticed, but at 80% channel + * busy the FIFO overflows inside it and the RX DMA stops for good. + * Measured: the inverted order delivers 3 frames where the correct one + * delivers 5500/s. mt7612u_start() has always derived this from + * rx_active - this makes the internal entry point equally safe. */ + if (enable_rx == MT_RX_DRAIN_RING && !(d->a && d->a->rx_active)) { + ERR("mac_start(MT_RX_DRAIN_RING) with no ring draining EP4 - call " + "mt7612u_rx_start() first, or pass MT_RX_DRAIN_SYNC"); + return -1; + } mt_wr(d, MT_MAC_SYS_CTRL, MT_MAC_SYS_CTRL_ENABLE_TX); if (!mt_poll(d, MT_WPDMA_GLO_CFG, MT_WPDMA_GLO_CFG_TX_DMA_BUSY | MT_WPDMA_GLO_CFG_RX_DMA_BUSY, diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index d10e60ec..12e487fd 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -217,6 +217,14 @@ uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off); /* --- init.c --- */ void mt_power_cycle(struct mt7612u_dev *d); int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir); +/* + * enable_rx is not a bool: the receiver must never come up with nothing + * draining EP 4, and "1" was written at three call sites that had no drainer + * yet. Naming the drain source makes mt_mac_start() able to check. + */ +#define MT_RX_DRAIN_NONE 0 /* TX only */ +#define MT_RX_DRAIN_RING 1 /* the async ring is already draining - asserted */ +#define MT_RX_DRAIN_SYNC 2 /* caller drains with mt_rx_one() on this thread */ int mt_mac_start(struct mt7612u_dev *d, int enable_rx); void mt_rx_flush(struct mt7612u_dev *d); int mt_mac_stop(struct mt7612u_dev *d); diff --git a/src/mt7612u/regs.h b/src/mt7612u/regs.h index 1c6607b3..80cc7639 100644 --- a/src/mt7612u/regs.h +++ b/src/mt7612u/regs.h @@ -97,6 +97,7 @@ #define MT_USB_U3DMA_CFG 0x9018 /* via CFG_ADDR() */ #define MT_USB_DMA_CFG_RX_BULK_AGG_TOUT GENMASK(7, 0) #define MT_USB_DMA_CFG_RX_DROP_OR_PAD BIT(18) +#define MT_USB_DMA_CFG_TX_CLR BIT(19) /* declared by mt76, written by nothing */ #define MT_USB_DMA_CFG_RX_BULK_AGG_EN BIT(21) #define MT_USB_DMA_CFG_RX_BULK_EN BIT(22) #define MT_USB_DMA_CFG_TX_BULK_EN BIT(23) diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index e449a77d..3ee2138d 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -319,7 +319,7 @@ static int gate_tx(uint8_t chan, int count, int phy, int mcs) printf("GATE E: FAIL - set_channel failed\n"); return 1; } /* TX only: this gate never reads EP 4, so do not switch the receiver on. */ - if (mt_mac_start(&dev, 0)) { + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { printf("GATE E: FAIL - mac_start failed\n"); return 1; } printf("MAC started: MT_MAC_SYS_CTRL=0x%08x (bit2 TX, bit3 RX)\n", @@ -376,7 +376,7 @@ static int gate_rx(uint8_t chan, int want) if (mt_set_channel(&dev, chan, MT7612U_BW_20)) { printf("GATE F: FAIL - set_channel failed\n"); return 1; } - if (mt_mac_start(&dev, 1)) { + if (mt_mac_start(&dev, MT_RX_DRAIN_SYNC)) { printf("GATE F: FAIL - mac_start failed\n"); return 1; } /* Monitor: drop only CRC and PHY errors, accept everything else. The @@ -474,7 +474,7 @@ static int gate_g(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memset(frame, 0, sizeof frame); frame[0] = 0x08; @@ -585,7 +585,7 @@ static int gate_mtu(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memset(frame, 0, sizeof frame); frame[0] = 0x08; /* data, 3-address */ @@ -672,7 +672,7 @@ static int gate_soak(uint8_t chan, int secs, int framelen) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memset(frame, 0, sizeof frame); frame[0] = 0x08; @@ -786,13 +786,11 @@ static int gate_arx(uint8_t chan, int secs) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 1)) return 1; - mt_wr(&dev, MT_RX_FILTR_CFG, - MT_RX_FILTR_CFG_CRC_ERR | MT_RX_FILTR_CFG_PHY_ERR); - if (mt7612u_rx_start(&dev, arx_cb, &ctx)) { printf("GATE arx: FAIL - rx_start failed\n"); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); wait_ms(secs * 1000.0); { @@ -803,9 +801,14 @@ static int gate_arx(uint8_t chan, int secs) double el = (now_ms() - t0) / 1000.0; mt_async_stats(&dev, &st); - printf("async RX on ch%u for %.1f s: %lu frames (%.0f/s), rx_err=%llu " + /* ring=rx_frames is what the ring accepted, cb=ctx.n is what the + * callback saw. They must agree; printing only the second one + * cannot tell "nothing arrived" from "arrived, not delivered". */ + printf("async RX on ch%u for %.1f s: %lu frames (%.0f/s), " + "ring=%llu rx_err=%llu " "rx_invalid=%llu rx_dropped=%llu\n", chan, el, ctx.n, ctx.n / (el > 0 ? el : 1), + (unsigned long long)st.rx_frames, (unsigned long long)st.rx_err, (unsigned long long)st.rx_invalid, (unsigned long long)st.rx_dropped); @@ -840,9 +843,6 @@ static int gate_duplex(uint8_t chan, int secs) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 1)) return 1; - mt_wr(&dev, MT_RX_FILTR_CFG, - MT_RX_FILTR_CFG_CRC_ERR | MT_RX_FILTR_CFG_PHY_ERR); memset(frame, 0, sizeof frame); frame[0] = 0x08; @@ -852,6 +852,8 @@ static int gate_duplex(uint8_t chan, int secs) memcpy(frame + 24, "MT7612U-HAL ", 12); if (mt7612u_rx_start(&dev, arx_cb, &ctx)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); while (now_ms() - t0 < secs * 1000.0) { @@ -991,7 +993,7 @@ static int gate_ampdu(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; /* A real station-table entry: aggregation is a per-peer notion, and * wcid 0xff (what the injector normally uses) names no peer. */ @@ -1113,7 +1115,7 @@ static int gate_caps(uint8_t chan) * with nothing reading, that is long enough to wedge the part below * the USB level, which no software reset recovers. */ if (mt_async_start(&dev, drain_cb, &drained)) return 1; - if (mt_mac_start(&dev, 1)) { mt_async_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt_async_stop(&dev); return 1; } mt7612u_get_caps(&dev, &c); printf("caps: %s rev 0x%08x %dTx%dRx bw_mask 0x%02x (20%s%s)\n", @@ -1263,7 +1265,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) /* Ring first, receiver second - see gate_caps. Arming the responder and * printing between the two would otherwise leave RX on and undrained. */ if (mt7612u_rx_start(&dev, ack_cb, &off)) return 1; - if (mt_mac_start(&dev, 1)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } /* CRC and PHY errors only: DUP must stay clear so retries reach us. */ mt_wr(&dev, MT_RX_FILTR_CFG, MT_RX_FILTR_CFG_CRC_ERR | MT_RX_FILTR_CFG_PHY_ERR); @@ -1387,7 +1389,7 @@ static int gate_rxbytes(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, rxbytes_cb, NULL)) return 1; - if (mt_mac_start(&dev, 1)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); @@ -1536,7 +1538,7 @@ static int gate_linktx(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memset(f, 0, sizeof f); f[0] = 0x08; @@ -1625,7 +1627,7 @@ static int gate_linkrx(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, linkrx_cb, NULL)) return 1; - if (mt_mac_start(&dev, 1)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; mt7612u_set_monitor_rx(&dev, 0); printf("RX on ch%u for %d s, filtering our own magic\n", chan, secs); @@ -1699,7 +1701,7 @@ static int gate_diversity(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memset(f, 0, sizeof f); f[0] = 0x08; @@ -1803,7 +1805,7 @@ static int gate_coding(uint8_t chan, int count, int bw) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, (enum mt7612u_bw)bw)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; mt_chan_group(chan, (uint8_t)bw, &hw_chan, NULL, NULL); printf("ch%u (hw centre %u) at %d MHz, %d frames per arm\n\n", @@ -1910,7 +1912,7 @@ static int gate_sweep(uint8_t chan, int count, int bw) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, (enum mt7612u_bw)bw)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; /* Report the centre the hardware actually tuned, not the control * channel: at 80 MHz they differ by up to 6, and a witness listening on @@ -2039,7 +2041,7 @@ static int gate_vht(uint8_t chan, int count, int bw) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, (enum mt7612u_bw)bw)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; mt_chan_group(chan, (uint8_t)bw, &hw_chan, NULL, NULL); printf("chainmask 0x%04x -> %d spatial streams, txwi[17]=0x%02x\n", @@ -2121,7 +2123,7 @@ static int gate_rtap(uint8_t chan, int count) if (mt_eeprom_init(&dev)) return 1; if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; - if (mt_mac_start(&dev, 0)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) return 1; memcpy(pkt, rtap, sizeof rtap); { diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index 88625df7..fd1fa464 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -513,6 +513,53 @@ int mt_open(struct mt7612u_dev *d, const char **err) libusb_release_interface(d->h, 0); goto fail; } + + /* Clear a USB TX DMA left stalled by a run that died mid transfer - a + * SIGKILL during a flood is enough, and it is reproducible on demand. + * The port reset above does NOT reach this: afterwards register reads + * and writes still round-trip, the MAC and RF are fine and the kernel + * mt76x2u driver binds nothing, but every bulk OUT NAKs forever and the + * next firmware upload times out mid-chunk. It was believed to need a + * physical replug. + * + * Isolated one step at a time against a freshly wedged adapter: + * 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) each left it wedged. This bit alone clears it. + * mt76 declares TX_CLR and writes it nowhere. + * + * The receive direction needs its own clean-up: mt_rx_flush() runs from + * mt_mac_stop(), which a killed process never reaches, so the pipe can + * still be full. Silence the receiver BEFORE draining or it refills as + * fast as it is read. */ + mt_wr(d, MT_MAC_SYS_CTRL, 0); + mt_rx_flush(d); + + /* The signature is TX_BUSY stuck set: a wedged adapter reads + * MT_USB_U3DMA_CFG 0x80c00020 where a healthy one reads 0x00c00020. + * One fixed-length pulse does not shift a busy engine, so drop + * TX_BULK_EN, then pulse TX_CLR until the busy bit falls. */ + if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) { + WARN("USB TX DMA busy on open - a previous run died mid transfer"); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); + for (int i = 0; i < 20; i++) { + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + mt_usleep(20000); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + if (!(mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & + MT_USB_DMA_CFG_TX_BUSY)) + break; + } + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); + if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) + WARN("USB TX DMA still busy - this open will likely fail"); + else + LOG("USB TX DMA recovered"); + } else { + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + mt_usleep(20000); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + } return 0; fail: From c17f370cc32b7a95a1ad18a6f2ea30fa76cefa61 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:45:06 +0200 Subject: [PATCH 02/14] mt7612u: port mt76's 1 Hz PHY work - fixes the receive collapse 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/init.c | 1 + src/mt7612u/internal.h | 16 ++++ src/mt7612u/mcu.c | 18 +++- src/mt7612u/phy.c | 159 ++++++++++++++++++++++++++++++++++++ src/mt7612u/rx.c | 6 ++ src/mt7612u/tools/bringup.c | 29 ++++++- src/mt7612u/usb.c | 22 ++++- 7 files changed, 245 insertions(+), 6 deletions(-) diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index 8c41b799..a4856467 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -479,6 +479,7 @@ void mt7612u_close(struct mt7612u_dev *d) mt_async_stop(d); if (d->h) mt_mac_stop(d); mt_close(d); + pthread_mutex_destroy(&d->io_lock); free(d); } diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 12e487fd..ed8a7321 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -48,6 +48,13 @@ struct mt7612u_cal { uint32_t mcu_gain; uint8_t agc_gain_init[2]; uint8_t tssi_cal_done; + /* mt76x2's periodic gain tracking. low_gain starts at -1 so the first + * tick always programs a gain class rather than trusting the initvals. */ + uint8_t agc_gain_cur[2]; + uint8_t agc_gain_adjust; + int8_t low_gain; + int8_t avg_rssi_all; + uint16_t false_cca; }; #define MT_RX_RING 16 @@ -140,6 +147,7 @@ struct mt7612u_dev { uint8_t mcu_seq; uint8_t chan; uint8_t bw; + pthread_mutex_t io_lock; /* recursive: guards register + MCU transactions */ uint8_t bw_clamp_warned; /* the "never widen" notice is once, not per frame */ int8_t txpower_conf; /* limit, 0.5 dB units (dBm * 2) */ int8_t target_power; @@ -216,6 +224,14 @@ uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off); /* --- init.c --- */ void mt_power_cycle(struct mt7612u_dev *d); +/* + * One round of mt76's 1 Hz cal_work (mt76x2/usb_phy.c:42). A consumer that + * receives MUST call this about once a second: without it the AGC never leaves + * its start-up gain and the receiver goes deaf against a strong nearby + * transmitter. Deliberately not a thread - a second thread issuing + * synchronous libusb transfers alongside the RX ring's event thread hangs. + */ +void mt_phy_tick(struct mt7612u_dev *d); int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir); /* * enable_rx is not a bool: the receiver must never come up with nothing diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c index c81679a1..e9cfe82a 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -55,12 +55,20 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, int wait_resp) { uint8_t buf[4 + MCU_MSG_MAX + 8]; + int mcu_rc; uint8_t seq = 0; uint32_t info; int pad, total, rc; if (len > MCU_MSG_MAX) { ERR("mcu payload %d too long", len); return -1; } + /* Send and response are one transaction: the 4-bit sequence number and + * the EP 5 reply belong together. mt_phy_tick() runs on the caller's + * thread so nothing contends today, but a consumer that drives the tick + * from a second thread would otherwise steal its own responses - the + * failure reads as "mcu resp mismatch ... (want 1)". */ + pthread_mutex_lock(&d->io_lock); + if (wait_resp) { seq = ++d->mcu_seq & 0xf; if (!seq) @@ -90,9 +98,15 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, } rc = mt_bulk(d, MT_EP_OUT_INBAND_CMD, buf, total, NULL, 500); - if (rc) { ERR("mcu cmd %d bulk out: %s", cmd, libusb_error_name(rc)); return -1; } + if (rc) { + ERR("mcu cmd %d bulk out: %s", cmd, libusb_error_name(rc)); + pthread_mutex_unlock(&d->io_lock); + return -1; + } - return wait_resp ? mcu_wait_resp(d, seq) : 0; + mcu_rc = wait_resp ? mcu_wait_resp(d, seq) : 0; + pthread_mutex_unlock(&d->io_lock); + return mcu_rc; } int mt_mcu_function_select(struct mt7612u_dev *d, int func, uint32_t val) diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 5a062651..42436d82 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -431,6 +431,165 @@ int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, /* fast=1 skips the firmware calibration burst, which is what a retune would do * if the chip tolerates it. Measured cost of each path: see BRINGUP-RESULTS. */ +/* + * The 1 Hz PHY work, ported from mt76's cal_work (mt76x2/usb_phy.c:42, + * MT_CALIBRATE_INTERVAL == HZ). mt76 runs channel_calibrate (self-guarded), + * tssi_compensate (which issues an MCU command every second) and + * update_channel_gain. This port ran NONE of it, and the cost is not subtle: + * against a transmitter 20 cm away airing 3037 fps, a receiver with no tick + * takes 3 frames in 10 s. With the tick it takes 4362/s. Bisected - reading + * the read-and-clear MT_RX_STAT_* counters alone does nothing (3 frames); it + * is the periodic MCU calibration that keeps the receiver alive. + */ +static int rssi_gain_thresh(uint8_t bw) +{ + return bw == MT7612U_BW_80 ? -62 : bw == MT7612U_BW_40 ? -65 : -68; +} + +static int low_rssi_gain_thresh(uint8_t bw) +{ + return bw == MT7612U_BW_80 ? -76 : bw == MT7612U_BW_40 ? -79 : -82; +} + +static int has_ext_lna(struct mt7612u_dev *d) +{ + uint16_t c1 = mt_ee(d, MT_EE_NIC_CONF_1); + + return d->chan <= 14 ? !!(c1 & MT_EE_NIC_CONF_1_LNA_EXT_2G) + : !!(c1 & MT_EE_NIC_CONF_1_LNA_EXT_5G); +} + +/* mt76x2_phy_set_gain_val() */ +static void phy_set_gain_val(struct mt7612u_dev *d) +{ + uint8_t g0 = (uint8_t)(d->cal.agc_gain_cur[0] - d->cal.agc_gain_adjust); + uint8_t g1 = (uint8_t)(d->cal.agc_gain_cur[1] - d->cal.agc_gain_adjust); + uint32_t val = 0x1836u << 16; + + if (!has_ext_lna(d) && d->bw >= MT7612U_BW_40) + val = 0x1e42u << 16; + if (has_ext_lna(d) && d->chan <= 14 && d->bw < MT7612U_BW_40) + val = 0x0f36u << 16; + val |= 0xf8; + + mt_wr(d, MT_BBP(AGC, 8), val | FIELD_PREP(MT_BBP_AGC_GAIN, g0)); + mt_wr(d, MT_BBP(AGC, 9), val | FIELD_PREP(MT_BBP_AGC_GAIN, g1)); +} + +/* mt76x02_phy_adjust_vga_gain(): false-CCA driven fine adjustment. */ +static int phy_adjust_vga_gain(struct mt7612u_dev *d) +{ + uint8_t limit = d->cal.low_gain > 0 ? 16 : 4; + uint32_t false_cca = FIELD_GET(MT_RX_STAT_1_CCA_ERRORS, + mt_rr(d, MT_RX_STAT_1)); + int changed = 0; + + d->cal.false_cca = (uint16_t)false_cca; + if (false_cca > 800 && d->cal.agc_gain_adjust < limit) { + d->cal.agc_gain_adjust += 2; + changed = 1; + } else if ((false_cca < 10 && d->cal.agc_gain_adjust > 0) || + (d->cal.agc_gain_adjust >= limit && false_cca < 500)) { + d->cal.agc_gain_adjust -= 2; + changed = 1; + } + return changed; +} + +/* mt76x2_phy_update_channel_gain() */ +static void phy_update_channel_gain(struct mt7612u_dev *d) +{ + const uint8_t *gain = d->cal.agc_gain_init; + uint8_t low_gain_delta, gain_delta; + uint32_t agc_35, agc_37, val; + int low_gain, gain_change; + + /* mt76 averages RSSI over associated stations. A monitor consumer has + * none, so this is fed from the RX path; -75 is mt76's own fallback. */ + if (!d->cal.avg_rssi_all) + d->cal.avg_rssi_all = -75; + + low_gain = (d->cal.avg_rssi_all > rssi_gain_thresh(d->bw)) + + (d->cal.avg_rssi_all > low_rssi_gain_thresh(d->bw)); + + gain_change = d->cal.low_gain < 0 || ((d->cal.low_gain & 2) ^ (low_gain & 2)); + d->cal.low_gain = (int8_t)low_gain; + + if (!gain_change) { + if (phy_adjust_vga_gain(d)) + phy_set_gain_val(d); + return; + } + + if (d->bw == MT7612U_BW_80) { + mt_wr(d, MT_BBP(RXO, 14), 0x00560211); + val = mt_rr(d, MT_BBP(AGC, 26)) & ~0xfu; + val |= (low_gain == 2) ? 0x3 : 0x5; + mt_wr(d, MT_BBP(AGC, 26), val); + } else { + mt_wr(d, MT_BBP(RXO, 14), 0x00560423); + } + + low_gain_delta = has_ext_lna(d) ? 10 : 14; + + agc_37 = 0x2121262c; + if (d->chan <= 14) agc_35 = 0x11111516; + else if (low_gain == 2) agc_35 = agc_37 = 0x08080808; + else if (d->bw == MT7612U_BW_80) agc_35 = 0x10101014; + else agc_35 = 0x11111116; + + if (low_gain == 2) { + mt_wr(d, MT_BBP(RXO, 18), 0xf000a990); + mt_wr(d, MT_BBP(AGC, 35), 0x08080808); + mt_wr(d, MT_BBP(AGC, 37), 0x08080808); + gain_delta = low_gain_delta; + d->cal.agc_gain_adjust = 0; + } else { + mt_wr(d, MT_BBP(RXO, 18), 0xf000a991); + gain_delta = 0; + d->cal.agc_gain_adjust = low_gain_delta; + } + + mt_wr(d, MT_BBP(AGC, 35), agc_35); + mt_wr(d, MT_BBP(AGC, 37), agc_37); + + d->cal.agc_gain_cur[0] = (uint8_t)(gain[0] - gain_delta); + d->cal.agc_gain_cur[1] = (uint8_t)(gain[1] - gain_delta); + phy_set_gain_val(d); + + mt_rr(d, MT_RX_STAT_1); /* clear false CCA, as mt76 does */ +} + +/* + * mt76's cal_work, once a second. + * + * The MCU calibration is the part that matters, and that was measured rather + * than assumed: with no tick a receiver takes 3 frames in 10 s from a peer + * airing 3037 fps; with a periodic MCU calibration it takes 4362/s. The gain + * update alone does NOT help (4 frames) - it is ported because it is the rest + * of mt76's 1 Hz work and it tracks signal strength, not because it fixes this. + * Reading the read-and-clear MT_RX_STAT_* counters alone does nothing either. + * + * Runs on the CALLER's thread on purpose. An earlier revision used its own + * thread and hit two failures: MCU commands from two threads share one 4-bit + * sequence number and one response endpoint ("mcu resp mismatch ... want 1"), + * and a second thread issuing synchronous libusb transfers alongside the RX + * ring's event thread hangs outright. + */ +void mt_phy_tick(struct mt7612u_dev *d) +{ + if (!d || !d->chan) return; + pthread_mutex_lock(&d->io_lock); + /* Self-guarded after the first run, exactly like mt76's. */ + channel_calibrate(d, d->chan > 14); + /* mt76 keeps the MCU in the loop every second through + * mt76x2_phy_tssi_compensate(); this port has no TSSI compensation, and + * the temperature calibration is the cheapest command that does. */ + mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); + phy_update_channel_gain(d); + pthread_mutex_unlock(&d->io_lock); +} + int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) { static const uint32_t ext_cca_chan[4] = { diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c index 83afcb38..723706e9 100644 --- a/src/mt7612u/rx.c +++ b/src/mt7612u/rx.c @@ -104,6 +104,12 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, * channel, so it is reported as no estimate rather than as a very quiet * channel. */ info->noise = info->rssi[2]; + /* mt76_get_min_avg_rssi() equivalent. Written on the RX thread and read + * by the PHY tick - a single byte, and a stale sample only delays a gain + * class change by one second. */ + d->cal.avg_rssi_all = d->cal.avg_rssi_all + ? (int8_t)((d->cal.avg_rssi_all * 7 + info->rssi[0]) / 8) + : info->rssi[0]; info->noise_valid = info->noise > -100 && info->noise < -30; info->snr_db = info->noise_valid ? (int8_t)(info->rssi[0] - info->noise) : 0; diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 3ee2138d..7bb7ed53 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -774,7 +774,7 @@ static void arx_cb(void *user, const void *frame, size_t len, } /* Async RX ring: the callback path StartRxLoop needs. */ -static int gate_arx(uint8_t chan, int secs) +static int gate_arx(uint8_t chan, int secs, int poke) { static /* Indexed with (phy & 7): MT_RATE_PHY is three bits, so 5-7 are * representable and named nothing. Five entries read past the end. */ @@ -792,7 +792,29 @@ static int gate_arx(uint8_t chan, int secs) if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); - wait_ms(secs * 1000.0); + /* Bisect: linkstat receives 5062 fps under the same peer where this gate + * receives 3, and the only thing it does differently is poll once a + * second - MT_RX_STAT_* reads (read-and-clear) plus an MCU temperature + * calibration. poke selects which, so the mechanism is identified + * rather than guessed: 0 = neither, 1 = stat reads, 2 = MCU calibrate, + * 4 = the ported 1 Hz PHY tick. */ + if (!poke) { + wait_ms(secs * 1000.0); + } else { + for (int i = 0; i < secs; i++) { + if (!wait_ms(1000.0)) break; + if (poke & 1) { + mt_rr(&dev, MT_CH_BUSY); mt_rr(&dev, MT_CH_IDLE); + mt_rr(&dev, MT_RX_STAT_0); + mt_rr(&dev, MT_RX_STAT_1); + mt_rr(&dev, MT_RX_STAT_2); + } + if (poke & 2) + mt_mcu_calibrate(&dev, MCU_CAL_TEMP_SENSOR, 0); + if (poke & 4) + mt_phy_tick(&dev); /* the ported 1 Hz gain work */ + } + } { struct mt_async_stats st; /* Actual elapsed, not the requested duration: an interrupt now @@ -2277,7 +2299,8 @@ int main(int argc, char **argv) argc > 3 ? atoi(argv[3]) : 5); } else if (!strcmp(cmd, "arx")) { rc = gate_arx(argc > 2 ? (uint8_t)atoi(argv[2]) : 1, - argc > 3 ? atoi(argv[3]) : 5); + argc > 3 ? atoi(argv[3]) : 5, + argc > 4 ? atoi(argv[4]) : 0); } else if (!strcmp(cmd, "gateg")) { rc = gate_g(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, argc > 3 ? atoi(argv[3]) : 300); diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index fd1fa464..8ac84f32 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -75,15 +75,21 @@ int mt_vendor_req(struct mt7612u_dev *d, uint8_t req, uint8_t type, { int rc = LIBUSB_ERROR_OTHER; + /* The PHY tick runs on its own thread and does register I/O; without + * this two control transfers can interleave on one endpoint. */ + pthread_mutex_lock(&d->io_lock); + for (int i = 0; i < VEND_RETRIES; i++) { rc = libusb_control_transfer(d->h, type, req, val, idx, (unsigned char *)buf, (uint16_t)len, CTRL_TIMEOUT_MS); if (rc >= 0 || rc == LIBUSB_ERROR_NO_DEVICE) - return rc; + goto out; mt_usleep(5000); } ERR("vendor req %02x idx %04x failed: %s", req, idx, libusb_error_name(rc)); +out: + pthread_mutex_unlock(&d->io_lock); return rc; } @@ -472,6 +478,20 @@ int mt_open(struct mt7612u_dev *d, const char **err) return -1; } + /* Every path into the device goes through here, including consumers that + * allocate the struct themselves - a zeroed pthread_mutex_t is a valid + * NON-recursive lock, and the PHY tick nests mt_vendor_req inside its own + * lock, so initialising this anywhere else self-deadlocks. */ + { + pthread_mutexattr_t ma; + + pthread_mutexattr_init(&ma); + pthread_mutexattr_settype(&ma, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&d->io_lock, &ma); + pthread_mutexattr_destroy(&ma); + d->cal.low_gain = -1; + } + d->kernel_was_attached = libusb_kernel_driver_active(d->h, 0) == 1; if (d->kernel_was_attached) { rc = libusb_detach_kernel_driver(d->h, 0); From eeac008512cd6b070377b66357c99b37ec2cc13c Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 19:38:30 +0200 Subject: [PATCH 03/14] docs(mt7612u): trace dedicated UDMA and FCE wedge resets 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. --- docs/mt7612u-usb-wedge.md | 215 ++++++++++++++++++++++++++++++++++++++ docs/mt7612u.md | 5 + 2 files changed, 220 insertions(+) create mode 100644 docs/mt7612u-usb-wedge.md diff --git a/docs/mt7612u-usb-wedge.md b/docs/mt7612u-usb-wedge.md new file mode 100644 index 00000000..41c3010f --- /dev/null +++ b/docs/mt7612u-usb-wedge.md @@ -0,0 +1,215 @@ +# MT7612U hard USB TX wedge: reset evidence + +Research date: 2026-09-07. Devourer HEAD initially: `4e7a793` on +`feat/mt7612u-mediatek-backend`; concurrent PHY/I/O work was subsequently +committed as `5757712`. Its separate host-side libusb hang report does not +establish the cause of this persistent device-side wedge. +Reference: `openwrt/mt76` at `be5ce7910521492d4a2e4ce7ee3843680a46c047`. +**Status: source-level investigation only; no new device measurements.** + +There is a concrete reset candidate beyond `TX_CLR`: the vendor-derived +MT76x2U source has separate UDMA TX and IFDMA/FCE reset controls. Its +`RTMPSwReset()` uses CFG `0x9014[6]` and CFG `0x0064[22:21]`, respectively. +Neither appears in the supplied list of failed experiments. Whether this +sequence clears this adapter's hard wedge remains untested. It is premature +to conclude that the silicon requires re-enumeration. + +## Observed failure, not a new measurement + +The handover reports EP0 CFG/MAC read/write round-trips still working, +`MT_ASIC_VERSION=0x76120044`, and bulk OUT timing out at the first ROM-patch +chunk on EP8. Kernel-driver binding also fails. + +| CFG `0x9018` | Reported behavior | +|---|---| +| `0x00c00020` | Soft wedge, TX_BUSY clear; bit19 `TX_CLR` set, 20 ms, clear recovered 3/3 trials. | +| `0x80c00020` | Hard wedge, TX_BUSY set; repeated TX_CLR, MAC/PBF/WLAN resets, USB port reset, halt clearing, authorization toggle and xHCI rebind failed; physical replug recovered. | + +The earlier undrained-RX report in `mt7612u.md` describes vendor requests +timing out too. Do not conflate that failure with this EP0-responsive state. +Physical replug changes power as well as enumeration; its success does not +prove that logical re-enumeration alone is sufficient. + +## The missing vendor sequence + +Source: [RTMPSwReset(), vendor-derived MT76x2U tree][vendor-reset]. The tree +[identifies itself as JEDI.MP2.mt76x2u.wifi.v3.2.1][vendor-version] and +[lists MT7612U and MT7662U among its targets][vendor-readme]. This is a pinned +public mirror, not an authenticated copy of an untouched MediaTek release. + +Use checked read-modify-write operations. In this table, **pulse** means set +the mask, wait **15 ms**, read again, clear the mask, wait **15 ms**. + +| Order | Space / offset | Operation | Source's purpose | +|---|---|---|---| +| 1 | CFG `0x9018` | Clear `0x00c00000` | Disable UDMA TX and RX | +| 2 | CFG `0x9080` | Pulse `0x03f00000` | Drop OUT EP4–EP9 data | +| 3 | CFG `0x9014` | Pulse `0x00000040` | Reset UDMA TX | +| 4 | CFG `0x0064` | Pulse `0x00600000` | Reset IFDMA/FCE | +| 5 | MAC `0x0400` | Pulse `0x0000000c` | Reset MAC/PBF | +| 6 | CFG `0x9014` | Pulse `0x00000020` | Reset UDMA RX | +| 7 | CFG `0x9018` | Set `0x00c00000`; wait 15 ms | Enable UDMA TX and RX | + +The function's surrounding contract calls for cancelling bulk OUT URBs, +disabling MAC TX/RX (`MAC 0x1004 = 0`), and polling MAC TX idle +(`MAC 0x1200 & 1 == 0`) first. Those actions, firmware reload and final MAC +enable are **commented out in this helper**. Its contract says reload firmware +without asserting WLAN reset at CFG `0x0064[19]`. + +There is an actual [call at the start of rt28xx_init()][vendor-caller], before +top-level initialization and `mcu_sys_init()`. Thus the helper is used during +bring-up; it is not evidence of a tested recovery from SIGKILL with TX_BUSY +stuck. Its active reset operations use EP0 register access and delays, with +no USB reset, configuration change or re-enumeration request. + +For a userspace experiment, stop new submissions and cancel/reap outstanding +transfers before touching the DMA. Bound and record the MAC-idle poll; do not +turn it into an infinite wait on the fault being investigated. Leave MAC RX +disabled until a live drain exists. Firmware/FCE/channel state must be +reinitialized after the sequence; resuming old queued frames is not a valid +success test. These are integration requirements, not measurements. + +## Exact EP0 encoding + +The vendor's [CFG accessors][vendor-access] agree with the pinned +[mt76 register accessors][mt76-access]. All offsets above fit in 16 bits. + +| Operation | bmRequestType | bRequest | wValue | wIndex | Data | +|---|---|---|---|---|---| +| Read CFG | `0xc0` | `0x47` | `0` | offset | Read 4 bytes, little-endian | +| Write CFG | `0x40` | `0x46` | `0` | offset | Write 4-byte little-endian value | +| Read MAC | `0xc0` | `0x07` | `0` | offset | Read 4 bytes, little-endian | +| Write MAC | `0x40` | `0x06` | `0` | offset | Write 4-byte little-endian value | + +In this backend, use `CFG_ADDR(offset)` with the checked register accessors +for CFG space; pass a plain offset for MAC space. Require exactly four bytes +for each read/write and stop on a failed transaction. Never perform an RMW +from a fallback `0xffffffff` read. `0x40` in the vendor's **address-space** +notation is not itself the register-request opcode. + +No EEPROM/eFuse write or unknown vendor opcode is needed for this sequence. + +## Other vendor requests and FCE state + +The following are established controls, but none is independently documented +as a hard USB-DMA recovery command. + +| Control | Exact request or write | Meaning / relevance | +|---|---|---| +| Firmware vendor reset | EP0 `(type=0x40, request=0x01, value=0x0001, index=0, length=0)` | [mt76x02u_mcu_fw_reset()][mt76-fw]. Already called by local `fw_reset()` before the failing patch upload. | +| WMT reset | EP0 `(type=0x20, request=0x01, value=0x0012, index=0, length=8)`, payload `6f fc 05 01 07 01 00 04` | [mt76x2u_mcu_reset_wmt()][mt76-wmt]. **Class**, not vendor request. Normally follows patch upload/enable, with 20 ms settling; the reported failure occurs before that point. No proof it works as a pre-upload DMA reset. | +| WRITE_FCE | Request `0x42`; two zero-data OUT requests encode the low/high 16-bit halves in `wValue`, at `wIndex=offset` and `offset+2` | [mt76u_single_wr()][mt76-single]. Writes FCE upload address/length; not a special reset command. Do not confuse this wire format with the four-byte CFG/MAC writes above. | + +The normal MAC-space FCE setup is `0x0800=1` (PSE), `0x09a0=0x400230` +(descriptor base), `0x09a4=1` (count), `0x09c4=0x44` (PDMA configuration), +and `0x0a6c=3` (skip FS). These are the [mt76 upload preamble][mt76-fce], +already present in local `fce_setup()`. Rewriting them is initialization, +not the dedicated CFG `0x0064[22:21]` reset above. + +`MT_TX_CPU_FROM_FCE_CPU_DESC_IDX` is MAC `0x09a8`. The +[upload code][mt76-fw] increments it **after** a successful bulk transfer. +Neither inspected loader establishes that zeroing or advancing it can repair +a NAKing endpoint. A stalled downstream descriptor is plausible, but the +observations do not isolate it; overwriting the index could instead lose the +relationship between submitted data and descriptors. Prefer the sourced +IFDMA/FCE reset over inventing an index value. + +## TXOP_HALT, EP_OUT_VALID and better diagnostics + +The [vendor USB layout][vendor-usb] calls TX_BUSY and EP_OUT_VALID debug +status. EP_OUT_VALID describes endpoint data validity, not whether the USB +configuration exposes an endpoint. Zero during healthy idle is consequently +not surprising; zero with TX_BUSY set does not identify where the engine is +waiting. The same header names bit20 `WL_LPK_EN` for MT76xx while retaining +the older TXOP-countdown comment. The [older rt2x00 header][rt2800-regs] +describes TXOP_HALT as pausing the countdown when the TX buffer fills. +Neither supplies evidence that bit20 aborts or resets an active UDMA +transaction. Do not infer a recovery write from that generic mt76 name. + +The vendor's [mt76x2_polling_txq_empty()][vendor-diagnostics] provides more +specific observations to record before and after each reset stage: + +| Space / offset | Idle/empty condition | +|---|---| +| CFG `0x2240`, `0x2250`, `0x2260`, `0x2270`, `0x2280`, `0x2290` | Bit17 set for each OUT endpoint EP4–EP9 | +| CFG `0x9100` | `(value & 0x07f00000) == 0`, UDMA TX state idle | +| MAC `0x0a30` | `(value & 0x000000ff) == 0`, FCE TX1 empty | +| MAC `0x0a34` | `(value & 0x0000ff00) == 0`, FCE TX2 empty | + +Capture these plus CFG `0x9018`, `0x9014`, `0x9080`, `0x0064` and MAC +`0x0400`, `0x09a8`. An idle status alone is insufficient: the subsequent +bounded bulk transfer and firmware/MCU exchange must also succeed. + +## SET_CONFIGURATION and SET_INTERFACE + +`libusb_set_configuration(handle, current_configuration)` requests a reset +of USB configuration state: alternate settings, endpoint halts and toggles. +Cancel/reap transfers, keep automatic kernel reattachment disabled, and +release claimed interfaces first; reclaim afterward. Discover the actual +configuration value rather than assuming 1. Use the library API so the host +stack tracks the change. The setup tuple is `(0x00, 0x09, configuration, 0, 0)`. +[libusb device-handling documentation][libusb-dev] + +`libusb_set_interface_alt_setting(handle, interface, advertised_alt)` maps to +`(0x01, 0x0b, alt, interface, 0)` with the interface claimed and its transfers +quiesced. Do not invent alt 1. Linux allows a single-alt device to stall this +request and then performs a host-side fallback, so API success alone need not +mean a device-side reset occurred. [Linux usb_set_interface()][linux-interface] + +Neither API promises to reset MediaTek's internal UDMA/FCE state. Moreover, +Linux's [usb_reset_and_verify_device()][linux-reset] already sends +SET_CONFIGURATION while restoring an existing configuration after a successful +port reset. If the reported `libusb_reset_device()` succeeded along that path, +this request has effectively already been exercised. A separate request is +still an order-dependent experiment, not a stronger reset or an established +solution. Confirm its presence in a USB trace if that distinction matters. + +## Related-chip evidence and remaining limit + +Linux [mt7601u firmware setup][mt7601-fw] disables FCE (`MAC 0x0800=0`) +before its vendor reset, then reinitializes FCE and pulses TX_CLR. Its USB DMA +register is **normal `0x0238`**, not MT7612U CFG `0x9018`. This supplies a +related-chip ordering precedent, not evidence that its unrelated magic +initialization values are safe or necessary on MT7662. + +Linux rt2x00 defines TX_CLEAR bit19 at **normal `0x02a0`**. Neither +[rt2800usb.c][rt2800-usb] nor [rt2800lib.c][rt2800-lib] in v6.12 uses that +field. It adds no stronger hard-wedge recovery sequence than the dedicated +MT76x2U resets identified above. + +The next discriminating experiment is the complete vendor sequence, with a +known hard-wedged starting state, no open-time USB reset hidden in the test, +and no power or enumeration transition. Record stage-by-stage registers and +control return lengths, then verify firmware upload, MCU reply, and ordinary +TX/RX against a live witness. Repeat on independently reproduced hard wedges +and include an unchanged recovery attempt as the failing control. If the full +sequence works, isolate its stages in later trials; do not attribute success +to bit6 alone when multiple resets were applied. + +Until that measurement exists, **physical replug is the only reported working +hard-wedge recovery; it is not a proven silicon requirement**. The sources +show separate MAC/PBF, UDMA and IFDMA/FCE reset controls, which explains why +the previously attempted reset names do not cover every block. They do not +publish enough MT7662 USB-core internals to prove an unrecoverable latch, +specific deadlock, or mandatory power-on-reset condition. No driver change, +hardware test, or recovery claim is made by this report. + +[vendor-reset]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/common/rtmp_init.c#L4214-L4392 +[vendor-version]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/include/mt76x2_version.h#L19 +[vendor-readme]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/README.md +[vendor-caller]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/common/rtmp_init_inf.c#L37-L57 +[vendor-access]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/common/rtusb_io.c#L377-L425 +[vendor-usb]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/include/mac_ral/mac_usb.h#L48-L117 +[vendor-diagnostics]: https://github.com/caruofc/MT7612U-Driver/blob/749e6fd1a559e454b17ae91959fe07d6ef58e6af/chips/mt76x2.c#L1225-L1273 +[mt76-access]: https://github.com/openwrt/mt76/blob/be5ce7910521492d4a2e4ce7ee3843680a46c047/usb.c#L104-L200 +[mt76-single]: https://github.com/openwrt/mt76/blob/be5ce7910521492d4a2e4ce7ee3843680a46c047/usb.c#L226-L255 +[mt76-fw]: https://github.com/openwrt/mt76/blob/be5ce7910521492d4a2e4ce7ee3843680a46c047/mt76x02_usb_mcu.c#L207-L251 +[mt76-wmt]: https://github.com/openwrt/mt76/blob/be5ce7910521492d4a2e4ce7ee3843680a46c047/mt76x2/usb_mcu.c#L43-L58 +[mt76-fce]: https://github.com/openwrt/mt76/blob/be5ce7910521492d4a2e4ce7ee3843680a46c047/mt76x2/usb_mcu.c#L94-L130 +[mt7601-fw]: https://github.com/torvalds/linux/blob/v6.12/drivers/net/wireless/mediatek/mt7601u/mcu.c#L446-L478 +[rt2800-regs]: https://github.com/torvalds/linux/blob/v6.12/drivers/net/wireless/ralink/rt2x00/rt2800.h#L527-L550 +[rt2800-usb]: https://github.com/torvalds/linux/blob/v6.12/drivers/net/wireless/ralink/rt2x00/rt2800usb.c +[rt2800-lib]: https://github.com/torvalds/linux/blob/v6.12/drivers/net/wireless/ralink/rt2x00/rt2800lib.c +[libusb-dev]: https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html +[linux-interface]: https://github.com/torvalds/linux/blob/v6.12/drivers/usb/core/message.c#L1512-L1665 +[linux-reset]: https://github.com/torvalds/linux/blob/v6.12/drivers/usb/core/hub.c#L6159-L6197 diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 6361488c..769f9e13 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -250,6 +250,11 @@ a caller that will not drain it, plus an endpoint flush; 20 consecutive init+TX cycles clean afterwards against a death after ~5 before. **Two things changed at once**, so that run does not attribute the wedge to one of them. +For the separate **EP0-responsive, bulk-OUT-stalled** failure, see +[USB TX wedge reset evidence](mt7612u-usb-wedge.md): the vendor-derived +MT76x2U tree has dedicated UDMA and IFDMA/FCE resets beyond `TX_CLR`. +Their recovery of the hard `TX_BUSY` state is not yet hardware-verified. + ## Offline tests `make -C src/mt7612u check` runs three binaries. No hardware, no privileges. From 867b858f8dffe27a3d94824e8c1a2f1a37a917ed Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 19:45:34 +0200 Subject: [PATCH 04/14] mt7612u: implement the vendor RTMPSwReset() sequence as a testable gate 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) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/tools/bringup.c | 93 +++++++++++++++++++++++++++++++++++++ src/mt7612u/usb.c | 8 ++++ 2 files changed, 101 insertions(+) diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 7bb7ed53..1dbcbf41 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -136,6 +136,97 @@ static int gate_regs(void) } /* Gate B: MCU transport + ROM patch + firmware, then a live MCU round-trip. */ +/* + * The vendor RTMPSwReset() sequence, from docs/mt7612u-usb-wedge.md. + * MT76x2U has SEPARATE UDMA TX/RX and IFDMA/FCE resets that neither mt76 nor + * this port ever touched - CFG 0x9014 and CFG 0x0064[22:21]. Every earlier + * failed attempt only ever hit CFG 0x9018 and MAC 0x0400, which is why they + * could not cover every block. + * + * `swreset 0` observes and repairs nothing: the failing control. + * `swreset 1` runs the sequence. Either way the verdict is a real firmware + * load afterwards, not an idle status register. + */ +#define CFG_UDMA_RESET 0x9014 +#define CFG_UDMA_CFG 0x9018 +#define CFG_EP_DROP 0x9080 +#define CFG_IFDMA_RESET 0x0064 +#define CFG_UDMA_TX_STAT 0x9100 + +static void swreset_dump(const char *when) +{ + static const uint16_t epq[] = { 0x2240, 0x2250, 0x2260, 0x2270, 0x2280, 0x2290 }; + uint32_t v; + int idle = 1; + + printf(" %-7s U3DMA=0x%08x UDMA_RST=0x%08x EP_DROP=0x%08x IFDMA=0x%08x\n", + when, mt_rr(&dev, CFG_ADDR(CFG_UDMA_CFG)), + mt_rr(&dev, CFG_ADDR(CFG_UDMA_RESET)), + mt_rr(&dev, CFG_ADDR(CFG_EP_DROP)), + mt_rr(&dev, CFG_ADDR(CFG_IFDMA_RESET))); + v = mt_rr(&dev, CFG_ADDR(CFG_UDMA_TX_STAT)); + printf(" UDMA_TX_STATE=0x%08x (idle=%d) PBF=0x%08x DESC_IDX=0x%08x\n", + v, (v & 0x07f00000u) == 0, mt_rr(&dev, MT_PBF_SYS_CTRL), + mt_rr(&dev, 0x09a8)); + printf(" FCE_TX1=0x%08x FCE_TX2=0x%08x EP4-9 empty:", + mt_rr(&dev, 0x0a30), mt_rr(&dev, 0x0a34)); + for (unsigned i = 0; i < sizeof epq / sizeof epq[0]; i++) { + int e = !!(mt_rr(&dev, CFG_ADDR(epq[i])) & BIT(17)); + + printf(" %d", e); + if (!e) idle = 0; + } + printf("%s\n", idle ? " (all empty)" : " (NOT all empty)"); +} + +static void swreset_pulse(uint32_t addr, uint32_t mask) +{ + mt_set(&dev, addr, mask); + mt_usleep(15000); + mt_clear(&dev, addr, mask); + mt_usleep(15000); +} + +static int gate_swreset(int apply) +{ + printf("swreset: %s\n\n", apply ? "running the vendor sequence" + : "OBSERVE ONLY (failing control)"); + swreset_dump("before"); + + if (apply) { + /* The helper's surrounding contract: stop the MAC and let TX + * drain before touching the DMA. Bounded - this is the fault + * 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; + mt_usleep(2000); + } + printf(" MAC stopped, TX idle=%d\n", + !(mt_rr(&dev, MT_MAC_STATUS) & BIT(0))); + + mt_clear(&dev, CFG_ADDR(CFG_UDMA_CFG), 0x00c00000); /* 1 */ + swreset_pulse(CFG_ADDR(CFG_EP_DROP), 0x03f00000); /* 2 */ + swreset_pulse(CFG_ADDR(CFG_UDMA_RESET), 0x00000040); /* 3 UDMA TX */ + swreset_pulse(CFG_ADDR(CFG_IFDMA_RESET),0x00600000); /* 4 IFDMA/FCE */ + swreset_pulse(MT_PBF_SYS_CTRL, 0x0000000c); /* 5 MAC/PBF */ + swreset_pulse(CFG_ADDR(CFG_UDMA_RESET), 0x00000020); /* 6 UDMA RX */ + mt_set(&dev, CFG_ADDR(CFG_UDMA_CFG), 0x00c00000); /* 7 */ + mt_usleep(15000); + printf(" sequence applied\n"); + swreset_dump("after"); + } + + /* The only verdict that counts: does the chip take firmware again? */ + if (mt_eeprom_init(&dev)) { printf("SWRESET: eeprom failed\n"); return 1; } + if (mt_fw_init(&dev, NULL)) { + printf("SWRESET: FAIL - firmware still will not load\n"); + return 1; + } + printf("SWRESET: PASS - firmware loaded\n"); + return 0; +} + static int gate_fw(const char *fw_dir) { uint32_t clk, com0; @@ -2319,6 +2410,8 @@ int main(int argc, char **argv) argc > 3 ? argv[3] : NULL); } else if (!strcmp(cmd, "init")) { rc = gate_init(argc > 2 ? argv[2] : NULL); + } else if (!strcmp(cmd, "swreset")) { + rc = gate_swreset(argc > 2 ? atoi(argv[2]) : 1); } else if (!strcmp(cmd, "fw")) { rc = gate_fw(argc > 2 ? argv[2] : NULL); } else { diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index 8ac84f32..eb4bc826 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -559,6 +559,14 @@ int mt_open(struct mt7612u_dev *d, const char **err) * MT_USB_U3DMA_CFG 0x80c00020 where a healthy one reads 0x00c00020. * One fixed-length pulse does not shift a busy engine, so drop * TX_BULK_EN, then pulse TX_CLR until the busy bit falls. */ + /* A wedge experiment must not have its recovery hidden inside open(). + * With this set, open() observes and reports but repairs nothing. */ + if (getenv("MT7612U_NO_AUTORECOVER")) { + LOG("auto-recovery disabled: U3DMA_CFG=0x%08x", + mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG))); + return 0; + } + if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) { WARN("USB TX DMA busy on open - a previous run died mid transfer"); mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); From 86b8fbdae32e8f33bdfbabb1005b594c55846a50 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:01:31 +0200 Subject: [PATCH 05/14] mt7612u: correct two comments that blamed libusb for a hang it did not 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/internal.h | 14 ++++++++++---- src/mt7612u/phy.c | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index ed8a7321..21f19ba3 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -226,10 +226,16 @@ uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off); void mt_power_cycle(struct mt7612u_dev *d); /* * One round of mt76's 1 Hz cal_work (mt76x2/usb_phy.c:42). A consumer that - * receives MUST call this about once a second: without it the AGC never leaves - * its start-up gain and the receiver goes deaf against a strong nearby - * transmitter. Deliberately not a thread - a second thread issuing - * synchronous libusb transfers alongside the RX ring's event thread hangs. + * receives MUST call this about once a second: without it a receiver takes 3 + * frames in 10 s from a strong nearby transmitter; with it ~4850/s, 9/9 runs. + * The periodic MCU calibration is what does it - the gain tracking alone does + * not recover the receiver. + * + * Caller-driven rather than a thread so the MCU command channel - one 4-bit + * sequence number, one response endpoint - has exactly one user. A threaded + * version once hung; that was later traced to io_lock being non-recursive on + * a caller-allocated device, not to libusb, whose sync-API-beside-an-event- + * thread shape is the supported one. It has not been re-tested as a thread. */ void mt_phy_tick(struct mt7612u_dev *d); int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir); diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 42436d82..6ad2b479 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -570,11 +570,10 @@ static void phy_update_channel_gain(struct mt7612u_dev *d) * of mt76's 1 Hz work and it tracks signal strength, not because it fixes this. * Reading the read-and-clear MT_RX_STAT_* counters alone does nothing either. * - * Runs on the CALLER's thread on purpose. An earlier revision used its own - * thread and hit two failures: MCU commands from two threads share one 4-bit - * sequence number and one response endpoint ("mcu resp mismatch ... want 1"), - * and a second thread issuing synchronous libusb transfers alongside the RX - * ring's event thread hangs outright. + * Runs on the CALLER's thread on purpose: MCU commands from two threads share + * one 4-bit sequence number and one response endpoint, and the failure reads + * as "mcu resp mismatch ... (want 1)". (A threaded revision also hung once; + * that was traced afterwards to a non-recursive io_lock, not to libusb.) */ void mt_phy_tick(struct mt7612u_dev *d) { From 74bb206409e27bc15b13cb09a30c4f3b6a84d179 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:18:10 +0200 Subject: [PATCH 06/14] mt7612u: review fixes - make the tick public and default, honest open() 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 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 23 ++++++++++ src/mt7612u/include/mt7612u/mt7612u.h | 15 +++++++ src/mt7612u/internal.h | 15 ------- src/mt7612u/phy.c | 6 +-- src/mt7612u/tools/bringup.c | 62 ++++++++++++++------------- src/mt7612u/usb.c | 56 ++++++++++++------------ 6 files changed, 102 insertions(+), 75 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 769f9e13..b33f1d4e 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -157,6 +157,29 @@ len=102 HT mcs=15 nss=2 bw=20 sgi=1 ldpc=1 stbc=0 rssi=[-71,-63] `nss` is derived as `1 + (15 >> 3)`; SGI and LDPC come from the same 16-bit rate word the TX path writes, so one codec serves both directions. +### A receiver needs the 1 Hz tick + +mt76 runs `cal_work` every second (`MT_CALIBRATE_INTERVAL == HZ`, +`mt76x2/usb_phy.c:42`): a self-guarded channel calibration, TSSI compensation +(an MCU command each second) and `mt76x2_phy_update_channel_gain()`. This +port ran none of it, and the cost was measured rather than assumed: against a +peer 20 cm away airing 1400-byte HT-MCS7 frames at 3037 fps, a receiver with +no tick took **3 frames in 10 s**; calling `mt7612u_phy_tick()` once a second +it takes **~4850/s** (nine runs). Not the adapter — symmetric in both +directions, and both hear 400–1000 fps of ambient on ch1/36/40/44/48 — not +the ring (its own counter agreed at 3), and not the air: an RTL8812AU witness +counted 103805 frames from the same transmitter. + +Bisected with one verified peer per arm: reading the read-and-clear +`MT_RX_STAT_*` counters alone does nothing (3 frames); a periodic MCU +calibration alone restores it (43902); the ported gain tracking alone does not +(4 frames). The tick therefore issues one `MCU_CAL_TEMP_SENSOR` per call — the +cheapest command that keeps the MCU in the loop, standing in for the TSSI +compensation this port lacks — and runs the gain tracking for fidelity. It is +caller-driven: MCU commands share one 4-bit sequence number and one response +endpoint, so they need exactly one user. Every receiving gate in `bringup` +ticks; `arx 1` is the negative control. + ### This part does not deliver the FCS Measured, because it constrains integration rather than being a detail. diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index 871aa100..506738e4 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -286,6 +286,21 @@ int mt7612u_link_stats_start(struct mt7612u_dev *dev); * call to this function or to _start(). */ int mt7612u_link_stats(struct mt7612u_dev *dev, struct mt7612u_link_stats *out); +/* + * One round of mt76's 1 Hz cal_work (mt76x2/usb_phy.c:42, MT_CALIBRATE_INTERVAL + * == HZ). A receiving consumer MUST call this about once a second. Measured: + * against a peer 20 cm away airing 3037 fps, a receiver with no tick takes 3 + * frames in 10 s; with it ~4850/s (nine runs). It issues one MCU calibration + * - the part that matters, bisected - and runs the channel-gain tracking. + * + * Caller-driven on purpose: MCU commands share one 4-bit sequence number and + * one response endpoint, so they need a single user. It reads and clears the + * false-CCA field of MT_RX_STAT_1, so a caller that also samples + * mt7612u_link_stats() will see that column reduced. Returns 0, or -1 before + * a channel is set. + */ +int mt7612u_phy_tick(struct mt7612u_dev *dev); + /* TSF, the hardware microsecond clock. Two register reads. */ uint64_t mt7612u_read_tsf(struct mt7612u_dev *dev); void mt7612u_write_tsf(struct mt7612u_dev *dev, uint64_t tsf); diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 21f19ba3..efb214e4 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -54,7 +54,6 @@ struct mt7612u_cal { uint8_t agc_gain_adjust; int8_t low_gain; int8_t avg_rssi_all; - uint16_t false_cca; }; #define MT_RX_RING 16 @@ -224,20 +223,6 @@ uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off); /* --- init.c --- */ void mt_power_cycle(struct mt7612u_dev *d); -/* - * One round of mt76's 1 Hz cal_work (mt76x2/usb_phy.c:42). A consumer that - * receives MUST call this about once a second: without it a receiver takes 3 - * frames in 10 s from a strong nearby transmitter; with it ~4850/s, 9/9 runs. - * The periodic MCU calibration is what does it - the gain tracking alone does - * not recover the receiver. - * - * Caller-driven rather than a thread so the MCU command channel - one 4-bit - * sequence number, one response endpoint - has exactly one user. A threaded - * version once hung; that was later traced to io_lock being non-recursive on - * a caller-allocated device, not to libusb, whose sync-API-beside-an-event- - * thread shape is the supported one. It has not been re-tested as a thread. - */ -void mt_phy_tick(struct mt7612u_dev *d); int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir); /* * enable_rx is not a bool: the receiver must never come up with nothing diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 6ad2b479..4c1905ae 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -484,7 +484,6 @@ static int phy_adjust_vga_gain(struct mt7612u_dev *d) mt_rr(d, MT_RX_STAT_1)); int changed = 0; - d->cal.false_cca = (uint16_t)false_cca; if (false_cca > 800 && d->cal.agc_gain_adjust < limit) { d->cal.agc_gain_adjust += 2; changed = 1; @@ -575,9 +574,9 @@ static void phy_update_channel_gain(struct mt7612u_dev *d) * as "mcu resp mismatch ... (want 1)". (A threaded revision also hung once; * that was traced afterwards to a non-recursive io_lock, not to libusb.) */ -void mt_phy_tick(struct mt7612u_dev *d) +int mt7612u_phy_tick(struct mt7612u_dev *d) { - if (!d || !d->chan) return; + if (!d || !d->chan) return -1; pthread_mutex_lock(&d->io_lock); /* Self-guarded after the first run, exactly like mt76's. */ channel_calibrate(d, d->chan > 14); @@ -587,6 +586,7 @@ void mt_phy_tick(struct mt7612u_dev *d) mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); phy_update_channel_gain(d); pthread_mutex_unlock(&d->io_lock); + return 0; } int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 1dbcbf41..722fd181 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -58,6 +58,22 @@ static int wait_ms(double ms) return !g_stop; } +/* wait_ms() plus mt7612u_phy_tick() once a second - what every receiving + * loop must do, see the public header. Same return contract as wait_ms(). */ +static int wait_ticking(double ms) +{ + double t0 = now_ms(); + + while (now_ms() - t0 < ms) { + double slice = ms - (now_ms() - t0); + + if (slice > 1000.0) slice = 1000.0; + if (!wait_ms(slice)) return 0; + mt7612u_phy_tick(&dev); + } + return !g_stop; +} + static int gate_regs(void) { @@ -167,7 +183,8 @@ static void swreset_dump(const char *when) v = mt_rr(&dev, CFG_ADDR(CFG_UDMA_TX_STAT)); printf(" UDMA_TX_STATE=0x%08x (idle=%d) PBF=0x%08x DESC_IDX=0x%08x\n", v, (v & 0x07f00000u) == 0, mt_rr(&dev, MT_PBF_SYS_CTRL), - mt_rr(&dev, 0x09a8)); + mt_rr(&dev, MT_TX_CPU_FROM_FCE_CPU_DESC_IDX)); + /* FCE TX1/TX2 fill: MAC 0x0a30/0x0a34, per docs/mt7612u-usb-wedge.md. */ printf(" FCE_TX1=0x%08x FCE_TX2=0x%08x EP4-9 empty:", mt_rr(&dev, 0x0a30), mt_rr(&dev, 0x0a34)); for (unsigned i = 0; i < sizeof epq / sizeof epq[0]; i++) { @@ -484,7 +501,13 @@ static int gate_rx(uint8_t chan, int want) printf(" MT_RX_STAT_1 = 0x%08x (CCA errors seen = RF is live)\n", mt_rr(&dev, MT_RX_STAT_1)); + double last_tick = now_ms(); + while (got < want && empty < 200) { + if (now_ms() - last_tick >= 1000.0) { + mt7612u_phy_tick(&dev); + last_tick = now_ms(); + } struct mt7612u_rx_info info; const uint8_t *f = NULL; int len = mt_rx_one(&dev, buf, sizeof buf, &f, &info, 50); @@ -865,7 +888,7 @@ static void arx_cb(void *user, const void *frame, size_t len, } /* Async RX ring: the callback path StartRxLoop needs. */ -static int gate_arx(uint8_t chan, int secs, int poke) +static int gate_arx(uint8_t chan, int secs, int notick) { static /* Indexed with (phy & 7): MT_RATE_PHY is three bits, so 5-7 are * representable and named nothing. Five entries read past the end. */ @@ -883,29 +906,10 @@ static int gate_arx(uint8_t chan, int secs, int poke) if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); - /* Bisect: linkstat receives 5062 fps under the same peer where this gate - * receives 3, and the only thing it does differently is poll once a - * second - MT_RX_STAT_* reads (read-and-clear) plus an MCU temperature - * calibration. poke selects which, so the mechanism is identified - * rather than guessed: 0 = neither, 1 = stat reads, 2 = MCU calibrate, - * 4 = the ported 1 Hz PHY tick. */ - if (!poke) { - wait_ms(secs * 1000.0); - } else { - for (int i = 0; i < secs; i++) { - if (!wait_ms(1000.0)) break; - if (poke & 1) { - mt_rr(&dev, MT_CH_BUSY); mt_rr(&dev, MT_CH_IDLE); - mt_rr(&dev, MT_RX_STAT_0); - mt_rr(&dev, MT_RX_STAT_1); - mt_rr(&dev, MT_RX_STAT_2); - } - if (poke & 2) - mt_mcu_calibrate(&dev, MCU_CAL_TEMP_SENSOR, 0); - if (poke & 4) - mt_phy_tick(&dev); /* the ported 1 Hz gain work */ - } - } + /* notick is the negative control: without the 1 Hz PHY tick this gate + * reads 3 frames in 10 s from a strong nearby peer (reproduced eight + * times); with it ~4850/s. See mt7612u_phy_tick(). */ + if (notick) wait_ms(secs * 1000.0); else wait_ticking(secs * 1000.0); { struct mt_async_stats st; /* Actual elapsed, not the requested duration: an interrupt now @@ -1006,7 +1010,7 @@ static int gate_duplex(uint8_t chan, int secs) printf(" TX stopped; listening %.1f s for the receiver to recover\n", RECOVER_S); - if (!wait_ms(RECOVER_S * 1000.0)) { + if (!wait_ticking(RECOVER_S * 1000.0)) { mt7612u_rx_stop(&dev); mt_mac_stop(&dev); return 1; @@ -1419,7 +1423,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) /* wait_ms, not mt_usleep: it honours SIGINT, where the old cast-to- * unsigned sleep both ignored the signal and turned a negative argument * into roughly 49 days with the receiver left running. */ - if (!wait_ms(secs * 1000.0)) { + if (!wait_ticking(secs * 1000.0)) { printf("GATE ack: interrupted\n"); mt7612u_rx_stop(&dev); mt_mac_stop(&dev); @@ -1506,7 +1510,7 @@ static int gate_rxbytes(uint8_t chan, int secs) mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); - wait_ms(secs * 1000.0); + wait_ticking(secs * 1000.0); mt7612u_link_stats(&dev, &st); mt7612u_rx_stop(&dev); mt_mac_stop(&dev); @@ -1744,7 +1748,7 @@ static int gate_linkrx(uint8_t chan, int secs) mt7612u_set_monitor_rx(&dev, 0); printf("RX on ch%u for %d s, filtering our own magic\n", chan, secs); - wait_ms(secs * 1000.0); + wait_ticking(secs * 1000.0); mt7612u_rx_stop(&dev); mt_mac_stop(&dev); diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index eb4bc826..dacc4066 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -75,8 +75,9 @@ int mt_vendor_req(struct mt7612u_dev *d, uint8_t req, uint8_t type, { int rc = LIBUSB_ERROR_OTHER; - /* The PHY tick runs on its own thread and does register I/O; without - * this two control transfers can interleave on one endpoint. */ + /* One control transfer at a time. Nothing contends in a single-threaded + * consumer; one that sends from a second thread would otherwise + * interleave two transfers on EP0. */ pthread_mutex_lock(&d->io_lock); for (int i = 0; i < VEND_RETRIES; i++) { @@ -534,31 +535,6 @@ int mt_open(struct mt7612u_dev *d, const char **err) goto fail; } - /* Clear a USB TX DMA left stalled by a run that died mid transfer - a - * SIGKILL during a flood is enough, and it is reproducible on demand. - * The port reset above does NOT reach this: afterwards register reads - * and writes still round-trip, the MAC and RF are fine and the kernel - * mt76x2u driver binds nothing, but every bulk OUT NAKs forever and the - * next firmware upload times out mid-chunk. It was believed to need a - * physical replug. - * - * Isolated one step at a time against a freshly wedged adapter: - * 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) each left it wedged. This bit alone clears it. - * mt76 declares TX_CLR and writes it nowhere. - * - * The receive direction needs its own clean-up: mt_rx_flush() runs from - * mt_mac_stop(), which a killed process never reaches, so the pipe can - * still be full. Silence the receiver BEFORE draining or it refills as - * fast as it is read. */ - mt_wr(d, MT_MAC_SYS_CTRL, 0); - mt_rx_flush(d); - - /* The signature is TX_BUSY stuck set: a wedged adapter reads - * MT_USB_U3DMA_CFG 0x80c00020 where a healthy one reads 0x00c00020. - * One fixed-length pulse does not shift a busy engine, so drop - * TX_BULK_EN, then pulse TX_CLR until the busy bit falls. */ /* A wedge experiment must not have its recovery hidden inside open(). * With this set, open() observes and reports but repairs nothing. */ if (getenv("MT7612U_NO_AUTORECOVER")) { @@ -567,6 +543,29 @@ int mt_open(struct mt7612u_dev *d, const char **err) return 0; } + /* Recover from a previous run that died mid transfer. The port reset + * above does not reach any of this: afterwards register reads and writes + * still round-trip and the MAC and RF are fine, but every bulk OUT NAKs + * and the next firmware upload times out at its first chunk - the kernel + * mt76x2u driver cannot bind such a device either. Two tiers, told apart + * by MT_USB_U3DMA_CFG: + * + * 0x00c00020, TX_BUSY clear: isolated one step at a time - clear_halt + * on all four endpoints, MAC + USB DMA stop, WLAN_EN/WLAN_CLK_EN down + * and the PBF block reset each left it wedged; pulsing TX_CLR alone + * clears it (3/3). mt76 declares TX_CLR and writes it nowhere. + * + * 0x80c00020, TX_BUSY stuck: the pulse loop below has NOT been seen to + * clear it, nor has anything else tried so far; the vendor-derived + * UDMA/IFDMA reset sequence in docs/mt7612u-usb-wedge.md (bringup + * swreset) is the untested candidate. The WARN is the honest verdict. + * + * The receive direction gets its own clean-up: mt_rx_flush() runs from + * mt_mac_stop(), which a killed process never reaches. Silence the + * receiver BEFORE draining or it refills as fast as it is read. */ + 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) { WARN("USB TX DMA busy on open - a previous run died mid transfer"); mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); @@ -580,7 +579,8 @@ int mt_open(struct mt7612u_dev *d, const char **err) } mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) - WARN("USB TX DMA still busy - this open will likely fail"); + WARN("USB TX DMA still busy - this open will likely fail; " + "see docs/mt7612u-usb-wedge.md"); else LOG("USB TX DMA recovered"); } else { From e52f660a4db7ed3b2652a9a5749b51c55a76abe7 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:43:19 +0200 Subject: [PATCH 07/14] mt7612u: retry the firmware upload once after a settle 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/fw.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/mt7612u/fw.c b/src/mt7612u/fw.c index 3e58bc50..81c32b39 100644 --- a/src/mt7612u/fw.c +++ b/src/mt7612u/fw.c @@ -231,7 +231,22 @@ static int load_firmware(struct mt7612u_dev *d, const char *dir) int mt_fw_init(struct mt7612u_dev *d, const char *fw_dir) { if (!fw_dir) fw_dir = "firmware"; - if (load_rom_patch(d, fw_dir)) - return -1; - return load_firmware(d, fw_dir); + + /* Two attempts, not one. Measured (12/12 cycles, both adapters): after + * a process dies mid transfer the first firmware upload times out at + * its first chunk, and the same upload succeeds by itself 3-8 s later + * with nothing repaired. That is a settling window, not a wedge, and + * 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++) { + if (attempt) { + WARN("firmware upload failed - a previous run may have died " + "mid transfer; retrying after a 3 s settle"); + mt_usleep(3000000); + } + if (!load_rom_patch(d, fw_dir) && !load_firmware(d, fw_dir)) + return 0; + } + return -1; } From c8aa51705af9ae57b3caed9504ba15b1213910ca Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:49:21 +0200 Subject: [PATCH 08/14] mt7612u: drain stale MCU replies before every command 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/mcu.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c index e9cfe82a..5ee0696e 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -69,6 +69,26 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, * failure reads as "mcu resp mismatch ... (want 1)". */ pthread_mutex_lock(&d->io_lock); + /* Drain replies nobody collected before sending. A reply that lands + * after mcu_wait_resp() gave up stays queued on EP 5, so the next + * command reads its predecessor's reply, mismatches, times out (~1.5 s) + * and leaves one more stale reply behind - the channel then never + * resyncs. Seen as "mcu resp mismatch: evt=0 seq=10..14 (want 15)" + * cascading through every 1 Hz tick. Bounded: a queue deeper than 16 + * is a fault worth reporting, not one worth looping on. */ + { + uint8_t stale[MCU_RESP_URB_SIZE]; + int n = 0, got; + + while (n < 16 && + !mt_bulk(d, MT_EP_IN_CMD_RESP, stale, sizeof stale, &got, 5) && + got >= 4) + n++; + if (n) + WARN("drained %d stale MCU repl%s before cmd %d", n, + n == 1 ? "y" : "ies", cmd); + } + if (wait_resp) { seq = ++d->mcu_seq & 0xf; if (!seq) From 1864fbd971437f7ad7b892bb1df0f73b86e183cd Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 20:53:19 +0200 Subject: [PATCH 09/14] mt7612u: wait longer for the MCU, and retry a calibration burst it did 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/mcu.c | 7 ++++++- src/mt7612u/phy.c | 23 +++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c index 5ee0696e..c27b9821 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -28,7 +28,12 @@ static int mcu_wait_resp(struct mt7612u_dev *d, uint8_t seq) uint8_t buf[MCU_RESP_URB_SIZE]; int len, rc; - for (int i = 0; i < 5; i++) { + /* Ten reads of 300 ms, not five. Measured on 2-1 with a peer 20 cm + * away saturating the channel: the MCU answered every command, but + * seconds late - seven calibrations in a row timed out at 1.5 s and + * their nine replies then arrived together. mt76's own budget is the + * same five reads; it is not enough for this part under RF load. */ + for (int i = 0; i < 10; i++) { rc = mt_bulk(d, MT_EP_IN_CMD_RESP, buf, sizeof buf, &len, 300); if (rc == LIBUSB_ERROR_TIMEOUT) continue; diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 4c1905ae..1be5ae3d 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -154,14 +154,16 @@ static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) if (d->cal.channel_cal_done) return; + int failed = 0; + if (is_5ghz) - mt_mcu_calibrate(d, MCU_CAL_LC, 0); + failed |= mt_mcu_calibrate(d, MCU_CAL_LC, 0); - mt_mcu_calibrate(d, MCU_CAL_TX_LOFT, (uint32_t)is_5ghz); - mt_mcu_calibrate(d, MCU_CAL_TXIQ, (uint32_t)is_5ghz); - mt_mcu_calibrate(d, MCU_CAL_RXIQC_FI, (uint32_t)is_5ghz); - mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); - mt_mcu_calibrate(d, MCU_CAL_TX_SHAPING, 0); + failed |= mt_mcu_calibrate(d, MCU_CAL_TX_LOFT, (uint32_t)is_5ghz); + failed |= mt_mcu_calibrate(d, MCU_CAL_TXIQ, (uint32_t)is_5ghz); + failed |= mt_mcu_calibrate(d, MCU_CAL_RXIQC_FI, (uint32_t)is_5ghz); + failed |= mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); + failed |= mt_mcu_calibrate(d, MCU_CAL_TX_SHAPING, 0); apply_gain_adj(d); @@ -173,6 +175,15 @@ static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) mt_wr(d, MT_BBP(AGC, 2), 0x00007070); mt_set(d, MT_TXOP_HLDR_ET, MT_TXOP_HLDR_TX40M_BLK_EN); + /* A burst that timed out leaves the channel half set up - measured as + * a receiver running at 153 fps where 4850 is normal. mt76 marks the + * channel calibrated regardless; this port marks it only when every + * command was answered, so the 1 Hz tick retries the burst until it + * is. The one-time MAC configuration above is applied either way. */ + if (failed) { + WARN("channel calibration incomplete - the 1 Hz tick will retry"); + return; + } d->cal.channel_cal_done = 1; } From 2bea9804dfd8dcce0dca3c2e5da3610cbaa82462 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 21:15:30 +0200 Subject: [PATCH 10/14] mt7612u: mark the channel calibrated once, as mt76 does - revert the 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/phy.c | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 1be5ae3d..2d1b84a5 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -154,16 +154,14 @@ static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) if (d->cal.channel_cal_done) return; - int failed = 0; - if (is_5ghz) - failed |= mt_mcu_calibrate(d, MCU_CAL_LC, 0); + mt_mcu_calibrate(d, MCU_CAL_LC, 0); - failed |= mt_mcu_calibrate(d, MCU_CAL_TX_LOFT, (uint32_t)is_5ghz); - failed |= mt_mcu_calibrate(d, MCU_CAL_TXIQ, (uint32_t)is_5ghz); - failed |= mt_mcu_calibrate(d, MCU_CAL_RXIQC_FI, (uint32_t)is_5ghz); - failed |= mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); - failed |= mt_mcu_calibrate(d, MCU_CAL_TX_SHAPING, 0); + mt_mcu_calibrate(d, MCU_CAL_TX_LOFT, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_TXIQ, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_RXIQC_FI, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); + mt_mcu_calibrate(d, MCU_CAL_TX_SHAPING, 0); apply_gain_adj(d); @@ -175,15 +173,12 @@ static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) mt_wr(d, MT_BBP(AGC, 2), 0x00007070); mt_set(d, MT_TXOP_HLDR_ET, MT_TXOP_HLDR_TX40M_BLK_EN); - /* A burst that timed out leaves the channel half set up - measured as - * a receiver running at 153 fps where 4850 is normal. mt76 marks the - * channel calibrated regardless; this port marks it only when every - * command was answered, so the 1 Hz tick retries the burst until it - * is. The one-time MAC configuration above is applied either way. */ - if (failed) { - WARN("channel calibration incomplete - the 1 Hz tick will retry"); - return; - } + /* Marked done once, as mt76 does. An individual mt_mcu_calibrate() can + * time out when the MCU answers slowly under RF load, and its reply then + * arrives late (mt_mcu_send drains those) - but the calibration still + * takes: the receiver runs at ~4850 fps either way, measured across + * eight runs that each logged those timeouts. Withholding this flag and + * re-running the whole burst every second made it WORSE (153 fps). */ d->cal.channel_cal_done = 1; } From 0c97f767e63e2849ab35c1ec5a0e7ea9d8d0cdc5 Mon Sep 17 00:00:00 2001 From: snokvist Date: Tue, 8 Sep 2026 17:51:49 +0200 Subject: [PATCH 11/14] docs(mt7612u): update the lock and RX-undrained recovery story 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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index b33f1d4e..539d1cb8 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -266,17 +266,41 @@ three defined values (20/40/80) and mt76 exposes no narrowband path for this part. **Enabling MAC RX without draining the bulk-IN endpoint wedges the chip below -USB level.** Every vendor request times out afterwards, and neither -`libusb_reset_device()`, the `authorized` toggle, nor the kernel driver -recovers it — only a physical replug does. Fixed here by never enabling RX for -a caller that will not drain it, plus an endpoint flush; 20 consecutive -init+TX cycles clean afterwards against a death after ~5 before. **Two things -changed at once**, so that run does not attribute the wedge to one of them. - -For the separate **EP0-responsive, bulk-OUT-stalled** failure, see -[USB TX wedge reset evidence](mt7612u-usb-wedge.md): the vendor-derived -MT76x2U tree has dedicated UDMA and IFDMA/FCE resets beyond `TX_CLR`. -Their recovery of the hard `TX_BUSY` state is not yet hardware-verified. +USB level.** Prevented at the source: `mt_mac_start()` takes an `MT_RX_DRAIN_*` +argument and refuses to enable the receiver unless a ring is draining EP 4 (the +public `mt7612u_start()` always derived this; the internal entry point now does +too). + +**A run that dies mid-transfer leaves the adapter unable to load firmware, and +`mt_open()` now recovers it.** Register reads *and* writes still round-trip and +the MAC and RF are fine, but every bulk OUT NAKs — `libusb_reset_device()` +(already called on every open) does not reach it, and the kernel `mt76x2u` +driver cannot bind it either. Two tiers, told apart by `MT_USB_U3DMA_CFG`: + +- **Soft (`TX_BUSY` clear).** Isolated one step at a time against a freshly + wedged adapter: `clear_halt` on all four endpoints, 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 and never writes — + clears it alone. `mt_open()` pulses it and drains the RX pipe (`mt_rx_flush` + runs from `mt_mac_stop`, which a killed process never reaches), so every + entry point self-heals. A post-kill firmware upload can also fail for 3–8 s + and then succeed by itself; `mt_fw_init()` absorbs that with one retry after + a settle. On a healthy idle adapter the recovery is a no-op (0/40 opens). + +- **Hard (`TX_BUSY` stuck, `0x80c00020`).** Seen once; not cleared by any of + the above, nor by the kernel driver — a physical replug was required, and it + has not reproduced since. The vendor-derived MT76x2U tree has dedicated UDMA + and IFDMA/FCE resets beyond `TX_CLR` + ([USB TX wedge reset evidence](mt7612u-usb-wedge.md)), implemented as + `bringup swreset` and **not yet hardware-verified** for want of a + reproduction. + +**Locking.** The 1 Hz `mt7612u_phy_tick()` issues MCU commands, and the RX ring +runs its own libusb event thread, so register and MCU I/O are serialised by a +recursive `io_lock` initialised in `mt_open()`. A single-threaded consumer never +contends; the lock is what lets a consumer safely drive the tick from a second +thread. `mt_mcu_send()` also drains any late reply off EP 5 before each command, +without which a reply that arrived after its wait expired desynced the channel. ## Offline tests From 7845c43bc1292d934422000426adab7388ff7b55 Mon Sep 17 00:00:00 2001 From: snokvist Date: Tue, 8 Sep 2026 18:27:40 +0200 Subject: [PATCH 12/14] mt7612u: fix the device-state lifecycle for the adopt path and retunes From the qodo review of #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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/init.c | 6 ++--- src/mt7612u/internal.h | 14 +++++++++++- src/mt7612u/phy.c | 24 +++++++++++++++----- src/mt7612u/rx.c | 16 +++++++++----- src/mt7612u/usb.c | 50 ++++++++++++++++++++++++++++++------------ 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index a4856467..5d6831f1 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -453,7 +453,7 @@ struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err) if (err) *err = "out of memory"; return NULL; } - if (mt_open(d, err)) { free(d); return NULL; } + if (mt_open(d, err)) { mt_dev_state_destroy(d); free(d); return NULL; } return bring_up(d, fw_dir, err); } @@ -467,6 +467,7 @@ struct mt7612u_dev *mt7612u_open_handle(void *h, void *ctx, const char *fw_dir, return NULL; } if (mt_adopt(d, (libusb_device_handle *)h, (libusb_context *)ctx, err)) { + mt_dev_state_destroy(d); free(d); return NULL; } @@ -478,8 +479,7 @@ void mt7612u_close(struct mt7612u_dev *d) if (!d) return; mt_async_stop(d); if (d->h) mt_mac_stop(d); - mt_close(d); - pthread_mutex_destroy(&d->io_lock); + mt_close(d); /* releases io_lock via mt_dev_state_destroy() */ free(d); } diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index efb214e4..a89e00b7 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -17,6 +17,7 @@ # include #endif #include +#include #include #include #include @@ -53,7 +54,10 @@ struct mt7612u_cal { uint8_t agc_gain_cur[2]; uint8_t agc_gain_adjust; int8_t low_gain; - int8_t avg_rssi_all; + /* Written by the RX callback (libusb event thread), read by the PHY tick + * on the caller's thread - atomic so the concurrent access is defined. + * Relaxed: a torn value only delays a gain-class change by ~1 s. */ + _Atomic int8_t avg_rssi_all; }; #define MT_RX_RING 16 @@ -147,6 +151,7 @@ struct mt7612u_dev { uint8_t chan; uint8_t bw; pthread_mutex_t io_lock; /* recursive: guards register + MCU transactions */ + uint8_t io_lock_ready; /* io_lock initialised - guards its destroy */ uint8_t bw_clamp_warned; /* the "never widen" notice is once, not per frame */ int8_t txpower_conf; /* limit, 0.5 dB units (dBm * 2) */ int8_t target_power; @@ -169,6 +174,13 @@ struct mt7612u_dev { }; /* --- usb.c --- */ +/* Per-device state both open paths need before ANY register I/O: the recursive + * io_lock and the calibration sentinels. Both mt_open() and mt_adopt() reach + * mt_vendor_req() (which locks io_lock) during identification, so this must run + * first on either path. Idempotent. mt_dev_state_destroy() is the matching + * teardown, guarded so it runs exactly once regardless of how far open got. */ +void mt_dev_state_init(struct mt7612u_dev *d); +void mt_dev_state_destroy(struct mt7612u_dev *d); int mt_open(struct mt7612u_dev *d, const char **err); /* Adopt a handle the caller already opened, reset and claimed. */ int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 2d1b84a5..82cce982 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -510,12 +510,18 @@ static void phy_update_channel_gain(struct mt7612u_dev *d) int low_gain, gain_change; /* mt76 averages RSSI over associated stations. A monitor consumer has - * none, so this is fed from the RX path; -75 is mt76's own fallback. */ - if (!d->cal.avg_rssi_all) - d->cal.avg_rssi_all = -75; + * none, so this is fed from the RX path (atomic; written there); -75 is + * mt76's own fallback. */ + int8_t avg = atomic_load_explicit(&d->cal.avg_rssi_all, + memory_order_relaxed); + + if (!avg) { + avg = -75; + atomic_store_explicit(&d->cal.avg_rssi_all, avg, memory_order_relaxed); + } - low_gain = (d->cal.avg_rssi_all > rssi_gain_thresh(d->bw)) + - (d->cal.avg_rssi_all > low_rssi_gain_thresh(d->bw)); + low_gain = (avg > rssi_gain_thresh(d->bw)) + + (avg > low_rssi_gain_thresh(d->bw)); gain_change = d->cal.low_gain < 0 || ((d->cal.low_gain & 2) ^ (low_gain & 2)); d->cal.low_gain = (int8_t)low_gain; @@ -625,6 +631,14 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) d->cal.channel_cal_done = fast; d->chan = chan; d->bw = bw; + /* Reset the periodic gain tracker for the new channel: its cached RSSI, + * 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. */ + atomic_store_explicit(&d->cal.avg_rssi_all, 0, memory_order_relaxed); + d->cal.low_gain = -1; + d->cal.agc_gain_adjust = 0; /* The TX "never widen" notice is once per width, not once per device: * a later tune to a narrower channel is a new situation and deserves * its own warning. Without this, a clamp consumed by a startup-ordering diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c index 723706e9..822cb5f6 100644 --- a/src/mt7612u/rx.c +++ b/src/mt7612u/rx.c @@ -104,12 +104,16 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, * channel, so it is reported as no estimate rather than as a very quiet * channel. */ info->noise = info->rssi[2]; - /* mt76_get_min_avg_rssi() equivalent. Written on the RX thread and read - * by the PHY tick - a single byte, and a stale sample only delays a gain - * class change by one second. */ - d->cal.avg_rssi_all = d->cal.avg_rssi_all - ? (int8_t)((d->cal.avg_rssi_all * 7 + info->rssi[0]) / 8) - : info->rssi[0]; + /* mt76_get_min_avg_rssi() equivalent, on the RX (libusb event) thread; + * the PHY tick reads it on the caller's thread. Relaxed atomic load+store: + * a torn value only delays a gain-class change by ~1 s, and taking io_lock + * here would block RX behind the tick's multi-second MCU calibration. */ + { + 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]; + atomic_store_explicit(&d->cal.avg_rssi_all, avg, memory_order_relaxed); + } info->noise_valid = info->noise > -100 && info->noise < -30; info->snr_db = info->noise_valid ? (int8_t)(info->rssi[0] - info->noise) : 0; diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index dacc4066..0359f28c 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -286,10 +286,37 @@ static int mt_identify(struct mt7612u_dev *d, const char **err) * reset here: it would invalidate the caller's own handle. No detach either - * a caller that got this far already dealt with the kernel driver. */ +void mt_dev_state_init(struct mt7612u_dev *d) +{ + pthread_mutexattr_t ma; + + if (d->io_lock_ready) return; + /* Recursive: the PHY tick locks io_lock and then nests mt_vendor_req / + * mt_mcu_send, which lock it again. A zeroed pthread_mutex_t is a valid + * NON-recursive lock, so a path that skipped this (the adopt path once + * did) would self-deadlock the tick or lock an uninitialised mutex. */ + pthread_mutexattr_init(&ma); + pthread_mutexattr_settype(&ma, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&d->io_lock, &ma); + pthread_mutexattr_destroy(&ma); + d->io_lock_ready = 1; + /* cal sentinels (low_gain=-1 etc.) are reset per-tune in + * mt_set_channel_ex(), which always runs before the first PHY tick. */ +} + +void mt_dev_state_destroy(struct mt7612u_dev *d) +{ + if (!d || !d->io_lock_ready) return; + pthread_mutex_destroy(&d->io_lock); + d->io_lock_ready = 0; +} + int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, libusb_context *ctx, const char **err) { if (!h) { if (err) *err = "no USB handle"; return -1; } + /* Before mt_identify(): it reads a register, which locks io_lock. */ + mt_dev_state_init(d); d->h = h; d->ctx = ctx; d->owns_handle = 0; @@ -470,6 +497,9 @@ int mt_open(struct mt7612u_dev *d, const char **err) { int rc; + /* Both open paths init this before any register I/O; see mt_adopt(). */ + mt_dev_state_init(d); + if (libusb_init(&d->ctx)) { if (err) *err = "libusb_init failed"; return -1; } d->owns_handle = 1; @@ -479,20 +509,6 @@ int mt_open(struct mt7612u_dev *d, const char **err) return -1; } - /* Every path into the device goes through here, including consumers that - * allocate the struct themselves - a zeroed pthread_mutex_t is a valid - * NON-recursive lock, and the PHY tick nests mt_vendor_req inside its own - * lock, so initialising this anywhere else self-deadlocks. */ - { - pthread_mutexattr_t ma; - - pthread_mutexattr_init(&ma); - pthread_mutexattr_settype(&ma, PTHREAD_MUTEX_RECURSIVE); - pthread_mutex_init(&d->io_lock, &ma); - pthread_mutexattr_destroy(&ma); - d->cal.low_gain = -1; - } - d->kernel_was_attached = libusb_kernel_driver_active(d->h, 0) == 1; if (d->kernel_was_attached) { rc = libusb_detach_kernel_driver(d->h, 0); @@ -600,6 +616,12 @@ int mt_open(struct mt7612u_dev *d, const char **err) void mt_close(struct mt7612u_dev *d) { + /* Terminal teardown, and the single place io_lock is released: callers + * do their last register I/O (mt_mac_stop) before mt_close, and mt_close + * itself takes no lock. Guarded, so it is a no-op if open never got far + * enough to init it. Covers mt7612u_close() and bring_up()'s fail path; + * the two open-failure returns that bypass mt_close destroy it directly. */ + mt_dev_state_destroy(d); if (d->wrlog) { fclose(d->wrlog); d->wrlog = NULL; } if (d->mculog) { fclose(d->mculog); d->mculog = NULL; } if (d->transfers_stranded) { From 49b1dd449d134406e14922518bec2d74e425a3cb Mon Sep 17 00:00:00 2001 From: snokvist Date: Tue, 8 Sep 2026 18:27:40 +0200 Subject: [PATCH 13/14] mt7612u: stop the RX ring on gate error paths; tick the duplex flood; deepen the MCU drain From the qodo review of #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 Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/mcu.c | 14 ++++++++++---- src/mt7612u/tools/bringup.c | 31 ++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c index c27b9821..1d7082e0 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -79,17 +79,23 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, * command reads its predecessor's reply, mismatches, times out (~1.5 s) * and leaves one more stale reply behind - the channel then never * resyncs. Seen as "mcu resp mismatch: evt=0 seq=10..14 (want 15)" - * cascading through every 1 Hz tick. Bounded: a queue deeper than 16 - * is a fault worth reporting, not one worth looping on. */ + * cascading through every 1 Hz tick. The loop exits when the queue is + * empty (a 5 ms read returns nothing); observed depth is ~5, but the cap + * is generous so a deeper transient is fully drained rather than leaving + * a straggler that re-desyncs the next command. Hitting the cap means + * the queue is still non-empty - a real fault, warned distinctly. */ { uint8_t stale[MCU_RESP_URB_SIZE]; int n = 0, got; - while (n < 16 && + while (n < 64 && !mt_bulk(d, MT_EP_IN_CMD_RESP, stale, sizeof stale, &got, 5) && got >= 4) n++; - if (n) + if (n == 64) + WARN("MCU reply queue still draining at the cap before cmd %d - " + "the response channel may be desynced", cmd); + else if (n) WARN("drained %d stale MCU repl%s before cmd %d", n, n == 1 ? "y" : "ies", cmd); } diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 722fd181..4a827c10 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -903,7 +903,7 @@ static int gate_arx(uint8_t chan, int secs, int notick) if (mt7612u_rx_start(&dev, arx_cb, &ctx)) { printf("GATE arx: FAIL - rx_start failed\n"); return 1; } - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); /* notick is the negative control: without the 1 Hz PHY tick this gate @@ -969,13 +969,23 @@ static int gate_duplex(uint8_t chan, int secs) memcpy(frame + 24, "MT7612U-HAL ", 12); if (mt7612u_rx_start(&dev, arx_cb, &ctx)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); - while (now_ms() - t0 < secs * 1000.0) { - frame[36] = (uint8_t)n; frame[37] = (uint8_t)(n >> 8); - if (mt7612u_tx(&dev, frame, 1400, &rate) == 0) n++; + { + double last_tick = t0; + + while (now_ms() - t0 < secs * 1000.0) { + frame[36] = (uint8_t)n; frame[37] = (uint8_t)(n >> 8); + if (mt7612u_tx(&dev, frame, 1400, &rate) == 0) n++; + /* RX stays enabled through the flood; without the 1 Hz tick the + * receiver decays and the concurrent-RX figure is confounded. */ + if (now_ms() - last_tick >= 1000.0) { + mt7612u_phy_tick(&dev); + last_tick = now_ms(); + } + } } wall = now_ms() - t0; printf("duplex on ch%u for %.1f s:\n", chan, wall / 1000.0); @@ -1506,7 +1516,7 @@ static int gate_rxbytes(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, rxbytes_cb, NULL)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); @@ -1585,7 +1595,7 @@ static int gate_linkstat(uint8_t chan, int secs, int with_rx) if (with_rx) { if (mt7612u_rx_start(&dev, drain_cb, &linkstat_drained)) return 1; } - if (mt_mac_start(&dev, with_rx)) return 1; + if (mt_mac_start(&dev, with_rx)) { if (with_rx) mt7612u_rx_stop(&dev); return 1; } if (with_rx) mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); @@ -1598,7 +1608,10 @@ static int gate_linkstat(uint8_t chan, int secs, int with_rx) double busy_pct; if (!wait_ms(1000.0)) break; - if (mt7612u_link_stats(&dev, &st)) return 1; + if (mt7612u_link_stats(&dev, &st)) { + if (with_rx) mt7612u_rx_stop(&dev); + return 1; + } busy_pct = (st.ch_busy + st.ch_idle) ? 100.0 * st.ch_busy / (double)(st.ch_busy + st.ch_idle) : 0.0; printf(" %5d %9u %9u %5.1f%% %5u %5u %8u %5u %5u %5u %4d\n", @@ -1744,7 +1757,7 @@ static int gate_linkrx(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, linkrx_cb, NULL)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) return 1; + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } mt7612u_set_monitor_rx(&dev, 0); printf("RX on ch%u for %d s, filtering our own magic\n", chan, secs); From 4a6a14cd2430bc6a90072f6e5490ba059db5250b Mon Sep 17 00:00:00 2001 From: snokvist Date: Tue, 8 Sep 2026 21:25:04 +0200 Subject: [PATCH 14/14] mt7612u: address the review - both blocking items, all eight inline points 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 #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) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u-usb-wedge.md | 8 +- docs/mt7612u.md | 87 +++++----- src/mt7612u/fw.c | 108 ++++++++---- src/mt7612u/include/mt7612u/mt7612u.h | 6 +- src/mt7612u/init.c | 22 +++ src/mt7612u/internal.h | 40 ++++- src/mt7612u/mcu.c | 76 ++++++--- src/mt7612u/phy.c | 108 +++++++++--- src/mt7612u/rx.c | 16 +- src/mt7612u/tools/bringup.c | 229 +++++++++++++++++++++++--- src/mt7612u/usb.c | 158 ++++++++++++------ 11 files changed, 632 insertions(+), 226 deletions(-) diff --git a/docs/mt7612u-usb-wedge.md b/docs/mt7612u-usb-wedge.md index 41c3010f..f39792ab 100644 --- a/docs/mt7612u-usb-wedge.md +++ b/docs/mt7612u-usb-wedge.md @@ -1,11 +1,7 @@ # MT7612U hard USB TX wedge: reset evidence -Research date: 2026-09-07. Devourer HEAD initially: `4e7a793` on -`feat/mt7612u-mediatek-backend`; concurrent PHY/I/O work was subsequently -committed as `5757712`. Its separate host-side libusb hang report does not -establish the cause of this persistent device-side wedge. -Reference: `openwrt/mt76` at `be5ce7910521492d4a2e4ce7ee3843680a46c047`. -**Status: source-level investigation only; no new device measurements.** +**Status: source-level investigation only; no new device measurements.** Every +source below is linked at a pinned commit. There is a concrete reset candidate beyond `TX_CLR`: the vendor-derived MT76x2U source has separate UDMA TX and IFDMA/FCE reset controls. Its diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 539d1cb8..0d0ff2a6 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -2,25 +2,8 @@ Everything below was measured on one MT7612U (`0e8d:7612`, `MT_ASIC_VERSION` `0x76120044`, MT7662 MAC core, 2T2R, SuperSpeed) against an RTL8812AU witness -running this project's own `rxdemo`/`txdemo`. Read `## Offline tests - -`make -C src/mt7612u check` runs three binaries. No hardware, no privileges. - -| test | what it holds | -|---|---| -| `api_link` | takes the address of all 20 public entry points while including only the public header, so a declaration that loses its definition is a link error | -| `frame_shape` | `mt_hdrlen_from_fc()` over management, all eight control subtypes and the five data shapes; the RX L2-pad fold on a synthetic QoS frame, with a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed; the radiotap VHT bandwidth mapping over all eleven codes the part can express | -| `field_macros` | `MT_CTZ` against `__builtin_ctz` over all 32 single-bit and all 528 contiguous masks, plus a `FIELD_PREP`/`FIELD_GET` round-trip, plus a static initialiser that fails to compile if the macro stops being constant-foldable | - -Each was mutation-tested: removing one public definition, reverting either -frame-shape fix, and reverting the header-length fix each make the suite fail, -with the RX one reporting `QoS Control zeroed by the pad fold: aa aa`. - -`tools/extract_mt7612u_tables.py --check` byte-compares the generated -`initvals.h` against `reference/mt76` at the pinned commit. - -## Counterparts` before -quoting any number here. +running this project's own `rxdemo`/`txdemo`. Read `## Offline tests` and +`## Counterparts` before quoting any number here. **This code is not wired into the build.** `CMakeLists.txt` is untouched, there is no `IRtlDevice` implementation and no `WiFiDriver` dispatch. It is a @@ -159,15 +142,15 @@ rate word the TX path writes, so one codec serves both directions. ### A receiver needs the 1 Hz tick -mt76 runs `cal_work` every second (`MT_CALIBRATE_INTERVAL == HZ`, -`mt76x2/usb_phy.c:42`): a self-guarded channel calibration, TSSI compensation -(an MCU command each second) and `mt76x2_phy_update_channel_gain()`. This -port ran none of it, and the cost was measured rather than assumed: against a -peer 20 cm away airing 1400-byte HT-MCS7 frames at 3037 fps, a receiver with -no tick took **3 frames in 10 s**; calling `mt7612u_phy_tick()` once a second -it takes **~4850/s** (nine runs). Not the adapter — symmetric in both -directions, and both hear 400–1000 fps of ambient on ch1/36/40/44/48 — not -the ring (its own counter agreed at 3), and not the air: an RTL8812AU witness +The cadence, what it costs to skip it and why it is caller-driven are the +contract on `mt7612u_phy_tick()` in the public header, which is where the +numbers live. Kept here: why the finding is believed, and which part of the +tick does the work. + +Stimulus throughout is a peer 20 cm away airing 1400-byte HT-MCS7 frames at +3037 fps. The no-tick collapse is not the adapter — it is symmetric in both +directions, and both hear 400–1000 fps of ambient on ch1/36/40/44/48; not the +ring — its own counter agreed at 3; and not the air — an RTL8812AU witness counted 103805 frames from the same transmitter. Bisected with one verified peer per arm: reading the read-and-clear @@ -175,10 +158,9 @@ Bisected with one verified peer per arm: reading the read-and-clear calibration alone restores it (43902); the ported gain tracking alone does not (4 frames). The tick therefore issues one `MCU_CAL_TEMP_SENSOR` per call — the cheapest command that keeps the MCU in the loop, standing in for the TSSI -compensation this port lacks — and runs the gain tracking for fidelity. It is -caller-driven: MCU commands share one 4-bit sequence number and one response -endpoint, so they need exactly one user. Every receiving gate in `bringup` -ticks; `arx 1` is the negative control. +compensation this port lacks — and runs the gain tracking for fidelity. Every +receiving gate in `bringup` ticks; `arx 1` is the negative +control. ### This part does not deliver the FCS @@ -266,13 +248,17 @@ three defined values (20/40/80) and mt76 exposes no narrowband path for this part. **Enabling MAC RX without draining the bulk-IN endpoint wedges the chip below -USB level.** Prevented at the source: `mt_mac_start()` takes an `MT_RX_DRAIN_*` -argument and refuses to enable the receiver unless a ring is draining EP 4 (the -public `mt7612u_start()` always derived this; the internal entry point now does -too). +USB level.** Prevented at both ends. Starting: `mt_mac_start()` takes an +`MT_RX_DRAIN_*` argument and refuses to enable the receiver unless a ring is +draining EP 4 (the public `mt7612u_start()` always derived this; the internal +entry point now does too). Stopping: `mt_mac_rx_disable()` clears `ENABLE_RX` +**before** the ring is cancelled, because `mt_async_stop()` reaps the EP 4 +drainer while `mt_mac_stop()` would not clear the bit until after its own +`mt_rx_flush()` and a TX-idle wait of up to 150 ms — the same undrained window, +at the end of every session. **A run that dies mid-transfer leaves the adapter unable to load firmware, and -`mt_open()` now recovers it.** Register reads *and* writes still round-trip and +both open paths now recover it.** Register reads *and* writes still round-trip and the MAC and RF are fine, but every bulk OUT NAKs — `libusb_reset_device()` (already called on every open) does not reach it, and the kernel `mt76x2u` driver cannot bind it either. Two tiers, told apart by `MT_USB_U3DMA_CFG`: @@ -281,11 +267,15 @@ driver cannot bind it either. Two tiers, told apart by `MT_USB_U3DMA_CFG`: wedged adapter: `clear_halt` on all four endpoints, 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 and never writes — - clears it alone. `mt_open()` pulses it and drains the RX pipe (`mt_rx_flush` - runs from `mt_mac_stop`, which a killed process never reaches), so every - entry point self-heals. A post-kill firmware upload can also fail for 3–8 s - and then succeed by itself; `mt_fw_init()` absorbs that with one retry after - a settle. On a healthy idle adapter the recovery is a no-op (0/40 opens). + clears it alone. `mt_recover_usb()` pulses it and drains the RX pipe + (`mt_rx_flush` runs from `mt_mac_stop`, which a killed process never + reaches). It runs after identify on **both** open paths — `mt_open()` and + `mt_adopt()` — so a libusb-owning consumer arriving through + `mt7612u_open_handle()` self-heals too; while it lived in `mt_open()` alone + that consumer got none of it. `bringup adopt` exercises that path. A post-kill firmware upload can also fail for 3–8 s + and then succeed by itself; `mt_fw_init()` absorbs that with one retry of + the upload after a settle. On a healthy idle adapter the recovery is a no-op + (0/40 opens). - **Hard (`TX_BUSY` stuck, `0x80c00020`).** Seen once; not cleared by any of the above, nor by the kernel driver — a physical replug was required, and it @@ -297,10 +287,13 @@ driver cannot bind it either. Two tiers, told apart by `MT_USB_U3DMA_CFG`: **Locking.** The 1 Hz `mt7612u_phy_tick()` issues MCU commands, and the RX ring runs its own libusb event thread, so register and MCU I/O are serialised by a -recursive `io_lock` initialised in `mt_open()`. A single-threaded consumer never -contends; the lock is what lets a consumer safely drive the tick from a second -thread. `mt_mcu_send()` also drains any late reply off EP 5 before each command, -without which a reply that arrived after its wait expired desynced the channel. +recursive `io_lock` initialised in `mt_dev_state_init()`, which both open paths +run before any register I/O. A single-threaded consumer never contends; the lock +is what lets a consumer safely drive the tick from a second thread. +`mt_mcu_send()` also drains any late reply off EP 5, but only when a previous +command actually gave up (`mcu_stale_pending`) — draining before *every* command +cost a guaranteed 5 ms bulk timeout each, including the 3-6 inside every tune, +and nothing was ever queued on the healthy path. ## Offline tests @@ -308,7 +301,7 @@ without which a reply that arrived after its wait expired desynced the channel. | test | what it holds | |---|---| -| `api_link` | takes the address of all 20 public entry points while including only the public header, so a declaration that loses its definition is a link error | +| `api_link` | takes the address of all 25 public entry points while including only the public header, so a declaration that loses its definition is a link error | | `frame_shape` | `mt_hdrlen_from_fc()` over management, all eight control subtypes and the five data shapes; the RX L2-pad fold on a synthetic QoS frame, with a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed; the radiotap VHT bandwidth mapping over all eleven codes the part can express | | `field_macros` | `MT_CTZ` against `__builtin_ctz` over all 32 single-bit and all 528 contiguous masks, plus a `FIELD_PREP`/`FIELD_GET` round-trip, plus a static initialiser that fails to compile if the macro stops being constant-foldable | diff --git a/src/mt7612u/fw.c b/src/mt7612u/fw.c index 81c32b39..9b221d80 100644 --- a/src/mt7612u/fw.c +++ b/src/mt7612u/fw.c @@ -17,6 +17,18 @@ #define PATCH_HDR_LEN 30 #define FW_HDR_LEN 32 +/* The two loaders below fail in two ways that call for different handling, so + * they say which one they hit: a blob that is absent or malformed will fail the + * same way forever, while the bulk upload itself is the transfer mt_fw_init() + * retries. */ +#define FW_ERR_FATAL (-1) /* blob missing, short or malformed; or no memory */ +#define FW_ERR_UPLOAD (-2) /* the bulk upload failed on the wire */ +/* The chip did not come up after a complete upload. A DEVICE failure, not a + * blob one: it is the same settling window the upload retry was measured + * against, so it must retry too - `bringup swreset`'s whole verdict is + * whether the chip takes firmware again, and it used to settle and pass. */ +#define FW_ERR_DEVICE (-3) /* uploaded, but the chip did not start it */ + static void put_le32(uint8_t *p, uint32_t v) { p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; @@ -92,7 +104,10 @@ static int fw_send_chunk(struct mt7612u_dev *d, uint8_t *scratch, total = 4 + rlen + 4; rc = mt_bulk(d, MT_EP_OUT_INBAND_CMD, scratch, total, NULL, 1000); - if (rc) { ERR("fw chunk bulk out: %s", libusb_error_name(rc)); return -1; } + if (rc) { + ERR("fw chunk bulk out: %s", libusb_error_name(rc)); + return FW_ERR_UPLOAD; + } idx = mt_rr(d, MT_TX_CPU_FROM_FCE_CPU_DESC_IDX) + 1; mt_wr(d, MT_TX_CPU_FROM_FCE_CPU_DESC_IDX, idx); @@ -105,7 +120,7 @@ static int fw_send_data(struct mt7612u_dev *d, const uint8_t *data, int data_len int max_len = (int)max_payload - 8, pos = 0, rc = 0; uint8_t *scratch = malloc(max_payload); - if (!scratch) return -1; + if (!scratch) return FW_ERR_FATAL; while (data_len > 0) { int len = data_len < max_len ? data_len : max_len; @@ -123,9 +138,9 @@ static int load_rom_patch(struct mt7612u_dev *d, const char *dir) { size_t n; uint8_t *fw = slurp(dir, "mt7662_rom_patch.bin", &n); - int rc = -1; + int rc = FW_ERR_FATAL, up; - if (!fw) return -1; + if (!fw) return FW_ERR_FATAL; if (n <= PATCH_HDR_LEN) { ERR("rom patch too short"); goto out; } LOG("ROM patch build: %.15s (%zu byte payload)", (char *)fw, n - PATCH_HDR_LEN); @@ -137,9 +152,9 @@ static int load_rom_patch(struct mt7612u_dev *d, const char *dir) mt_usleep(7500); fce_setup(d); - if (fw_send_data(d, fw + PATCH_HDR_LEN, (int)(n - PATCH_HDR_LEN), - MCU_ROM_PATCH_MAX_PAYLOAD, MCU_ROM_PATCH_OFFSET)) - goto out; + up = fw_send_data(d, fw + PATCH_HDR_LEN, (int)(n - PATCH_HDR_LEN), + MCU_ROM_PATCH_MAX_PAYLOAD, MCU_ROM_PATCH_OFFSET); + if (up) { rc = up; goto out; } /* enable_patch and reset_wmt are USB_TYPE_CLASS, not VENDOR. */ { @@ -162,6 +177,7 @@ static int load_rom_patch(struct mt7612u_dev *d, const char *dir) if (!mt_poll(d, MT_MCU_CLOCK_CTL, BIT(0), BIT(0), 100000)) { ERR("ROM patch did not apply (MT_MCU_CLOCK_CTL=0x%08x)", mt_rr(d, MT_MCU_CLOCK_CTL)); + rc = FW_ERR_DEVICE; goto out; } LOG("ROM patch applied"); @@ -177,9 +193,9 @@ static int load_firmware(struct mt7612u_dev *d, const char *dir) uint8_t *fw = slurp(dir, "mt7662.bin", &n); uint32_t ilm_len, dlm_len, dlm_offset = MCU_DLM_OFFSET; uint16_t fw_ver, build_ver; - int rc = -1; + int rc = FW_ERR_FATAL, up; - if (!fw) return -1; + if (!fw) return FW_ERR_FATAL; if (n < FW_HDR_LEN) { ERR("firmware too short"); goto out; } ilm_len = get_le32(fw); @@ -199,15 +215,15 @@ static int load_firmware(struct mt7612u_dev *d, const char *dir) mt_usleep(7500); fce_setup(d); - if (fw_send_data(d, fw + FW_HDR_LEN, (int)ilm_len, - MCU_FW_URB_MAX_PAYLOAD, MCU_ILM_OFFSET)) - goto out; + up = fw_send_data(d, fw + FW_HDR_LEN, (int)ilm_len, + MCU_FW_URB_MAX_PAYLOAD, MCU_ILM_OFFSET); + if (up) { rc = up; goto out; } /* rev >= E3: DLM lands at 0x110800. Confirmed on the wire. */ dlm_offset += 0x800; - if (fw_send_data(d, fw + FW_HDR_LEN + ilm_len, (int)dlm_len, - MCU_FW_URB_MAX_PAYLOAD, dlm_offset)) - goto out; + up = fw_send_data(d, fw + FW_HDR_LEN + ilm_len, (int)dlm_len, + MCU_FW_URB_MAX_PAYLOAD, dlm_offset); + if (up) { rc = up; goto out; } /* load IVB: MT_VEND_DEV_MODE, VENDOR type, wValue 0x12, no data. */ mt_vendor_req(d, MT_VEND_DEV_MODE, @@ -217,6 +233,7 @@ static int load_firmware(struct mt7612u_dev *d, const char *dir) if (!mt_poll(d, MT_MCU_COM_REG0, BIT(0), BIT(0), 100000)) { ERR("firmware failed to start (MT_MCU_COM_REG0=0x%08x)", mt_rr(d, MT_MCU_COM_REG0)); + rc = FW_ERR_DEVICE; goto out; } mt_set(d, MT_MCU_COM_REG0, BIT(1)); @@ -230,23 +247,54 @@ static int load_firmware(struct mt7612u_dev *d, const char *dir) int mt_fw_init(struct mt7612u_dev *d, const char *fw_dir) { + /* The caller's bracket is already open: mt_init_hardware() calls + * mt_io_clear() and then runs mt_power_cycle() and mt_wait_for_mac() + * BEFORE us, and there is only one accumulator. Sample it so the retry + * can discard its own failed attempt without forgiving the power-on + * phase's - which would let a partly powered WLAN core report success. */ + unsigned io_err_pre = mt_io_errors(d); + if (!fw_dir) fw_dir = "firmware"; - /* Two attempts, not one. Measured (12/12 cycles, both adapters): after - * a process dies mid transfer the first firmware upload times out at - * its first chunk, and the same upload succeeds by itself 3-8 s later - * with nothing repaired. That is a settling window, not a wedge, and - * 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++) { - if (attempt) { - WARN("firmware upload failed - a previous run may have died " - "mid transfer; retrying after a 3 s settle"); - mt_usleep(3000000); - } - if (!load_rom_patch(d, fw_dir) && !load_firmware(d, fw_dir)) + for (int attempt = 0; ; attempt++) { + int rc = load_rom_patch(d, fw_dir); + + if (!rc) + rc = load_firmware(d, fw_dir); + if (!rc) return 0; + + /* Retry the upload transfer, and only that. Measured (12/12 + * cycles, both adapters): after a process dies mid transfer the + * first firmware upload times out at its first chunk, and the + * same upload succeeds by itself 3-8 s later with nothing + * repaired. That is a settling window, not a wedge, and an open + * that lands inside it must not fail. A blob that is missing or + * malformed fails identically 3 s later, so it fails now with + * the error that names the file rather than after a warning + * about a transfer that never started. Everything that is NOT a + * blob problem retries: the wire upload (FW_ERR_UPLOAD) and the + * chip failing to start what was uploaded (FW_ERR_DEVICE) are + * both the settling window. */ + if (attempt || rc == FW_ERR_FATAL) + return -1; + + WARN("firmware load failed (%s) - a previous run may have died " + "mid transfer; retrying after a 3 s settle", + rc == FW_ERR_UPLOAD ? "upload" : "chip did not start it"); + mt_usleep(3000000); + + /* The retry must not inherit the failed attempt's register + * errors. The chunk that timed out also drove EP0 accessors + * either side of it - the FCE address and length writes, the + * CPU_DESC_IDX read-modify-write, mt_poll - and each failure + * bumps the accumulator that mt_init_hardware() brackets the + * bring-up with. It refuses to report success while that is + * non-zero, so without this the firmware loads on the second + * attempt and mt_open() still fails with "failed register + * transfers". Restored to the value on entry rather than + * zeroed: mt_init_hardware()'s bracket opened before the + * power-on phase, so zeroing would forgive its failures too. */ + mt_io_restore(d, io_err_pre); } - return -1; } diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index 506738e4..9696cb90 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -290,8 +290,10 @@ int mt7612u_link_stats(struct mt7612u_dev *dev, struct mt7612u_link_stats *out); * One round of mt76's 1 Hz cal_work (mt76x2/usb_phy.c:42, MT_CALIBRATE_INTERVAL * == HZ). A receiving consumer MUST call this about once a second. Measured: * against a peer 20 cm away airing 3037 fps, a receiver with no tick takes 3 - * frames in 10 s; with it ~4850/s (nine runs). It issues one MCU calibration - * - the part that matters, bisected - and runs the channel-gain tracking. + * frames in 10 s; with it 5415-5470/s (eight consecutive runs, both adapters). + * This comment is the one home for that figure - everything else points here. + * It issues one MCU calibration - the part that matters, bisected - and runs + * the channel-gain tracking. * * Caller-driven on purpose: MCU commands share one 4-bit sequence number and * one response endpoint, so they need a single user. It reads and clears the diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index 5d6831f1..99556e77 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -297,6 +297,24 @@ int mt_mac_start(struct mt7612u_dev *d, int enable_rx) return 0; } +/* + * Silence the receiver, leaving TX as it was. This is the first half of a + * teardown: mt_mac_stop() below does not clear ENABLE_RX until after its own + * mt_rx_flush() and a TX-idle wait of up to 150 ms, and mt_async_stop() reaps + * the EP 4 ring before even that - so without this the MAC keeps filling a + * receive pipe nobody is draining, which on a busy channel is the FIFO + * overflow that stops RX DMA for good. It is the exact window mt_mac_start() + * refuses to create, reached at the end of every session. + * + * mt_clear() leaves RX enabled if its read half fails; acceptable here because + * mt_mac_stop() clears the register outright a moment later. + */ +void mt_mac_rx_disable(struct mt7612u_dev *d) +{ + if (!d || !d->h) return; + mt_clear(d, MT_MAC_SYS_CTRL, MT_MAC_SYS_CTRL_ENABLE_RX); +} + int mt_mac_stop(struct mt7612u_dev *d) { uint32_t rts_cfg = mt_rr(d, MT_TX_RTS_CFG); @@ -477,6 +495,10 @@ struct mt7612u_dev *mt7612u_open_handle(void *h, void *ctx, const char *fw_dir, void mt7612u_close(struct mt7612u_dev *d) { if (!d) return; + /* RX off BEFORE the ring is cancelled - mt_async_stop() reaps the EP 4 + * drainer, and mt_mac_stop() would not clear ENABLE_RX until after its + * flush and TX-idle wait. See mt_mac_rx_disable(). */ + if (d->h) mt_mac_rx_disable(d); mt_async_stop(d); if (d->h) mt_mac_stop(d); mt_close(d); /* releases io_lock via mt_dev_state_destroy() */ diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index a89e00b7..29e0abc3 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -17,7 +17,6 @@ # include #endif #include -#include #include #include #include @@ -54,10 +53,14 @@ struct mt7612u_cal { uint8_t agc_gain_cur[2]; uint8_t agc_gain_adjust; int8_t low_gain; - /* Written by the RX callback (libusb event thread), read by the PHY tick - * on the caller's thread - atomic so the concurrent access is defined. - * Relaxed: a torn value only delays a gain-class change by ~1 s. */ - _Atomic int8_t avg_rssi_all; + /* There is deliberately no averaged-RSSI field here. mt76 feeds the gain + * class from mt76_get_min_avg_rssi() (util.c:72), which averages ASSOCIATED + * stations; a monitor consumer has none, so mt76 substitutes -75 and never + * leaves the middle class. Driving it from every frame the receiver parses + * instead - neighbours included - let a -40 dBm neighbour AP step the gain + * down against a wanted peer at -80 dBm, which is less faithful than doing + * nothing. phy_update_channel_gain() now uses mt76's -75 directly, and the + * RX hot path does no cross-thread write at all. */ }; #define MT_RX_RING 16 @@ -148,10 +151,19 @@ struct mt7612u_dev { uint8_t macaddr[6]; uint16_t chainmask; /* 0x202 = 2T2R */ uint8_t mcu_seq; + /* Set when a mcu_wait_resp() gave up, so its reply may still land on + * EP 5 and desync the next command. The stale-reply drain is paid only + * then: an unconditional drain costs a guaranteed 5 ms bulk timeout on + * every command, including the 3-6 inside each tune. */ + uint8_t mcu_stale_pending; uint8_t chan; uint8_t bw; pthread_mutex_t io_lock; /* recursive: guards register + MCU transactions */ uint8_t io_lock_ready; /* io_lock initialised - guards its destroy */ + /* Observe-but-do-not-repair, for wedge experiments. A field, not a + * getenv: this library reads no environment - the tool that wants the + * behaviour sets it before mt_open() (bringup does). */ + uint8_t no_autorecover; uint8_t bw_clamp_warned; /* the "never widen" notice is once, not per frame */ int8_t txpower_conf; /* limit, 0.5 dB units (dBm * 2) */ int8_t target_power; @@ -185,6 +197,14 @@ int mt_open(struct mt7612u_dev *d, const char **err); /* Adopt a handle the caller already opened, reset and claimed. */ int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, libusb_context *ctx, const char **err); +/* + * Repair a device left wedged by a run that died mid transfer: silence the + * receiver, drain EP 4, and pulse MT_USB_DMA_CFG_TX_CLR. Runs after identify + * on BOTH open paths - a libusb-owning consumer arrives through mt_adopt(), + * and skipping it there means the very failure this recovery exists to fix. + * Honours d->no_autorecover (observe and report, repair nothing). + */ +void mt_recover_usb(struct mt7612u_dev *d); void mt_close(struct mt7612u_dev *d); /* Checked read: 0 on success with *val filled, -1 on transport failure. * Prefer this anywhere the value drives a decision - 0xffffffff is a real @@ -197,6 +217,8 @@ int mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t va int mt_wr_chk(struct mt7612u_dev *d, uint32_t addr, uint32_t val); /* Register-I/O failure accumulator; see the comment above mt_io_clear(). */ void mt_io_clear(struct mt7612u_dev *d); +/* Restore a previously sampled accumulator; see the note in usb.c. */ +void mt_io_restore(struct mt7612u_dev *d, unsigned v); unsigned mt_io_errors(struct mt7612u_dev *d); #define mt_set(d, a, v) mt_rmw(d, a, v, v) #define mt_clear(d, a, v) mt_rmw(d, a, v, 0) @@ -247,6 +269,14 @@ int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir); int mt_mac_start(struct mt7612u_dev *d, int enable_rx); void mt_rx_flush(struct mt7612u_dev *d); int mt_mac_stop(struct mt7612u_dev *d); +/* + * Silence the receiver while leaving TX up. Must run BEFORE the EP 4 ring is + * cancelled: mt_async_stop() reaps the ring first and mt_mac_stop() does not + * clear ENABLE_RX until after its own mt_rx_flush() and TX-idle wait, so the + * teardown otherwise reproduces exactly the undrained-receiver window that + * mt_mac_start() refuses to create - at the end of every session. + */ +void mt_mac_rx_disable(struct mt7612u_dev *d); /* --- tx.c --- */ uint16_t mt_tx_rate_word(const struct mt7612u_tx_rate *r); diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c index 1d7082e0..9334a041 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -39,7 +39,7 @@ static int mcu_wait_resp(struct mt7612u_dev *d, uint8_t seq) continue; if (rc) { ERR("mcu resp bulk: %s", libusb_error_name(rc)); - return -1; + goto unanswered; } if (len < 4) continue; @@ -53,6 +53,12 @@ static int mcu_wait_resp(struct mt7612u_dev *d, uint8_t seq) FIELD_GET(MT_RX_FCE_INFO_CMD_SEQ, rxfce), seq); } ERR("mcu command timed out waiting for response"); +unanswered: + /* Giving up leaves the reply outstanding: it can still land on EP 5 and be + * read as the next command's. Arm the drain in mt_mcu_send() - both exits + * here are that case, and arming it is what lets the drain stay off the + * healthy path entirely. */ + d->mcu_stale_pending = 1; return -1; } @@ -74,30 +80,56 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, * failure reads as "mcu resp mismatch ... (want 1)". */ pthread_mutex_lock(&d->io_lock); - /* Drain replies nobody collected before sending. A reply that lands - * after mcu_wait_resp() gave up stays queued on EP 5, so the next - * command reads its predecessor's reply, mismatches, times out (~1.5 s) - * and leaves one more stale reply behind - the channel then never - * resyncs. Seen as "mcu resp mismatch: evt=0 seq=10..14 (want 15)" - * cascading through every 1 Hz tick. The loop exits when the queue is - * empty (a 5 ms read returns nothing); observed depth is ~5, but the cap - * is generous so a deeper transient is fully drained rather than leaving - * a straggler that re-desyncs the next command. Hitting the cap means - * the queue is still non-empty - a real fault, warned distinctly. */ - { + /* Drain replies nobody collected before sending, but only when one is + * actually outstanding. A reply that lands after mcu_wait_resp() gave up + * stays queued on EP 5, so the next command reads its predecessor's reply, + * mismatches, times out (~1.5 s) and leaves one more stale reply behind - + * the channel then never resyncs. Seen as "mcu resp mismatch: evt=0 + * seq=10..14 (want 15)" cascading through every 1 Hz tick. + * + * Gated on mcu_stale_pending rather than run before every command: with an + * empty queue the 5 ms read is a guaranteed LIBUSB_ERROR_TIMEOUT, and it + * would be paid by all of them - the 3-6 inside each mt_set_channel_ex(), + * fast retune included, and the no-wait commands that never produce a reply + * at all - putting +15-30 ms on every hop to drain nothing. Zero cost when + * healthy, unchanged protection when not. + * + * The loop exits when the queue is empty (a 5 ms read returns nothing); + * observed depth is ~5, but the cap is generous so a deeper transient is + * fully drained rather than leaving a straggler that re-desyncs the next + * command. Hitting the cap means the queue is still non-empty - a real + * fault, warned distinctly, and the flag stays armed so the next command + * drains again rather than reading a straggler as its own reply. */ + if (d->mcu_stale_pending) { uint8_t stale[MCU_RESP_URB_SIZE]; int n = 0, got; - while (n < 64 && - !mt_bulk(d, MT_EP_IN_CMD_RESP, stale, sizeof stale, &got, 5) && - got >= 4) + int drained_clean = 0; + + while (n < 64) { + int brc = mt_bulk(d, MT_EP_IN_CMD_RESP, stale, sizeof stale, + &got, 5); + + /* A timeout is the only exit that PROVES the queue is empty. + * Exiting on a short packet says nothing about what is still + * behind it, so it must not disarm the drain - the old + * unconditional version retried on the next command, and + * dropping the flag there would lose that protection for + * good. */ + if (brc == LIBUSB_ERROR_TIMEOUT) { drained_clean = 1; break; } + if (brc || got < 4) + break; n++; - if (n == 64) + } + if (n == 64) { WARN("MCU reply queue still draining at the cap before cmd %d - " "the response channel may be desynced", cmd); - else if (n) - WARN("drained %d stale MCU repl%s before cmd %d", n, - n == 1 ? "y" : "ies", cmd); + } else if (drained_clean) { + d->mcu_stale_pending = 0; + if (n) + WARN("drained %d stale MCU repl%s before cmd %d", n, + n == 1 ? "y" : "ies", cmd); + } } if (wait_resp) { @@ -131,6 +163,12 @@ int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, rc = mt_bulk(d, MT_EP_OUT_INBAND_CMD, buf, total, NULL, 500); if (rc) { ERR("mcu cmd %d bulk out: %s", cmd, libusb_error_name(rc)); + /* A failed OUT does not mean the MCU never saw it: a timeout that + * still delivered, or a stall after the data stage, both leave a + * reply that can land on EP 5 with nothing waiting for it. This + * is the third way a command goes unanswered, so it arms the + * drain like the two in mcu_wait_resp(). */ + d->mcu_stale_pending = 1; pthread_mutex_unlock(&d->io_lock); return -1; } diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 82cce982..dce4fef3 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -176,7 +176,7 @@ static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) /* Marked done once, as mt76 does. An individual mt_mcu_calibrate() can * time out when the MCU answers slowly under RF load, and its reply then * arrives late (mt_mcu_send drains those) - but the calibration still - * takes: the receiver runs at ~4850 fps either way, measured across + * takes: the receiver runs at its normal rate either way, measured across * eight runs that each logged those timeouts. Withholding this flag and * re-running the whole burst every second made it WORSE (153 fps). */ d->cal.channel_cal_done = 1; @@ -443,7 +443,7 @@ int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, * tssi_compensate (which issues an MCU command every second) and * update_channel_gain. This port ran NONE of it, and the cost is not subtle: * against a transmitter 20 cm away airing 3037 fps, a receiver with no tick - * takes 3 frames in 10 s. With the tick it takes 4362/s. Bisected - reading + * takes 3 frames in 10 s. With the tick it takes the rate quoted on mt7612u_phy_tick(). Bisected - reading * the read-and-clear MT_RX_STAT_* counters alone does nothing (3 frames); it * is the periodic MCU calibration that keeps the receiver alive. */ @@ -486,10 +486,17 @@ static void phy_set_gain_val(struct mt7612u_dev *d) static int phy_adjust_vga_gain(struct mt7612u_dev *d) { uint8_t limit = d->cal.low_gain > 0 ? 16 : 4; - uint32_t false_cca = FIELD_GET(MT_RX_STAT_1_CCA_ERRORS, - mt_rr(d, MT_RX_STAT_1)); + uint32_t stat, false_cca; int changed = 0; + /* Checked read: mt_rr()'s ~0u failure value comes through + * MT_RX_STAT_1_CCA_ERRORS as 65535, which is over the 800 threshold, so + * every failed control transfer stepped the gain down 2 dB. A read that + * did not happen is not evidence about the channel - leave the gain. */ + if (mt_rr_chk(d, MT_RX_STAT_1, &stat)) + return 0; + false_cca = FIELD_GET(MT_RX_STAT_1_CCA_ERRORS, stat); + if (false_cca > 800 && d->cal.agc_gain_adjust < limit) { d->cal.agc_gain_adjust += 2; changed = 1; @@ -509,16 +516,29 @@ static void phy_update_channel_gain(struct mt7612u_dev *d) uint32_t agc_35, agc_37, val; int low_gain, gain_change; - /* mt76 averages RSSI over associated stations. A monitor consumer has - * none, so this is fed from the RX path (atomic; written there); -75 is - * mt76's own fallback. */ - int8_t avg = atomic_load_explicit(&d->cal.avg_rssi_all, - memory_order_relaxed); - - if (!avg) { - avg = -75; - atomic_store_explicit(&d->cal.avg_rssi_all, avg, memory_order_relaxed); - } + /* + * mt76 takes this from mt76_get_min_avg_rssi() (util.c:72), which walks + * the associated-station table and returns the weakest station's average. + * A monitor consumer associates with nobody, so that walk finds no wcid + * and returns 0, and mt76 substitutes -75 (mt76x2/phy.c:283-285): in + * monitor mode upstream never leaves the middle gain class. + * + * This port fed an EMA of rssi[0] over every frame the receiver parsed, + * which on a monitor receiver is every transmitter on the channel - one + * -40 dBm neighbour AP drove low_gain=2, AGC 35/37 to 0x08080808 and + * gain_cur down 10-14 against a wanted peer at -80 dBm. The bisect above + * says gain tracking is not what keeps RX alive, so that EMA was + * fidelity-only code that was less faithful than the constant it + * replaced. Pinned to mt76's monitor value; rx.c no longer feeds it. + */ + const int avg = -75; + /* Pinned, so `low_gain` is 1 at every width (the thresholds are + * -68/-82, -65/-79, -62/-76) and the `low_gain == 2` arms below - AGC + * 26's 0x3 case, the 0x08080808 pair and the low_gain_delta block - are + * unreachable today. They are kept because they are the faithful + * mt76x2_phy_update_channel_gain() port and become live the moment a + * real per-peer RSSI source exists; the compiler cannot see that they + * are dead, so this note is the only thing stopping them rotting. */ low_gain = (avg > rssi_gain_thresh(d->bw)) + (avg > low_rssi_gain_thresh(d->bw)); @@ -576,7 +596,7 @@ static void phy_update_channel_gain(struct mt7612u_dev *d) * * The MCU calibration is the part that matters, and that was measured rather * than assumed: with no tick a receiver takes 3 frames in 10 s from a peer - * airing 3037 fps; with a periodic MCU calibration it takes 4362/s. The gain + * airing 3037 fps; with a periodic MCU calibration it recovers to the rate quoted on mt7612u_phy_tick(). The gain * update alone does NOT help (4 frames) - it is ported because it is the rest * of mt76's 1 Hz work and it tracks signal strength, not because it fixes this. * Reading the read-and-clear MT_RX_STAT_* counters alone does nothing either. @@ -631,12 +651,10 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) d->cal.channel_cal_done = fast; d->chan = chan; d->bw = bw; - /* Reset the periodic gain tracker for the new channel: its cached RSSI, - * 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. */ - atomic_store_explicit(&d->cal.avg_rssi_all, 0, memory_order_relaxed); + /* Reset the periodic gain tracker for the new channel: its gain class and + * fine VGA offset both 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). */ d->cal.low_gain = -1; d->cal.agc_gain_adjust = 0; /* The TX "never widen" notice is once per width, not once per device: @@ -714,14 +732,52 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) channel_calibrate(d, band == BAND_5GHZ); + /* + * mt76x02_init_agc_gain(): host-side snapshot of the AGC gain the + * firmware settled on, used later by the RX gain tracking. + * + * Above the fast return, unlike mt76 (mt76x2/usb_phy.c:170-174 takes it + * after `if (scan) return 0`). mt_mcu_init_gain() has just reprogrammed + * AGC 8/9 for the new channel, and this function resets low_gain to -1 on + * every tune - including a fast one - so the next tick takes the + * gain_change branch and rebuilds agc_gain_cur from this snapshot. Left + * from the previous tune it would write the old channel's gain base. + * mt76 gets away with skipping it because its scan path leaves low_gain + * alone, so its next tick changes no gain class. + * + * Still after channel_calibrate(): on the full path that runs + * apply_gain_adj(), which writes AGC 8/9, and mt76 snapshots after it for + * the same reason. On a fast tune it is a no-op (channel_cal_done). + */ + /* + * Checked, because hoisting these above the fast return also moved them + * out of the only thing that was guarding them. The mt_io_errors() check + * at the end of this function fails the tune on a bad transfer, but the + * fast path returns before reaching it. mt_rr() reports a failed control + * transfer as ~0u, and FIELD_GET(MT_BBP_AGC_GAIN, ~0u) is 0x7f - so one + * EP0 hiccup during a fast retune would store a gain base of 127, and the + * next tick (low_gain was just reset to -1, so gain_change is true) would + * program 127 - agc_gain_adjust into AGC 8/9: roughly double a normal + * base, deaf until the next full tune, with nothing logged. Keep the + * previous snapshot instead; it is at worst one channel stale, which is + * the condition this hoist set out to fix and is survivable, unlike 0x7f. + */ + { + uint32_t g8, g9; + + if (mt_rr_chk(d, MT_BBP(AGC, 8), &g8) || + mt_rr_chk(d, MT_BBP(AGC, 9), &g9)) { + WARN("AGC gain snapshot unreadable on ch%u - keeping the " + "previous base", chan); + } else { + d->cal.agc_gain_init[0] = FIELD_GET(MT_BBP_AGC_GAIN, g8); + d->cal.agc_gain_init[1] = FIELD_GET(MT_BBP_AGC_GAIN, g9); + } + } + if (fast) return 0; - /* mt76x02_init_agc_gain(): host-side snapshot of the AGC gain the - * firmware settled on, used later by the RX gain tracking. */ - d->cal.agc_gain_init[0] = FIELD_GET(MT_BBP_AGC_GAIN, mt_rr(d, MT_BBP(AGC, 8))); - d->cal.agc_gain_init[1] = FIELD_GET(MT_BBP_AGC_GAIN, mt_rr(d, MT_BBP(AGC, 9))); - if (tssi_enabled(d)) { uint32_t flag = 0; diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c index 822cb5f6..27ba3a77 100644 --- a/src/mt7612u/rx.c +++ b/src/mt7612u/rx.c @@ -104,16 +104,12 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, * channel, so it is reported as no estimate rather than as a very quiet * channel. */ info->noise = info->rssi[2]; - /* mt76_get_min_avg_rssi() equivalent, on the RX (libusb event) thread; - * the PHY tick reads it on the caller's thread. Relaxed atomic load+store: - * a torn value only delays a gain-class change by ~1 s, and taking io_lock - * here would block RX behind the tick's multi-second MCU calibration. */ - { - 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]; - atomic_store_explicit(&d->cal.avg_rssi_all, avg, memory_order_relaxed); - } + /* Nothing here feeds the PHY gain tracker, deliberately: mt76's input is + * mt76_get_min_avg_rssi() (util.c:72), which sees associated stations + * only, and a monitor receiver has none - see phy_update_channel_gain(). + * An EMA over every parsed frame would let any third-party transmitter on + * the channel set the RX gain class, and put a cross-thread write in this + * hot path for a tracker the bisect showed does not restore RX. */ info->noise_valid = info->noise > -100 && info->noise < -30; info->snr_db = info->noise_valid ? (int8_t)(info->rssi[0] - info->noise) : 0; diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 4a827c10..60d61129 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -74,6 +74,21 @@ static int wait_ticking(double ms) return !g_stop; } +/* + * RX off, then the ring. mt_async_stop() reaps the EP 4 drainer and + * mt_mac_stop() does not clear ENABLE_RX until after its own flush and TX-idle + * wait, so cancelling the ring first leaves the MAC filling a receive pipe + * nobody reads - the FIFO overflow that stops RX DMA for good, reached at the + * end of every gate that receives. One helper rather than the pair open-coded + * at each of the twenty teardowns, error paths included: an invariant spelled + * out twenty times is one that gets half-enforced. See mt_mac_rx_disable(). + */ +static void rx_teardown(void) +{ + mt_mac_rx_disable(&dev); + mt7612u_rx_stop(&dev); +} + static int gate_regs(void) { @@ -215,12 +230,18 @@ static int gate_swreset(int apply) * drain before touching the DMA. Bounded - this is the fault * 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; - mt_usleep(2000); - } - printf(" MAC stopped, TX idle=%d\n", - !(mt_rr(&dev, MT_MAC_STATUS) & BIT(0))); + /* MT_MAC_STATUS_TX, not BIT(0) - BIT(0) is _RX. Waiting on RX + * idle pulsed the UDMA TX reset with TX possibly still in + * flight, which is the one precondition the vendor sequence + * states, so a FAIL below could not separate "the sequence does + * not work" from "it ran too early". Same test init.c polls, + * and the same 100 ms budget as the 50 x 2 ms loop it replaces. */ + int tx_idle = mt_poll(&dev, MT_MAC_STATUS, MT_MAC_STATUS_TX, 0, + 100000); + printf(" MAC stopped, TX idle=%d\n", tx_idle); + if (!tx_idle) + printf(" TX never went idle - the sequence ran without its " + "precondition, so a FAIL below is about that\n"); mt_clear(&dev, CFG_ADDR(CFG_UDMA_CFG), 0x00c00000); /* 1 */ swreset_pulse(CFG_ADDR(CFG_EP_DROP), 0x03f00000); /* 2 */ @@ -400,6 +421,126 @@ static int gate_chan(uint8_t chan, const char *fw_dir) return 0; } +/* + * Exercise the adopt path - how a libusb-owning consumer (the IRtlDevice + * wrapper) reaches this subtree. Everything else in this tool arrives through + * mt_open(), so without this gate the second entry point is never opened on + * hardware at all. + * + * That mattered: the wedge recovery used to live inside mt_open() alone, so a + * caller adopting a handle after a run died mid-transfer paid both mt_fw_init() + * attempts and failed with the exact error the recovery removes. A PASS here + * after a killed run is the evidence that the recovery is shared. + * + * It runs mt_adopt() + mt_init_hardware() on bringup's own device rather than + * calling mt7612u_open_handle() - that function is exactly those two steps, and + * driving them directly is what lets MT7612U_NO_AUTORECOVER reach this gate: + * the public entry point allocates the device itself, so an observe-only wedge + * experiment could never be set up through it. + */ +static int gate_adopt(const char *sel) +{ + libusb_context *ctx = NULL; + libusb_device_handle *h = NULL; + libusb_device **list = NULL; + const char *err = NULL; + ssize_t n; + int detached = 0, matches = 0, rrc, rc = 1; + + if (libusb_init(&ctx)) { printf("ADOPT: FAIL - libusb_init\n"); return 1; } + + /* Same selector spellings open_selected() accepts - a "bus-port" like + * "2-1", or a bare index - so a two-adapter bench does not silently test + * the other unit, and MT7612U_DEV=0 does not read as "no adapter". */ + n = libusb_get_device_list(ctx, &list); + for (ssize_t i = 0; i < n && !h; i++) { + struct libusb_device_descriptor desc; + uint8_t ports[8]; + char id[32], idx[8]; + int np, off; + + if (libusb_get_device_descriptor(list[i], &desc)) + continue; + if (desc.idVendor != 0x0e8d || desc.idProduct != 0x7612) + continue; + off = snprintf(id, sizeof id, "%u", libusb_get_bus_number(list[i])); + np = libusb_get_port_numbers(list[i], ports, sizeof ports); + for (int p = 0; p < np && off > 0 && off < (int)sizeof id; p++) + off += snprintf(id + off, sizeof id - (size_t)off, "%s%u", + p ? "." : "-", ports[p]); + snprintf(idx, sizeof idx, "%d", matches); + if (sel && *sel && strcmp(sel, id) && strcmp(sel, idx)) { + matches++; + continue; + } + if (!libusb_open(list[i], &h)) + printf("adopting the caller-owned handle at %s\n", id); + matches++; + } + if (list) libusb_free_device_list(list, 1); + if (!h) { + printf("ADOPT: FAIL - no MT7612U%s%s\n", sel ? " at " : "", sel ? sel : ""); + libusb_exit(ctx); + return 1; + } + + if (libusb_kernel_driver_active(h, 0) == 1 && + libusb_detach_kernel_driver(h, 0) == 0) + detached = 1; + + /* Claim BEFORE resetting. This gate opens libusb itself - that is the + * point of it - so it cannot take the exclusive adapter lock that + * open_selected() uses, and a reset would otherwise yank the device out + * from under a live capture in another process and only fail afterwards. + * A failed claim here is that other process still holding the interface. */ + if (libusb_claim_interface(h, 0)) { + printf("ADOPT: FAIL - interface 0 is claimed elsewhere; not resetting\n"); + goto out; + } + libusb_release_interface(h, 0); + + rrc = libusb_reset_device(h); + if (rrc) { + printf("ADOPT: FAIL - libusb_reset_device: %s\n", libusb_error_name(rrc)); + goto out; + } + if (libusb_claim_interface(h, 0)) { + printf("ADOPT: FAIL - could not claim interface 0 after reset\n"); + goto out; + } + + /* The caller owns the handle and the context; mt_adopt() records that and + * mt_close() then leaves both to us. */ + if (mt_adopt(&dev, h, ctx, &err)) { + printf("ADOPT: FAIL - mt_adopt: %s\n", err ? err : "?"); + printf(" (a wedged adapter failing HERE but not via `bringup regs` is\n" + " the recovery being unreachable from the adopt path)\n"); + goto out_release; + } + if (mt_eeprom_init(&dev) || mt_init_hardware(&dev, NULL)) { + printf("ADOPT: FAIL - bring-up after adopt\n"); + goto out_close; + } + + printf("U3DMA_CFG after adopt = 0x%08x (0x00c00020 = soft wedge, " + "0x80c00020 = hard)\n", mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG))); + printf("ADOPT: PASS - the adopt path brought the device up; the wedge " + "recovery runs here too\n"); + rc = 0; + +out_close: + mt_mac_stop(&dev); + mt_close(&dev); /* adopted: releases nothing of ours, drops io_lock */ +out_release: + libusb_release_interface(h, 0); +out: + if (detached) + libusb_attach_kernel_driver(h, 0); + libusb_close(h); + libusb_exit(ctx); + return rc; +} + /* Gate E: inject frames. The witness is a separate radio - our own RX seeing * these would prove nothing. */ static int gate_tx(uint8_t chan, int count, int phy, int mcs) @@ -500,6 +641,23 @@ static int gate_rx(uint8_t chan, int want) printf(" MT_MAC_STATUS = 0x%08x\n", mt_rr(&dev, MT_MAC_STATUS)); printf(" MT_RX_STAT_1 = 0x%08x (CCA errors seen = RF is live)\n", mt_rr(&dev, MT_RX_STAT_1)); + /* + * Declared, not fixed. The tick below runs on this thread, which under + * MT_RX_DRAIN_SYNC is the only EP 4 drainer, and mt7612u_phy_tick() can + * block in the MCU for ~3.3 s when the part answers late under RF load - + * so the gate opens the very undrained-receiver window it exists to + * observe. Gating RX around the tick would close that window, but the + * re-enable runs through mt_mac_start(), which rewrites MT_RX_FILTR_CFG + * back to the initvals value and would silently undo the monitor filter + * set above - a quieter gate measuring something else. A sync gate that + * can no longer reproduce the hazard is worth less than one that reports + * it, and gate_arx is the witness that holds anyway: its ring drains on + * the libusb event thread, and its notick arm is the measured control. + */ + printf(" NOTE: the 1 Hz tick blocks this thread, the only EP 4 drainer -\n" + " under load this gate is NOT a valid tick witness. Use\n" + " `arx ` for that (`arx 1` is its\n" + " negative control).\n"); double last_tick = now_ms(); @@ -526,6 +684,12 @@ static int gate_rx(uint8_t chan, int want) } printf("\nreceived %d frames\n", got); + /* The same invariant every ring-cancelling gate now follows, and it + * applies here even though there is no ring: mt_mac_stop() does not clear + * ENABLE_RX until after its own mt_rx_flush() and a TX-idle wait of up to + * 150 ms, and this thread has already stopped draining EP 4 - on the busy + * channel this gate was just listening to, that IS the undrained window. */ + mt_mac_rx_disable(&dev); mt_mac_stop(&dev); if (got == 0) { printf("GATE F: FAIL - no frames received\n"); @@ -748,7 +912,7 @@ static int gate_mtu(uint8_t chan, int count) (*ok)++; mt_usleep(1500); } - if (pass) mt7612u_rx_stop(&dev); + if (pass) rx_teardown(); } printf(" %-6d %ld/%-8d %ld/%-8d %s\n", len, ok_sync, count, @@ -903,12 +1067,12 @@ static int gate_arx(uint8_t chan, int secs, int notick) if (mt7612u_rx_start(&dev, arx_cb, &ctx)) { printf("GATE arx: FAIL - rx_start failed\n"); return 1; } - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); /* notick is the negative control: without the 1 Hz PHY tick this gate * reads 3 frames in 10 s from a strong nearby peer (reproduced eight - * times); with it ~4850/s. See mt7612u_phy_tick(). */ + * times); with it the rate quoted on mt7612u_phy_tick(). See that comment. */ if (notick) wait_ms(secs * 1000.0); else wait_ticking(secs * 1000.0); { struct mt_async_stats st; @@ -932,7 +1096,7 @@ static int gate_arx(uint8_t chan, int secs, int notick) } for (int i = 0; i < 8; i++) if (ctx.by_phy[i]) printf(" %-6s %lu\n", phy_name[i], ctx.by_phy[i]); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); return ctx.n ? 0 : 1; } @@ -969,7 +1133,7 @@ static int gate_duplex(uint8_t chan, int secs) memcpy(frame + 24, "MT7612U-HAL ", 12); if (mt7612u_rx_start(&dev, arx_cb, &ctx)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); @@ -1021,13 +1185,13 @@ static int gate_duplex(uint8_t chan, int secs) printf(" TX stopped; listening %.1f s for the receiver to recover\n", RECOVER_S); if (!wait_ticking(RECOVER_S * 1000.0)) { - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); return 1; } after = atomic_load_explicit(&ctx.n, memory_order_relaxed); printf(" RX after the flood: %lu frames\n", after - before); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); if (!n) { @@ -1242,7 +1406,7 @@ static int gate_caps(uint8_t chan) * with nothing reading, that is long enough to wedge the part below * the USB level, which no software reset recovers. */ if (mt_async_start(&dev, drain_cb, &drained)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt_async_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } mt7612u_get_caps(&dev, &c); printf("caps: %s rev 0x%08x %dTx%dRx bw_mask 0x%02x (20%s%s)\n", @@ -1343,7 +1507,7 @@ static int gate_caps(uint8_t chan) } } - mt_async_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); printf("\n%lu frames drained from EP 4 while the receiver was on\n", drained); printf("\nGATE caps: %s\n", bad ? "FAIL" : "PASS"); @@ -1392,7 +1556,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) /* Ring first, receiver second - see gate_caps. Arming the responder and * printing between the two would otherwise leave RX on and undrained. */ if (mt7612u_rx_start(&dev, ack_cb, &off)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } /* CRC and PHY errors only: DUP must stay clear so retries reach us. */ mt_wr(&dev, MT_RX_FILTR_CFG, MT_RX_FILTR_CFG_CRC_ERR | MT_RX_FILTR_CFG_PHY_ERR); @@ -1408,7 +1572,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) if (arm) { if (mt7612u_set_ack_responder(&dev, g_ack_mac)) { printf("GATE ack: FAIL - could not arm\n"); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); return 1; } @@ -1425,7 +1589,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) if (secs <= 0 || secs > 3600) { printf("GATE ack: FAIL - listen duration %d out of range (1..3600 s)\n", secs); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); return 1; } @@ -1435,11 +1599,11 @@ static int gate_ack(uint8_t chan, int secs, int arm) * into roughly 49 days with the receiver left running. */ if (!wait_ticking(secs * 1000.0)) { printf("GATE ack: interrupted\n"); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); return 1; } - mt7612u_rx_stop(&dev); + rx_teardown(); printf(" stimulus frames addressed to the responder MAC: %lu (retries %lu)\n", off.to_us, off.retry_to_us); @@ -1516,13 +1680,13 @@ static int gate_rxbytes(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, rxbytes_cb, NULL)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); wait_ticking(secs * 1000.0); mt7612u_link_stats(&dev, &st); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); printf("ch%u, %d s ambient. false CCA this interval: %u (mt76 calls >800 " @@ -1595,7 +1759,7 @@ static int gate_linkstat(uint8_t chan, int secs, int with_rx) if (with_rx) { if (mt7612u_rx_start(&dev, drain_cb, &linkstat_drained)) return 1; } - if (mt_mac_start(&dev, with_rx)) { if (with_rx) mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, with_rx)) { if (with_rx) rx_teardown(); return 1; } if (with_rx) mt7612u_set_monitor_rx(&dev, 0); mt7612u_link_stats_start(&dev); @@ -1609,7 +1773,7 @@ static int gate_linkstat(uint8_t chan, int secs, int with_rx) if (!wait_ms(1000.0)) break; if (mt7612u_link_stats(&dev, &st)) { - if (with_rx) mt7612u_rx_stop(&dev); + if (with_rx) rx_teardown(); return 1; } busy_pct = (st.ch_busy + st.ch_idle) @@ -1633,7 +1797,7 @@ static int gate_linkstat(uint8_t chan, int secs, int with_rx) } } if (with_rx) { - mt7612u_rx_stop(&dev); + rx_teardown(); printf(" %lu frames reached the ring over the run\n", linkstat_drained); } mt_mac_stop(&dev); @@ -1757,12 +1921,12 @@ static int gate_linkrx(uint8_t chan, int secs) if (mt_init_hardware(&dev, NULL)) return 1; if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; if (mt7612u_rx_start(&dev, linkrx_cb, NULL)) return 1; - if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { mt7612u_rx_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { rx_teardown(); return 1; } mt7612u_set_monitor_rx(&dev, 0); printf("RX on ch%u for %d s, filtering our own magic\n", chan, secs); wait_ticking(secs * 1000.0); - mt7612u_rx_stop(&dev); + rx_teardown(); mt_mac_stop(&dev); { @@ -2349,6 +2513,16 @@ int main(int argc, char **argv) signal(SIGINT, on_signal); signal(SIGTERM, on_signal); + /* The knob lives here, not in the library: mt_recover_usb() reads the + * field, and setting it before mt_open() is what makes the wedge + * experiments observe-only. Same spelling as before. */ + if (getenv("MT7612U_NO_AUTORECOVER")) + dev.no_autorecover = 1; + + /* Runs before the global mt_open() below, because it IS an open - of the + * other public entry point. */ + if (!strcmp(cmd, "adopt")) + return gate_adopt(getenv("MT7612U_DEV")); if (mt_open(&dev, &err)) { fprintf(stderr, "open failed: %s\n", err ? err : "?"); return 1; @@ -2434,6 +2608,7 @@ int main(int argc, char **argv) } else { fprintf(stderr, "unknown subcommand '%s'\n", cmd); fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); + fprintf(stderr, " bringup adopt (the mt_adopt path a libusb-owning consumer uses)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); fprintf(stderr, " the witness must listen at the same width (DEVOURER_BW=40|80)\n"); rc = 2; diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index 0359f28c..ac41b1c9 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -180,6 +180,11 @@ void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val) * Optional or diagnostic writes stay best-effort by not being bracketed. */ void mt_io_clear(struct mt7612u_dev *d) { d->io_err = 0; } +/* Put the accumulator back to a value taken earlier. For a nested retry that + * must discard only its OWN failed attempt: there is one accumulator, and the + * caller's bracket may already have counted failures before the retrying code + * was reached, so zeroing would silently forgive those too. */ +void mt_io_restore(struct mt7612u_dev *d, unsigned v) { d->io_err = v; } unsigned mt_io_errors(struct mt7612u_dev *d) { return d->io_err; } /* Checked single write, for a caller that wants to fail at the write rather @@ -311,6 +316,87 @@ void mt_dev_state_destroy(struct mt7612u_dev *d) d->io_lock_ready = 0; } +/* + * Recover from a previous run that died mid transfer. A USB port reset does + * not reach any of this: afterwards register reads and writes still round-trip + * and the MAC and RF are fine, but every bulk OUT NAKs and the next firmware + * upload times out at its first chunk - the kernel mt76x2u driver cannot bind + * such a device either. Two tiers, told apart by MT_USB_U3DMA_CFG: + * + * 0x00c00020, TX_BUSY clear: isolated one step at a time - clear_halt on all + * four endpoints, MAC + USB DMA stop, WLAN_EN/WLAN_CLK_EN down and the PBF + * block reset each left it wedged; pulsing TX_CLR alone clears it (3/3). + * mt76 declares TX_CLR and writes it nowhere. + * + * 0x80c00020, TX_BUSY stuck: the pulse loop below has NOT been seen to clear + * it, nor has anything else tried so far; the vendor-derived UDMA/IFDMA reset + * sequence in docs/mt7612u-usb-wedge.md (bringup swreset) is the untested + * candidate. The WARN is the honest verdict. + * + * The receive direction gets its own clean-up: mt_rx_flush() runs from + * mt_mac_stop(), which a killed process never reaches. Silence the receiver + * BEFORE draining or it refills as fast as it is read. + * + * This runs on BOTH open paths. It lived in mt_open() alone, which left the + * consumer this subtree exists for - a libusb-owning caller arriving through + * mt7612u_open_handle() / mt_adopt() - paying both mt_fw_init() attempts and + * failing with the exact error the recovery removes. + */ +void mt_recover_usb(struct mt7612u_dev *d) +{ + uint32_t cfg; + + /* A wedge experiment must not have its recovery hidden inside open(). + * With this set, open() observes and reports but repairs nothing. It is + * a field rather than a getenv(): this library reads no environment, so + * the tool that wants the behaviour sets it (bringup does, from + * MT7612U_NO_AUTORECOVER). */ + if (d->no_autorecover) { + if (mt_rr_chk(d, CFG_ADDR(MT_USB_U3DMA_CFG), &cfg)) + LOG("auto-recovery disabled: U3DMA_CFG unreadable"); + else + LOG("auto-recovery disabled: U3DMA_CFG=0x%08x", cfg); + return; + } + + mt_wr(d, MT_MAC_SYS_CTRL, 0); + mt_rx_flush(d); + + /* Checked read: mt_rr() reports a failed control transfer as ~0u, which + * has TX_BUSY set. One EP0 hiccup would otherwise send a healthy adapter + * through TX_BULK_EN off, twenty TX_CLR pulses (400 ms) and a false "still + * busy" verdict. A read we cannot trust is not evidence of a wedge. */ + if (mt_rr_chk(d, CFG_ADDR(MT_USB_U3DMA_CFG), &cfg)) { + WARN("U3DMA_CFG unreadable on open - skipping wedge recovery"); + return; + } + + if (cfg & MT_USB_DMA_CFG_TX_BUSY) { + WARN("USB TX DMA busy on open - a previous run died mid transfer"); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); + for (int i = 0; i < 20; i++) { + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + mt_usleep(20000); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + if (!mt_rr_chk(d, CFG_ADDR(MT_USB_U3DMA_CFG), &cfg) && + !(cfg & MT_USB_DMA_CFG_TX_BUSY)) + break; + } + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); + if (mt_rr_chk(d, CFG_ADDR(MT_USB_U3DMA_CFG), &cfg)) + WARN("USB TX DMA state unreadable after recovery"); + else if (cfg & MT_USB_DMA_CFG_TX_BUSY) + WARN("USB TX DMA still busy - this open will likely fail; " + "see docs/mt7612u-usb-wedge.md"); + else + LOG("USB TX DMA recovered"); + } else { + mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + mt_usleep(20000); + mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); + } +} + int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, libusb_context *ctx, const char **err) { @@ -321,7 +407,12 @@ int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, d->ctx = ctx; d->owns_handle = 0; d->kernel_was_attached = 0; - return mt_identify(d, err); + if (mt_identify(d, err)) + return -1; + /* Same recovery mt_open() gets: this is the path the IRtlDevice wrapper + * takes, and a killed previous run wedges the device for it identically. */ + mt_recover_usb(d); + return 0; } /* @@ -332,6 +423,14 @@ int mt_adopt(struct mt7612u_dev *d, libusb_device_handle *h, * with two - a measurement then attributes itself to whichever unit the bus * happened to hand over. MT7612U_DEV takes a "bus-port" as lsusb and sysfs * spell it ("2-1"), or a bare index into the matches in enumeration order. + * + * This is the ONE environment read left in the library, and it stays deferred + * to integration as agreed in #412 rather than being removed here: there is no + * public way to pass a selector (mt7612u_open() allocates the device itself and + * the struct is opaque), so dropping it would leave a multi-adapter consumer + * unable to choose an adapter at all. The wrapper does not need it - it arrives + * through mt7612u_open_handle() having already selected the device itself. + * MT7612U_NO_AUTORECOVER, which had no such constraint, is now d->no_autorecover. */ /* * Exclusive per-adapter lock - the same lock devourer's own UsbDeviceLock @@ -420,6 +519,7 @@ static int lock_adapter(libusb_device *dev, const char **err) static libusb_device_handle *open_selected(libusb_context *ctx, const char **err) { const char *sel = getenv("MT7612U_DEV"); + libusb_device **list = NULL; libusb_device_handle *h = NULL; ssize_t n = libusb_get_device_list(ctx, &list); @@ -551,59 +651,9 @@ int mt_open(struct mt7612u_dev *d, const char **err) goto fail; } - /* A wedge experiment must not have its recovery hidden inside open(). - * With this set, open() observes and reports but repairs nothing. */ - if (getenv("MT7612U_NO_AUTORECOVER")) { - LOG("auto-recovery disabled: U3DMA_CFG=0x%08x", - mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG))); - return 0; - } - - /* Recover from a previous run that died mid transfer. The port reset - * above does not reach any of this: afterwards register reads and writes - * still round-trip and the MAC and RF are fine, but every bulk OUT NAKs - * and the next firmware upload times out at its first chunk - the kernel - * mt76x2u driver cannot bind such a device either. Two tiers, told apart - * by MT_USB_U3DMA_CFG: - * - * 0x00c00020, TX_BUSY clear: isolated one step at a time - clear_halt - * on all four endpoints, MAC + USB DMA stop, WLAN_EN/WLAN_CLK_EN down - * and the PBF block reset each left it wedged; pulsing TX_CLR alone - * clears it (3/3). mt76 declares TX_CLR and writes it nowhere. - * - * 0x80c00020, TX_BUSY stuck: the pulse loop below has NOT been seen to - * clear it, nor has anything else tried so far; the vendor-derived - * UDMA/IFDMA reset sequence in docs/mt7612u-usb-wedge.md (bringup - * swreset) is the untested candidate. The WARN is the honest verdict. - * - * The receive direction gets its own clean-up: mt_rx_flush() runs from - * mt_mac_stop(), which a killed process never reaches. Silence the - * receiver BEFORE draining or it refills as fast as it is read. */ - 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) { - WARN("USB TX DMA busy on open - a previous run died mid transfer"); - mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); - for (int i = 0; i < 20; i++) { - mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); - mt_usleep(20000); - mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); - if (!(mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & - MT_USB_DMA_CFG_TX_BUSY)) - break; - } - mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_BULK_EN); - if (mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) - WARN("USB TX DMA still busy - this open will likely fail; " - "see docs/mt7612u-usb-wedge.md"); - else - LOG("USB TX DMA recovered"); - } else { - mt_set(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); - mt_usleep(20000); - mt_clear(d, CFG_ADDR(MT_USB_U3DMA_CFG), MT_USB_DMA_CFG_TX_CLR); - } + /* Shared with mt_adopt(): the wedge is a property of the device, not of + * how this process got hold of it. */ + mt_recover_usb(d); return 0; fail: