diff --git a/docs/mt7612u-usb-wedge.md b/docs/mt7612u-usb-wedge.md new file mode 100644 index 0000000..f39792a --- /dev/null +++ b/docs/mt7612u-usb-wedge.md @@ -0,0 +1,211 @@ +# MT7612U hard USB TX wedge: reset evidence + +**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 +`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 6361488..0d0ff2a 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 @@ -157,6 +140,28 @@ 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 + +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 +`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. 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. @@ -243,12 +248,52 @@ 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. +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 +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`: + +- **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_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 + 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_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 @@ -256,7 +301,7 @@ changed at once**, so that run does not attribute the wedge to one of them. | 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 3e58bc5..9b221d8 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,8 +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"; - if (load_rom_patch(d, fw_dir)) - return -1; - return 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); + } } diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index 871aa10..9696cb9 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -286,6 +286,23 @@ 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 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 + * 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/init.c b/src/mt7612u/init.c index 6880eaf..99556e7 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, @@ -285,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); @@ -441,7 +471,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); } @@ -455,6 +485,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; } @@ -464,9 +495,13 @@ 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); + 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 d10e60e..29e0abc 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -48,6 +48,19 @@ 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; + /* 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 @@ -138,8 +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; @@ -162,10 +186,25 @@ 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, 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 @@ -178,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) @@ -217,9 +258,25 @@ 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); +/* + * 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 c81679a..9334a04 100644 --- a/src/mt7612u/mcu.c +++ b/src/mt7612u/mcu.c @@ -28,13 +28,18 @@ 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; if (rc) { ERR("mcu resp bulk: %s", libusb_error_name(rc)); - return -1; + goto unanswered; } if (len < 4) continue; @@ -48,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; } @@ -55,12 +66,72 @@ 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); + + /* 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; + + 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) { + WARN("MCU reply queue still draining at the cap before cmd %d - " + "the response channel may be desynced", 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) { seq = ++d->mcu_seq & 0xf; if (!seq) @@ -90,9 +161,21 @@ 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)); + /* 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; + } - 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 5a06265..dce4fef 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -173,6 +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); + /* 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 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; } @@ -431,6 +437,190 @@ 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 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. + */ +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 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; + } 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 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)); + + 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 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. + * + * 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.) + */ +int mt7612u_phy_tick(struct mt7612u_dev *d) +{ + 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); + /* 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); + return 0; +} + 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] = { @@ -461,6 +651,12 @@ 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 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: * 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 @@ -536,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/regs.h b/src/mt7612u/regs.h index 1c6607b..80cc763 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/rx.c b/src/mt7612u/rx.c index 83afcb3..27ba3a7 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]; + /* 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 e449a77..60d6112 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -58,6 +58,37 @@ 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; +} + +/* + * 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) { @@ -136,6 +167,104 @@ 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, 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++) { + 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); + /* 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 */ + 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; @@ -292,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) @@ -319,7 +568,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 +625,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 @@ -392,8 +641,31 @@ 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(); 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); @@ -412,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"); @@ -474,7 +752,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 +863,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 */ @@ -634,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, @@ -672,7 +950,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; @@ -774,7 +1052,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 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. */ @@ -786,15 +1064,16 @@ 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)) { rx_teardown(); return 1; } + mt7612u_set_monitor_rx(&dev, 0); t0 = now_ms(); - wait_ms(secs * 1000.0); + /* 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 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; /* Actual elapsed, not the requested duration: an interrupt now @@ -803,16 +1082,21 @@ 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); } 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; } @@ -840,9 +1124,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,11 +1133,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)) { rx_teardown(); 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); @@ -891,14 +1184,14 @@ 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)) { - mt7612u_rx_stop(&dev); + if (!wait_ticking(RECOVER_S * 1000.0)) { + 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) { @@ -991,7 +1284,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 +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, 1)) { 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", @@ -1214,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"); @@ -1263,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, 1)) { 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); @@ -1279,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; } @@ -1296,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; } @@ -1304,13 +1597,13 @@ 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); + 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); @@ -1387,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, 1)) 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_ms(secs * 1000.0); + 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 " @@ -1466,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)) 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); @@ -1479,7 +1772,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) rx_teardown(); + 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", @@ -1501,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); @@ -1536,7 +1832,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,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, 1)) 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_ms(secs * 1000.0); - mt7612u_rx_stop(&dev); + wait_ticking(secs * 1000.0); + rx_teardown(); mt_mac_stop(&dev); { @@ -1699,7 +1995,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 +2099,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 +2206,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 +2335,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 +2417,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); { @@ -2217,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; @@ -2275,7 +2581,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); @@ -2294,11 +2601,14 @@ 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 { 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 88625df..ac41b1c 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -75,15 +75,22 @@ int mt_vendor_req(struct mt7612u_dev *d, uint8_t req, uint8_t type, { int rc = LIBUSB_ERROR_OTHER; + /* 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++) { 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; } @@ -173,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 @@ -279,15 +291,128 @@ 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; +} + +/* + * 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) { 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; 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; } /* @@ -298,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 @@ -386,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); @@ -463,6 +597,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; @@ -513,6 +650,10 @@ int mt_open(struct mt7612u_dev *d, const char **err) libusb_release_interface(d->h, 0); goto fail; } + + /* 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: @@ -525,6 +666,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) {