diff --git a/.gitmodules b/.gitmodules index b5781d34..91c08b99 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,8 @@ url = https://github.com/libc0607/rtl8733bu-20230626.git branch = v5.13.0.1 shallow = true +[submodule "reference/mt76"] + path = reference/mt76 + url = https://github.com/openwrt/mt76.git + branch = master + shallow = true diff --git a/docs/mt7612u.md b/docs/mt7612u.md new file mode 100644 index 00000000..6361488c --- /dev/null +++ b/docs/mt7612u.md @@ -0,0 +1,320 @@ +# MT7612U — a MediaTek backend, measured + +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. + +**This code is not wired into the build.** `CMakeLists.txt` is untouched, there +is no `IRtlDevice` implementation and no `WiFiDriver` dispatch. It is a +standalone C library plus a per-gate bringup harness, offered for the scope +question in the PR rather than for merge. + +## Why a MediaTek port is small + +On the Realtek generations the host programs the synthesizer and carries the +PHY tables. On MT7612U the RF plane lives behind the MCU: a channel change is +one 8-byte `CMD_SWITCH_CHANNEL_OP` plus a firmware calibration burst, and there +are no RF register tables to reimplement at all. The whole backend is ~4600 +lines including the harness. + +The same property is why it can never hop fast — see `## What it cannot do`. + +## TX rate is authoritative, unconditionally + +The MAC transmits at the 16-bit `txwi.rate` word verbatim. Three rates, three +exact matches on the witness: + +| requested | `txwi.rate` | witness `rate` | +|---|---|---| +| OFDM index 0 | `0x2000` | 4 (OFDM 6 Mbps) | +| OFDM index 7 | `0x2007` | 11 (OFDM 54 Mbps) | +| HT MCS7 | `0x4007` | 19 (12 + 7) | + +Per-frame alternation, 300 frames, even index MCS0 / odd MCS7, each carrying +its own index in the payload: **229 received, 229 aired the rate their index +called for, 0 mismatches.** Correlating on the index rather than demanding an +unbroken alternating sequence is deliberate — frames 1, 7 and 9 were lost, and +a strict-sequence test fails a working driver for that. + +The hardware rate LUT cannot override it. `MT_WCID_TX_RATE(1)` loaded with +OFDM 6 Mbps (`0x80012000`, read back), frames sent with `txwi.wcid = 1` and +`txwi.rate` = HT MCS7: + +| `MT_TXWI_FLAGS_TX_RATE_LUT` | frames | aired | +|---|---|---| +| clear | 84 | 83 × MCS7, 1 × MCS6 | +| **set** | 93 | 93 × MCS7 | + +The flag is defined in mt76 and set nowhere; setting it here changed nothing, +so on this part the LUT appears unreachable rather than merely unused. + +## TX power + +Ported from mt76's EEPROM path and checked against a `usbmon` capture of the +kernel driver configuring the same channel. All nine registers identical: + +| register | ours | kernel (ch149) | +|---|---|---| +| `MT_TX_PWR_CFG_0..4` | `04070606` `04060202` `04060101` `04060101` `00000101` | identical | +| `MT_TX_PWR_CFG_7/8/9` | `00010002` `00000001` `00000001` | identical | +| `MT_TX_ALC_CFG_0` | `2f2f171a` | identical | + +`mt76x02_sign_extend()` treats the **top bit set as positive**; reading it the +usual way silently halves output power. + +## A-MPDU works on injected frames + +Three arms, identical QoS-data frame and rate, differing only in the TXWI AMPDU +flag. `paggr` on the witness is the observable: + +| arm | frames | `paggr=1` | +|---|---|---| +| no AMPDU flag (control) | 352 | **0** | +| AMPDU, `QSEL_EDCA` | 289 | 288 | +| AMPDU, `QSEL_MGMT` | 326 | **326** | + +Throughput, broadcast QoS, HT MCS7, 20 MHz, 3 s per cell, with `paggr` +confirming every frame in the "on" cells aggregated (25547/25547 and +10444/10444) against 0/12010 and 0/8210 for the controls: + +| frame bytes | AMPDU off | AMPDU on | gain | +|---|---|---|---| +| 200 | 4380 fps / 7.01 Mbit/s | 9686 fps / 15.50 Mbit/s | **2.21×** | +| 1400 | 3038 fps / 34.03 Mbit/s | 3977 fps / **44.55 Mbit/s** | **1.31×** | + +This is the opposite of the Jaguar1 result recorded in `docs/aggregation.md`, +where monitor-injected aggregation airs nothing. + +### Unicast injection is a 40× cliff + +Chasing an unexplained 54 fps. Four variables differed at once, so they were +bisected one at a time: + +| configuration | fps | Mbit/s | +|---|---|---| +| bcast non-QoS wcid=ff | 2106 | 23.59 | +| bcast QoS wcid=ff | 3037 | 34.01 | +| **ucast** QoS wcid=ff | **75** | **0.83** | +| ucast QoS wcid=1 | 51 | 0.57 | +| ucast QoS wcid=1 + AMPDU | 85 | 0.95 | +| bcast QoS wcid=1 + AMPDU | 3217 | 36.03 | + +Unicast alone is the cliff. The MAC arms an ACK timeout for a peer that never +answers; clearing `txwi.ack_ctl`'s REQ bit does not prevent it, and neither +does setting the QoS Ack Policy to No Ack (tried, no change). A one-way +injected link must address frames to broadcast or multicast. + +## Hardware ACK responder + +No separate responder-address register exists here, so arming retargets the +port identity (`MT_MAC_ADDR_DW0/DW1`) with `MT_AUTO_RSP_EN` gating the engine. +Following this project's own finding that closing the gate alone does not +silence a die matching on identity, clear moves the identity back. + +Stimulus: an 8812AU sending ACK-requiring QoS data to the responder address and +receiving concurrently (`DEVOURER_TX_WITH_RX=thread`). + +| responder | frames the stimulus radio received | +|---|---| +| off (control) | **0** | +| armed | **3500+, every one `len` 14** | + +An ACK is 10 bytes + FCS. The armed count tracks the 3660 frames the stimulus +sent roughly 1:1. + +The first attempt used this project's retry-collapse method and returned +**INCONCLUSIVE, not a pass** — `txdemo` injects without retries, so there was +nothing to collapse. The gate says so rather than reporting success. + +## RX + +Verified against ambient traffic from equipment not under our control: + +``` +len=248 CCK mcs=0 nss=1 bw=20 sgi=0 ldpc=0 stbc=0 rssi=[-68,-66] +len=28 OFDM mcs=4 nss=1 bw=20 sgi=0 ldpc=0 stbc=0 rssi=[-45,-45] +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. + +### This part does not deliver the FCS + +Measured, because it constrains integration rather than being a detail. +`Packet::Data` in `src/RxPacket.h` is documented as the full 802.11 frame +**including the trailing FCS**, and every Realtek parser here honours that. +MT7612U cannot: the MAC strips it. + +Four to seven bytes do sit past `MPDU_LEN` in every RX buffer — over 4263 +ambient frames the tail was 4 bytes on 3375 of them and 5-7 on the rest, +which reads exactly like a fixed 4-byte field plus USB 4-byte alignment. So +the obvious move is to hand the consumer `len + 4` and call it the FCS. + +It is not the FCS. CRC-32 over the MPDU matched those four bytes on **0 of +4263** frames. (The probe's own CRC-32 was checked against the standard +`"123456789"` -> `0xcbf43926` vector first, so the negative means something.) +They are the FCE info trailer: mt76's `mt76u_get_rx_entry_len()` computes +`min_len = MT_DMA_HDR_LEN + MT_RX_RXWI_LEN + MT_FCE_INFO_LEN`, and +`dma.h:48` defines `MT_FCE_INFO_LEN 4`. + +A consumer that trims four bytes at its protocol boundary — which the +`Packet::Data` contract invites, and which `tools/bf_report_decode.py` +already does — would eat four bytes of payload off every frame. Whatever +shape integration takes, this divergence has to be declared at the boundary, +not smoothed over. + +## USB bulk aggregation + +`MT_TXD_INFO_NEXT_VLD` chains blocks inside one bulk-OUT transfer. mt76 never +sets it, so this is the one mechanism here that is not a port and needed +on-air proof: **400 frames in 25 transfers (16 per transfer), 352 aired** at +rate 19 / len 44. + +## Async rings bought no throughput + +Stated plainly because it would be easy to claim otherwise: + +| | fps | Mbit/s | CPU | submit mean | +|---|---|---|---|---| +| sync, saturated | 3040 | 34.05 | 4.2 % | 0.329 ms | +| async, saturated | 3044 | 34.10 | 4.8 % | 0.328 ms | +| sync, paced ~750 fps | 730 | — | 2.3 % | 0.065 ms | +| async, paced ~750 fps | 754 | — | 2.6 % | **0.021 ms** | + +At saturation both sit at the same number because that is the airtime of one +frame. What the rings bought is the callback RX path, concurrent TX+RX on one +handle (2560 fps TX alongside RX, no errors), and 3× lower submit latency below +saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. + +## Other measured items + +- **40 MHz**: 242 of 242 frames reached a 40 MHz witness reporting `bw=1`. +- **TSF**: 200231 µs over a 200000 µs sleep. Two traps — the counter does not + run until `MT_BEACON_TIME_CFG_TIMER_EN` is set (part of beacon config, which + an injector otherwise skips), and **DW0 is the low word**. mt76 assembles + `(dw0 << 32) | dw1`, which is backwards, but that value only feeds a + `dev_dbg()` print upstream so the bug is never exercised. Ported faithfully + it produced a clock advancing 8.6e14 "µs" per 200 ms. +- **Register-stream equivalence**: our EP0 write stream during bring-up was + diffed against a `usbmon` capture of the kernel driver's own probe. 522 + kernel writes vs 521 ours, 376 common addresses, one final-value mismatch + (`MT_MAC_BSSID_DW1`, which the kernel's beacon config rewrites afterwards), + and six kernel-only addresses — all beacon configuration. **Zero addresses + we write that the kernel does not.** + +## What it cannot do + +**Channel switch is 20–100× slower than the Realtek parts.** + +| path | measured, mean of 8 | +|---|---| +| full switch, with the firmware calibration burst | 526 ms | +| calibration skipped | 48 ms | +| this project on Realtek, for reference | 0.5–2.5 ms | + +Our implementation has headroom (a 7.5 ms inter-command sleep, register writes +that could batch through `CMD_RANDOM_WRITE`, an `MCU_CAL_RXDCOC` still running +on the fast path), but the floor is four MCU request/response round trips over +bulk endpoints plus firmware time. A tuned version might reach 10–20 ms. It +will not reach 2 ms. **FHSS and per-packet hopping are out of reach for this +part**; seconds-scale channel migration is not. + +**Narrowband 5/10 MHz has no encoding.** `MT_RATE_BW` is a two-bit field with +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. + +## 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 + +Stated because the numbers above are uniformly favourable. + +- **One physical unit, one sample.** No second MT7612U, no second board + revision, no vendor-driver A/B beyond the register diff. +- **One witness generation.** Every on-air number is an RTL8812AU running this + project's `rxdemo`. `paggr`, `bw` and `rate` are that implementation's + reading, not an independent instrument. +- **TX power was verified against the kernel's registers, not against radiated + power.** No spectrum analyser, no power meter. Register equality is not + dBm — the correctness claim is "identical to what mt76 programs", nothing + more. +- **The RX gain correction has never done anything.** This EEPROM carries no + gain calibration (`LNA_GAIN=0x0000`, `GRP4_5_RX_HIGH_GAIN=0xffff`), so every + correction term is zero and that code path is unexercised. An adapter with a + programmed EEPROM would run code that has never run. +- **The ACK identification rests on length, the 1:1 count and a zero control + arm.** The RA bytes inside those ACKs were not read — the dump path omits + control-frame bodies. +- **No cold boot was ever tested.** No hub on the test host supports per-port + power switching, so every run started from a chip something else had already + initialised. `reset_wlan` + `power_on` demonstrably does *not* clear the + firmware-running bit. +- **The 48 ms fast retune is our implementation, not the floor.** The floor is + unmeasured. +- **Nothing here runs in CI.** The offline tests above and the table + generator's `--check` exist and pass locally, but no workflow invokes them, + and the subtree is not reachable from `CMakeLists.txt`. No sanitizer build, + no lifecycle soak of the kind the Realtek backends carry. +- **80 MHz, VHT on air, and NSS=2 are unexercised.** The rate word encodes + them and the RX path decodes them; neither has been transmitted. +- **The RX path cannot satisfy `Packet::Data`'s FCS contract** (see above). + That is a measured hardware limit, not something the port can fix. + +## Open list + +Ordered, and honest about which are unknowns rather than typing: + +1. `IRtlDevice` implementation, `WiFiDriver` dispatch, `DeviceConfig` plumbing, + `CMakeLists.txt`, `ctest` cells. None of this exists. +2. `mt76x2_phy_tssi_compensate()` — periodic temperature correction. Without + it output power drifts with die temperature. +3. Cold-boot verification on a host with switchable USB power. +4. A second sample, and a second witness generation. +5. 80 MHz; VHT and NSS=2 on air. +6. Retune tuning — batch registers via `CMD_RANDOM_WRITE`, drop the inter-command + sleep, skip the `RXDCOC` on a fast path. Worth doing only if 10–20 ms is + useful to someone. +7. Whether the single MCS6 frame in the rate-LUT control arm (1 of 84) is a + witness decode artefact or a real fallback. Unexplained. diff --git a/reference/README.md b/reference/README.md index fd46bd9e..495a3562 100644 --- a/reference/README.md +++ b/reference/README.md @@ -1,10 +1,14 @@ -# reference/ — vendor kernel drivers (git submodules) +# reference/ — reference kernel drivers (git submodules) devourer is a userspace re-implementation of Realtek's out-of-tree Wi-Fi drivers. The vendor kernel modules live here as **git submodules** so every developer works against the same fork + commit devourer treats as ground-truth, without committing driver source into devourer's own history. +One entry is not a Realtek vendor drop: `mt76` is the mainline Linux driver for +the MediaTek parts, pinned for the same reason — it is the ground-truth the +MT7612U backend is derived from and generated against. + ## Fetching ```sh @@ -28,6 +32,7 @@ upstream history. | `rtl8852bu` | `josephnef/rtl8852bu` (fork of morrownr/rtl8852bu-20250826) | default | Kestrel — 8852BU/8832BU (Wi-Fi 6) | | `rtl8852cu` | `josephnef/rtl8852cu` (fork of morrownr/rtl8852cu-20251113) | default | Kestrel — 8852CU/8832CU (Wi-Fi 6) | | `rtl8733bu-20230626` | `libc0607/rtl8733bu-20230626` | `v5.13.0.1` | RTL8733B — 8731BU/8733BU (HALMAC 87xx) | +| `mt76` | `openwrt/mt76` | `master` @ `be5ce79` | MediaTek — MT7612U (mainline, not a vendor drop) | ## Why these forks @@ -75,6 +80,13 @@ upstream history. `hal/` is generated from it and the already hardware-validated artifacts were not replaced with untested ones. `docs/rtl8733b.md` records what was compared. +- **openwrt/mt76 for MT7612U.** The MediaTek parts have no out-of-tree vendor + drop to mirror: mt76 *is* the reference, maintained in mainline Linux and + BSD-3-Clause-Clear rather than GPL-2-only, which is why `src/mt7612u/` can + carry ported sequences at all. Pinned at `be5ce79` so + `tools/extract_mt7612u_tables.py --check` and every register sequence + transcribed into `src/mt7612u/` stay re-verifiable from a fresh checkout. + ## Consumers - `tools/extract_*.py` read PHY / firmware / txpwr tables out of these trees to diff --git a/reference/mt76 b/reference/mt76 new file mode 160000 index 00000000..be5ce791 --- /dev/null +++ b/reference/mt76 @@ -0,0 +1 @@ +Subproject commit be5ce7910521492d4a2e4ce7ee3843680a46c047 diff --git a/src/mt7612u/.gitignore b/src/mt7612u/.gitignore new file mode 100644 index 00000000..1f879736 --- /dev/null +++ b/src/mt7612u/.gitignore @@ -0,0 +1,9 @@ +bringup +tests/api_link +tests/frame_shape +tests/field_macros +*.o +*.d +firmware/ +wrlog.txt +mculog.txt diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile new file mode 100644 index 00000000..e0b93558 --- /dev/null +++ b/src/mt7612u/Makefile @@ -0,0 +1,46 @@ +# Standalone: this subtree is not reachable from the project's CMakeLists.txt. +# tests/api_link.c links against the public header alone, which is what keeps +# that an honest test of the exported surface. +CC ?= gcc +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter +# -MMD -MP: without header deps, editing internal.h leaves stale objects with +# the old struct layout linked against new ones. That corrupts the device +# struct at runtime and presents as a wild pointer, not a build problem. +DEPFLAGS = -MMD -MP +LDLIBS = -lusb-1.0 -lpthread +LIBSRCS = $(wildcard *.c) +TESTS = tests/api_link tests/frame_shape tests/field_macros +SRCS = $(LIBSRCS) tools/bringup.c $(TESTS:=.c) +OBJS = $(SRCS:.c=.o) +LIBOBJS = $(LIBSRCS:.c=.o) +DEPS = $(OBJS:.o=.d) + +all: bringup $(TESTS) + +bringup: $(LIBOBJS) tools/bringup.o + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) + +# Links against the public header only, so a declaration with no definition +# is a link error here rather than a surprise at the first caller. +tests/api_link: $(LIBOBJS) tests/api_link.o + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) + +tests/frame_shape: $(LIBOBJS) tests/frame_shape.o + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) + +tests/field_macros: $(LIBOBJS) tests/field_macros.o + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) + +%.o: %.c + $(CC) $(CFLAGS) $(DEPFLAGS) -I. -Iinclude -c -o $@ $< + +# No hardware and no privileges needed: safe to run anywhere. `bringup` is a +# dependency but is not run: nothing else compiles it, so without it here a +# change that breaks a gate ships and no test notices. +check: $(TESTS) bringup + @for t in $(TESTS); do ./$$t || exit 1; done + +clean: + rm -f $(OBJS) $(DEPS) bringup $(TESTS) +-include $(DEPS) +.PHONY: all check clean diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md new file mode 100644 index 00000000..4194a3c8 --- /dev/null +++ b/src/mt7612u/README.md @@ -0,0 +1,136 @@ +# src/mt7612u — MediaTek MT7612U + +**Not reachable from `CMakeLists.txt` yet.** This subtree is a complete, +self-contained C library for the part — a public header, its own transport, no +dependency on `RtlAdapter` — plus the bring-up harness that produced every +measurement in `docs/mt7612u.md`. Wiring it in behind `IRtlDevice` is a +follow-up PR; nothing in the shipped library links against this today. + +It builds and tests on its own: + +```sh +make -C src/mt7612u # -> src/mt7612u/bringup +make -C src/mt7612u check # offline tests: no hardware, no privileges +sudo ./src/mt7612u/bringup regs +``` + +Measurements, methods and limits: [`../../docs/mt7612u.md`](../../docs/mt7612u.md). + +## Layout + +| file | what | +|---|---| +| `usb.c` | libusb transport: EP0 vendor register access, sync bulk, open/claim/reset | +| `async.c` | event thread, 16-deep RX ring, 32-slot TX pool | +| `mcu.c` | in-band MCU command framing (EP 8 out, EP 5 in, 4-bit sequence) | +| `fw.c` | ROM patch + ILM/DLM firmware upload | +| `eeprom.c` | 512-byte EEPROM: identity, TX power tables, RX gain | +| `init.c` | power-on, MAC initvals, mac_start/stop, EP-4 flush | +| `phy.c` | band/bandwidth/TX power registers, channel + calibration sequence | +| `tx.c` | TXWI + TXINFO construction | +| `rx.c` | RXWI parse, per-chain RSSI, rate decode | +| `radiotap.c` | `send_packet` / `send_packets` (USB chaining via `NEXT_VLD`) | +| `caps.c` | TSF, capability descriptor, ACK responder | +| `tools/bringup.c` | one subcommand per verified gate | +| `tests/` | offline tests (`make check`): public-API link, frame shapes | +| `initvals.h` | **generated** — see Provenance | + +## The receiver must never run undrained + +Enabling MAC RX with nothing reading the bulk-IN endpoint wedges this part +*below* the USB level: `libusb_reset_device`, the sysfs `authorized` toggle +and rebinding the kernel driver all fail to recover it, and only a physical +replug does. So `mt_mac_start()` takes the receiver as an explicit argument, +`mt7612u_start()` enables RX only when `mt7612u_rx_start()` is already +running, and every gate that turns RX on starts the ring *first*. + +## Portability + +Done here, because these are correctness issues regardless of compiler: + +- `FIELD_PREP`/`FIELD_GET` no longer use `__builtin_ctz`. MSVC has no such + builtin, and its `_BitScanForward` takes an out-parameter, so it cannot + appear in a constant expression - which these must be, since `FIELD_PREP` + initialises static tables. `MT_CTZ` is a constant expression everywhere and + folds to one instruction. `tests/field_macros` checks it against the + builtin over all 32 single-bit and all 528 contiguous masks, and fails to + compile if it ever stops being constant-foldable. +- The shift macro was named `_SHIFT`. Leading underscore plus a capital is + reserved to the implementation in every scope. +- `` (this project's spelling) is tried first, with the + distribution's `` as the fallback. + +**Not** done here: `async.c` uses pthreads and `usb.c` uses `nanosleep` / +`clock_gettime`. This project has no C threading or time shim - its shim is +the C++ standard library, which every other backend uses directly. Building +a throwaway C shim now would be deleted at integration, so those two files +keep POSIX until the subtree joins the build. They are the only two. + +## Firmware + +Needs `mt7662_rom_patch.bin` and `mt7662.bin` from `linux-firmware` +(`/lib/firmware/mediatek/`, zstd-compressed on most distributions). Not +vendored here. Point `bringup` at a directory holding the decompressed pair: + +```sh +zstd -d /lib/firmware/mediatek/mt7662{,_rom_patch}.bin.zst -o firmware/ +``` + +## Gates + +Each subcommand is a hardware check that fails loudly, in dependency order: + +``` +regs registers + EEPROM round-trip chan channel set, 20 MHz +fw ROM patch + firmware + MCU ack tx inject at a fixed rate +init full bring-up + register-stream log rx monitor receive +caps capabilities, TSF, 40 MHz soak sync vs async throughput +pwr TX power vs the kernel's values ampdu aggregation A/B +gateg per-frame rate control ack ACK responder (needs a stimulus) +rtap send_packet / send_packets hop channel-switch cost +``` + +`make` here builds it as `./bringup`, which is what the hardware notes use. +The integration PR adds a CMake target for the same source, named +`mt7612uprobe` to sit beside `pcieprobe` / `kestrelprobe` / `rtl8733bprobe`, so +the chip-specific tool is not the one part of this backend that only a second +build system can produce. + +`sweep`, `coding` and `vht` take a width as their fourth argument, in the +`MT7612U_BW_*` numbering — `0` = 20, `1` = 40, `2` = 80 MHz: + +```sh +./bringup sweep 149 120 2 # VHT ladder at 80 MHz, control channel 149 +``` + +The witness has to listen at the same width (`DEVOURER_BW=40|80` for +devourer's own `rxdemo`). A 20 MHz receiver decodes *none* of an 80 MHz +frame — which makes it a good negative control and a misleading oracle. + +At 80 MHz the HT ladder is skipped: 802.11n has no 80 MHz, so a rate word +naming `PHY=HT` with `BW=80` is not a wide HT frame, it is an unspecified one. + +## Provenance + +Register sequences and descriptor layouts are derived from `openwrt/mt76` +(`mt76x2/`, `mt76x02*`, `usb.c`), BSD-3-Clause-Clear, Copyright (C) 2016 Felix +Fietkau, (C) 2018 Lorenzo Bianconi / Stanislaw Gruszka. Files carrying ported +sequences keep that notice. The tree is pinned as `reference/mt76` at commit +`be5ce79`. + +`initvals.h` is **generated** from it, not transcribed: + +```sh +tools/extract_mt7612u_tables.py # regenerate +tools/extract_mt7612u_tables.py --check # byte-compare the checked-in file +``` + +The generator resolves the symbolic register names against `mt76x02_regs.h` and +evaluates the four `DEFAULT_PROT_CFG_*` macros, so a mistyped address cannot +survive as a plausible-looking number. It reproduces the previously hand-typed +table byte for byte, all sixty rows. + +Two things here are **not** ports and were proven on air rather than copied: +the `MT_TXD_INFO_NEXT_VLD` USB chaining in `radiotap.c`, and the ACK responder +in `caps.c`. One thing copied from mt76 was **wrong** — see the TSF note in the +docs. diff --git a/src/mt7612u/async.c b/src/mt7612u/async.c new file mode 100644 index 00000000..aa8b84f0 --- /dev/null +++ b/src/mt7612u/async.c @@ -0,0 +1,359 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Async TX and RX rings over libusb, plus the event thread that drives them. + * This is what mt76 gets from URBs and NAPI; here it is one pthread calling + * libusb_handle_events plus two pools of libusb_transfer. + * + * RX: MT_RX_RING transfers permanently in flight on EP 4 IN. A completion + * parses the RXWI and resubmits immediately, so the endpoint is never idle - + * which is also what keeps the chip from wedging (BRINGUP-RESULTS.md). + * + * TX: a pool of MT_TX_RING transfers on EP 4 OUT with a free list. Submitting + * does not wait for the wire; mt7612u_tx only blocks when every slot is in + * flight, which is the back-pressure point. + */ +#include +#include +#include "internal.h" + +/* + * Every field shared between the event thread and the caller lives under + * a->lock. `volatile` alone is not a memory model: it orders nothing and + * makes no read-modify-write atomic, and rx_inflight is decremented from the + * completion callback while mt_async_stop() waits on it. + */ +static int locked_get(struct mt_async *a, const int *field) +{ + int v; + + pthread_mutex_lock(&a->lock); + v = *field; + pthread_mutex_unlock(&a->lock); + return v; +} + +static void *evt_thread(void *arg) +{ + struct mt7612u_dev *d = arg; + struct mt_async *a = d->a; + struct timeval tv = { .tv_sec = 0, .tv_usec = 50000 }; + + while (locked_get(a, &a->running)) + libusb_handle_events_timeout_completed(d->ctx, &tv, NULL); + return NULL; +} + +static void LIBUSB_CALL rx_done(struct libusb_transfer *t) +{ + struct mt_slot *s = t->user_data; + struct mt7612u_dev *d = s->d; + struct mt_async *a = s->a; + int resubmit; + + if (t->status == LIBUSB_TRANSFER_COMPLETED) { + const uint8_t *frame = NULL; + struct mt7612u_rx_info info; + int len = mt_rx_parse(d, t->buffer, t->actual_length, &frame, &info); + + if (len <= 0) { + /* A frame the parser rejected used to move no counter at + * all, which is indistinguishable from one never sent. + * + * This does NOT cover the oversize case, and it was + * measured not to: frames above the MAC's MT_MAX_LEN_CFG + * ceiling never reach here, never complete a transfer and + * never raise rx_err. The MAC discards them before USB, so + * that loss is invisible from this layer by construction - + * see mt7612u_caps.max_mpdu_rx. What this counts is a + * short or malformed transfer. */ + pthread_mutex_lock(&a->lock); + a->rx_dropped++; + pthread_mutex_unlock(&a->lock); + } else { + pthread_mutex_lock(&a->lock); + a->rx_frames++; + pthread_mutex_unlock(&a->lock); + /* Outside the lock: a callback is allowed to transmit, + * and mt_async_tx_submit() takes this same mutex. */ + if (a->cb) + a->cb(a->cb_user, frame, (size_t)len, &info); + } + } else if (t->status != LIBUSB_TRANSFER_CANCELLED) { + pthread_mutex_lock(&a->lock); + a->rx_err++; + pthread_mutex_unlock(&a->lock); + } + + resubmit = locked_get(a, &a->rx_active) && + t->status != LIBUSB_TRANSFER_CANCELLED; + if (resubmit && libusb_submit_transfer(t) == 0) + return; + + /* Not resubmitted: this transfer is now owned by us again. */ + pthread_mutex_lock(&a->lock); + if (resubmit) + a->rx_err++; + a->rx_inflight--; + pthread_cond_broadcast(&a->cv); + pthread_mutex_unlock(&a->lock); +} + +static void LIBUSB_CALL tx_done(struct libusb_transfer *t) +{ + struct mt_slot *s = t->user_data; + struct mt_async *a = s->a; + + pthread_mutex_lock(&a->lock); + if (t->status == LIBUSB_TRANSFER_COMPLETED && + t->actual_length == t->length) + a->tx_done_n++; + else + a->tx_err++; + a->tx_busy[s->idx] = 0; + a->tx_inflight--; + pthread_cond_broadcast(&a->cv); + pthread_mutex_unlock(&a->lock); +} + +int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) +{ + if (d->transfers_stranded) { + ERR("async start refused: a previous ring's transfers are still " + "owned by libusb on these endpoints"); + return -1; + } + struct mt_async *a; + + if (d->a) return 0; + a = calloc(1, sizeof *a); + if (!a) return -1; + d->a = a; + a->cb = cb; + a->cb_user = user; + pthread_mutex_init(&a->lock, NULL); + pthread_cond_init(&a->cv, NULL); + + for (int i = 0; i < MT_TX_RING; i++) { + a->tx_slot[i].d = d; + a->tx_slot[i].a = a; + a->tx_slot[i].idx = i; + a->tx[i] = libusb_alloc_transfer(0); + if (!a->tx[i]) goto fail; + } + for (int i = 0; i < MT_RX_RING; i++) { + a->rx_slot[i].d = d; + a->rx_slot[i].a = a; + a->rx_slot[i].idx = i; + a->rx[i] = libusb_alloc_transfer(0); + if (!a->rx[i]) goto fail; + } + + a->running = 1; + if (pthread_create(&a->evt, NULL, evt_thread, d)) { a->running = 0; goto fail; } + a->evt_started = 1; + + if (cb) { + pthread_mutex_lock(&a->lock); + a->rx_active = 1; + pthread_mutex_unlock(&a->lock); + for (int i = 0; i < MT_RX_RING; i++) { + libusb_fill_bulk_transfer(a->rx[i], d->h, MT_EP_IN_PKT_RX, + a->rx_buf[i], MT_RX_BUFSZ, + rx_done, &a->rx_slot[i], 0); + if (libusb_submit_transfer(a->rx[i])) { + ERR("could not submit RX transfer %d", i); + goto fail; + } + pthread_mutex_lock(&a->lock); + a->rx_inflight++; + pthread_mutex_unlock(&a->lock); + } + LOG("async: %d RX transfers in flight, %d TX slots", + MT_RX_RING, MT_TX_RING); + } else { + LOG("async: %d TX slots (RX ring not started)", MT_TX_RING); + } + return 0; + +fail: + mt_async_stop(d); + return -1; +} + +/* + * Tear the rings down. The ordering matters and the failure mode is not a + * leak but a use-after-free: libusb owns a submitted transfer until its + * callback runs, so nothing may be freed while it is still in flight. + * + * A wedged chip is the case that makes this real - TX URBs that never + * complete are exactly the situation this driver's own notes describe - so + * both rings are cancelled, both are waited on, and if either still has + * transfers outstanding when the deadline expires we deliberately leak the + * whole mt_async rather than free memory the kernel may still write into. + */ +void mt_async_stop(struct mt7612u_dev *d) +{ + struct mt_async *a = d->a; + int stuck_tx, stuck_rx; + + if (!a) return; + + pthread_mutex_lock(&a->lock); + a->rx_active = 0; + pthread_mutex_unlock(&a->lock); + + /* Cancel *both* rings. Cancelling only RX leaves TX transfers owned by + * libusb, and the wait below would then time out with them in flight. */ + for (int i = 0; i < MT_RX_RING; i++) + if (a->rx[i]) libusb_cancel_transfer(a->rx[i]); + for (int i = 0; i < MT_TX_RING; i++) + if (a->tx[i]) libusb_cancel_transfer(a->tx[i]); + + /* The event thread is still running, so completions keep arriving. */ + pthread_mutex_lock(&a->lock); + for (int spins = 0; (a->tx_inflight || a->rx_inflight) && spins < 200; spins++) { + struct timespec ts; + + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 10000000; + if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; } + pthread_cond_timedwait(&a->cv, &a->lock, &ts); + } + stuck_tx = a->tx_inflight; + stuck_rx = a->rx_inflight; + a->running = 0; + pthread_mutex_unlock(&a->lock); + + if (a->evt_started) + pthread_join(a->evt, NULL); + + d->a = NULL; + if (stuck_tx || stuck_rx) { + /* Leaking the ring is the safe half. The other half is that libusb + * still owns those transfers while the event thread has just been + * joined, so nothing will ever complete them - and releasing the + * interface, closing the handle or exiting the context underneath + * them is undefined. Mark the device stranded: mt_close() then + * leaks the USB objects too rather than freeing what libusb holds, + * and mt_async_start() refuses to submit a second ring onto the + * same endpoints. Consistent with the leak, not a new policy. */ + d->transfers_stranded = 1; + ERR("async stop: %d TX and %d RX transfers still in flight after 2 s " + "- leaking the ring, and the USB handle with it, rather than " + "freeing memory libusb still owns", stuck_tx, stuck_rx); + return; + } + + for (int i = 0; i < MT_TX_RING; i++) + if (a->tx[i]) libusb_free_transfer(a->tx[i]); + for (int i = 0; i < MT_RX_RING; i++) + if (a->rx[i]) libusb_free_transfer(a->rx[i]); + pthread_mutex_destroy(&a->lock); + pthread_cond_destroy(&a->cv); + free(a); +} + +/* + * Hand a fully framed buffer to the TX pool. Blocks only when every slot is + * in flight. Returns 0 on submit, -1 on error. + */ +int mt_async_tx_submit(struct mt7612u_dev *d, const uint8_t *buf, int len) +{ + struct mt_async *a = d->a; + int idx = -1, rc; + + if (!a || len > MT_TX_BUFSZ) return -1; + + pthread_mutex_lock(&a->lock); + for (;;) { + /* A teardown must not leave a caller parked here forever. */ + if (!a->running) { pthread_mutex_unlock(&a->lock); return -1; } + for (int i = 0; i < MT_TX_RING; i++) + if (!a->tx_busy[i]) { idx = i; break; } + if (idx >= 0) break; + pthread_cond_wait(&a->cv, &a->lock); + } + a->tx_busy[idx] = 1; + a->tx_inflight++; + pthread_mutex_unlock(&a->lock); + + memcpy(a->tx_buf[idx], buf, (size_t)len); + libusb_fill_bulk_transfer(a->tx[idx], d->h, MT_EP_OUT_AC_BE, + a->tx_buf[idx], len, tx_done, + &a->tx_slot[idx], 1000); + rc = libusb_submit_transfer(a->tx[idx]); + + pthread_mutex_lock(&a->lock); + if (rc) { + a->tx_busy[idx] = 0; + a->tx_inflight--; + a->tx_err++; + pthread_cond_broadcast(&a->cv); + } else { + a->tx_submitted++; + } + pthread_mutex_unlock(&a->lock); + return rc ? -1 : 0; +} + +/* + * Consistent snapshot of the ring counters. Reading the fields directly races + * with the event thread, and after a teardown that had to leak a stuck ring + * there is no ring to read at all - so callers go through this. + */ +void mt_async_stats(struct mt7612u_dev *d, struct mt_async_stats *out) +{ + struct mt_async *a = d->a; + + memset(out, 0, sizeof *out); + if (!a) return; + pthread_mutex_lock(&a->lock); + out->tx_submitted = a->tx_submitted; + out->tx_done = a->tx_done_n; + out->tx_err = a->tx_err; + out->rx_frames = a->rx_frames; + out->rx_err = a->rx_err; + out->rx_invalid = a->rx_invalid; + out->rx_dropped = a->rx_dropped; + pthread_mutex_unlock(&a->lock); +} + +int mt7612u_rx_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) +{ + if (!cb) return -1; + return mt_async_start(d, cb, user); +} + +int mt7612u_rx_stop(struct mt7612u_dev *d) +{ + mt_async_stop(d); + return 0; +} + +/* A frame whose rate word named no valid PHY. Counted under the ring's lock + * when one is running; on the synchronous bring-up path there is no ring and + * nothing to count into, which is fine - that path prints every frame. */ +void mt_async_note_invalid(struct mt7612u_dev *d) +{ + struct mt_async *a = d->a; + + if (!a) return; + pthread_mutex_lock(&a->lock); + a->rx_invalid++; + pthread_mutex_unlock(&a->lock); +} + +/* Public form of the snapshot above. */ +void mt7612u_get_stats(struct mt7612u_dev *d, struct mt7612u_stats *out) +{ + struct mt_async_stats st; + + mt_async_stats(d, &st); + out->tx_submitted = st.tx_submitted; + out->tx_done = st.tx_done; + out->tx_err = st.tx_err; + out->rx_frames = st.rx_frames; + out->rx_err = st.rx_err; + out->rx_invalid = st.rx_invalid; + out->rx_dropped = st.rx_dropped; +} diff --git a/src/mt7612u/caps.c b/src/mt7612u/caps.c new file mode 100644 index 00000000..e3875601 --- /dev/null +++ b/src/mt7612u/caps.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* TSF access and the static capability descriptor. */ +#include +#include "internal.h" + +/* + * DW0 is the LOW word. + * + * mt76 assembles it the other way round, in mt76x02u_restart_pre_tbtt_timer() + * (mt76x02_usb_core.c:155-158): + * + * dw0 = mt76_rr(dev, MT_TSF_TIMER_DW0); + * dw1 = mt76_rr(dev, MT_TSF_TIMER_DW1); + * tsf = (u64)dw0 << 32 | dw1; + * dev_dbg(dev->mt76.dev, "TSF: %llu us TBTT %u us\n", tsf, tbtt); + * + * `tsf` there is consumed only by the dev_dbg() on the next line, so the order + * is never exercised and the mistake has survived upstream. Copying it here + * produced a clock that advanced by 8.6e14 "us" per 200 ms; measured, + * (DW1 << 32) | DW0 gives 200159 us over a 200000 us sleep. The `caps` gate + * prints both orders against a known sleep so the claim is re-checkable on + * any sample. + */ +uint64_t mt7612u_read_tsf(struct mt7612u_dev *d) +{ + uint32_t dw0 = mt_rr(d, MT_TSF_TIMER_DW0); + uint32_t dw1 = mt_rr(d, MT_TSF_TIMER_DW1); + + return ((uint64_t)dw1 << 32) | dw0; +} + +void mt7612u_write_tsf(struct mt7612u_dev *d, uint64_t tsf) +{ + mt_wr(d, MT_TSF_TIMER_DW0, (uint32_t)tsf); + mt_wr(d, MT_TSF_TIMER_DW1, (uint32_t)(tsf >> 32)); +} + +void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) +{ + memset(c, 0, sizeof *c); + c->chip_name = "MT7612U"; + c->rev = d->rev; + c->nss_rx = c->nss_tx = (uint8_t)((d->chainmask & 0xf) > 1 ? 2 : 1); + /* 20, 40 and 80 MHz. Note the mask is per width, not a ceiling: the + * widths above 20 are 5 GHz-only on this backend, because a bare + * control-channel number cannot name the secondary side in 2.4 GHz + * except for channels 4-11. mt7612u_set_channel() refuses the rest. */ + c->bw_mask = 0x7; /* 20, 40 and 80 MHz */ + /* TX is this backend's buffer, RX is the MAC's MT_MAX_LEN_CFG ceiling + * minus the FCS. See the struct for how each was measured. */ + c->max_mpdu_tx = MT_TX_BUF_MAX - 32; + c->max_mpdu_rx = d->max_mpdu_rx; + c->band_5g_min_mhz = 5180; c->band_5g_max_mhz = 5825; + c->band_2g_min_mhz = 2412; c->band_2g_max_mhz = 2484; + c->ampdu_tx = 1; + c->per_chain_rssi = 1; + c->narrowband = 0; /* MT_RATE_BW has no 5/10 MHz encoding */ + c->fast_retune = 0; /* measured 48 ms even with calibration skipped */ +} + +/* + * Hardware ACK responder. + * + * On this MAC the immediate-response engine answers frames whose address 1 + * matches MT_MAC_ADDR_DW0/DW1, gated by MT_AUTO_RSP_EN. There is no separate + * "responder address" register as on the Realtek parts, so arming means + * retargeting the port identity - and, following devourer's own finding that + * closing the gate alone does not stop a die that matches on identity, the + * clear path moves the identity back rather than only clearing the gate. + */ +int mt7612u_set_ack_responder(struct mt7612u_dev *d, const uint8_t mac[6]) +{ + uint32_t dw0, rb; + + if (!mac || (mac[0] & 0x01)) { + ERR("ack responder address must be unicast"); + return -1; + } + + if (!d->ack_saved) { + memcpy(d->ack_saved_mac, d->macaddr, 6); + d->ack_saved = 1; + } + + dw0 = (uint32_t)mac[0] | ((uint32_t)mac[1] << 8) | + ((uint32_t)mac[2] << 16) | ((uint32_t)mac[3] << 24); + mt_wr(d, MT_MAC_ADDR_DW0, dw0); + mt_wr(d, MT_MAC_ADDR_DW1, (uint32_t)mac[4] | ((uint32_t)mac[5] << 8) | + FIELD_PREP(MT_MAC_ADDR_DW1_U2ME_MASK, 0xff)); + mt_set(d, MT_AUTO_RSP_CFG, MT_AUTO_RSP_EN); + + /* Verify the arm. The U2ME byte of DW1 is write-only on this silicon, + * so only DW0 and the low half of DW1 can be read back. */ + rb = mt_rr(d, MT_MAC_ADDR_DW0); + if (rb != dw0) { + ERR("ack responder arm not verified: MT_MAC_ADDR_DW0 %08x != %08x", + rb, dw0); + return -1; + } + if (!(mt_rr(d, MT_AUTO_RSP_CFG) & MT_AUTO_RSP_EN)) { + ERR("ack responder arm not verified: MT_AUTO_RSP_EN clear"); + return -1; + } + return 0; +} + +void mt7612u_clear_ack_responder(struct mt7612u_dev *d) +{ + if (!d->ack_saved) + return; + + /* Move the identity off the responder address first: on a MAC that + * matches on address 1, clearing the gate alone leaves it answering + * for whatever address is still programmed. + * + * MT_AUTO_RSP_EN is deliberately NOT cleared here. mt_init_hardware() + * writes MT_AUTO_RSP_CFG = 0x13 (init.c), and MT_AUTO_RSP_EN is BIT(0), + * so the gate is already on before any caller arms a responder - the + * mt_set() in mt7612u_set_ack_responder() is a no-op on it. Clearing it + * here would leave the device in a state its own init never produces; + * restoring the address is what actually stops it answering. */ + { + const uint8_t *a = d->ack_saved_mac; + + mt_wr(d, MT_MAC_ADDR_DW0, (uint32_t)a[0] | ((uint32_t)a[1] << 8) | + ((uint32_t)a[2] << 16) | ((uint32_t)a[3] << 24)); + mt_wr(d, MT_MAC_ADDR_DW1, (uint32_t)a[4] | ((uint32_t)a[5] << 8) | + FIELD_PREP(MT_MAC_ADDR_DW1_U2ME_MASK, 0xff)); + } + d->ack_saved = 0; +} diff --git a/src/mt7612u/eeprom.c b/src/mt7612u/eeprom.c new file mode 100644 index 00000000..b286275d --- /dev/null +++ b/src/mt7612u/eeprom.c @@ -0,0 +1,300 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * EEPROM read and parse. The whole 512-byte image comes back through 128 + * EP0 vendor reads (bRequest 0x09) - there is no hardware EEPROM state + * machine to drive. Ported from mt76/mt76x2/usb_init.c + mt76x02_eeprom.c. + */ +#include +#include "internal.h" + +uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off) +{ + if (off + 1 >= sizeof d->eeprom) + return 0; + return (uint16_t)d->eeprom[off] | ((uint16_t)d->eeprom[off + 1] << 8); +} + +int mt_eeprom_init(struct mt7612u_dev *d) +{ + /* Do NOT treat 0xffffffff as a read error by VALUE: unprogrammed EEPROM + * cells legitimately read as all-ones (this image is 0xff from 0x010 + * onward for a stretch), so the value alone cannot tell a blank cell + * from a failed transfer. + * + * The accumulator can. mt_rr() returns ~0u on failure AND bumps + * d->io_err, so bracketing the slurp separates the two cases without + * giving up the blank-cell behaviour. This matters more here than + * anywhere else in the driver: a failure outside the two cells checked + * below leaves all-ones power and calibration bytes in memory, and the + * device then opens successfully and transmits at whatever those + * garbage terms produce. */ + mt_io_clear(d); + for (unsigned i = 0; i + 4 <= MT7612U_EEPROM_SIZE; i += 4) { + uint32_t v = mt_rr(d, EEP_ADDR(i)); + + d->eeprom[i] = v & 0xff; + d->eeprom[i + 1] = (v >> 8) & 0xff; + d->eeprom[i + 2] = (v >> 16) & 0xff; + d->eeprom[i + 3] = (v >> 24) & 0xff; + } + if (mt_io_errors(d)) { + ERR("EEPROM read failed on %u of %u transfers - refusing to " + "calibrate from a partial image", + mt_io_errors(d), MT7612U_EEPROM_SIZE / 4); + return -1; + } + + if (mt_ee(d, MT_EE_CHIP_ID) != 0x7612) { + ERR("EEPROM chip id 0x%04x != 0x7612 - read path is wrong", + mt_ee(d, MT_EE_CHIP_ID)); + return -1; + } + + memcpy(d->macaddr, d->eeprom + MT_EE_MAC_ADDR, 6); + if (d->macaddr[0] == 0xff || (d->macaddr[0] & 1)) { + ERR("EEPROM MAC looks invalid: %02x:%02x:%02x:%02x:%02x:%02x", + d->macaddr[0], d->macaddr[1], d->macaddr[2], + d->macaddr[3], d->macaddr[4], d->macaddr[5]); + return -1; + } + + /* mt76x02_init_device() hardcodes this for every mt76x2 - it is not + * derived from NIC_CONF_0. 0x202 = 2T2R. */ + d->chainmask = 0x202; + + LOG("EEPROM: chip 0x%04x MAC %02x:%02x:%02x:%02x:%02x:%02x " + "NIC_CONF 0/1/2 = %04x/%04x/%04x", + mt_ee(d, MT_EE_CHIP_ID), + d->macaddr[0], d->macaddr[1], d->macaddr[2], + d->macaddr[3], d->macaddr[4], d->macaddr[5], + mt_ee(d, MT_EE_NIC_CONF_0), mt_ee(d, MT_EE_NIC_CONF_1), + mt_ee(d, MT_EE_NIC_CONF_2)); + return 0; +} + +/* ---- TX power tables ---- + * Ported from mt76/mt76x2/eeprom.c. Note mt76x02_sign_extend()'s convention: + * the top bit SET means positive, clear means negate. Getting that backwards + * silently halves or doubles output power. + */ +static int field_valid(uint8_t v) { return v != 0 && v != 0xff; } + +static int sign_extend(uint32_t val, unsigned size) +{ + int sign = val & (1u << (size - 1)); + + val &= (1u << (size - 1)) - 1; + return sign ? (int)val : -(int)val; +} + +static int sign_extend_optional(uint32_t val, unsigned size) +{ + return (val & (1u << size)) ? sign_extend(val, size) : 0; +} + +static int8_t rate_power_val(uint8_t v) +{ + return field_valid(v) ? (int8_t)sign_extend_optional(v, 7) : 0; +} + +void mt_get_rate_power(struct mt7612u_dev *d, struct mt_rate_power *t, int band) +{ + int is_5ghz = band != 0; + uint16_t v; + + memset(t, 0, sizeof *t); + + v = mt_ee(d, MT_EE_TX_POWER_CCK); + t->cck[0] = t->cck[1] = rate_power_val(v & 0xff); + t->cck[2] = t->cck[3] = rate_power_val(v >> 8); + + v = mt_ee(d, is_5ghz ? MT_EE_TX_POWER_OFDM_5G_6M : MT_EE_TX_POWER_OFDM_2G_6M); + t->ofdm[0] = t->ofdm[1] = rate_power_val(v & 0xff); + t->ofdm[2] = t->ofdm[3] = rate_power_val(v >> 8); + + v = mt_ee(d, is_5ghz ? MT_EE_TX_POWER_OFDM_5G_24M : MT_EE_TX_POWER_OFDM_2G_24M); + t->ofdm[4] = t->ofdm[5] = rate_power_val(v & 0xff); + t->ofdm[6] = t->ofdm[7] = rate_power_val(v >> 8); + + v = mt_ee(d, MT_EE_TX_POWER_HT_MCS0); + t->ht[0] = t->ht[1] = rate_power_val(v & 0xff); + t->ht[2] = t->ht[3] = rate_power_val(v >> 8); + v = mt_ee(d, MT_EE_TX_POWER_HT_MCS4); + t->ht[4] = t->ht[5] = rate_power_val(v & 0xff); + t->ht[6] = t->ht[7] = rate_power_val(v >> 8); + v = mt_ee(d, MT_EE_TX_POWER_HT_MCS8); + t->ht[8] = t->ht[9] = rate_power_val(v & 0xff); + t->ht[10] = t->ht[11] = rate_power_val(v >> 8); + v = mt_ee(d, MT_EE_TX_POWER_HT_MCS12); + t->ht[12] = t->ht[13] = rate_power_val(v & 0xff); + t->ht[14] = t->ht[15] = rate_power_val(v >> 8); + + /* The double shift on 2.4 GHz is upstream's, not a transcription slip: + * mt76x2_get_rate_power() is byte-for-byte this, and rate_power_val() + * takes a u8 on both sides. So on 2.4 GHz these two entries decode from + * zero, i.e. VHT MCS8/9 there get no per-rate offset off the base target + * power. The EEPROM field is a 5 GHz one and 2.4 GHz VHT is an extension + * outside 802.11ac, so this is left matching mt76 deliberately - fixing + * it here would be a silent divergence from the reference this port is + * checked against. */ + v = mt_ee(d, MT_EE_TX_POWER_VHT_MCS8); + if (!is_5ghz) + v >>= 8; + t->vht[0] = t->vht[1] = rate_power_val(v >> 8); +} + +/* mt76x2_get_cal_channel_group() */ +static int cal_channel_group(int ch) +{ + if (ch >= 184 && ch <= 196) return 0; /* JAPAN */ + if (ch <= 48) return 1; /* UNII-1 */ + if (ch <= 64) return 2; /* UNII-2 */ + if (ch <= 114) return 3; /* UNII-2E-1 */ + if (ch <= 144) return 4; /* UNII-2E-2 */ + return 5; /* UNII-3 */ +} + +static void power_info_5g(struct mt7612u_dev *d, struct mt_tx_power_info *t, + uint8_t ch, int chain, unsigned offset) +{ + int delta_idx; + const uint8_t *p; + + offset += (unsigned)cal_channel_group(ch) * MT_TX_POWER_GROUP_SIZE_5G; + + if (ch >= 192) delta_idx = 4; + else if (ch >= 184) delta_idx = 3; + else if (ch < 44) delta_idx = 3; + else if (ch < 52) delta_idx = 4; + else if (ch < 58) delta_idx = 3; + else if (ch < 98) delta_idx = 4; + else if (ch < 106) delta_idx = 3; + else if (ch < 116) delta_idx = 4; + else if (ch < 130) delta_idx = 3; + else if (ch < 149) delta_idx = 4; + else if (ch < 157) delta_idx = 3; + else delta_idx = 4; + + if (offset + 5 > sizeof d->eeprom) return; + p = d->eeprom + offset; + t->chain[chain].tssi_slope = (int8_t)p[0]; + t->chain[chain].tssi_offset = (int8_t)p[1]; + t->chain[chain].target_power = (int8_t)p[2]; + t->chain[chain].delta = (int8_t)sign_extend_optional(p[delta_idx], 7); + + t->target_power = mt_ee(d, MT_EE_RF_2G_RX_HIGH_GAIN) & 0xff; +} + +static void power_info_2g(struct mt7612u_dev *d, struct mt_tx_power_info *t, + uint8_t ch, int chain, unsigned offset) +{ + int delta_idx = ch < 6 ? 3 : (ch < 11 ? 4 : 5); + const uint8_t *p; + + if (offset + 6 > sizeof d->eeprom) return; + p = d->eeprom + offset; + t->chain[chain].tssi_slope = (int8_t)p[0]; + t->chain[chain].tssi_offset = (int8_t)p[1]; + t->chain[chain].target_power = (int8_t)p[2]; + t->chain[chain].delta = (int8_t)sign_extend_optional(p[delta_idx], 7); + + t->target_power = mt_ee(d, MT_EE_RF_2G_TSSI_OFF_TXPOWER) >> 8; +} + +void mt_get_power_info(struct mt7612u_dev *d, struct mt_tx_power_info *t, + uint8_t chan, int band) +{ + uint16_t bw40 = mt_ee(d, MT_EE_TX_POWER_DELTA_BW40); + uint16_t bw80 = mt_ee(d, MT_EE_TX_POWER_DELTA_BW80); + + memset(t, 0, sizeof *t); + + if (band != 0) { + bw40 >>= 8; + power_info_5g(d, t, chan, 0, MT_EE_TX_POWER_0_START_5G); + power_info_5g(d, t, chan, 1, MT_EE_TX_POWER_1_START_5G); + } else { + power_info_2g(d, t, chan, 0, MT_EE_TX_POWER_0_START_2G); + power_info_2g(d, t, chan, 1, MT_EE_TX_POWER_1_START_2G); + } + + /* mt76 also takes chain 0's value whenever TSSI is enabled, not only + * when the EEPROM field is unprogrammed - and TSSI IS enabled on this + * part (NIC_CONF_1 TX_ALC_EN set, TEMP_TX_ALC clear). */ + if (mt_tssi_enabled(d) || !field_valid(t->target_power)) + t->target_power = (uint8_t)t->chain[0].target_power; + + t->delta_bw40 = rate_power_val(bw40 & 0xff); + t->delta_bw80 = rate_power_val(bw80 & 0xff); +} + +/* ---- RX gain / RSSI correction ---- + * mt76x2_read_rx_gain(). Without this the per-chain RSSI the RX path reports + * is the raw chip value: no LNA gain removed and no per-chain offset applied. + */ +static int8_t se4(uint8_t v) { return (int8_t)sign_extend(v, 4); } + +static uint8_t get_5g_rx_gain(struct mt7612u_dev *d, uint8_t chan) +{ + switch (cal_channel_group(chan)) { + case 0: return mt_ee(d, MT_EE_RF_5G_GRP0_1_RX_HIGH_GAIN) & 0xff; + case 1: return mt_ee(d, MT_EE_RF_5G_GRP0_1_RX_HIGH_GAIN) >> 8; + case 2: return mt_ee(d, MT_EE_RF_5G_GRP2_3_RX_HIGH_GAIN) & 0xff; + case 3: return mt_ee(d, MT_EE_RF_5G_GRP2_3_RX_HIGH_GAIN) >> 8; + case 4: return mt_ee(d, MT_EE_RF_5G_GRP4_5_RX_HIGH_GAIN) & 0xff; + default: return mt_ee(d, MT_EE_RF_5G_GRP4_5_RX_HIGH_GAIN) >> 8; + } +} + +void mt_read_rx_gain(struct mt7612u_dev *d, uint8_t chan, int band) +{ + int8_t lna_2g, lna_5g[3]; + uint16_t rssi_off, v; + uint8_t gain, lna = 0; + + gain = (band == 0) ? (uint8_t)(mt_ee(d, MT_EE_RF_2G_RX_HIGH_GAIN) >> 8) + : get_5g_rx_gain(d, chan); + + /* mt76x2_set_rx_gain_group(): two nibbles, sign-extended from 4 bits. */ + if (field_valid(gain)) { + d->cal.high_gain[0] = se4(gain & 0xf); + d->cal.high_gain[1] = se4((gain >> 4) & 0xf); + } else { + d->cal.high_gain[0] = d->cal.high_gain[1] = 0; + } + + v = mt_ee(d, MT_EE_LNA_GAIN); + lna_2g = (int8_t)(v & 0xff); + lna_5g[0] = (int8_t)(v >> 8); + lna_5g[1] = (int8_t)(mt_ee(d, MT_EE_RSSI_OFFSET_2G_1) >> 8); + lna_5g[2] = (int8_t)(mt_ee(d, MT_EE_RSSI_OFFSET_5G_1) >> 8); + if (!field_valid((uint8_t)lna_5g[1])) lna_5g[1] = lna_5g[0]; + if (!field_valid((uint8_t)lna_5g[2])) lna_5g[2] = lna_5g[0]; + + rssi_off = (band == 0) ? mt_ee(d, MT_EE_RSSI_OFFSET_2G_0) + : mt_ee(d, MT_EE_RSSI_OFFSET_5G_0); + d->cal.rssi_offset[0] = field_valid(rssi_off & 0xff) + ? (int8_t)sign_extend_optional(rssi_off & 0xff, 7) : 0; + d->cal.rssi_offset[1] = field_valid(rssi_off >> 8) + ? (int8_t)sign_extend_optional(rssi_off >> 8, 7) : 0; + + /* mt76x02_get_lna_gain(): which LNA entry applies to this channel. */ + if (band == 0) lna = (uint8_t)lna_2g; + else if (chan <= 64) lna = (uint8_t)lna_5g[0]; + else if (chan <= 128) lna = (uint8_t)lna_5g[1]; + else lna = (uint8_t)lna_5g[2]; + if (lna == 0xff) lna = 0; + + /* mt76x2_has_ext_lna(): an external LNA means its gain is NOT removed. */ + { + uint16_t c1 = mt_ee(d, MT_EE_NIC_CONF_1); + int ext = (band == 0) ? (c1 & MT_EE_NIC_CONF_1_LNA_EXT_2G) + : (c1 & MT_EE_NIC_CONF_1_LNA_EXT_5G); + d->cal.lna_gain = ext ? 0 : (int8_t)sign_extend(lna, 8); + } + + d->cal.mcu_gain = (uint32_t)(lna_2g & 0xff); + d->cal.mcu_gain |= (uint32_t)(lna_5g[0] & 0xff) << 8; + d->cal.mcu_gain |= (uint32_t)(lna_5g[1] & 0xff) << 16; + d->cal.mcu_gain |= (uint32_t)(lna_5g[2] & 0xff) << 24; +} diff --git a/src/mt7612u/fw.c b/src/mt7612u/fw.c new file mode 100644 index 00000000..3e58bc50 --- /dev/null +++ b/src/mt7612u/fw.c @@ -0,0 +1,237 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * ROM patch + ILM/DLM firmware load. Ported from mt76/mt76x2/usb_mcu.c and + * mt76x02_usb_mcu.c. Chunk sizes and destination addresses are confirmed + * against a usbmon capture of the kernel driver (INVESTIGATION.md §4). + */ +#include +#include +#include +#include "internal.h" + +#define MCU_FW_URB_MAX_PAYLOAD 0x3900 +#define MCU_ROM_PATCH_MAX_PAYLOAD 2048 +#define MCU_ILM_OFFSET 0x80000 +#define MCU_DLM_OFFSET 0x110000 +#define MCU_ROM_PATCH_OFFSET 0x90000 +#define PATCH_HDR_LEN 30 +#define FW_HDR_LEN 32 + +static void put_le32(uint8_t *p, uint32_t v) +{ + p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; + p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff; +} + +static uint32_t get_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint8_t *slurp(const char *dir, const char *name, size_t *out_len) +{ + char path[512]; + FILE *f; + uint8_t *buf; + long n; + + snprintf(path, sizeof path, "%s/%s", dir, name); + f = fopen(path, "rb"); + if (!f) { ERR("cannot open %s", path); return NULL; } + fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET); + if (n <= 0) { fclose(f); ERR("%s is empty", path); return NULL; } + buf = malloc((size_t)n); + if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) { + free(buf); fclose(f); ERR("short read on %s", path); return NULL; + } + fclose(f); + *out_len = (size_t)n; + return buf; +} + +/* MT_VEND_DEV_MODE with wValue 0x1 - the vendor reset before each blob. */ +static void fw_reset(struct mt7612u_dev *d) +{ + mt_vendor_req(d, MT_VEND_DEV_MODE, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, 0x1, 0, NULL, 0); +} + +/* The FCE preamble that must precede every blob upload. */ +static void fce_setup(struct mt7612u_dev *d) +{ + uint32_t val = MT_USB_DMA_CFG_RX_BULK_EN | MT_USB_DMA_CFG_TX_BULK_EN | + FIELD_PREP(MT_USB_DMA_CFG_RX_BULK_AGG_TOUT, 0x20); + + mt_wr(d, CFG_ADDR(MT_USB_U3DMA_CFG), val); + mt_wr(d, MT_FCE_PSE_CTRL, 0x1); + mt_wr(d, MT_TX_CPU_FROM_FCE_BASE_PTR, 0x400230); + mt_wr(d, MT_TX_CPU_FROM_FCE_MAX_COUNT, 0x1); + mt_wr(d, MT_FCE_PDMA_GLOBAL_CONF, 0x44); + mt_wr(d, MT_FCE_SKIP_FS, 0x3); +} + +static int fw_send_chunk(struct mt7612u_dev *d, uint8_t *scratch, + const uint8_t *src, int len, uint32_t dst) +{ + uint32_t info, idx; + int rlen, total, rc; + + info = FIELD_PREP(MT_MCU_MSG_PORT, CPU_TX_PORT) | + FIELD_PREP(MT_MCU_MSG_LEN, (uint32_t)len) | + MT_MCU_MSG_TYPE_CMD; + + put_le32(scratch, info); + memcpy(scratch + 4, src, (size_t)len); + memset(scratch + 4 + len, 0, 4); + + mt_single_wr(d, MT_VEND_WRITE_FCE, MT_FCE_DMA_ADDR, dst); + rlen = (len + 3) & ~3; + mt_single_wr(d, MT_VEND_WRITE_FCE, MT_FCE_DMA_LEN, (uint32_t)rlen << 16); + + 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; } + + 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); + return 0; +} + +static int fw_send_data(struct mt7612u_dev *d, const uint8_t *data, int data_len, + uint32_t max_payload, uint32_t offset) +{ + int max_len = (int)max_payload - 8, pos = 0, rc = 0; + uint8_t *scratch = malloc(max_payload); + + if (!scratch) return -1; + while (data_len > 0) { + int len = data_len < max_len ? data_len : max_len; + + rc = fw_send_chunk(d, scratch, data + pos, len, offset + (uint32_t)pos); + if (rc) break; + data_len -= len; + pos += len; + mt_usleep(7500); + } + free(scratch); + return rc; +} + +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; + + if (!fw) return -1; + 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); + + /* is_mt7612() -> rom_protect is false: no MT_MCU_SEMAPHORE_03 handshake + * and no "already applied" early-out. The patch reloads every time. */ + fce_setup(d); + fw_reset(d); + 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; + + /* enable_patch and reset_wmt are USB_TYPE_CLASS, not VENDOR. */ + { + static const uint8_t enable[] = { 0x6f, 0xfc, 0x08, 0x01, 0x20, 0x04, + 0x00, 0x00, 0x00, 0x09, 0x00 }; + static const uint8_t wmt[] = { 0x6f, 0xfc, 0x05, 0x01, + 0x07, 0x01, 0x00, 0x04 }; + uint8_t b[16]; + const uint8_t type = LIBUSB_ENDPOINT_OUT | + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_DEVICE; + + memcpy(b, enable, sizeof enable); + mt_vendor_req(d, MT_VEND_DEV_MODE, type, 0x12, 0, b, sizeof enable); + memcpy(b, wmt, sizeof wmt); + mt_vendor_req(d, MT_VEND_DEV_MODE, type, 0x12, 0, b, sizeof wmt); + } + mt_usleep(20000); + + /* rev >= E3 -> the applied bit is MT_MCU_CLOCK_CTL bit 0. */ + 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)); + goto out; + } + LOG("ROM patch applied"); + rc = 0; +out: + free(fw); + return rc; +} + +static int load_firmware(struct mt7612u_dev *d, const char *dir) +{ + size_t n; + 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; + + if (!fw) return -1; + if (n < FW_HDR_LEN) { ERR("firmware too short"); goto out; } + + ilm_len = get_le32(fw); + dlm_len = get_le32(fw + 4); + build_ver = (uint16_t)(fw[8] | (fw[9] << 8)); + fw_ver = (uint16_t)(fw[10] | (fw[11] << 8)); + + if (n != FW_HDR_LEN + ilm_len + dlm_len) { + ERR("firmware size %zu != 32+%u+%u", n, ilm_len, dlm_len); + goto out; + } + LOG("firmware %d.%d.%02d build 0x%x %.16s (ilm %u dlm %u)", + (fw_ver >> 12) & 0xf, (fw_ver >> 8) & 0xf, fw_ver & 0xf, + build_ver, (char *)(fw + 16), ilm_len, dlm_len); + + fw_reset(d); + 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; + + /* 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; + + /* load IVB: MT_VEND_DEV_MODE, VENDOR type, wValue 0x12, no data. */ + mt_vendor_req(d, MT_VEND_DEV_MODE, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, 0x12, 0, NULL, 0); + + 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)); + goto out; + } + mt_set(d, MT_MCU_COM_REG0, BIT(1)); + mt_wr(d, MT_FCE_PSE_CTRL, 0x1); + LOG("firmware running"); + rc = 0; +out: + free(fw); + return rc; +} + +int mt_fw_init(struct mt7612u_dev *d, const char *fw_dir) +{ + if (!fw_dir) fw_dir = "firmware"; + if (load_rom_patch(d, fw_dir)) + return -1; + return load_firmware(d, fw_dir); +} diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h new file mode 100644 index 00000000..871aa100 --- /dev/null +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -0,0 +1,335 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * mt7612u-hal - minimal userspace HAL for MediaTek MT7612U over libusb. + * + * Descriptor layouts and register sequences are derived from openwrt/mt76 + * (mt76x2/, mt76x02*), Copyright (C) 2016 Felix Fietkau, (C) 2018 Lorenzo + * Bianconi / Stanislaw Gruszka. + * + * Design notes and the evidence behind every constant here: ../INVESTIGATION.md + */ +#ifndef MT7612U_H +#define MT7612U_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MT7612U_VID 0x0e8d +#define MT7612U_PID 0x7612 + +/* txwi.rate bits 15:13 - the PHY type the frame is sent with. */ +enum mt7612u_phy { + MT7612U_PHY_CCK = 0, + MT7612U_PHY_OFDM = 1, + MT7612U_PHY_HT = 2, + MT7612U_PHY_HT_GF = 3, + MT7612U_PHY_VHT = 4, +}; + +/* txwi.rate bits 8:7. A frame may narrow below the channel width, not exceed it. */ +enum mt7612u_bw { + MT7612U_BW_20 = 0, + MT7612U_BW_40 = 1, + MT7612U_BW_80 = 2, +}; + +/* + * Per-packet PHY selection. Every field here maps into the 16-bit txwi.rate + * word plus one bit of txwi.ack_ctl, so all of it is genuinely per frame - + * nothing is cached in firmware or in a per-station rate table. + */ +struct mt7612u_tx_rate { + enum mt7612u_phy phy; + uint8_t mcs; /* legacy index, HT MCS 0-31, or VHT MCS 0-9 */ + uint8_t nss; /* 1 or 2; folded into the rate index for HT/VHT */ + enum mt7612u_bw bw; + unsigned sgi : 1; + unsigned ldpc : 1; + unsigned stbc : 1; /* hardware honours it only at nss == 1 */ + unsigned no_ack : 1; + int8_t power_adj; /* txwi.ctl2 MT_TX_PWR_ADJ, 4-bit relative offset */ +}; + +/* Filled from the 32-byte RXWI in front of every received frame. */ +struct mt7612u_rx_info { + enum mt7612u_phy phy; + uint8_t mcs; + uint8_t nss; + enum mt7612u_bw bw; + unsigned sgi : 1; + unsigned ldpc : 1; + unsigned stbc : 1; + unsigned crc_err : 1; + unsigned ampdu : 1; + int8_t rssi[4]; /* per chain, already EEPROM-corrected */ + /* + * Noise floor in dBm, from RXWI byte 14 - the `rssi[2]` slot that mt76 + * declares and never reads (mt76x02_mac_process_rx uses only [0] and + * [1]). Identified by measurement, not by documentation: + * + * - It is signal-independent. On ch36 it read -92, -94, -92, -91, -91 + * dBm across five bands whose signal spanned 43 dB. + * - It orders with how noisy the channel is, against the MAC's own + * false-CCA count: ch36 (689 false CCA) -92 dBm, ch1 (28567) -88, + * ch6 (27313) -85, ch11 (17256) -81. + * + * `snr_db` below is simply rssi[0] - noise. + * + * CAVEAT, unresolved: on a quiet 5 GHz channel carrying only our own + * very strong transmitter this reads a fixed -116 dBm, which is below + * the thermal floor of a 20 MHz channel and so cannot be a real + * measurement. Treat a reading below about -100 dBm as "no valid + * estimate" rather than as an extraordinarily quiet channel; noise_valid + * says so. + */ + int8_t noise; + int8_t snr_db; /* rssi[0] - noise; 0 when !noise_valid */ + unsigned noise_valid : 1; + uint8_t n_chains; + uint16_t mpdu_len; + uint16_t seq; + /* RXWI bytes 16-31, the `bbp_rxinfo[4]` that mt76 declares in + * mt76x02_mac.h and never reads. Raw and unparsed: exposed because + * whatever the baseband puts here is the only per-frame quality + * signal this MAC offers beyond RSSI, and it cannot be identified + * without looking at it. Do not build on the layout until it is. */ + uint32_t bbp[4]; +}; + +struct mt7612u_dev; + +/* + * open: claims the interface, detaching mt76x2u if it holds it, then runs the + * full power-on, firmware load and MAC/PHY init. fw_dir may be NULL for the + * system default. Returns NULL on failure; err (optional) receives a message. + */ +struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err); + +/* + * Same, but adopting a libusb handle the caller already opened, reset and + * claimed interface 0 on. Neither the handle nor the context is closed by + * mt7612u_close() - the caller keeps ownership of both, and of any exclusive + * lock it took over them. + * + * Adopting rather than reopening matters: reopening would race whatever lock + * the caller holds, and libusb_reset_device() here would invalidate the + * caller's own handle. + * + * `h` is `libusb_device_handle *` and `ctx` is `libusb_context *`, spelled + * void * so this header does not force libusb on a consumer that only wants + * the descriptor types above. + */ +struct mt7612u_dev *mt7612u_open_handle(void *h, void *ctx, const char *fw_dir, + const char **err); + +void mt7612u_close(struct mt7612u_dev *dev); + +/* Reattaches the kernel driver on close unless this is set. */ +void mt7612u_keep_detached(struct mt7612u_dev *dev, int keep); + +/* + * Channel + width. `chan` is always the *control* channel, at every width, + * and is an 802.11 channel number rather than a frequency. Issues + * CMD_SWITCH_CHANNEL_OP and the firmware calibration burst, so it is not + * cheap - it is a setup call, not a per-frame one. + * + * 20, 40 and 80 MHz are implemented. The hardware tunes the *centre* of a + * widened channel, derived here from the control channel: at 80 MHz, channel + * 36 tunes centre 42. A control channel that cannot carry the requested + * width is refused rather than tuned near - that covers an off-grid channel, + * one whose widened span would leave the band `mt7612u_caps` declares, and + * 40 MHz in 2.4 GHz outside channels 4-11, where a bare channel number + * cannot say which side the secondary sits on. + */ +int mt7612u_set_channel(struct mt7612u_dev *dev, uint8_t chan, enum mt7612u_bw bw); + +/* Absolute TX power base, dBm. Per-frame trim is mt7612u_tx_rate.power_adj. */ +int mt7612u_set_txpower(struct mt7612u_dev *dev, int dbm); + +/* 0x202 = 2T2R (default), 0x101 = 1T1R. Global; takes effect at next channel set. */ +int mt7612u_set_chainmask(struct mt7612u_dev *dev, uint16_t chainmask); + +/* + * Enable the MAC. TX always; RX only if mt7612u_rx_start() is already + * running. That condition is not a convenience: with the receiver on and + * nothing draining the bulk-IN endpoint, this part wedges below the USB + * level and no software reset recovers it - only a physical replug. + */ +int mt7612u_start(struct mt7612u_dev *dev); +int mt7612u_stop(struct mt7612u_dev *dev); + +/* + * Inject one complete 802.11 frame (no FCS - the MAC appends it). Builds the + * TXWI + TXINFO and submits a single bulk transfer. Fire-and-forget. + */ +int mt7612u_tx(struct mt7612u_dev *dev, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate); + +/* + * RX callback, invoked from the libusb event thread. `frame`/`len` are the + * 802.11 MPDU with the RXWI removed and **without the trailing FCS** - the + * MAC strips it, and the four bytes that follow the MPDU in the DMA buffer + * are the FCE info trailer, not a checksum (measured: CRC-32 matched them on + * 0 of 4263 frames). Must not block and must not call back into the device. + */ +typedef void (*mt7612u_rx_cb)(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info); +int mt7612u_rx_start(struct mt7612u_dev *dev, mt7612u_rx_cb cb, void *user); +int mt7612u_rx_stop(struct mt7612u_dev *dev); + +/* + * Put the receive filter into monitor mode: pass everything the PHY decodes, + * dropping only PHY errors and (unless keep_corrupted) frames that failed FCS. + * + * This is NOT the default. mt7612u_start() leaves the filter mt76's managed + * -mode value, which drops control frames and anything not addressed to this + * station - on ambient 2.4 GHz traffic that is the difference between seeing + * the whole mix and seeing almost nothing but beacons. Call this after + * mt7612u_start(), which rewrites the register. + */ +int mt7612u_set_monitor_rx(struct mt7612u_dev *dev, int keep_corrupted); + +/* + * Radiotap-framed inject, matching devourer's send_packet() contract: one + * buffer holding a radiotap header followed by the 802.11 MPDU, with the + * per-frame rate carried in the header. + */ +int mt7612u_send_packet(struct mt7612u_dev *dev, const void *buf, size_t len); + +/* One radiotap-framed MPDU, as handed to mt7612u_send_packets(). */ +struct mt7612u_tx_view { const uint8_t *data; size_t len; }; + +/* + * Submit several frames in one call. MT7612U chains them into a single + * bulk-OUT transfer via the TXDMA's "next valid" bit, so a burst costs one USB + * transaction instead of one per frame. Returns the number accepted. + */ +size_t mt7612u_send_packets(struct mt7612u_dev *dev, + const struct mt7612u_tx_view *pkts, size_t count); + +/* + * Hardware ACK responder: make the MAC answer frames addressed to `mac` with + * a SIFS-timed ACK, with no host involvement. `mac` must be unicast. + * Returns 0 on success, negative when unsupported or the arm cannot be + * verified. Clear is best effort. + */ +int mt7612u_set_ack_responder(struct mt7612u_dev *dev, const uint8_t mac[6]); +void mt7612u_clear_ack_responder(struct mt7612u_dev *dev); + +/* + * TX/RX counters from the async rings. Zeroed when no ring is running, and + * taken under the ring's own lock - reading the fields directly would race + * the libusb event thread. + */ +struct mt7612u_stats { + uint64_t tx_submitted, tx_done, tx_err, rx_frames, rx_err; + /* Frames dropped because the rate word named no valid PHY. MT_RATE_PHY + * is three bits, so 5-7 are representable and mean nothing; mt76 drops + * them too. Nonzero here means the RX path is seeing garbage, not that + * the radio is slow. */ + uint64_t rx_invalid; + /* Frames the parser rejected on length - a short or malformed + * transfer. Counted because such a frame used to move no counter at + * all, which is indistinguishable from one that was never sent. + * + * It does NOT count an oversize frame: those are discarded by the MAC + * above max_mpdu_rx, before USB, so they raise nothing here. That + * limit is a capability to read, not an error to count. */ + uint64_t rx_dropped; +}; +void mt7612u_get_stats(struct mt7612u_dev *dev, struct mt7612u_stats *out); + +/* + * Per-interval link statistics from the MAC's MIB counters. + * + * These are the rich part of this MAC's link reporting, and none of it is per + * frame: the RX descriptor carries RSSI and nothing else - its + * `bbp_rxinfo[4]`, which mt76 declares and never reads, turns out to be two + * words of zero plus a duplicate of the same two RSSI values, so there is no + * per-frame SNR or EVM to surface. + * + * Every counter below is READ-AND-CLEAR in hardware. Each call therefore + * returns the interval since the previous call, not a running total, and two + * readers would steal each other's counts. mt7612u_link_stats_start() enables + * the channel timers and zeroes everything; call it once, then poll. + */ +struct mt7612u_link_stats { + uint32_t interval_us; /* host-measured, for turning counts into rates */ + + /* Channel occupancy, in the MAC's own clock units. busy counts TX, RX, + * NAV and EIFS as busy, which is the definition mt76 configures. */ + uint32_t ch_busy, ch_idle; + + /* Receive error classes. false_cca is the interference signal: energy + * that started a receive and did not become a frame. */ + uint16_t rx_crc_err, rx_phy_err, rx_false_cca, rx_plcp_err; + uint16_t rx_dup_err, rx_overflow; + + /* A-MPDU length histogram: agg_cnt[n] is the number of aggregates that + * carried n+1 MPDUs, up to 32. */ + uint16_t agg_cnt[32]; + + int8_t temp_c; /* die temperature, degrees C; INT8_MIN if unread */ +}; + +/* + * Enable the channel timers and clear every counter. Safe to call again to + * re-zero. Returns 0 on success. + */ +int mt7612u_link_stats_start(struct mt7612u_dev *dev); + +/* Read and clear. Returns 0 on success; fills the interval since the previous + * call to this function or to _start(). */ +int mt7612u_link_stats(struct mt7612u_dev *dev, struct mt7612u_link_stats *out); + +/* 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); + +/* What this adapter can do, so a caller need not assume. */ +struct mt7612u_caps { + const char *chip_name; + uint32_t rev; + uint8_t nss_rx, nss_tx; + uint8_t bw_mask; /* bit0 = 20, bit1 = 40, bit2 = 80 MHz */ + /* + * Largest MPDU, excluding FCS, in each direction. They differ, and the + * asymmetry is real rather than an oversight: + * + * max_mpdu_tx what this backend will submit. A buffer choice, not a + * silicon limit - measured on air, the part transmits at + * least 7900 B and an RTL8812AU decoded 60/60 at every + * size up to that with zero CRC errors, far past the + * 802.11 non-A-MSDU ceiling of 2304. + * max_mpdu_rx what the MAC will hand back. Set by MT_MAX_LEN_CFG, + * whose low 12 bits are the on-air length INCLUDING the + * 4-byte FCS; at the 0xf00 this driver programs that is + * 3840 on air, so 3836 of MPDU. Measured to the byte: + * 3836 arrives, 3837 does not. + * + * A frame above max_mpdu_rx is discarded by the MAC before USB. Nothing + * counts it - not rx_err, not rx_dropped - so a caller that needs to + * know must read this rather than watch for an error. + */ + uint16_t max_mpdu_tx, max_mpdu_rx; + uint16_t band_5g_min_mhz, band_5g_max_mhz; + uint16_t band_2g_min_mhz, band_2g_max_mhz; + unsigned ampdu_tx : 1; /* aggregation works on injected frames */ + unsigned per_chain_rssi : 1; + unsigned narrowband : 1; /* 5/10 MHz - not available on this part */ + unsigned fast_retune : 1; /* sub-ms channel change - not on this part */ +}; +void mt7612u_get_caps(const struct mt7612u_dev *dev, struct mt7612u_caps *caps); + +/* Identity, for logging and for refusing to run on an unexpected revision. */ +uint32_t mt7612u_asic_version(const struct mt7612u_dev *dev); /* 0x76120044 here */ +const uint8_t *mt7612u_mac_addr(const struct mt7612u_dev *dev); + +#ifdef __cplusplus +} +#endif +#endif /* MT7612U_H */ diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c new file mode 100644 index 00000000..6880eaf2 --- /dev/null +++ b/src/mt7612u/init.c @@ -0,0 +1,592 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Power-on, MAC reset and MAC start/stop. Ported verbatim from + * mt76/mt76x2/usb_init.c, usb_mac.c and init.c - deliberately NOT minimised. + * See docs/mt7612u.md: trimming this sequence is a post-bring-up activity, because a + * 95%-correct init answers every register read and still radiates nothing. + */ +#include +#include +#include +#include "internal.h" +#include "initvals.h" + +/* ---- power ---- */ + +static void set_wlan_state(struct mt7612u_dev *d, int enable) +{ + uint32_t val = mt_rr(d, MT_WLAN_FUN_CTRL); + + if (enable) + val |= MT_WLAN_FUN_CTRL_WLAN_EN | MT_WLAN_FUN_CTRL_WLAN_CLK_EN; + else + val &= ~(MT_WLAN_FUN_CTRL_WLAN_EN | MT_WLAN_FUN_CTRL_WLAN_CLK_EN); + + mt_wr(d, MT_WLAN_FUN_CTRL, val); + mt_usleep(20); +} + +static void reset_wlan(struct mt7612u_dev *d, int enable) +{ + uint32_t val; + + if (!enable) { set_wlan_state(d, enable); return; } + + val = mt_rr(d, MT_WLAN_FUN_CTRL); + val &= ~MT_WLAN_FUN_CTRL_FRC_WL_ANT_SEL; + + if (val & MT_WLAN_FUN_CTRL_WLAN_EN) { + val |= MT_WLAN_FUN_CTRL_WLAN_RESET_RF; + mt_wr(d, MT_WLAN_FUN_CTRL, val); + mt_usleep(20); + val &= ~MT_WLAN_FUN_CTRL_WLAN_RESET_RF; + } + mt_wr(d, MT_WLAN_FUN_CTRL, val); + mt_usleep(20); + + set_wlan_state(d, enable); +} + +static void power_on_rf_patch(struct mt7612u_dev *d) +{ + mt_set(d, CFG_ADDR(0x130), BIT(0) | BIT(16)); + mt_usleep(1); + mt_clear(d, CFG_ADDR(0x1c), 0xff); + mt_set(d, CFG_ADDR(0x1c), 0x30); + mt_wr(d, CFG_ADDR(0x14), 0x484f); + mt_usleep(1); + mt_set(d, CFG_ADDR(0x130), BIT(17)); + mt_usleep(200); + mt_clear(d, CFG_ADDR(0x130), BIT(16)); + mt_usleep(100); + mt_set(d, CFG_ADDR(0x14c), BIT(19) | BIT(20)); +} + +static void power_on_rf(struct mt7612u_dev *d, int unit) +{ + int shift = unit ? 8 : 0; + uint32_t val = (BIT(1) | BIT(3) | BIT(4) | BIT(5)) << shift; + + mt_set(d, CFG_ADDR(0x130), BIT(0) << shift); /* RF BG */ + mt_usleep(20); + mt_set(d, CFG_ADDR(0x130), val); /* RFDIG LDO/AFE/ABB/ADDA */ + mt_usleep(20); + mt_clear(d, CFG_ADDR(0x130), BIT(2) << shift); /* RFDIG -> internal LDO */ + mt_usleep(20); + + power_on_rf_patch(d); + mt_set(d, 0x530, 0xf); +} + +static void power_on(struct mt7612u_dev *d) +{ + uint32_t val; + + mt_set(d, CFG_ADDR(MT_CFG_MTC_CTRL), MT_WLAN_MTC_CTRL_MTCMOS_PWR_UP); + + val = MT_WLAN_MTC_CTRL_STATE_UP | MT_WLAN_MTC_CTRL_PWR_ACK | + MT_WLAN_MTC_CTRL_PWR_ACK_S; + if (!mt_poll(d, CFG_ADDR(MT_CFG_MTC_CTRL), val, val, 1000)) + WARN("MTCMOS power-up did not ack"); + + mt_clear(d, CFG_ADDR(MT_CFG_MTC_CTRL), 0x7fu << 16); + mt_usleep(20); + mt_clear(d, CFG_ADDR(MT_CFG_MTC_CTRL), 0xfu << 24); + mt_usleep(20); + mt_set(d, CFG_ADDR(MT_CFG_MTC_CTRL), 0xfu << 24); + mt_clear(d, CFG_ADDR(MT_CFG_MTC_CTRL), 0xfff); + + mt_clear(d, CFG_ADDR(0x1204), BIT(3)); /* AD/DA power down off */ + mt_set(d, CFG_ADDR(0x80), BIT(0)); /* WLAN function enable */ + mt_clear(d, CFG_ADDR(0x64), BIT(18)); /* release BBP soft reset */ + + power_on_rf(d, 0); + power_on_rf(d, 1); +} + +static void init_dma(struct mt7612u_dev *d) +{ + uint32_t val = mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)); + + val |= MT_USB_DMA_CFG_RX_DROP_OR_PAD | MT_USB_DMA_CFG_RX_BULK_EN | + MT_USB_DMA_CFG_TX_BULK_EN; + /* Aggregation off: one URB carries exactly one RX frame, which is what + * makes the RX path in rx.c a straight parse with no de-aggregation. */ + val &= ~MT_USB_DMA_CFG_RX_BULK_AGG_EN; + mt_wr(d, CFG_ADDR(MT_USB_U3DMA_CFG), val); +} + +/* ---- MAC ---- */ + +static void mac_fixup_xtal(struct mt7612u_dev *d) +{ + int8_t offset = 0; + uint16_t eep = mt_ee(d, MT_EE_XTAL_TRIM_2); + + offset = eep & 0x7f; + if ((eep & 0xff) == 0xff) + offset = 0; + else if (eep & 0x80) + offset = (int8_t)-offset; + + eep >>= 8; + if (eep == 0x00 || eep == 0xff) { + eep = mt_ee(d, MT_EE_XTAL_TRIM_1) & 0xff; + if (eep == 0x00 || eep == 0xff) + eep = 0x14; + } + eep &= 0x7f; + + mt_rmw(d, CFG_ADDR(MT_XO_CTRL5), MT_XO_CTRL5_C2_VAL, + FIELD_PREP(MT_XO_CTRL5_C2_VAL, (uint32_t)(eep + offset))); + mt_set(d, CFG_ADDR(MT_XO_CTRL6), MT_XO_CTRL6_C2_CTRL); + + mt_wr(d, 0x504, 0x06000000); + mt_wr(d, 0x50c, 0x08800000); + mt_usleep(5000); + mt_wr(d, 0x504, 0x0); + + /* SIFS 16us -> 13us */ + mt_rmw(d, MT_XIFS_TIME_CFG, MT_XIFS_TIME_CFG_OFDM_SIFS, + FIELD_PREP(MT_XIFS_TIME_CFG_OFDM_SIFS, 0xd)); + mt_rmw(d, MT_BKOFF_SLOT_CFG, MT_BKOFF_SLOT_CFG_CC_DELAY, + FIELD_PREP(MT_BKOFF_SLOT_CFG_CC_DELAY, 1)); + + mt_clear(d, MT_FCE_L2_STUFF, MT_FCE_L2_STUFF_WR_MPDU_LEN_EN); + + switch (FIELD_GET(MT_EE_NIC_CONF_2_XTAL_OPTION, mt_ee(d, MT_EE_NIC_CONF_2))) { + case 0: mt_wr(d, MT_XO_CTRL7, 0x5c1fee80); break; + case 1: mt_wr(d, MT_XO_CTRL7, 0x5c1feed0); break; + default: break; + } +} + +static void mac_reset(struct mt7612u_dev *d) +{ + mt_wr(d, MT_WPDMA_GLO_CFG, BIT(4) | BIT(5)); + mt_wr(d, MT_PBF_TX_MAX_PCNT, 0xefef3f1f); + mt_wr(d, MT_PBF_RX_MAX_PCNT, 0xfebf); + + for (unsigned i = 0; i < sizeof mt7612u_mac_initvals / sizeof mt7612u_mac_initvals[0]; i++) + mt_wr(d, mt7612u_mac_initvals[i].reg, mt7612u_mac_initvals[i].val); + + mt_wr(d, MT_TX_LINK_CFG, 0x1020); + mt_wr(d, MT_AUTO_RSP_CFG, 0x13); + /* Low 12 bits are the maximum on-air length INCLUDING the 4-byte FCS, + * so this 0xf00 is 3840 on air and 3836 of MPDU. Cached rather than + * re-read by mt7612u_get_caps(), which is a const snapshot and must not + * touch the bus. Measured to the byte: 3836 arrives, 3837 does not. */ + mt_wr(d, MT_MAX_LEN_CFG, 0x2f00); + d->max_mpdu_rx = (uint16_t)((0x2f00 & 0xfff) - 4); + + mt_wr(d, MT_WMM_AIFSN, 0x2273); + mt_wr(d, MT_WMM_CWMIN, 0x2344); + mt_wr(d, MT_WMM_CWMAX, 0x34aa); + + mt_clear(d, MT_MAC_SYS_CTRL, + MT_MAC_SYS_CTRL_RESET_CSR | MT_MAC_SYS_CTRL_RESET_BBP); + + /* is_mt7612(): coexistence off. */ + mt_clear(d, MT_COEXCFG0, MT_COEXCFG0_COEX_EN); + + mt_set(d, MT_EXT_CCA_CFG, 0xf000); + mt_clear(d, MT_TX_ALC_CFG_4, BIT(31)); + + mac_fixup_xtal(d); +} + +static void mac_setaddr(struct mt7612u_dev *d) +{ + const uint8_t *a = d->macaddr; + uint8_t zero[8] = { 0 }; + uint32_t dw0 = (uint32_t)a[0] | ((uint32_t)a[1] << 8) | + ((uint32_t)a[2] << 16) | ((uint32_t)a[3] << 24); + uint32_t dw1 = (uint32_t)a[4] | ((uint32_t)a[5] << 8); + + mt_wr(d, MT_MAC_ADDR_DW0, dw0); + /* NOTE: the U2ME byte (bits 23:16) is write-only on this silicon - it + * always reads back 0. Do not use it as a write-probe. */ + mt_wr(d, MT_MAC_ADDR_DW1, dw1 | FIELD_PREP(MT_MAC_ADDR_DW1_U2ME_MASK, 0xff)); + + mt_wr(d, MT_MAC_BSSID_DW0, dw0); + mt_wr(d, MT_MAC_BSSID_DW1, dw1 | + FIELD_PREP(MT_MAC_BSSID_DW1_MBSS_MODE, 3) | + MT_MAC_BSSID_DW1_MBSS_LOCAL_BIT); + mt_rmw(d, MT_MAC_BSSID_DW1, MT_MAC_BSSID_DW1_MBEACON_N, + FIELD_PREP(MT_MAC_BSSID_DW1_MBEACON_N, 7)); + + /* mt76x02_mac_set_bssid() masks the index to 3 bits, so this covers + * APC BSSID slots 0..7 at 0x1090..0x10cc. Do NOT guess this base: + * 0x1200 is MT_MAC_STATUS, and zeroing 128 bytes from there walks over + * live MAC registers. */ + for (int i = 0; i < 16; i++) { + int idx = i & 7; + + mt_wr(d, MT_MAC_APC_BSSID_L(idx), 0); + mt_rmw(d, MT_MAC_APC_BSSID_H(idx), MT_MAC_APC_BSSID_H_ADDR, 0); + } + (void)zero; +} + +static void wcid_and_key_clear(struct mt7612u_dev *d) +{ + uint8_t zero32[32] = { 0 }; + + for (int i = 0; i < 256; i++) { + mt_wr(d, MT_WCID_ATTR(i), 0); + if (i < 128) + mt_wr_copy(d, MT_WCID_ADDR(i), zero32, 8); + } + for (int bss = 0; bss < 16; bss++) { + for (int k = 0; k < 4; k++) { + uint32_t v = mt_rr(d, MT_SKEY_MODE(bss)); + + v &= ~(MT_SKEY_MODE_MASK << MT_SKEY_MODE_SHIFT(bss, k)); + mt_wr(d, MT_SKEY_MODE(bss), v); + mt_wr_copy(d, MT_SKEY(bss, k), zero32, 32); + } + } +} + +/* + * Drain and discard whatever is sitting in the RX bulk endpoint. The MAC will + * happily fill its RX pool with ambient frames the moment RX is enabled, and + * nothing else in this HAL reads EP 4 unless the caller asked for RX - a full + * pool is the leading suspect for the chip wedging after repeated TX cycles + * (see BRINGUP-RESULTS.md). Cheap insurance either way. + */ +void mt_rx_flush(struct mt7612u_dev *d) +{ + uint8_t buf[4096]; + int n; + + for (int i = 0; i < 64; i++) { + if (mt_bulk(d, MT_EP_IN_PKT_RX, buf, sizeof buf, &n, 10) || n == 0) + break; + } +} + +int mt_mac_start(struct mt7612u_dev *d, int enable_rx) +{ + 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, + 0, 200000)) { + ERR("mac_start: WPDMA stayed busy"); + return -1; + } + mt_wr(d, MT_RX_FILTR_CFG, 0x00015f97); + /* Only turn the receiver on when the caller will actually drain EP 4. + * mt76's mac_start always sets both bits, but mt76 also keeps RX URBs + * permanently queued; a TX-only injector that never reads has no such + * backstop. */ + mt_wr(d, MT_MAC_SYS_CTRL, MT_MAC_SYS_CTRL_ENABLE_TX | + (enable_rx ? MT_MAC_SYS_CTRL_ENABLE_RX : 0)); + return 0; +} + +int mt_mac_stop(struct mt7612u_dev *d) +{ + uint32_t rts_cfg = mt_rr(d, MT_TX_RTS_CFG); + + mt_rx_flush(d); + + mt_wr(d, MT_TX_RTS_CFG, rts_cfg & ~MT_TX_RTS_CFG_RETRY_LIMIT); + mt_clear(d, MT_TXOP_CTRL_CFG, MT_TXOP_ED_CCA_EN); + mt_clear(d, MT_TXOP_HLDR_ET, MT_TXOP_HLDR_TX40M_BLK_EN); + + for (int i = 0; i < 2000; i++) { + if (!(mt_rr(d, CFG_ADDR(MT_USB_U3DMA_CFG)) & MT_USB_DMA_CFG_TX_BUSY) && i > 10) + break; + mt_usleep(75); + } + mt_clear(d, MT_MAC_SYS_CTRL, + MT_MAC_SYS_CTRL_ENABLE_RX | MT_MAC_SYS_CTRL_ENABLE_TX); + + for (int i = 0; i < 1000; i++) { + if (!(mt_rr(d, MT_MAC_STATUS) & MT_MAC_STATUS_TX) && + !mt_rr(d, MT_BBP(IBI, 12))) + break; + mt_usleep(15); + } + if (!mt_poll(d, MT_MAC_STATUS, MT_MAC_STATUS_RX, 0, 200000)) + WARN("MAC RX failed to stop"); + + mt_wr(d, MT_TX_RTS_CFG, rts_cfg); + return 0; +} + +void mt_phy_set_rxpath(struct mt7612u_dev *d) +{ + uint32_t val = mt_rr(d, MT_BBP(AGC, 0)); + + val &= ~BIT(4); + if ((d->chainmask & 0xf) == 2) + val |= BIT(3); + else + val &= ~BIT(3); + mt_wr(d, MT_BBP(AGC, 0), val); + (void)mt_rr(d, MT_BBP(AGC, 0)); +} + +void mt_phy_set_txdac(struct mt7612u_dev *d) +{ + if (((d->chainmask >> 8) & 0xf) == 2) + mt_set(d, MT_BBP(TXBE, 5), 0x3); + else + mt_clear(d, MT_BBP(TXBE, 5), 0x3); +} + +/* Exposed so the bringup gate can check whether our reset actually clears the + * firmware-running state - the closest thing to a cold boot available here, + * since no hub on this host supports per-port power switching. */ +void mt_power_cycle(struct mt7612u_dev *d) +{ + reset_wlan(d, 1); + power_on(d); +} + +int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir) +{ + /* Bracket the whole bring-up: several hundred writes here are a + * verbatim port of mt76's init sequence and are best-effort at the call + * site, so the sequence checks the accumulator at its boundary instead. + * Without this a write that exhausted its retries left the MAC or the + * BBP partly programmed and this function still returned success. */ + mt_io_clear(d); + + mt_power_cycle(d); + + if (!mt_wait_for_mac(d)) { ERR("MAC not ready after power on"); return -1; } + + if (mt_fw_init(d, fw_dir)) + return -1; + + if (!mt_poll(d, MT_WPDMA_GLO_CFG, + MT_WPDMA_GLO_CFG_TX_DMA_BUSY | MT_WPDMA_GLO_CFG_RX_DMA_BUSY, + 0, 100000)) { + ERR("WPDMA busy after firmware load"); + return -1; + } + if (!mt_wait_for_mac(d)) { ERR("MAC not ready after firmware"); return -1; } + + init_dma(d); + + if (mt_mcu_function_select(d, Q_SELECT, 1)) return -1; + if (mt_mcu_set_radio_state(d, 1)) return -1; + + mac_reset(d); + mac_setaddr(d); + + if (!mt_poll(d, MT_MAC_STATUS, MT_MAC_STATUS_TX | MT_MAC_STATUS_RX, 0, 100000)) + WARN("TX/RX not idle before table clear"); + + wcid_and_key_clear(d); + + /* Free-run the TSF counter. mt76 turns this on as part of beacon + * configuration, which an injector otherwise skips entirely - but + * without it MT_TSF_TIMER_DW0/DW1 read zero forever and ReadTsf is + * useless. TIMER_EN only; no TBTT, no beacon transmission. */ + mt_set(d, MT_BEACON_TIME_CFG, MT_BEACON_TIME_CFG_TIMER_EN); + + mt_rmw(d, MT_US_CYC_CFG, MT_US_CYC_CNT, FIELD_PREP(MT_US_CYC_CNT, 0x1e)); + mt_wr(d, MT_TXOP_CTRL_CFG, 0x583f); + + if (mt_mcu_load_cr(d, MT_RF_BBP_CR, 0, 0)) return -1; + + mt_phy_set_rxpath(d); + mt_phy_set_txdac(d); + + /* Leave no half-full RX ring behind for the next run to inherit. */ + mt_rx_flush(d); + + if (mt_mac_stop(d)) + return -1; + if (mt_io_errors(d)) { + ERR("bring-up completed with %u failed register transfers - the " + "MAC or BBP is only partly programmed, refusing to report " + "success", mt_io_errors(d)); + return -1; + } + return 0; +} + +/* --- the public lifecycle, as declared in include/mt7612u/mt7612u.h --- */ + +/* EEPROM + full bring-up, once the transport is up either way. */ +static struct mt7612u_dev *bring_up(struct mt7612u_dev *d, const char *fw_dir, + const char **err) +{ + if (mt_eeprom_init(d)) { + if (err) *err = "EEPROM image did not validate"; + goto fail; + } + if (mt_init_hardware(d, fw_dir)) { + if (err) *err = "hardware init failed (see log)"; + goto fail; + } + return d; + +fail: + mt_close(d); + free(d); + return NULL; +} + +struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err) +{ + struct mt7612u_dev *d = calloc(1, sizeof *d); + + if (!d) { + if (err) *err = "out of memory"; + return NULL; + } + if (mt_open(d, err)) { free(d); return NULL; } + return bring_up(d, fw_dir, err); +} + +struct mt7612u_dev *mt7612u_open_handle(void *h, void *ctx, const char *fw_dir, + const char **err) +{ + struct mt7612u_dev *d = calloc(1, sizeof *d); + + if (!d) { + if (err) *err = "out of memory"; + return NULL; + } + if (mt_adopt(d, (libusb_device_handle *)h, (libusb_context *)ctx, err)) { + free(d); + return NULL; + } + return bring_up(d, fw_dir, err); +} + +void mt7612u_close(struct mt7612u_dev *d) +{ + if (!d) return; + mt_async_stop(d); + if (d->h) mt_mac_stop(d); + mt_close(d); + free(d); +} + +/* + * Enable the MAC. TX always; RX only when an RX ring is already draining + * EP 4 - the receiver running with nothing reading is what wedges this part + * below the USB level, recoverable only by a physical replug. Call + * mt7612u_rx_start() first if you want to receive. + */ +int mt7612u_start(struct mt7612u_dev *d) +{ + if (!d) return -1; + return mt_mac_start(d, d->a && d->a->rx_active); +} + +int mt7612u_stop(struct mt7612u_dev *d) +{ + if (!d) return -1; + return mt_mac_stop(d); +} + +/* + * Monitor receive filter. + * + * mt_mac_start() leaves MT_RX_FILTR_CFG at 0x00015f97, which is what mt76 + * programs for a managed station: control frames, other-BSS frames and + * frames not addressed here are all dropped. A monitor consumer wants the + * opposite, so this clears everything except the two error classes. + * + * DUP deliberately stays clear: duplicate suppression would hide the + * retransmissions an ACK-responder test counts. + */ +int mt7612u_set_monitor_rx(struct mt7612u_dev *d, int keep_corrupted) +{ + uint32_t filtr = MT_RX_FILTR_CFG_PHY_ERR; + + if (!d) return -1; + if (!keep_corrupted) + filtr |= MT_RX_FILTR_CFG_CRC_ERR; + mt_wr(d, MT_RX_FILTR_CFG, filtr); + return 0; +} + +/* --- MIB link statistics -------------------------------------------------- + * + * Every counter here is read-and-clear in hardware: mt76x02_mac_reset_counters() + * zeroes them by reading, and mt76x02_mac_cc_reset() documents the channel + * timers the same way. So there is nothing to difference - each read *is* the + * interval - and two pollers would steal each other's counts. + */ +static uint64_t stats_now_us(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)(ts.tv_nsec / 1000); +} + +/* Was file-static, so two adapters opened in one process overwrote each + * other's mark and both reported a nonsense interval_us - and every MIB + * counter here is read-and-clear, so the rates derived from it were wrong for + * both. Per device now. */ + +int mt7612u_link_stats_start(struct mt7612u_dev *d) +{ + struct mt7612u_link_stats discard; + + if (!d) return -1; + /* Exactly mt76x02_mac_cc_reset()'s configuration: TX, RX, NAV and EIFS + * all count as busy, which is what makes ch_busy an airtime figure + * rather than a receive-only one. */ + mt_wr(d, MT_CH_TIME_CFG, + MT_CH_TIME_CFG_TIMER_EN | MT_CH_TIME_CFG_TX_AS_BUSY | + MT_CH_TIME_CFG_RX_AS_BUSY | MT_CH_TIME_CFG_NAV_AS_BUSY | + MT_CH_TIME_CFG_EIFS_AS_BUSY | MT_CH_CCA_RC_EN | + FIELD_PREP(MT_CH_TIME_CFG_CH_TIMER_CLR, 1)); + /* One read to clear everything, so the first real sample is clean. */ + mt7612u_link_stats(d, &discard); + return 0; +} + +int mt7612u_link_stats(struct mt7612u_dev *d, struct mt7612u_link_stats *out) +{ + uint64_t now = stats_now_us(); + uint32_t v; + + if (!d || !out) return -1; + memset(out, 0, sizeof *out); + out->interval_us = d->stats_last_us ? (uint32_t)(now - d->stats_last_us) : 0; + d->stats_last_us = now; + + out->ch_busy = mt_rr(d, MT_CH_BUSY); + out->ch_idle = mt_rr(d, MT_CH_IDLE); + + v = mt_rr(d, MT_RX_STAT_0); + out->rx_crc_err = (uint16_t)FIELD_GET(MT_RX_STAT_0_CRC_ERRORS, v); + out->rx_phy_err = (uint16_t)FIELD_GET(MT_RX_STAT_0_PHY_ERRORS, v); + v = mt_rr(d, MT_RX_STAT_1); + out->rx_false_cca = (uint16_t)FIELD_GET(MT_RX_STAT_1_CCA_ERRORS, v); + out->rx_plcp_err = (uint16_t)FIELD_GET(MT_RX_STAT_1_PLCP_ERRORS, v); + v = mt_rr(d, MT_RX_STAT_2); + out->rx_dup_err = (uint16_t)FIELD_GET(MT_RX_STAT_2_DUP_ERRORS, v); + out->rx_overflow = (uint16_t)FIELD_GET(MT_RX_STAT_2_OVERFLOW_ERRORS, v); + + for (int i = 0; i < 16; i++) { + v = mt_rr(d, MT_TX_AGG_CNT(i)); + out->agg_cnt[i * 2] = (uint16_t)(v & 0xffff); + out->agg_cnt[i * 2 + 1] = (uint16_t)(v >> 16); + } + + /* The temperature sensor needs the MCU to sample it first. Its raw + * value is relative to a per-part 25 C reference in the EEPROM, which + * this port does not read, so the raw code is reported as-is rather + * than converted to a wrong number of degrees. */ + out->temp_c = INT8_MIN; + if (!mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0)) { + uint32_t t; + + if (!mt_rr_chk(d, MT_TEMP_SENSOR, &t)) + out->temp_c = (int8_t)FIELD_GET(MT_TEMP_SENSOR_VAL, t); + } + return 0; +} diff --git a/src/mt7612u/initvals.h b/src/mt7612u/initvals.h new file mode 100644 index 00000000..942f3c4c --- /dev/null +++ b/src/mt7612u/initvals.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* GENERATED - do not hand-edit. + * Source: openwrt/mt76 commit be5ce79, mt76_write_mac_initvals() in mt76x2/init.c, + * with register names and the DEFAULT_PROT_CFG_* macros resolved + * against mt76x02_regs.h. + * Regenerate: tools/extract_mt7612u_tables.py + * Verify: tools/extract_mt7612u_tables.py --check + * Table SHA-256 (LE reg,val stream): ba381217626e876b27a2aa9150440ff7c6af86740a3c64afbb36e81347df3719 + */ +#ifndef MT7612U_INITVALS_H +#define MT7612U_INITVALS_H + +static const struct { uint32_t reg; uint32_t val; } mt7612u_mac_initvals[] = { + { 0x0400, 0x00080c00 }, /* MT_PBF_SYS_CTRL */ + { 0x0404, 0x1efebcff }, /* MT_PBF_CFG */ + { 0x0800, 0x00000001 }, /* MT_FCE_PSE_CTRL */ + { 0x1004, 0x00000000 }, /* MT_MAC_SYS_CTRL */ + { 0x1018, 0x003e3f00 }, /* MT_MAX_LEN_CFG */ + { 0x1030, 0xaaa99887 }, /* MT_AMPDU_MAX_LEN_20M1S */ + { 0x1034, 0x000000aa }, /* MT_AMPDU_MAX_LEN_20M2S */ + { 0x1100, 0x33a40d0a }, /* MT_XIFS_TIME_CFG */ + { 0x1104, 0x00000209 }, /* MT_BKOFF_SLOT_CFG */ + { 0x1118, 0x00422010 }, /* MT_TBTT_SYNC_CFG */ + { 0x1204, 0x00000000 }, /* MT_PWR_PIN_CFG */ + { 0x1238, 0x001700c8 }, + { 0x1330, 0x00101001 }, /* MT_TX_SW_CFG0 */ + { 0x1334, 0x00010000 }, /* MT_TX_SW_CFG1 */ + { 0x1338, 0x00000000 }, /* MT_TX_SW_CFG2 */ + { 0x1340, 0x0400583f }, /* MT_TXOP_CTRL_CFG */ + { 0x1344, 0x00ffff20 }, /* MT_TX_RTS_CFG */ + { 0x1348, 0x000a2290 }, /* MT_TX_TIMEOUT_CFG */ + { 0x134c, 0x47f01f0f }, /* MT_TX_RETRY_CFG */ + { 0x1380, 0x002c00dc }, /* MT_EXP_ACK_TIME */ + { 0x13e0, 0xe3f42004 }, /* MT_TX_PROT_CFG6 */ + { 0x13e4, 0xe3f42084 }, /* MT_TX_PROT_CFG7 */ + { 0x13e8, 0xe3f42104 }, /* MT_TX_PROT_CFG8 */ + { 0x13ec, 0x00060fff }, /* MT_PIFS_TX_CFG */ + { 0x1400, 0x00015f97 }, /* MT_RX_FILTR_CFG */ + { 0x1408, 0x0000017f }, /* MT_LEGACY_BASIC_RATE */ + { 0x140c, 0x00004003 }, /* MT_HT_BASIC_RATE */ + { 0x150c, 0x00000003 }, /* MT_PN_PAD_MODE */ + { 0x1608, 0x00000002 }, /* MT_TXOP_HLDR_ET */ + { 0x0a44, 0x00000000 }, + { 0x0260, 0x00000000 }, /* MT_HEADER_TRANS_CTRL_REG */ + { 0x0250, 0x00000000 }, /* MT_TSO_CTRL */ + { 0x120c, 0x00000000 }, /* MT_AUX_CLK_CFG */ + { 0x1264, 0x00000000 }, /* MT_DACCLK_EN_DLY_CFG */ + { 0x13c0, 0x00000000 }, /* MT_TX_ALC_CFG_4 */ + { 0x13c8, 0x00000000 }, /* MT_TX_ALC_VGA3 */ + { 0x1314, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_0 */ + { 0x1318, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_1 */ + { 0x131c, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_2 */ + { 0x1320, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_3 */ + { 0x1324, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_4 */ + { 0x13d4, 0x3a3a3a3a }, /* MT_TX_PWR_CFG_7 */ + { 0x13d8, 0x0000003a }, /* MT_TX_PWR_CFG_8 */ + { 0x13dc, 0x0000003a }, /* MT_TX_PWR_CFG_9 */ + { 0x0024, 0x0000d000 }, /* MT_EFUSE_CTRL */ + { 0x0a38, 0x0000000a }, /* MT_PAUSE_ENABLE_CONTROL1 */ + { 0x0824, 0x60401c18 }, /* MT_FCE_WLAN_FLOW_CONTROL1 */ + { 0x0210, 0x94ff0000 }, /* MT_WPDMA_DELAY_INT_CFG */ + { 0x1478, 0x00000004 }, /* MT_TX_SW_CFG3 */ + { 0x1384, 0x00001818 }, /* MT_HT_FBK_TO_LEGACY */ + { 0x1358, 0xedcba980 }, /* MT_VHT_HT_FBK_CFG1 */ + { 0x1648, 0x00830083 }, /* MT_PROT_AUTO_TX_CFG */ + { 0x1410, 0x000001ff }, /* MT_HT_CTRL_CFG */ + { 0x1350, 0x00001020 }, /* MT_TX_LINK_CFG */ + { 0x1364, 0x07f40003 }, /* MT_CCK_PROT_CFG */ + { 0x1368, 0x07f42004 }, /* MT_OFDM_PROT_CFG */ + { 0x136c, 0x01752004 }, /* MT_MM20_PROT_CFG */ + { 0x1370, 0x03f52084 }, /* MT_MM40_PROT_CFG */ + { 0x1374, 0x01752004 }, /* MT_GF20_PROT_CFG */ + { 0x1378, 0x03f52084 }, /* MT_GF40_PROT_CFG */ +}; +#endif diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h new file mode 100644 index 00000000..d10e60ec --- /dev/null +++ b/src/mt7612u/internal.h @@ -0,0 +1,296 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +#ifndef MT7612U_INTERNAL_H +#define MT7612U_INTERNAL_H + +/* The rest of this project includes and lets the build system + * supply the include directory; the standalone Makefile here has no + * pkg-config step, and most distributions install the header under + * libusb-1.0/. Try the project's spelling first, fall back to the + * distribution path. */ +#if defined(__has_include) +# if __has_include() +# include +# else +# include +# endif +#else +# include +#endif +#include +#include +#include +#include +#include +#include "regs.h" +#include "include/mt7612u/mt7612u.h" + +/* Per-rate TX power, 0.5 dB units, exactly mt76x02_rate_power's layout. */ +struct mt_rate_power { + union { + struct { int8_t cck[4], ofdm[8], ht[16], vht[2]; }; + int8_t all[30]; + }; +}; + +struct mt_tx_power_info { + uint8_t target_power; + int8_t delta_bw40, delta_bw80; + struct { int8_t tssi_slope, tssi_offset, target_power, delta; } chain[2]; +}; + +/* EEPROM-derived values the host computes with (firmware does the rest). */ +struct mt7612u_cal { + int8_t rssi_offset[2]; + int8_t lna_gain; + int8_t high_gain[2]; + uint8_t init_cal_done; + uint8_t channel_cal_done; + uint32_t mcu_gain; + uint8_t agc_gain_init[2]; + uint8_t tssi_cal_done; +}; + +#define MT_RX_RING 16 +/* 16 slots, not 32: the slots now carry a full aggregate, so this is the + * difference between 256 KB and 512 KB of ring. Depth is not what buys + * throughput here - see "Async rings bought no throughput" in + * docs/mt7612u.md - and soak/ampdu were re-measured after the change. */ +#define MT_TX_RING 16 +#define MT_RX_BUFSZ 4096 +#define MT_USB_AGG_BUF 16384 /* one aggregated bulk-OUT transfer */ + +/* + * ONE ceiling for everything that reaches the async ring. + * + * This was 2048 while mt_tx_raw() built up to MT_TX_BUF_MAX (4096) and + * mt7612u_send_packets() up to MT_USB_AGG_BUF (16384), and both route through + * the ring whenever an RX loop is running - which is the normal integrated + * shape. So a frame over ~2 KB was refused after being built, and every + * multi-frame batch reported zero accepted. The sync path has no ring, which + * is exactly why the mtu sweep never saw it. + * + * Sized to the aggregate buffer and checked at compile time below, so a future + * change to either builder cannot silently reintroduce the mismatch. + */ +#define MT_TX_BUFSZ MT_USB_AGG_BUF + +/* + * One page, matching MT_RX_BUFSZ. The old 2048 silently capped a single frame + * at 2016 B while mt7612u_send_packets() bounded only against the 16 KB + * aggregate buffer, so the two public TX entry points disagreed about the + * largest frame this backend accepts. + * + * 4096 is a deliberate choice, not the hardware's limit: measured on air, this + * part transmits at least 7900 B and an RTL8812AU decoded 60/60 at every size + * up to that with zero CRC errors - far past 802.11's 2304 non-A-MSDU MPDU + * ceiling. It is capped here to match what this backend can RECEIVE, because + * MT_RX_BUFSZ is one page and a frame larger than that is dropped without a + * counter moving. Raising this means raising the RX buffer too. + */ +#define MT_TX_BUF_MAX 4096 + +/* Neither TX builder may outgrow the ring. A mismatch here is what made every + * aggregate fail silently once an RX loop was up, so it is a build error now + * rather than a runtime refusal. */ +typedef char mt_tx_ceiling_covers_single_frame[MT_TX_BUF_MAX <= MT_TX_BUFSZ ? 1 : -1]; +typedef char mt_tx_ceiling_covers_aggregate[MT_USB_AGG_BUF <= MT_TX_BUFSZ ? 1 : -1]; +#define MT_USB_AGG_MAX 32 /* frames chained per transfer */ + +struct mt7612u_dev; +struct mt_async; +/* Each slot names its own ring, not dev->a: if a teardown has to leak a ring + * whose transfers are still in flight, their completions must keep touching + * the leaked ring and never a replacement one. */ +struct mt_slot { struct mt7612u_dev *d; struct mt_async *a; int idx; }; + +struct mt_async { + struct libusb_transfer *rx[MT_RX_RING], *tx[MT_TX_RING]; + struct mt_slot rx_slot[MT_RX_RING], tx_slot[MT_TX_RING]; + uint8_t rx_buf[MT_RX_RING][MT_RX_BUFSZ]; + uint8_t tx_buf[MT_TX_RING][MT_TX_BUFSZ]; + int tx_busy[MT_TX_RING]; + /* Guards running, rx_active, tx_busy[], tx_inflight and rx_inflight - + * all of which the event thread writes and the caller reads. */ + pthread_mutex_t lock; + pthread_cond_t cv; + pthread_t evt; + int evt_started; + int running, rx_active; + int tx_inflight, rx_inflight; + mt7612u_rx_cb cb; + void *cb_user; + uint64_t tx_submitted, tx_done_n, tx_err, rx_frames, rx_err, rx_invalid; + uint64_t rx_dropped; /* rejected on length: truncated, or > MT_RX_BUFSZ */ +}; + +struct mt7612u_dev { + libusb_context *ctx; + libusb_device_handle *h; + int kernel_was_attached; + int keep_detached; + /* 0 when the handle and context were handed in by a caller that keeps + * ownership of them - mt_close() must then release the interface but + * neither close the handle nor exit the context. */ + int owns_handle; + + uint32_t rev; /* MT_ASIC_VERSION, e.g. 0x76120044 */ + uint8_t eeprom[MT7612U_EEPROM_SIZE]; + uint8_t macaddr[6]; + uint16_t chainmask; /* 0x202 = 2T2R */ + uint8_t mcu_seq; + uint8_t chan; + uint8_t bw; + 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; + int8_t target_power_delta[2]; + int enable_tpc; /* per-packet TX_PWR_ADJ; mt76 defaults it off */ + struct mt_rate_power rate_power; + struct mt7612u_cal cal; + + unsigned io_err; /* EP0 transfers that exhausted their retries */ + int transfers_stranded; /* libusb still owns a cancelled ring */ + uint16_t max_mpdu_rx; /* from MT_MAX_LEN_CFG at init, less the FCS */ + uint64_t stats_last_us; /* previous mt7612u_link_stats() mark */ + + /* Oracle-diff log: every EP0 write we emit, in order. */ + uint8_t ack_saved_mac[6]; + int ack_saved; + struct mt_async *a; + FILE *wrlog; + FILE *mculog; +}; + +/* --- usb.c --- */ +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); +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 + * register value here and cannot serve as an error sentinel. */ +int mt_rr_chk(struct mt7612u_dev *d, uint32_t addr, uint32_t *val); +uint32_t mt_rr(struct mt7612u_dev *d, uint32_t addr); +void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val); +/* Returns -1 without writing when the read half fails. */ +int mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val); +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); +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) +/* Poll until (rr(addr) & mask) == val. Returns 1 on success, 0 on timeout. */ +int mt_poll(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, + uint32_t val, int timeout_us); +int mt_vendor_req(struct mt7612u_dev *d, uint8_t req, uint8_t type, + uint16_t val, uint16_t idx, void *buf, size_t len); +/* Two 16-bit halves, as mt76u_single_wr(). Used for the FCE DMA regs. */ +void mt_wr_copy(struct mt7612u_dev *d, uint32_t offset, const void *data, int len); +void mt_single_wr(struct mt7612u_dev *d, uint8_t req, uint16_t off, uint32_t val); +int mt_bulk(struct mt7612u_dev *d, uint8_t ep, void *buf, int len, + int *xfered, unsigned timeout_ms); +int mt_wait_for_mac(struct mt7612u_dev *d); +void mt_usleep(unsigned us); + +/* --- mcu.c --- */ +int mt_mcu_send(struct mt7612u_dev *d, int cmd, const void *data, int len, int wait_resp); +int mt_mcu_function_select(struct mt7612u_dev *d, int func, uint32_t val); +int mt_mcu_set_radio_state(struct mt7612u_dev *d, int on); +int mt_mcu_calibrate(struct mt7612u_dev *d, int type, uint32_t param); +int mt_mcu_load_cr(struct mt7612u_dev *d, uint8_t type, uint8_t temp, uint8_t ch); +int mt_mcu_set_channel(struct mt7612u_dev *d, uint8_t ch, uint8_t bw, uint8_t bw_index, int scan); +int mt_mcu_init_gain(struct mt7612u_dev *d, uint8_t ch, uint32_t gain, int force); + +/* --- fw.c --- */ +int mt_fw_init(struct mt7612u_dev *d, const char *fw_dir); + +/* --- eeprom.c --- */ +void mt_get_rate_power(struct mt7612u_dev *d, struct mt_rate_power *t, int band); +void mt_get_power_info(struct mt7612u_dev *d, struct mt_tx_power_info *t, + uint8_t chan, int band); +void mt_read_rx_gain(struct mt7612u_dev *d, uint8_t chan, int band); +int mt_eeprom_init(struct mt7612u_dev *d); +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); +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); + +/* --- tx.c --- */ +uint16_t mt_tx_rate_word(const struct mt7612u_tx_rate *r); +/* ieee80211_hdrlen(), ported. Shared by TX (where to insert the L2 pad) and + * RX (how many bytes to move when folding it back out) - they must agree. */ +int mt_hdrlen_from_fc(const uint8_t *frame); +#define MT_TXOPT_RATE_LUT 0x01 /* set MT_TXWI_FLAGS_TX_RATE_LUT */ +#define MT_TXOPT_AMPDU 0x02 /* AMPDU flag + density + BA window */ +#define MT_TXOPT_QSEL_MGMT 0x04 /* mt76 uses MT_QSEL_MGMT for aggregated TX */ +int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, + const void *frame, size_t len, + const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts, + int next_vld, int trailer); +int mt_tx_raw(struct mt7612u_dev *d, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts); +void mt_wcid_setup(struct mt7612u_dev *d, uint8_t idx, const uint8_t *mac); + +/* --- radiotap.c --- */ +int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r); + +/* --- async.c --- */ +struct mt_async_stats { + uint64_t tx_submitted, tx_done, tx_err, rx_frames, rx_err, rx_invalid; + uint64_t rx_dropped; +}; +void mt_async_stats(struct mt7612u_dev *d, struct mt_async_stats *out); +void mt_async_note_invalid(struct mt7612u_dev *d); +int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user); +void mt_async_stop(struct mt7612u_dev *d); +int mt_async_tx_submit(struct mt7612u_dev *d, const uint8_t *buf, int len); + +/* --- rx.c --- */ +int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, + const uint8_t **frame, struct mt7612u_rx_info *info); +int mt_rx_one(struct mt7612u_dev *d, uint8_t *buf, int bufsize, + const uint8_t **frame, struct mt7612u_rx_info *info, + unsigned timeout_ms); + +/* --- phy.c / chan.c --- */ +void mt_phy_set_rxpath(struct mt7612u_dev *d); +void mt_phy_set_txpower(struct mt7612u_dev *d, int band); +int mt_tssi_enabled(struct mt7612u_dev *d); +int8_t mt_tx_get_max_txpwr_adj(struct mt7612u_dev *d, + const struct mt7612u_tx_rate *r); +int8_t mt_tx_get_txpwr_adj(struct mt7612u_dev *d, int8_t txpwr, int8_t max_adj); +void mt_phy_set_txdac(struct mt7612u_dev *d); +int mt_set_channel(struct mt7612u_dev *d, uint8_t chan, uint8_t bw); +int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast); +int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, + uint8_t *bw_index, uint8_t *ch_group); + +/* + * The diagnostic plane, per docs/logging.md: stderr, one line of + * `devourer [X] message` with X in T/D/I/W/E. + * + * The old macros broke that contract three ways. They used a private + * `[mt7612u]` prefix rather than the documented one, so a consumer filtering + * on level saw nothing; they emitted the text and its newline as two separate + * stdio calls, so a line written from the libusb event thread could be split + * by one from a caller; and they never flushed, so a piped reader could sit on + * a full buffer while the device was mid-bring-up. mt_diag() formats the whole + * line first and emits it with a single fwrite + fflush, which is what + * src/Event.h does for the machine plane and for the same reason. + */ +void mt_diag(char level, const char *fmt, ...) +#if defined(__GNUC__) + __attribute__((format(printf, 2, 3))) +#endif + ; +#define LOG(...) mt_diag('I', __VA_ARGS__) +#define WARN(...) mt_diag('W', __VA_ARGS__) +#define ERR(...) mt_diag('E', __VA_ARGS__) + +#endif diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c new file mode 100644 index 00000000..c81679a1 --- /dev/null +++ b/src/mt7612u/mcu.c @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * In-band MCU transport. Requests go out on EP 8 wrapped in the same 4-byte + * TXINFO header as data frames (DPORT = CPU_TX_PORT, TYPE_CMD); responses come + * back on EP 5 and are matched on the 4-bit sequence number. + * Ported from mt76/mt76x02_usb_mcu.c and mt76x2/mcu.c. + */ +#include +#include "internal.h" + +#define MCU_RESP_URB_SIZE 1024 +#define MCU_MSG_MAX 192 /* MT_INBAND_PACKET_MAX_LEN */ + +static void put_le32(uint8_t *p, uint32_t v) +{ + p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; + p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff; +} + +static uint32_t get_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +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++) { + 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; + } + if (len < 4) + continue; + + uint32_t rxfce = get_le32(buf); + if (FIELD_GET(MT_RX_FCE_INFO_CMD_SEQ, rxfce) == seq && + FIELD_GET(MT_RX_FCE_INFO_EVT_TYPE, rxfce) == MT_EVT_CMD_DONE) + return 0; + ERR("mcu resp mismatch: evt=%u seq=%u (want %u)", + FIELD_GET(MT_RX_FCE_INFO_EVT_TYPE, rxfce), + FIELD_GET(MT_RX_FCE_INFO_CMD_SEQ, rxfce), seq); + } + ERR("mcu command timed out waiting for response"); + return -1; +} + +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]; + 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; } + + if (wait_resp) { + seq = ++d->mcu_seq & 0xf; + if (!seq) + seq = ++d->mcu_seq & 0xf; + } + + /* TXINFO: LEN is the payload rounded to 4, computed before the header + * is prepended - matching mt76x02u_skb_dma_info(). */ + info = FIELD_PREP(MT_TXD_INFO_LEN, (uint32_t)((len + 3) & ~3)) | + FIELD_PREP(MT_TXD_INFO_DPORT, CPU_TX_PORT) | + FIELD_PREP(MT_MCU_MSG_CMD_SEQ, seq) | + FIELD_PREP(MT_MCU_MSG_CMD_TYPE, (uint32_t)cmd) | + MT_MCU_MSG_TYPE_CMD; + + put_le32(buf, info); + memcpy(buf + 4, data, len); + /* pad the (header + payload) to 4, then a 4-byte zero terminator */ + pad = (((4 + len) + 3) & ~3) + 4 - (4 + len); + memset(buf + 4 + len, 0, pad); + total = 4 + len + pad; + + if (d->mculog) { + fprintf(d->mculog, "cmd=%-2d seq=%u len=%d payload=", cmd, seq, len); + for (int i = 0; i < len; i++) + fprintf(d->mculog, "%02x", buf[4 + i]); + fputc('\n', d->mculog); + } + + 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; } + + return wait_resp ? mcu_wait_resp(d, seq) : 0; +} + +int mt_mcu_function_select(struct mt7612u_dev *d, int func, uint32_t val) +{ + uint8_t msg[8]; + put_le32(msg, (uint32_t)func); + put_le32(msg + 4, val); + /* Q_SELECT is the one function mt76 does not wait on. */ + return mt_mcu_send(d, CMD_FUN_SET_OP, msg, sizeof msg, func != Q_SELECT); +} + +int mt_mcu_set_radio_state(struct mt7612u_dev *d, int on) +{ + uint8_t msg[8]; + put_le32(msg, on ? RADIO_ON : RADIO_OFF); + put_le32(msg + 4, 0); + return mt_mcu_send(d, CMD_POWER_SAVING_OP, msg, sizeof msg, 0); +} + +int mt_mcu_calibrate(struct mt7612u_dev *d, int type, uint32_t param) +{ + uint8_t msg[8]; + put_le32(msg, (uint32_t)type); + put_le32(msg + 4, param); + return mt_mcu_send(d, CMD_CALIBRATION_OP, msg, sizeof msg, 1); +} + +int mt_mcu_load_cr(struct mt7612u_dev *d, uint8_t type, uint8_t temp, uint8_t ch) +{ + uint8_t msg[8]; + uint32_t val = BIT(31); + + val |= (uint32_t)(mt_ee(d, MT_EE_NIC_CONF_0) >> 8) & 0x00ff; + val |= (uint32_t)(mt_ee(d, MT_EE_NIC_CONF_1) << 8) & 0xff00; + + msg[0] = type; msg[1] = temp; msg[2] = ch; msg[3] = 0; + put_le32(msg + 4, val); + return mt_mcu_send(d, CMD_LOAD_CR, msg, sizeof msg, 1); +} + +int mt_mcu_init_gain(struct mt7612u_dev *d, uint8_t ch, uint32_t gain, int force) +{ + uint8_t msg[8]; + put_le32(msg, (uint32_t)ch | (force ? BIT(31) : 0)); + put_le32(msg + 4, gain); + return mt_mcu_send(d, CMD_INIT_GAIN_OP, msg, sizeof msg, 1); +} + +int mt_mcu_set_channel(struct mt7612u_dev *d, uint8_t ch, uint8_t bw, + uint8_t bw_index, int scan) +{ + uint8_t msg[8]; + + msg[0] = ch; + msg[1] = (uint8_t)scan; + msg[2] = bw; + msg[3] = 0; + msg[4] = d->chainmask & 0xff; + msg[5] = (d->chainmask >> 8) & 0xff; + msg[6] = 0; /* ext_chan: first pass carries none */ + msg[7] = 0; + + if (mt_mcu_send(d, CMD_SWITCH_CHANNEL_OP, msg, sizeof msg, 1)) + return -1; + mt_usleep(7500); + + msg[6] = (uint8_t)(0xe0 + bw_index); + return mt_mcu_send(d, CMD_SWITCH_CHANNEL_OP, msg, sizeof msg, 1); +} diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c new file mode 100644 index 00000000..5a062651 --- /dev/null +++ b/src/mt7612u/phy.c @@ -0,0 +1,619 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Band / bandwidth / TX-power register setup and the channel sequence. + * Ported from mt76/mt76x02_phy.c, mt76x2/phy.c and mt76x2/usb_phy.c. + * + * The RF synthesizer itself is never touched here - CMD_SWITCH_CHANNEL_OP + * hands the channel to firmware, which owns synthesis, AGC and calibration. + */ +#include +#include "internal.h" + +#define BAND_2GHZ 0 +#define BAND_5GHZ 1 + +static int ext_pa_enabled(struct mt7612u_dev *d, int band) +{ + uint16_t conf0 = mt_ee(d, MT_EE_NIC_CONF_0); + + return band == BAND_5GHZ ? !(conf0 & MT_EE_NIC_CONF_0_PA_INT_5G) + : !(conf0 & MT_EE_NIC_CONF_0_PA_INT_2G); +} + +static void phy_set_band(struct mt7612u_dev *d, int band, int primary_upper) +{ + if (band == BAND_2GHZ) { + mt_set(d, MT_TX_BAND_CFG, MT_TX_BAND_CFG_2G); + mt_clear(d, MT_TX_BAND_CFG, MT_TX_BAND_CFG_5G); + } else { + mt_clear(d, MT_TX_BAND_CFG, MT_TX_BAND_CFG_2G); + mt_set(d, MT_TX_BAND_CFG, MT_TX_BAND_CFG_5G); + } + mt_rmw(d, MT_TX_BAND_CFG, MT_TX_BAND_CFG_UPPER_40M, + FIELD_PREP(MT_TX_BAND_CFG_UPPER_40M, (uint32_t)!!primary_upper)); +} + +static void phy_set_bw(struct mt7612u_dev *d, uint8_t bw, uint8_t ctrl) +{ + int core_val, agc_val; + + switch (bw) { + case MT7612U_BW_80: core_val = 3; agc_val = 7; break; + case MT7612U_BW_40: core_val = 2; agc_val = 3; break; + default: core_val = 0; agc_val = 1; break; + } + mt_rmw(d, MT_BBP(CORE, 1), MT_BBP_CORE_R1_BW, + FIELD_PREP(MT_BBP_CORE_R1_BW, (uint32_t)core_val)); + mt_rmw(d, MT_BBP(AGC, 0), MT_BBP_AGC_R0_BW, + FIELD_PREP(MT_BBP_AGC_R0_BW, (uint32_t)agc_val)); + mt_rmw(d, MT_BBP(AGC, 0), MT_BBP_AGC_R0_CTRL_CHAN, + FIELD_PREP(MT_BBP_AGC_R0_CTRL_CHAN, ctrl)); + mt_rmw(d, MT_BBP(TXBE, 0), MT_BBP_TXBE_R0_CTRL_CHAN, + FIELD_PREP(MT_BBP_TXBE_R0_CTRL_CHAN, ctrl)); +} + +static void phy_set_txpower_regs(struct mt7612u_dev *d, int band) +{ + uint32_t pa_mode[2], pa_mode_adj; + + if (band == BAND_2GHZ) { + pa_mode[0] = 0x010055ff; + pa_mode[1] = 0x00550055; + mt_wr(d, MT_TX_ALC_CFG_2, 0x35160a00); + mt_wr(d, MT_TX_ALC_CFG_3, 0x35160a06); + if (ext_pa_enabled(d, band)) { + mt_wr(d, MT_RF_PA_MODE_ADJ0, 0x0000ec00); + mt_wr(d, MT_RF_PA_MODE_ADJ1, 0x0000ec00); + } else { + mt_wr(d, MT_RF_PA_MODE_ADJ0, 0xf4000200); + mt_wr(d, MT_RF_PA_MODE_ADJ1, 0xfa000200); + } + } else { + pa_mode[0] = 0x0000ffff; + pa_mode[1] = 0x00ff00ff; + if (ext_pa_enabled(d, band)) { + mt_wr(d, MT_TX_ALC_CFG_2, 0x2f0f0400); + mt_wr(d, MT_TX_ALC_CFG_3, 0x2f0f0476); + pa_mode_adj = 0x04000000; + } else { + mt_wr(d, MT_TX_ALC_CFG_2, 0x1b0f0400); + mt_wr(d, MT_TX_ALC_CFG_3, 0x1b0f0476); + pa_mode_adj = 0; + } + mt_wr(d, MT_RF_PA_MODE_ADJ0, pa_mode_adj); + mt_wr(d, MT_RF_PA_MODE_ADJ1, pa_mode_adj); + } + + mt_wr(d, MT_BB_PA_MODE_CFG0, pa_mode[0]); + mt_wr(d, MT_BB_PA_MODE_CFG1, pa_mode[1]); + mt_wr(d, MT_RF_PA_MODE_CFG0, pa_mode[0]); + mt_wr(d, MT_RF_PA_MODE_CFG1, pa_mode[1]); + + if (ext_pa_enabled(d, band)) { + uint32_t val = (band == BAND_2GHZ) ? 0x3c3c023c : 0x363c023c; + + mt_wr(d, MT_TX0_RF_GAIN_CORR, val); + mt_wr(d, MT_TX1_RF_GAIN_CORR, val); + mt_wr(d, MT_TX_ALC_CFG_4, 0x00001818); + } else if (band == BAND_2GHZ) { + mt_wr(d, MT_TX0_RF_GAIN_CORR, 0x0f3c3c3c); + mt_wr(d, MT_TX1_RF_GAIN_CORR, 0x0f3c3c3c); + mt_wr(d, MT_TX_ALC_CFG_4, 0x00000606); + } else { + mt_wr(d, MT_TX0_RF_GAIN_CORR, 0x383c023c); + mt_wr(d, MT_TX1_RF_GAIN_CORR, 0x24282e28); + mt_wr(d, MT_TX_ALC_CFG_4, 0); + } +} + +static void configure_tx_delay(struct mt7612u_dev *d, int band, uint8_t bw) +{ + uint32_t cfg0, cfg1; + + if (ext_pa_enabled(d, band)) { + cfg0 = bw ? 0x000b0c01 : 0x00101101; + cfg1 = 0x00011414; + } else { + cfg0 = bw ? 0x000b0b01 : 0x00101001; + cfg1 = 0x00021414; + } + mt_wr(d, MT_TX_SW_CFG0, cfg0); + mt_wr(d, MT_TX_SW_CFG1, cfg1); + mt_rmw(d, MT_XIFS_TIME_CFG, MT_XIFS_TIME_CFG_OFDM_SIFS, + FIELD_PREP(MT_XIFS_TIME_CFG_OFDM_SIFS, 15)); +} + +static void adjust_high_lna_gain(struct mt7612u_dev *d, int reg, int8_t offset) +{ + int8_t gain = (int8_t)FIELD_GET(MT_BBP_AGC_LNA_HIGH_GAIN, + mt_rr(d, MT_BBP(AGC, reg))); + gain -= offset / 2; + mt_rmw(d, MT_BBP(AGC, reg), MT_BBP_AGC_LNA_HIGH_GAIN, + FIELD_PREP(MT_BBP_AGC_LNA_HIGH_GAIN, (uint32_t)gain)); +} + +static void adjust_agc_gain(struct mt7612u_dev *d, int reg, int8_t offset) +{ + int8_t gain = (int8_t)FIELD_GET(MT_BBP_AGC_GAIN, + mt_rr(d, MT_BBP(AGC, reg))); + gain += offset; + mt_rmw(d, MT_BBP(AGC, reg), MT_BBP_AGC_GAIN, + FIELD_PREP(MT_BBP_AGC_GAIN, (uint32_t)gain)); +} + +static void apply_gain_adj(struct mt7612u_dev *d) +{ + adjust_high_lna_gain(d, 4, d->cal.high_gain[0]); + adjust_high_lna_gain(d, 5, d->cal.high_gain[1]); + adjust_agc_gain(d, 8, d->cal.high_gain[0]); + adjust_agc_gain(d, 9, d->cal.high_gain[1]); +} + +static void channel_calibrate(struct mt7612u_dev *d, int is_5ghz) +{ + if (d->cal.channel_cal_done) + return; + + if (is_5ghz) + mt_mcu_calibrate(d, MCU_CAL_LC, 0); + + mt_mcu_calibrate(d, MCU_CAL_TX_LOFT, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_TXIQ, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_RXIQC_FI, (uint32_t)is_5ghz); + mt_mcu_calibrate(d, MCU_CAL_TEMP_SENSOR, 0); + mt_mcu_calibrate(d, MCU_CAL_TX_SHAPING, 0); + + apply_gain_adj(d); + + /* mt76x02_edcca_init(), ed_monitor off: energy-detect CCA disabled, + * which is what an injector wants - the MAC will not withhold a frame + * because it sees energy on the channel. */ + mt_set(d, MT_TX_LINK_CFG, MT_TX_CFACK_EN); + mt_clear(d, MT_TXOP_CTRL_CFG, MT_TXOP_ED_CCA_EN); + mt_wr(d, MT_BBP(AGC, 2), 0x00007070); + mt_set(d, MT_TXOP_HLDR_ET, MT_TXOP_HLDR_TX40M_BLK_EN); + + d->cal.channel_cal_done = 1; +} + +/* mt76x2_tssi_enabled(): TX_ALC_EN set and temperature-compensated ALC off. */ +int mt_tssi_enabled(struct mt7612u_dev *d) +{ + uint16_t c1 = mt_ee(d, MT_EE_NIC_CONF_1); + + return !(c1 & MT_EE_NIC_CONF_1_TEMP_TX_ALC) && (c1 & MT_EE_NIC_CONF_1_TX_ALC_EN); +} +#define tssi_enabled mt_tssi_enabled + +/* ---- per-rate TX power ---- */ + +static uint32_t tx_power_mask(uint8_t v1, uint8_t v2, uint8_t v3, uint8_t v4) +{ + return ((uint32_t)(v1 & 0x3f)) | ((uint32_t)(v2 & 0x3f) << 8) | + ((uint32_t)(v3 & 0x3f) << 16) | ((uint32_t)(v4 & 0x3f) << 24); +} + +static void add_rate_power_offset(struct mt_rate_power *r, int offset) +{ + for (unsigned i = 0; i < sizeof r->all; i++) + r->all[i] = (int8_t)(r->all[i] + offset); +} + +static void limit_rate_power(struct mt_rate_power *r, int limit) +{ + for (unsigned i = 0; i < sizeof r->all; i++) + if (r->all[i] > limit) + r->all[i] = (int8_t)limit; +} + +static int get_min_rate_power(const struct mt_rate_power *r) +{ + int8_t ret = 0; + + for (unsigned i = 0; i < sizeof r->all; i++) { + if (!r->all[i]) continue; + ret = ret ? (r->all[i] < ret ? r->all[i] : ret) : r->all[i]; + } + return ret; +} + +/* + * mt76x2_phy_set_txpower(). Builds the per-rate power table from the EEPROM, + * normalises it against the chain target powers, and writes the eight + * MT_TX_PWR_CFG_* registers plus the two ALC chain-init fields. + * Until this ran, those registers held the initvals 0x3a3a3a3a. + */ +void mt_phy_set_txpower(struct mt7612u_dev *d, int band) +{ + struct mt_tx_power_info txp; + struct mt_rate_power t; + int txp_0, txp_1, delta = 0, base_power, gain; + + mt_get_power_info(d, &txp, d->chan, band); + + if (d->bw == MT7612U_BW_40) delta = txp.delta_bw40; + else if (d->bw == MT7612U_BW_80) delta = txp.delta_bw80; + + mt_get_rate_power(d, &t, band); + add_rate_power_offset(&t, txp.target_power + delta); + limit_rate_power(&t, d->txpower_conf); + + base_power = get_min_rate_power(&t); + delta = base_power - txp.target_power; + txp_0 = txp.chain[0].target_power + txp.chain[0].delta + delta; + txp_1 = txp.chain[1].target_power + txp.chain[1].delta + delta; + + gain = txp_0 < txp_1 ? txp_0 : txp_1; + if (gain < 0) { + base_power -= gain; + txp_0 -= gain; + txp_1 -= gain; + } else if (gain > 0x2f) { + base_power -= gain - 0x2f; + txp_0 = 0x2f; + txp_1 = 0x2f; + } + + add_rate_power_offset(&t, -base_power); + d->target_power = (int8_t)txp.target_power; + d->target_power_delta[0] = (int8_t)(txp_0 - txp.chain[0].target_power); + d->target_power_delta[1] = (int8_t)(txp_1 - txp.chain[0].target_power); + d->rate_power = t; + + mt_rmw(d, MT_TX_ALC_CFG_0, MT_TX_ALC_CFG_0_CH_INIT_0, + FIELD_PREP(MT_TX_ALC_CFG_0_CH_INIT_0, (uint32_t)txp_0)); + mt_rmw(d, MT_TX_ALC_CFG_0, MT_TX_ALC_CFG_0_CH_INIT_1, + FIELD_PREP(MT_TX_ALC_CFG_0_CH_INIT_1, (uint32_t)txp_1)); + + mt_wr(d, MT_TX_PWR_CFG_0, tx_power_mask(t.cck[0], t.cck[2], t.ofdm[0], t.ofdm[2])); + mt_wr(d, MT_TX_PWR_CFG_1, tx_power_mask(t.ofdm[4], t.ofdm[6], t.ht[0], t.ht[2])); + mt_wr(d, MT_TX_PWR_CFG_2, tx_power_mask(t.ht[4], t.ht[6], t.ht[8], t.ht[10])); + mt_wr(d, MT_TX_PWR_CFG_3, tx_power_mask(t.ht[12], t.ht[14], t.ht[0], t.ht[2])); + mt_wr(d, MT_TX_PWR_CFG_4, tx_power_mask(t.ht[4], t.ht[6], 0, 0)); + mt_wr(d, MT_TX_PWR_CFG_7, tx_power_mask(t.ofdm[7], t.vht[0], t.ht[7], t.vht[1])); + mt_wr(d, MT_TX_PWR_CFG_8, tx_power_mask(t.ht[14], 0, t.vht[0], t.vht[1])); + mt_wr(d, MT_TX_PWR_CFG_9, tx_power_mask(t.ht[7], 0, t.vht[0], t.vht[1])); +} + +/* mt76x02_tx_get_max_txpwr_adj(): the per-rate ceiling for this frame. */ +int8_t mt_tx_get_max_txpwr_adj(struct mt7612u_dev *d, + const struct mt7612u_tx_rate *r) +{ + const struct mt_rate_power *t = &d->rate_power; + + switch (r->phy) { + case MT7612U_PHY_VHT: + if (r->mcs == 8 || r->mcs == 9) + return t->vht[0]; + return t->ht[(((r->nss ? r->nss - 1 : 0) << 3) + r->mcs) & 0xf]; + case MT7612U_PHY_HT: + case MT7612U_PHY_HT_GF: + return t->ht[r->mcs & 0xf]; + case MT7612U_PHY_CCK: + return t->cck[r->mcs & 0x3]; + default: + return t->ofdm[r->mcs & 0x7]; + } +} + +/* mt76x02_tx_get_txpwr_adj(): the 4-bit per-packet trim in txwi.ctl2. */ +int8_t mt_tx_get_txpwr_adj(struct mt7612u_dev *d, int8_t txpwr, int8_t max_adj) +{ + int v = txpwr < d->txpower_conf ? txpwr : d->txpower_conf; + + v -= (d->target_power + d->target_power_delta[0]); + if (v > max_adj) v = max_adj; + + if (!d->enable_tpc) + return 0; + if (v >= 0) + return (int8_t)(v < 7 ? v : 7); + return (int8_t)(v < -16 ? 8 : (v + 32) / 2); +} + +/* + * Control channel + width -> the three values the rest of the tune needs: + * the *hardware* channel, which is the centre of the widened channel and not + * the control channel (36 at 80 MHz tunes 42); `bw_index`, which the firmware + * reads; and `ch_group_index`, which says which 20 MHz slot of the group the + * control channel occupies and so picks both the CCA ordering and the + * primary-channel bits in the BBP. + * + * mt76 does not compute any of this: mt76x2u_phy_set_channel() gets the + * segment centre from cfg80211's chandef, which carries `center_freq1` + * alongside the control channel. This API takes a bare channel number, so the + * centre has to be derived from the standard groupings instead. That + * derivation is an assumption about how the channel is meant to be grouped, + * and getting it wrong transmits a full-width signal centred somewhere the + * caller did not ask for - silently, because every register write still + * succeeds. So the 80 MHz result is checked against the centres the spec + * actually defines rather than trusted. + * + * Returns 0, or -1 when the control channel cannot carry the width. + */ +int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, + uint8_t *bw_index, uint8_t *ch_group) +{ + /* + * Legal *centre* channels per band and width. Validating the centre + * rather than the control channel catches three failures with one + * check: an off-grid control channel, one whose widened span leaves + * the band this driver declares, and the integer wrap that a uint8_t + * would otherwise hide (control channel 254 at 40 MHz computes 256, + * which truncates to 0 and would tune the MCU to channel index 0 with + * the 5 GHz register set loaded). + * + * The ceilings match caps.c's band_5g_max_mhz = 5825, i.e. channel + * 165. That is why the 165/169/173/177 VHT80 group (centre 171 = + * 5855 MHz, spanning to 5895) and the 40 MHz centres 167 and 175 are + * absent: mt76's own channel list has them, but tuning them would put + * most of the carrier outside the band this driver advertises. Widen + * the declared band first if they are ever wanted. + */ + static const uint8_t centre_40_5g[] = { + 38, 46, 54, 62, 102, 110, 118, 126, 134, 142, 151, 159, + }; + /* 2.4 GHz 40 MHz reaches only centres 6-9 through the pairing below, + * i.e. control channels 4-11. 1-3 would need a secondary at or below + * channel 0 and 12-13 one above 13; both are what the old wrap + * produced silently. */ + static const uint8_t centre_40_2g[] = { 6, 7, 8, 9 }; + /* VHT80 centres. 5.35-5.47 GHz (centres 74 and 90) is not allocated. */ + static const uint8_t centre_80_5g[] = { 42, 58, 106, 122, 138, 155 }; + + const uint8_t *ok = NULL; + size_t n_ok = 0, i; + /* int, not uint8_t: the arithmetic below can leave 0..255 before it has + * been validated. The grid check is what actually catches that - no + * wrapped value aliases onto a legal centre at either width, so a + * uint8_t here would still be refused - but it would be refused while + * reporting the wrapped number, and "would centre on 0" sends whoever + * reads that message looking for the wrong bug. */ + int idx = 0, group = 0, hw = chan; + + switch (bw) { + case MT7612U_BW_20: + goto out; /* the control channel is the centre */ + case MT7612U_BW_40: + /* mt76x2u_phy_set_channel()'s 40 MHz case. Which side the + * secondary sits on follows the standard pairing: 36/44/149/157 + * take the upper half, 40/48/153/161 the lower. */ + if ((chan / 4) & 1) { idx = 1; group = 0; } + else { idx = 3; group = 1; } + hw = chan + 2 - group * 4; + if (chan <= 14) { + ok = centre_40_2g; + n_ok = sizeof centre_40_2g / sizeof centre_40_2g[0]; + } else { + ok = centre_40_5g; + n_ok = sizeof centre_40_5g / sizeof centre_40_5g[0]; + } + break; + case MT7612U_BW_80: + /* mt76 computes this as (freq - freq1 + 30) / 20, i.e. the + * control channel's offset from the segment centre in 20 MHz + * steps. Over the 5 GHz grid that is the same value as the + * channel's position within its group of four, and this form + * needs no centre to be supplied by the caller. It stays + * correct across the U-NII-3 discontinuity (the group starts at + * 149, not 148) because the integer divide absorbs the +1 that + * the whole group carries. */ + group = ((chan / 4) + 3) & 3; + idx = group; + hw = chan + 6 - group * 4; + /* 802.11ac defines no 80 MHz channel in 2.4 GHz. */ + if (chan <= 14) { + ERR("80 MHz needs a 5 GHz channel (asked for %u)", chan); + return -1; + } + ok = centre_80_5g; + n_ok = sizeof centre_80_5g / sizeof centre_80_5g[0]; + break; + default: + ERR("unsupported bandwidth %u", bw); + return -1; + } + + for (i = 0; i < n_ok; i++) + if (hw == ok[i]) + goto out; + ERR("channel %u cannot carry %d MHz: it would centre on %d, which is " + "not a usable %d MHz centre channel in this band", + chan, 20 << bw, hw, 20 << bw); + return -1; + +out: + if (hw_chan) *hw_chan = (uint8_t)hw; + if (bw_index) *bw_index = (uint8_t)idx; + if (ch_group) *ch_group = (uint8_t)group; + return 0; +} + +/* 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. */ +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] = { + FIELD_PREP(MT_EXT_CCA_CFG_CCA0, 0) | FIELD_PREP(MT_EXT_CCA_CFG_CCA1, 1) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA2, 2) | FIELD_PREP(MT_EXT_CCA_CFG_CCA3, 3) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA_MASK, BIT(0)), + FIELD_PREP(MT_EXT_CCA_CFG_CCA0, 1) | FIELD_PREP(MT_EXT_CCA_CFG_CCA1, 0) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA2, 2) | FIELD_PREP(MT_EXT_CCA_CFG_CCA3, 3) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA_MASK, BIT(1)), + FIELD_PREP(MT_EXT_CCA_CFG_CCA0, 2) | FIELD_PREP(MT_EXT_CCA_CFG_CCA1, 3) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA2, 1) | FIELD_PREP(MT_EXT_CCA_CFG_CCA3, 0) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA_MASK, BIT(2)), + FIELD_PREP(MT_EXT_CCA_CFG_CCA0, 3) | FIELD_PREP(MT_EXT_CCA_CFG_CCA1, 2) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA2, 1) | FIELD_PREP(MT_EXT_CCA_CFG_CCA3, 0) | + FIELD_PREP(MT_EXT_CCA_CFG_CCA_MASK, BIT(3)), + }; + int band = chan > 14 ? BAND_5GHZ : BAND_2GHZ; + uint8_t bw_index = 0, ch_group_index = 0, hw_chan = chan; + + if (mt_chan_group(chan, bw, &hw_chan, &bw_index, &ch_group_index)) + return -1; + + /* Same bracket as the bring-up: the tune is a long run of best-effort + * writes, and a channel reported as set while half its registers never + * landed is worse than a refusal. */ + mt_io_clear(d); + + d->cal.channel_cal_done = fast; + d->chan = chan; + d->bw = bw; + /* 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 + * quirk silences every genuine one that follows. */ + d->bw_clamp_warned = 0; + + mt_read_rx_gain(d, chan, band); + phy_set_txpower_regs(d, band); + configure_tx_delay(d, band, bw); + mt_phy_set_txpower(d, band); + phy_set_band(d, band, ch_group_index & 1); + phy_set_bw(d, bw, ch_group_index); + + mt_rmw(d, MT_EXT_CCA_CFG, + MT_EXT_CCA_CFG_CCA0 | MT_EXT_CCA_CFG_CCA1 | MT_EXT_CCA_CFG_CCA2 | + MT_EXT_CCA_CFG_CCA3 | MT_EXT_CCA_CFG_CCA_MASK, + ext_cca_chan[ch_group_index]); + + /* The BBP is already at the new width by here, so a failed synth leaves + * the part genuinely half-tuned. Clear d->chan on the way out: it is the + * "never tuned" sentinel the TX width clamp and the power setter already + * test, and claiming a channel we did not reach is worse than claiming + * none. */ + if (mt_mcu_set_channel(d, hw_chan, bw, bw_index, 0)) { + d->chan = 0; + return -1; + } + if (mt_mcu_init_gain(d, hw_chan, d->cal.mcu_gain, 1)) { + d->chan = 0; + return -1; + } + + /* rev >= E3: enable LDPC Rx */ + mt_set(d, MT_BBP(RXO, 13), BIT(10)); + + /* The width-dependent half of mt76x2_phy_update_channel_gain() + * (mt76x2/phy.c:300-323). That function is upstream's periodic RX gain + * worker, which this port does not have; but two of its writes depend + * only on the channel width, not on the measured gain, so they belong + * with the tune. Without them an 80 MHz channel runs the RX front end + * on the 20/40 MHz values the firmware init left behind - the part + * still receives, just with the wrong AGC shape. The gain-tracking + * remainder (low_gain, AGC 26's low nibble, the per-width RSSI + * thresholds) stays unported and is noted in the README. */ + if (bw == MT7612U_BW_80) { + mt_wr(d, MT_BBP(RXO, 14), 0x00560211); + mt_wr(d, MT_BBP(AGC, 35), 0x10101014); + } else { + mt_wr(d, MT_BBP(RXO, 14), 0x00560423); + mt_wr(d, MT_BBP(AGC, 35), + band == BAND_2GHZ ? 0x11111516 : 0x11111116); + } + + if (!d->cal.init_cal_done) { + uint8_t v = d->eeprom[MT_EE_BT_RCAL_RESULT]; + + if (v != 0xff) + mt_mcu_calibrate(d, MCU_CAL_R, 0); + } + mt_mcu_calibrate(d, MCU_CAL_RXDCOC, hw_chan); + if (!d->cal.init_cal_done) + mt_mcu_calibrate(d, MCU_CAL_RC, 0); + d->cal.init_cal_done = 1; + + mt_wr(d, MT_BBP(AGC, 61), 0xff64a4e2); + mt_wr(d, MT_BBP(AGC, 7), 0x08081010); + mt_wr(d, MT_BBP(AGC, 11), 0x00000404); + mt_wr(d, MT_BBP(AGC, 2), 0x00007070); + mt_wr(d, MT_TXOP_CTRL_CFG, 0x04101b3f); + + mt_set(d, MT_BBP(TXO, 4), BIT(25)); + mt_set(d, MT_BBP(RXO, 13), BIT(8)); + + channel_calibrate(d, band == BAND_5GHZ); + + 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; + + mt_rmw(d, MT_TX_ALC_CFG_1, MT_TX_ALC_CFG_1_TEMP_COMP, + FIELD_PREP(MT_TX_ALC_CFG_1_TEMP_COMP, 0x38)); + mt_rmw(d, MT_TX_ALC_CFG_2, MT_TX_ALC_CFG_2_TEMP_COMP, + FIELD_PREP(MT_TX_ALC_CFG_2_TEMP_COMP, 0x38)); + + if (band == BAND_5GHZ) + flag |= BIT(0); + if (ext_pa_enabled(d, band)) + flag |= BIT(8); + mt_mcu_calibrate(d, MCU_CAL_TSSI, flag); + d->cal.tssi_cal_done = 1; + } + if (mt_io_errors(d)) { + ERR("channel %u set with %u failed register transfers - the tune " + "is incomplete", chan, mt_io_errors(d)); + d->chan = 0; + return -1; + } + return 0; +} + +int mt_set_channel(struct mt7612u_dev *d, uint8_t chan, uint8_t bw) +{ + return mt_set_channel_ex(d, chan, bw, 0); +} + +/* Public: absolute TX power limit in dBm. Takes effect at the next channel + * set, which is where the per-rate table is recomputed. */ +int mt7612u_set_txpower(struct mt7612u_dev *d, int dbm) +{ + if (dbm < 0 || dbm > 30) return -1; + d->txpower_conf = (int8_t)(dbm * 2); + if (d->chan) + mt_phy_set_txpower(d, d->chan > 14); + return 0; +} + +/* + * Public channel set. `chan` is an 802.11 channel number, not a frequency; + * the width may narrow a frame below the channel but never widen it. + * + * `chan` is always the *control* channel, at every width. mt_chan_group() + * derives the centre the hardware actually tunes, and refuses a channel that + * cannot carry the requested width rather than tuning near it. + */ +int mt7612u_set_channel(struct mt7612u_dev *d, uint8_t chan, enum mt7612u_bw bw) +{ + if (!d || !chan) return -1; + if (bw != MT7612U_BW_20 && bw != MT7612U_BW_40 && bw != MT7612U_BW_80) { + ERR("set_channel: bandwidth %d is not a MT7612U_BW_* value", (int)bw); + return -1; + } + return mt_set_channel(d, chan, (uint8_t)bw); +} + +/* + * 0x202 = 2T2R, 0x101 = 1T1R. Stored now, applied at the next channel set, + * which is where the chainmask reaches the firmware in CMD_SWITCH_CHANNEL_OP. + */ +int mt7612u_set_chainmask(struct mt7612u_dev *d, uint16_t chainmask) +{ + if (!d) return -1; + if (chainmask != 0x0202 && chainmask != 0x0101) { + ERR("set_chainmask: 0x%04x is neither 2T2R (0x0202) nor 1T1R (0x0101)", + chainmask); + return -1; + } + d->chainmask = chainmask; + return 0; +} diff --git a/src/mt7612u/radiotap.c b/src/mt7612u/radiotap.c new file mode 100644 index 00000000..b946b08f --- /dev/null +++ b/src/mt7612u/radiotap.c @@ -0,0 +1,338 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Minimal radiotap TX parser, so a caller can hand us the same + * "radiotap header + 802.11 MPDU" buffer devourer's send_packet() takes + * instead of filling a struct. + * + * Only the inject-relevant fields are decoded; everything else is skipped by + * the alignment/size table, which is what makes skipping correct rather than + * lucky. Field order, alignment and size follow the radiotap spec. + */ +#include +#include "internal.h" + +/* {align, size} per radiotap bit index. size 0 = unknown -> stop parsing. */ +static const struct { uint8_t align, size; } rt_field[] = { + { 8, 8 }, /* 0 TSFT */ { 1, 1 }, /* 1 FLAGS */ + { 1, 1 }, /* 2 RATE */ { 2, 4 }, /* 3 CHANNEL */ + { 2, 2 }, /* 4 FHSS */ { 1, 1 }, /* 5 DBM_ANTSIGNAL */ + { 1, 1 }, /* 6 DBM_ANTNOISE */ { 2, 2 }, /* 7 LOCK_QUALITY */ + { 2, 2 }, /* 8 TX_ATTENUATION */ { 2, 2 }, /* 9 DB_TX_ATTENUATION */ + { 1, 1 }, /* 10 DBM_TX_POWER */ { 1, 1 }, /* 11 ANTENNA */ + { 1, 1 }, /* 12 DB_ANTSIGNAL */ { 1, 1 }, /* 13 DB_ANTNOISE */ + { 2, 2 }, /* 14 RX_FLAGS */ { 2, 2 }, /* 15 TX_FLAGS */ + { 1, 1 }, /* 16 RTS_RETRIES */ { 1, 1 }, /* 17 DATA_RETRIES */ + { 0, 0 }, /* 18 (unused) */ { 1, 3 }, /* 19 MCS */ + { 4, 8 }, /* 20 AMPDU_STATUS */ { 2, 12 },/* 21 VHT */ + { 8, 12 },/* 22 TIMESTAMP */ { 2, 12 },/* 23 HE */ +}; +#define RT_RATE 2 +#define RT_DBM_TX_POWER 10 +#define RT_TX_FLAGS 15 +#define RT_MCS 19 +#define RT_VHT 21 +#define RT_TX_FLAGS_NOACK 0x0008 + +/* One diagnostic per process for a field we parse but cannot honour. */ +static int warned_tx_power; + +static uint16_t rd16(const uint8_t *p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); } +static uint32_t rd32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +/* Legacy radiotap RATE is in 500 kbps units; the hardware wants an index. */ +static int legacy_rate_index(uint8_t r500, enum mt7612u_phy *phy) +{ + switch (r500) { + case 2: *phy = MT7612U_PHY_CCK; return 0; /* 1 Mbps */ + case 4: *phy = MT7612U_PHY_CCK; return 1; /* 2 */ + case 11: *phy = MT7612U_PHY_CCK; return 2; /* 5.5 */ + case 22: *phy = MT7612U_PHY_CCK; return 3; /* 11 */ + case 12: *phy = MT7612U_PHY_OFDM; return 0; /* 6 */ + case 18: *phy = MT7612U_PHY_OFDM; return 1; /* 9 */ + case 24: *phy = MT7612U_PHY_OFDM; return 2; /* 12 */ + case 36: *phy = MT7612U_PHY_OFDM; return 3; /* 18 */ + case 48: *phy = MT7612U_PHY_OFDM; return 4; /* 24 */ + case 72: *phy = MT7612U_PHY_OFDM; return 5; /* 36 */ + case 96: *phy = MT7612U_PHY_OFDM; return 6; /* 48 */ + case 108:*phy = MT7612U_PHY_OFDM; return 7; /* 54 */ + default: *phy = MT7612U_PHY_OFDM; return 0; + } +} + +/* + * Radiotap VHT bandwidth code -> the width the frame is actually sent at. + * + * The code names a channel width *and* the sub-channel within it: 2 is + * "40 (20L)", a 20 MHz frame in the lower half of a 40 MHz channel, not a + * 40 MHz frame. Treating 1-3 as 40 and >=4 as 80 airs a requested 20-in-40 + * at 40 MHz and a requested 20-in-80 at 80. + * + * This project's own ieee80211_radiotap.h names the equivalent HT codes + * IEEE80211_RADIOTAP_MCS_BW_20L (2) and _20U (3), and the HT branch below + * already reads them that way - the VHT branch used to disagree with it. + */ +static enum mt7612u_bw vht_bandwidth(uint8_t code) +{ + static const uint8_t width[] = { + MT7612U_BW_20, /* 0 20 */ + MT7612U_BW_40, /* 1 40 */ + MT7612U_BW_20, /* 2 40 (20L) */ + MT7612U_BW_20, /* 3 40 (20U) */ + MT7612U_BW_80, /* 4 80 */ + MT7612U_BW_40, /* 5 80 (40L) */ + MT7612U_BW_40, /* 6 80 (40U) */ + MT7612U_BW_20, /* 7 80 (20LL) */ + MT7612U_BW_20, /* 8 80 (20LU) */ + MT7612U_BW_20, /* 9 80 (20UL) */ + MT7612U_BW_20, /* 10 80 (20UU) */ + }; + + if (code < sizeof width / sizeof width[0]) + return (enum mt7612u_bw)width[code]; + /* 11 and above are 160 MHz and its sub-channels. This part has no + * 160 MHz encoding in the rate word, and narrowing silently would be + * worse than saying so. */ + LOG("radiotap VHT bandwidth code %u is 160 MHz or a sub-channel of it; " + "unsupported, sending at 20 MHz", code); + return MT7612U_BW_20; +} + +/* + * Parse a radiotap header into a tx_rate. Returns the header length, or 0 if + * the buffer is not a usable radiotap header. + */ +int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r) +{ + uint32_t present[8]; + unsigned n_present = 0, bit = 0; + size_t rlen, off; + + if (!buf || len < 8 || buf[0] != 0) return 0; /* version must be 0 */ + rlen = rd16(buf + 2); + if (rlen < 8 || rlen > len) return 0; + + /* Walk the extended present bitmaps. */ + off = 4; + do { + if (off + 4 > rlen || n_present == 8) return 0; + present[n_present] = rd32(buf + off); + off += 4; + } while (present[n_present++] & 0x80000000u); + + memset(r, 0, sizeof *r); + r->phy = MT7612U_PHY_OFDM; + r->nss = 1; + r->bw = MT7612U_BW_20; + + for (unsigned w = 0; w < n_present; w++) { + for (unsigned b = 0; b < 31; b++, bit++) { + const uint8_t *p; + uint8_t align, size; + + if (!(present[w] & (1u << b))) + continue; + if (bit >= sizeof rt_field / sizeof rt_field[0]) + return (int)rlen; /* unknown tail: stop */ + align = rt_field[bit].align; + size = rt_field[bit].size; + if (!size) + return (int)rlen; + off = (off + align - 1) & ~((size_t)align - 1); + /* A declared field that runs past the declared header + * length is a malformed header, not a short one. Returning + * rlen here reported success, and both injection entry + * points then transmitted the frame at whatever defaults + * or half-parsed rate had accumulated - a silently wrong + * rate rather than a refused frame. An unknown *trailing* + * present bit is different and still stops cleanly above: + * there the header is well formed, we just cannot read the + * rest of it. */ + if (off + size > rlen) { + ERR("radiotap: field for present bit %u runs past the " + "declared %zu-byte header", bit, rlen); + return -1; + } + p = buf + off; + off += size; + + switch (bit) { + case RT_RATE: + if (!(present[0] & (1u << RT_MCS))) + r->mcs = (uint8_t)legacy_rate_index(p[0], &r->phy); + break; + case RT_TX_FLAGS: + if (rd16(p) & RT_TX_FLAGS_NOACK) r->no_ack = 1; + break; + case RT_DBM_TX_POWER: + /* Absolute dBm is a device-level knob here + * (mt7612u_set_txpower); txwi carries only a + * 4-bit relative trim, and mapping an absolute + * target onto it needs the per-rate EEPROM + * ceiling for the current channel. Say so + * rather than accept the field and drop it. */ + if (!warned_tx_power) { + warned_tx_power = 1; + LOG("radiotap DBM_TX_POWER (%d dBm) ignored: " + "use mt7612u_set_txpower() for the base level " + "and mt7612u_tx_rate.power_adj for per-frame trim", + (int)(int8_t)p[0]); + } + break; + case RT_MCS: { + uint8_t known = p[0], flags = p[1]; + + r->phy = MT7612U_PHY_HT; + r->mcs = p[2]; + r->nss = (uint8_t)(1 + (p[2] >> 3)); + /* The bandwidth field is declared by HAVE_BW (0x01), + * NOT by HAVE_MCS (0x02). Gating it on 0x02 narrowed a + * requested 40 MHz frame to 20 whenever a caller + * declared bandwidth without also declaring an MCS + * index - legal radiotap, and silent on air. Same shape + * as the VHT bandwidth bug fixed earlier; this is its + * HT twin. Flags bits 1:0 are 0=20 1=40 2=20L 3=20U, so + * only 1 is a 40 MHz frame. */ + if ((known & 0x01) && ((flags & 0x03) == 1)) + r->bw = MT7612U_BW_40; + if (known & 0x04) r->sgi = (flags >> 2) & 1; + if (known & 0x10) r->ldpc = (flags >> 4) & 1; + if (known & 0x20) r->stbc = ((flags >> 5) & 3) ? 1 : 0; + break; + } + case RT_VHT: { + uint16_t known = rd16(p); + uint8_t flags = p[2], bwc = p[3], mcs_nss = p[4], coding = p[8]; + + r->phy = MT7612U_PHY_VHT; + r->mcs = (uint8_t)(mcs_nss >> 4); + r->nss = (uint8_t)(mcs_nss & 0x0f); + if (!r->nss) r->nss = 1; + if (known & 0x0004) r->stbc = flags & 1; + + if (flags & 0x04) r->sgi = 1; + if (coding & 0x01) r->ldpc = 1; + r->bw = vht_bandwidth(bwc); + break; + } + default: + break; + } + } + } + return (int)rlen; +} + +/* + * devourer's send_packet() contract: one buffer, radiotap header followed by + * the 802.11 MPDU. Per-frame rate comes from the header. + */ +int mt7612u_send_packet(struct mt7612u_dev *d, const void *buf, size_t len) +{ + struct mt7612u_tx_rate r; + const uint8_t *p = buf; + int rlen = mt_radiotap_parse(p, len, &r); + + if (rlen <= 0 || (size_t)rlen >= len) { + ERR("send_packet: no usable radiotap header"); + return -1; + } + return mt7612u_tx(d, p + rlen, len - (size_t)rlen, &r); +} + +/* + * devourer's send_packets() contract: several radiotap-framed MPDUs in one + * call. MT7612U packs them into a single bulk-OUT transfer using + * MT_TXD_INFO_NEXT_VLD, so a burst costs one USB transaction rather than one + * per frame. Returns the number accepted. + */ +size_t mt7612u_send_packets(struct mt7612u_dev *d, + const struct mt7612u_tx_view *pkts, size_t count) +{ + uint8_t buf[MT_USB_AGG_BUF]; + size_t sent = 0, i = 0; + + if (!pkts) return 0; + + while (i < count) { + size_t off = 0, n_in_buf = 0, j; + struct { const uint8_t *mpdu; size_t len; struct mt7612u_tx_rate r; } + sel[MT_USB_AGG_MAX]; + + /* + * Pass 1 selects and fully validates. The radiotap parse happens + * here, not in pass 2: a frame that pass 2 could still reject + * would break the chain it is building - NEXT_VLD and the single + * trailing zero word are assigned by position, so dropping the + * frame that happens to be last leaves the transfer unterminated. + */ + while (i < count && n_in_buf < MT_USB_AGG_MAX) { + const uint8_t *p = pkts[i].data; + size_t plen = pkts[i].len, need; + struct mt7612u_tx_rate r; + int rlen; + + if (!p || plen < 8) { i++; continue; } + rlen = mt_radiotap_parse(p, plen, &r); + if (rlen <= 0 || (size_t)rlen >= plen) { i++; continue; } + + /* Worst case for one block: TXINFO + TXWI + hdr pad + + * MPDU + alignment + trailer. */ + need = 4 + MT_TXWI_LEN + 2 + (plen - (size_t)rlen) + 3 + 4; + /* The same per-frame ceiling mt7612u_tx() applies. Without + * it this path accepted anything that fit the 16 KB + * aggregate buffer, so one public entry point refused a + * frame the other aired - and the caller could not tell + * which limit it was under. */ + if (need > MT_TX_BUF_MAX) { + ERR("frame of %zu bytes exceeds the %d-byte per-frame " + "ceiling", plen - (size_t)rlen, MT_TX_BUF_MAX); + i++; + continue; + } + if (off + need > sizeof buf) break; + + sel[n_in_buf].mpdu = p + rlen; + sel[n_in_buf].len = plen - (size_t)rlen; + sel[n_in_buf].r = r; + n_in_buf++; + off += need; + i++; + } + if (!n_in_buf) break; + + /* Pass 2 only builds. NEXT_VLD on every block except the last, + * and only the last carries the 4-byte zero trailer. */ + off = 0; + for (j = 0; j < n_in_buf; j++) { + int last = (j + 1 == n_in_buf); + int blk = mt_tx_build(d, buf + off, sizeof buf - off, + sel[j].mpdu, sel[j].len, &sel[j].r, + 0xff, 0, !last, last); + + if (blk < 0) { + /* Cannot happen after pass 1's checks, but if it + * ever does the chain is unterminated - drop the + * whole transfer rather than air a truncated one. */ + ERR("send_packets: block %zu failed to build", j); + off = 0; + break; + } + off += (size_t)blk; + } + if (!off) break; + + if (d->a) { + if (mt_async_tx_submit(d, buf, (int)off) == 0) sent += n_in_buf; + } else { + int n = 0; + + if (mt_bulk(d, MT_EP_OUT_AC_BE, buf, (int)off, &n, 500) == 0) + sent += n_in_buf; + } + } + return sent; +} diff --git a/src/mt7612u/regs.h b/src/mt7612u/regs.h new file mode 100644 index 00000000..1c6607b3 --- /dev/null +++ b/src/mt7612u/regs.h @@ -0,0 +1,439 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * MT7612U register definitions, trimmed from openwrt/mt76 @ be5ce79 + * (mt76x02_regs.h, mt76x2/mcu.h, mt76x02_mcu.h, mt76x02_eeprom.h). + * Copyright (C) 2016 Felix Fietkau, (C) 2018 Lorenzo Bianconi / Stanislaw Gruszka. + * + * Only the registers this HAL actually touches are here. See + * ../../INVESTIGATION.md for what each init block is for. + */ +#ifndef MT7612U_REGS_H +#define MT7612U_REGS_H + +#include + +#define BIT(n) (1u << (n)) +#define GENMASK(h, l) (((~0u) - (1u << (l)) + 1) & (~0u >> (31 - (h)))) + +/* + * Position of the lowest set bit of a mask, for FIELD_PREP/FIELD_GET. + * + * Not __builtin_ctz: MSVC has no such builtin, and its _BitScanForward is a + * function taking an out-parameter, so it cannot appear in a constant + * expression - which these must be, because FIELD_PREP initialises static + * tables (see ext_cca_chan in phy.c). The isolate-lowest-bit plus binary + * search below is a constant expression on every compiler and folds to a + * single instruction under optimisation. + * + * `m` is evaluated several times; every mask here is a compile-time constant, + * so that is a documentation point rather than a hazard. + * + * The old name for this was `_SHIFT`, which is reserved to the implementation + * in every scope - leading underscore followed by a capital. + */ +#define MT_LOWBIT(m) ((uint32_t)(m) & (~(uint32_t)(m) + 1u)) +#define MT_CTZ(m) ( \ + ((MT_LOWBIT(m) & 0xffff0000u) ? 16u : 0u) | \ + ((MT_LOWBIT(m) & 0xff00ff00u) ? 8u : 0u) | \ + ((MT_LOWBIT(m) & 0xf0f0f0f0u) ? 4u : 0u) | \ + ((MT_LOWBIT(m) & 0xccccccccu) ? 2u : 0u) | \ + ((MT_LOWBIT(m) & 0xaaaaaaaau) ? 1u : 0u)) +#define FIELD_PREP(m, v) (((uint32_t)(v) << MT_CTZ(m)) & (m)) +#define FIELD_GET(m, v) (((uint32_t)(v) & (m)) >> MT_CTZ(m)) + +/* Address-space selectors. Stripped before the transfer; they pick bRequest. */ +#define MT_VEND_TYPE_EEPROM BIT(31) +#define MT_VEND_TYPE_CFG BIT(30) +#define MT_VEND_TYPE_MASK (MT_VEND_TYPE_EEPROM | MT_VEND_TYPE_CFG) +#define CFG_ADDR(n) (MT_VEND_TYPE_CFG | (n)) +#define EEP_ADDR(n) (MT_VEND_TYPE_EEPROM | (n)) + +/* EP0 vendor requests (mt76.h enum mt_vendor_req) */ +#define MT_VEND_DEV_MODE 0x01 +#define MT_VEND_WRITE 0x02 +#define MT_VEND_POWER_ON 0x04 +#define MT_VEND_MULTI_WRITE 0x06 +#define MT_VEND_MULTI_READ 0x07 +#define MT_VEND_READ_EEPROM 0x09 +#define MT_VEND_WRITE_FCE 0x42 +#define MT_VEND_WRITE_CFG 0x46 +#define MT_VEND_READ_CFG 0x47 +#define MT_VEND_READ_EXT 0x63 +#define MT_VEND_WRITE_EXT 0x66 + +/* Bulk endpoints, in mt76u_set_endpoints() descriptor order. Verified live. */ +#define MT_EP_IN_PKT_RX 0x84 +#define MT_EP_IN_CMD_RESP 0x85 +#define MT_EP_OUT_INBAND_CMD 0x08 +#define MT_EP_OUT_AC_BE 0x04 +#define MT_EP_OUT_AC_BK 0x05 +#define MT_EP_OUT_AC_VI 0x06 +#define MT_EP_OUT_AC_VO 0x07 +#define MT_EP_OUT_HCCA 0x09 + +/* ---- identity / power ---- */ +#define MT_ASIC_VERSION 0x0000 +#define MT_COEXCFG0 0x0040 +#define MT_COEXCFG0_COEX_EN BIT(0) +#define MT_WLAN_FUN_CTRL 0x0080 +#define MT_WLAN_FUN_CTRL_WLAN_EN BIT(0) +#define MT_WLAN_FUN_CTRL_WLAN_CLK_EN BIT(1) +#define MT_WLAN_FUN_CTRL_WLAN_RESET_RF BIT(2) +#define MT_WLAN_FUN_CTRL_FRC_WL_ANT_SEL BIT(5) + +#define MT_XO_CTRL5 0x0114 +#define MT_XO_CTRL5_C2_VAL GENMASK(14, 8) +#define MT_XO_CTRL6 0x0118 +#define MT_XO_CTRL6_C2_CTRL GENMASK(14, 8) +#define MT_XO_CTRL7 0x011c + +/* CFG-space power-up block (mt76x2u_power_on) */ +#define MT_CFG_MTC_CTRL 0x148 /* via CFG_ADDR() */ +#define MT_WLAN_MTC_CTRL_MTCMOS_PWR_UP BIT(0) +#define MT_WLAN_MTC_CTRL_PWR_ACK BIT(12) +#define MT_WLAN_MTC_CTRL_PWR_ACK_S BIT(13) +#define MT_WLAN_MTC_CTRL_STATE_UP BIT(28) + +#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_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) +#define MT_USB_DMA_CFG_RX_BUSY BIT(30) +#define MT_USB_DMA_CFG_TX_BUSY BIT(31) + +/* ---- MCU ---- */ +#define MT_MCU_CPU_CTL 0x0704 +#define MT_MCU_CLOCK_CTL 0x0708 +#define MT_MCU_RESET_CTL 0x070c +#define MT_MCU_INT_LEVEL 0x0718 +#define MT_MCU_COM_REG0 0x0730 +#define MT_MCU_COM_REG1 0x0734 +#define MT_MCU_SEMAPHORE_03 0x07bc + +#define MT_FCE_PSE_CTRL 0x0800 +#define MT_FCE_L2_STUFF 0x080c +#define MT_FCE_L2_STUFF_WR_MPDU_LEN_EN BIT(4) +#define MT_FCE_DMA_ADDR 0x0230 +#define MT_FCE_DMA_LEN 0x0234 +#define MT_TX_CPU_FROM_FCE_BASE_PTR 0x09a0 +#define MT_TX_CPU_FROM_FCE_MAX_COUNT 0x09a4 +#define MT_TX_CPU_FROM_FCE_CPU_DESC_IDX 0x09a8 +#define MT_FCE_PDMA_GLOBAL_CONF 0x09c4 +#define MT_FCE_SKIP_FS 0x0a6c + +/* MCU in-band message header (mt76x02_dma.h) */ +#define MT_MCU_MSG_LEN GENMASK(15, 0) +#define MT_MCU_MSG_CMD_SEQ GENMASK(19, 16) +#define MT_MCU_MSG_CMD_TYPE GENMASK(26, 20) +#define MT_MCU_MSG_PORT GENMASK(29, 27) +#define MT_MCU_MSG_TYPE_CMD BIT(30) + +#define MT_RX_FCE_INFO_LEN GENMASK(13, 0) +#define MT_RX_FCE_INFO_CMD_SEQ GENMASK(19, 16) +#define MT_RX_FCE_INFO_EVT_TYPE GENMASK(23, 20) +#define MT_EVT_CMD_DONE 0 + +enum mt_dma_msg_port { WLAN_PORT, CPU_RX_PORT, CPU_TX_PORT, HOST_PORT }; + +enum mt_mcu_cmd { + CMD_FUN_SET_OP = 1, CMD_LOAD_CR = 2, CMD_INIT_GAIN_OP = 3, + CMD_RANDOM_READ = 10, CMD_RANDOM_WRITE = 12, + CMD_POWER_SAVING_OP = 20, CMD_SWITCH_CHANNEL_OP = 30, + CMD_CALIBRATION_OP = 31, +}; +enum mt_mcu_function { Q_SELECT = 1, BW_SETTING = 2, GET_FW_VERSION = 5 }; +enum mt_mcu_power_mode { RADIO_OFF = 0x30, RADIO_ON = 0x31 }; +enum mt_mcu_calibration { + MCU_CAL_R = 1, MCU_CAL_TEMP_SENSOR, MCU_CAL_RXDCOC, MCU_CAL_RC, + MCU_CAL_SX_LOGEN, MCU_CAL_LC, MCU_CAL_TX_LOFT, MCU_CAL_TXIQ, + MCU_CAL_TSSI, MCU_CAL_TSSI_COMP, MCU_CAL_DPD, MCU_CAL_RXIQC_FI, + MCU_CAL_RXIQC_FD, MCU_CAL_PWRON, MCU_CAL_TX_SHAPING, +}; +enum mt_mcu_cr_mode { MT_RF_CR, MT_BBP_CR, MT_RF_BBP_CR, MT_HL_TEMP_CR_UPDATE }; + +/* ---- MAC ---- */ +#define MT_MAC_CSR0 0x1000 +#define MT_MAC_SYS_CTRL 0x1004 +#define MT_MAC_SYS_CTRL_RESET_CSR BIT(0) +#define MT_MAC_SYS_CTRL_RESET_BBP BIT(1) +#define MT_MAC_SYS_CTRL_ENABLE_TX BIT(2) +#define MT_MAC_SYS_CTRL_ENABLE_RX BIT(3) +#define MT_MAC_ADDR_DW0 0x1008 +#define MT_MAC_ADDR_DW1 0x100c +#define MT_MAC_ADDR_DW1_U2ME_MASK GENMASK(23, 16) +#define MT_MAC_BSSID_DW0 0x1010 +#define MT_MAC_BSSID_DW1 0x1014 +#define MT_MAC_BSSID_DW1_MBSS_MODE GENMASK(19, 18) +#define MT_MAC_BSSID_DW1_MBEACON_N GENMASK(22, 20) +#define MT_MAC_BSSID_DW1_MBSS_LOCAL_BIT BIT(23) +#define MT_MAX_LEN_CFG 0x1018 +#define MT_XIFS_TIME_CFG 0x1100 +#define MT_XIFS_TIME_CFG_OFDM_SIFS GENMASK(15, 8) +#define MT_BKOFF_SLOT_CFG 0x1104 +#define MT_BKOFF_SLOT_CFG_CC_DELAY GENMASK(11, 8) +#define MT_BEACON_TIME_CFG 0x1114 +#define MT_BEACON_TIME_CFG_INTVAL GENMASK(15, 0) +#define MT_BEACON_TIME_CFG_TIMER_EN BIT(16) +#define MT_BEACON_TIME_CFG_TBTT_EN BIT(19) +#define MT_BEACON_TIME_CFG_BEACON_TX BIT(20) +#define MT_TSF_TIMER_DW0 0x111c +#define MT_TSF_TIMER_DW1 0x1120 +#define MT_MAC_STATUS 0x1200 +#define MT_MAC_STATUS_TX BIT(1) +#define MT_MAC_STATUS_RX BIT(0) + +#define MT_WPDMA_GLO_CFG 0x0208 +#define MT_WPDMA_GLO_CFG_TX_DMA_BUSY BIT(1) +#define MT_WPDMA_GLO_CFG_RX_DMA_BUSY BIT(3) +#define MT_PBF_TX_MAX_PCNT 0x0408 +#define MT_PBF_RX_MAX_PCNT 0x040c +#define MT_WMM_AIFSN 0x0214 +#define MT_WMM_CWMIN 0x0218 +#define MT_WMM_CWMAX 0x021c +#define MT_US_CYC_CFG 0x02a4 +#define MT_US_CYC_CNT GENMASK(7, 0) + +#define MT_TXOP_CTRL_CFG 0x1340 +#define MT_TXOP_ED_CCA_EN BIT(20) +#define MT_TX_RTS_CFG 0x1344 +#define MT_TX_RTS_CFG_RETRY_LIMIT GENMASK(7, 0) +#define MT_TX_RETRY_CFG 0x134c +#define MT_TX_LINK_CFG 0x1350 +#define MT_TX_CFACK_EN BIT(12) +#define MT_TX_PWR_CFG_0 0x1314 +#define MT_TX_PWR_CFG_1 0x1318 +#define MT_TX_PWR_CFG_2 0x131c +#define MT_TX_PWR_CFG_3 0x1320 +#define MT_TX_PWR_CFG_4 0x1324 +#define MT_TX_PWR_CFG_7 0x13d4 +#define MT_TX_PWR_CFG_8 0x13d8 +#define MT_TX_PWR_CFG_9 0x13dc +#define MT_TX_ALC_CFG_0 0x13b0 +#define MT_TX_ALC_CFG_1 0x13b4 +#define MT_TX_ALC_CFG_2 0x13a8 +#define MT_TX_ALC_CFG_3 0x13ac +#define MT_TX_ALC_CFG_4 0x13c0 +#define MT_RX_FILTR_CFG 0x1400 +#define MT_RX_FILTR_CFG_CRC_ERR BIT(0) +#define MT_RX_FILTR_CFG_PHY_ERR BIT(1) +#define MT_RX_FILTR_CFG_PROMISC BIT(2) +#define MT_RX_FILTR_CFG_OTHER_BSS BIT(3) +#define MT_RX_FILTR_CFG_VER_ERR BIT(4) +#define MT_RX_FILTR_CFG_DUP BIT(7) +#define MT_RX_FILTR_CFG_CTRL_RSV BIT(16) +#define MT_AUTO_RSP_CFG 0x1404 +#define MT_AUTO_RSP_EN BIT(0) +#define MT_AUTO_RSP_PREAMB_SHORT BIT(4) +#define MT_EXT_CCA_CFG 0x141c +#define MT_EXT_CCA_CFG_CCA0 GENMASK(1, 0) +#define MT_EXT_CCA_CFG_CCA1 GENMASK(3, 2) +#define MT_EXT_CCA_CFG_CCA2 GENMASK(5, 4) +#define MT_EXT_CCA_CFG_CCA3 GENMASK(7, 6) +#define MT_EXT_CCA_CFG_CCA_MASK GENMASK(11, 8) +#define MT_TXOP_HLDR_ET 0x1608 +#define MT_TXOP_HLDR_TX40M_BLK_EN BIT(1) +#define MT_PROT_AUTO_TX_CFG 0x1648 + +#define MT_WCID_ATTR_BASE 0xa800 +#define MT_WCID_ATTR(_n) (MT_WCID_ATTR_BASE + ((_n) & 0xff) * 4) +#define MT_WCID_ADDR_BASE 0x1800 +#define MT_WCID_ADDR(_n) (MT_WCID_ADDR_BASE + (_n) * 8) +#define MT_SKEY_BASE_0 0xac00 +#define MT_SKEY_BASE_1 0xb400 +#define MT_SKEY_0(_b, _i) (MT_SKEY_BASE_0 + (4 * (_b) + (_i)) * 32) +#define MT_SKEY_1(_b, _i) (MT_SKEY_BASE_1 + (4 * ((_b) & 7) + (_i)) * 32) +#define MT_SKEY(_b, _i) (((_b) & 8) ? MT_SKEY_1(_b, _i) : MT_SKEY_0(_b, _i)) +#define MT_SKEY_MODE_BASE_0 0xb000 +#define MT_SKEY_MODE_BASE_1 0xb3f0 +#define MT_SKEY_MODE_0(_b) (MT_SKEY_MODE_BASE_0 + (((_b) / 2) << 2)) +#define MT_SKEY_MODE_1(_b) (MT_SKEY_MODE_BASE_1 + ((((_b) & 7) / 2) << 2)) +#define MT_SKEY_MODE(_b) (((_b) & 8) ? MT_SKEY_MODE_1(_b) : MT_SKEY_MODE_0(_b)) +#define MT_SKEY_MODE_MASK GENMASK(3, 0) +#define MT_SKEY_MODE_SHIFT(_b, _i) (4 * ((_i) + 4 * ((_b) & 1))) +#define MT_PBF_SYS_CTRL 0x0400 +#define MT_WCID_TX_RATE_BASE 0x1c00 +#define MT_WCID_TX_RATE(_i) (MT_WCID_TX_RATE_BASE + ((_i) << 3)) +#define MT_WCID_TX_INFO_RATE GENMASK(15, 0) +#define MT_WCID_TX_INFO_NSS GENMASK(17, 16) +#define MT_WCID_TX_INFO_TXPWR_ADJ GENMASK(25, 18) +#define MT_WCID_TX_INFO_SET BIT(31) + +/* BBP indirect. NOTE: each group base is an ABSOLUTE address, not an offset + * from a common base - getting this wrong silently writes into a different + * BBP group (AGC 0x2300 vs RXC 0x2500 differ by exactly that mistake). */ +#define MT_BBP_CORE_BASE 0x2000 +#define MT_BBP_IBI_BASE 0x2100 +#define MT_BBP_AGC_BASE 0x2300 +#define MT_BBP_TXC_BASE 0x2400 +#define MT_BBP_RXC_BASE 0x2500 +#define MT_BBP_TXO_BASE 0x2600 +#define MT_BBP_TXBE_BASE 0x2700 +#define MT_BBP_RXFE_BASE 0x2800 +#define MT_BBP_RXO_BASE 0x2900 +#define MT_BBP_DFS_BASE 0x2a00 +#define MT_BBP_TR_BASE 0x2b00 +#define MT_BBP_CAL_BASE 0x2c00 +#define MT_BBP_DSC_BASE 0x2e00 +#define MT_BBP_PFMU_BASE 0x2f00 +#define MT_BBP(_type, _n) (MT_BBP_##_type##_BASE + ((_n) << 2)) + +#define MT_MAC_APC_BSSID_BASE 0x1090 +#define MT_MAC_APC_BSSID_L(_n) (MT_MAC_APC_BSSID_BASE + ((_n) * 8)) +#define MT_MAC_APC_BSSID_H(_n) (MT_MAC_APC_BSSID_BASE + ((_n) * 8 + 4)) +#define MT_MAC_APC_BSSID_H_ADDR GENMASK(15, 0) + +/* ---- PHY / TX power ---- */ +#define MT_BB_PA_MODE_CFG0 0x1214 +#define MT_BB_PA_MODE_CFG1 0x1218 +#define MT_RF_PA_MODE_CFG0 0x121c +#define MT_RF_PA_MODE_CFG1 0x1220 +#define MT_RF_PA_MODE_ADJ0 0x1228 +#define MT_RF_PA_MODE_ADJ1 0x122c +#define MT_TX_BAND_CFG 0x132c +#define MT_TX_BAND_CFG_UPPER_40M BIT(0) +#define MT_TX_BAND_CFG_5G BIT(1) +#define MT_TX_BAND_CFG_2G BIT(2) +#define MT_TX_SW_CFG0 0x1330 +#define MT_TX_SW_CFG1 0x1334 +#define MT_TX0_RF_GAIN_CORR 0x13a0 +#define MT_TX1_RF_GAIN_CORR 0x13a4 +#define MT_TX_ALC_CFG_0_CH_INIT_0 GENMASK(5, 0) +#define MT_TX_ALC_CFG_0_CH_INIT_1 GENMASK(13, 8) +#define MT_TX_ALC_CFG_1_TEMP_COMP GENMASK(5, 0) +#define MT_TX_ALC_CFG_2_TEMP_COMP GENMASK(5, 0) +/* --- MIB counters. All read-and-clear: mt76x02_mac_reset_counters() zeroes + * them by reading, and mt76x02_mac_cc_reset() says so of the channel timers. + * A caller must therefore difference nothing - each read IS the interval. --- */ +#define MT_CH_TIME_CFG 0x110c +#define MT_CH_TIME_CFG_TIMER_EN BIT(0) +#define MT_CH_TIME_CFG_TX_AS_BUSY BIT(1) +#define MT_CH_TIME_CFG_RX_AS_BUSY BIT(2) +#define MT_CH_TIME_CFG_NAV_AS_BUSY BIT(3) +#define MT_CH_TIME_CFG_EIFS_AS_BUSY BIT(4) +#define MT_CH_TIME_CFG_MDRDY_CNT_EN BIT(5) +#define MT_CH_CCA_RC_EN BIT(6) +#define MT_CH_TIME_CFG_CH_TIMER_CLR GENMASK(9, 8) +#define MT_CH_IDLE 0x1130 +#define MT_CH_BUSY 0x1134 + +#define MT_RX_STAT_0 0x1700 +#define MT_RX_STAT_0_CRC_ERRORS GENMASK(15, 0) +#define MT_RX_STAT_0_PHY_ERRORS GENMASK(31, 16) +#define MT_RX_STAT_1 0x1704 +#define MT_RX_STAT_1_CCA_ERRORS GENMASK(15, 0) +#define MT_RX_STAT_1_PLCP_ERRORS GENMASK(31, 16) +#define MT_RX_STAT_2 0x1708 +#define MT_RX_STAT_2_DUP_ERRORS GENMASK(15, 0) +#define MT_RX_STAT_2_OVERFLOW_ERRORS GENMASK(31, 16) +#define MT_TX_STA_0 0x170c +#define MT_TX_STA_1 0x1710 +#define MT_TX_STA_2 0x1714 +/* 16 registers, two 16-bit buckets each: the A-MPDU length histogram. */ +#define MT_TX_AGG_CNT_BASE0 0x1720 +#define MT_TX_AGG_CNT_BASE1 0x174c +#define MT_TX_AGG_CNT(_id) ((_id) < 8 ? MT_TX_AGG_CNT_BASE0 + ((_id) << 2) \ + : MT_TX_AGG_CNT_BASE1 + (((_id) - 8) << 2)) +#define MT_TEMP_SENSOR 0x1d000 +#define MT_TEMP_SENSOR_VAL GENMASK(6, 0) +#define MT_BBP_CORE_R1_BW GENMASK(4, 3) +#define MT_BBP_AGC_R0_BW GENMASK(14, 12) +#define MT_BBP_AGC_R0_CTRL_CHAN GENMASK(9, 8) +#define MT_BBP_TXBE_R0_CTRL_CHAN GENMASK(1, 0) +#define MT_BBP_AGC_GAIN GENMASK(14, 8) +#define MT_BBP_AGC_LNA_HIGH_GAIN GENMASK(21, 16) +#define MT_EE_NIC_CONF_0_PA_INT_2G BIT(8) +#define MT_EE_NIC_CONF_0_PA_INT_5G BIT(9) + +/* ---- EEPROM ---- */ +#define MT7612U_EEPROM_SIZE 512 +enum mt_ee_field { + MT_EE_CHIP_ID = 0x000, + MT_EE_MAC_ADDR = 0x004, + MT_EE_NIC_CONF_0 = 0x034, + MT_EE_NIC_CONF_1 = 0x036, + MT_EE_NIC_CONF_2 = 0x042, + MT_EE_XTAL_TRIM_1 = 0x03a, + MT_EE_XTAL_TRIM_2 = 0x09e, + MT_EE_BT_RCAL_RESULT = 0x138, + /* TX power */ + MT_EE_TX_POWER_DELTA_BW40 = 0x050, + MT_EE_TX_POWER_DELTA_BW80 = 0x052, + MT_EE_TX_POWER_0_START_2G = 0x056, + MT_EE_TX_POWER_1_START_2G = 0x05c, + MT_EE_TX_POWER_0_START_5G = 0x062, + MT_EE_TX_POWER_1_START_5G = 0x080, + MT_EE_TX_POWER_CCK = 0x0a0, + MT_EE_TX_POWER_OFDM_2G_6M = 0x0a2, + MT_EE_TX_POWER_OFDM_2G_24M = 0x0a4, + MT_EE_TX_POWER_HT_MCS0 = 0x0a6, + MT_EE_TX_POWER_HT_MCS4 = 0x0a8, + MT_EE_TX_POWER_HT_MCS8 = 0x0aa, + MT_EE_TX_POWER_HT_MCS12 = 0x0ac, + MT_EE_TX_POWER_OFDM_5G_6M = 0x0b2, + MT_EE_TX_POWER_OFDM_5G_24M = 0x0b4, + MT_EE_TX_POWER_VHT_MCS8 = 0x0be, + MT_EE_RF_2G_TSSI_OFF_TXPOWER = 0x0f6, + MT_EE_RF_2G_RX_HIGH_GAIN = 0x0f8, + /* RX gain / RSSI correction */ + MT_EE_LNA_GAIN = 0x044, + MT_EE_RSSI_OFFSET_2G_0 = 0x046, + MT_EE_RSSI_OFFSET_2G_1 = 0x048, + MT_EE_RSSI_OFFSET_5G_0 = 0x04a, + MT_EE_RSSI_OFFSET_5G_1 = 0x04c, + MT_EE_RF_5G_GRP0_1_RX_HIGH_GAIN = 0x0fa, + MT_EE_RF_5G_GRP2_3_RX_HIGH_GAIN = 0x0fc, + MT_EE_RF_5G_GRP4_5_RX_HIGH_GAIN = 0x0fe, +}; +#define MT_TX_POWER_GROUP_SIZE_5G 5 +#define MT_EE_NIC_CONF_0_RX_PATH GENMASK(3, 0) +#define MT_EE_NIC_CONF_0_TX_PATH GENMASK(7, 4) +#define MT_EE_NIC_CONF_1_TEMP_TX_ALC BIT(1) +#define MT_EE_NIC_CONF_1_LNA_EXT_2G BIT(2) +#define MT_EE_NIC_CONF_1_LNA_EXT_5G BIT(3) +#define MT_EE_NIC_CONF_1_TX_ALC_EN BIT(13) +#define MT_EE_NIC_CONF_2_XTAL_OPTION GENMASK(9, 8) + +/* ---- descriptors ---- */ +#define MT_TXD_INFO_LEN GENMASK(15, 0) +#define MT_TXD_INFO_NEXT_VLD BIT(16) +#define MT_TXD_INFO_TX_BURST BIT(17) +#define MT_TXD_INFO_80211 BIT(19) +#define MT_TXD_INFO_WIV BIT(24) +#define MT_TXD_INFO_QSEL GENMASK(26, 25) +#define MT_TXD_INFO_DPORT GENMASK(29, 27) +#define MT_QSEL_MGMT 0 +#define MT_QSEL_EDCA 1 + +#define MT_TXWI_LEN 20 +#define MT_RXWI_LEN 32 +#define MT_DMA_HDR_LEN 4 + +#define MT_TXWI_FLAGS_AMPDU BIT(4) +#define MT_TXWI_FLAGS_MPDU_DENSITY GENMASK(7, 5) +#define MT_TXWI_ACK_CTL_BA_WINDOW GENMASK(7, 2) +#define MT_TXWI_FLAGS_TX_RATE_LUT BIT(15) +#define MT_TXWI_ACK_CTL_REQ BIT(0) +#define MT_TXWI_ACK_CTL_NSEQ BIT(1) +#define MT_TX_PWR_ADJ GENMASK(3, 0) + +/* The 16-bit per-packet rate word, shared by TXWI and RXWI. */ +#define MT_RATE_INDEX GENMASK(5, 0) +#define MT_RATE_LDPC BIT(6) +#define MT_RATE_BW GENMASK(8, 7) +#define MT_RATE_SGI BIT(9) +#define MT_RATE_STBC BIT(10) +#define MT_RATE_PHY GENMASK(15, 13) +#define MT_RATE_VHT_IDX GENMASK(3, 0) +#define MT_RATE_VHT_NSS GENMASK(5, 4) + +#define MT_RXWI_CTL_WCID GENMASK(7, 0) +#define MT_RXWI_CTL_MPDU_LEN GENMASK(29, 16) +#define MT_RXINFO_CRCERR BIT(8) +#define MT_RXINFO_RSSI BIT(13) +#define MT_RXINFO_L2PAD BIT(14) +#define MT_RXINFO_AMPDU BIT(15) +#define MT_RXINFO_PN_LEN GENMASK(21, 19) + +#endif /* MT7612U_REGS_H */ diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c new file mode 100644 index 00000000..83afcb38 --- /dev/null +++ b/src/mt7612u/rx.c @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * RX path. RX_BULK_AGG_EN is left off in init_dma(), so one bulk transfer + * carries exactly one frame and this is a straight parse - no de-aggregation. + * + * Wire layout (INVESTIGATION.md §9): + * [FCE info 4B][RXWI 32B][802.11 frame] + */ +#include +#include "internal.h" + +#define RX_BUF_SIZE 4096 + +static uint32_t get_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} +static uint16_t get_le16(const uint8_t *p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); } + +/* + * Decode the 16-bit rate word - the same encoding the TX path writes. + * Returns -1 when the PHY field names no format this radio can produce. + * + * MT_RATE_PHY is three bits, so 5, 6 and 7 are representable and mean + * nothing; mt76 returns -EINVAL for them in mt76x02_mac_process_rate() and + * drops the frame. Decoding one anyway lands it in the caller's `default` + * arm and reports it as the lowest legacy rate, which reads as a real CCK + * frame rather than as garbage. + */ +static int decode_rate(uint16_t rate, struct mt7612u_rx_info *out) +{ + uint32_t idx = FIELD_GET(MT_RATE_INDEX, rate); + + out->phy = (enum mt7612u_phy)FIELD_GET(MT_RATE_PHY, rate); + if (out->phy > MT7612U_PHY_VHT) + return -1; + out->bw = (enum mt7612u_bw)FIELD_GET(MT_RATE_BW, rate); + out->sgi = !!(rate & MT_RATE_SGI); + out->ldpc = !!(rate & MT_RATE_LDPC); + out->stbc = !!(rate & MT_RATE_STBC); + + switch (out->phy) { + case MT7612U_PHY_VHT: + out->mcs = FIELD_GET(MT_RATE_VHT_IDX, idx); + out->nss = FIELD_GET(MT_RATE_VHT_NSS, idx) + 1; + break; + case MT7612U_PHY_HT: + case MT7612U_PHY_HT_GF: + out->mcs = (uint8_t)idx; + out->nss = (uint8_t)(1 + (idx >> 3)); + break; + default: + out->mcs = (uint8_t)idx; + out->nss = 1; + break; + } + return 0; +} + +/* + * Parse one completed RX buffer. Returns the 802.11 frame length (excluding + * the RXWI), or 0 if the buffer holds nothing usable. `frame` receives a + * pointer into `buf`. Shared by the synchronous reader and the async ring, so + * both decode identically. + */ +int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, + const uint8_t **frame, struct mt7612u_rx_info *info) +{ + int len, pad = 0; + uint32_t rxinfo, ctl; + const uint8_t *rxwi; + + if (n < MT_DMA_HDR_LEN + MT_RXWI_LEN) return 0; + + rxwi = buf + MT_DMA_HDR_LEN; + rxinfo = get_le32(rxwi); + ctl = get_le32(rxwi + 4); + + memset(info, 0, sizeof *info); + info->mpdu_len = (uint16_t)FIELD_GET(MT_RXWI_CTL_MPDU_LEN, ctl); + info->seq = (uint16_t)(get_le16(rxwi + 8) >> 4); + info->crc_err = !!(rxinfo & MT_RXINFO_CRCERR); + info->ampdu = !!(rxinfo & MT_RXINFO_AMPDU); + if (decode_rate(get_le16(rxwi + 10), info)) { + mt_async_note_invalid(d); + return 0; + } + + /* Per-chain RSSI is a fixed 4-byte field. Correction terms come from + * the EEPROM (mt76x02_mac_get_rssi); with them at zero these are the + * raw chip values, which is still enough to compare two chains. */ + info->n_chains = (uint8_t)((d->chainmask & 0xf) > 1 ? 2 : 1); + for (int c = 0; c < 4; c++) + info->rssi[c] = (int8_t)((int8_t)rxwi[12 + c] + + (c < 2 ? d->cal.rssi_offset[c] : 0) - + d->cal.lna_gain); + + for (int i = 0; i < 4; i++) + info->bbp[i] = get_le32(rxwi + 16 + 4 * i); + + /* RXWI byte 14 is a noise floor - see the header for how that was + * established. Below -100 dBm is under the thermal floor of a 20 MHz + * channel, so it is reported as no estimate rather than as a very quiet + * channel. */ + info->noise = info->rssi[2]; + info->noise_valid = info->noise > -100 && info->noise < -30; + info->snr_db = info->noise_valid + ? (int8_t)(info->rssi[0] - info->noise) : 0; + + if (rxinfo & MT_RXINFO_L2PAD) + pad = 2; + + /* MPDU_LEN excludes the pad: mt76 trims to it only after removing the + * pad (mt76x02_mac_process_rx), so the buffer holds hdrlen + pad + body. */ + len = (int)info->mpdu_len; + if (len > n - MT_DMA_HDR_LEN - MT_RXWI_LEN - pad) + len = n - MT_DMA_HDR_LEN - MT_RXWI_LEN - pad; + if (len < 0) return 0; + + *frame = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN; + /* + * Fold the L2 pad out, exactly as mt76x02_remove_hdr_pad(): move the + * *header* up over the pad, then start the frame two bytes in. + * + * The length moved must be the real header length. L2PAD is only ever + * set when the header is not 4-aligned - 26 bytes (QoS) or 30 (4-addr) - + * so moving a fixed 24 leaves the last two header bytes behind and + * overwrites them with pad. On a QoS frame those two bytes are the QoS + * Control field, i.e. every TID and ack-policy read as zero. + */ + if (pad) { + uint8_t *base = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN; + int avail = n - MT_DMA_HDR_LEN - MT_RXWI_LEN, hdrlen; + + if (avail < 2) return 0; + hdrlen = mt_hdrlen_from_fc(base); + if (len < hdrlen || avail < hdrlen + pad) return 0; + memmove(base + pad, base, (size_t)hdrlen); + *frame = base + pad; + } + return len; +} + +int mt_rx_one(struct mt7612u_dev *d, uint8_t *buf, int bufsize, + const uint8_t **frame, struct mt7612u_rx_info *info, + unsigned timeout_ms) +{ + int n = 0, rc = mt_bulk(d, MT_EP_IN_PKT_RX, buf, bufsize, &n, timeout_ms); + + if (rc == LIBUSB_ERROR_TIMEOUT) return 0; + if (rc) return -1; + return mt_rx_parse(d, buf, n, frame, info); +} diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c new file mode 100644 index 00000000..5d2a9552 --- /dev/null +++ b/src/mt7612u/tests/api_link.c @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Link check for the public API. It includes only - no + * internal header - and takes the address of every function the header + * declares, so the link fails if any of them is declared without a + * definition. It touches no hardware and is never run; building it is the + * whole test. + * + * This exists because the header once declared nine functions that had no + * definition anywhere, which made the "standalone library" claim false while + * the bring-up tool still built and ran - the tool calls internals directly. + */ +#include +#include + +static void *const api[] = { + (void *)mt7612u_open, + (void *)mt7612u_open_handle, + (void *)mt7612u_close, + (void *)mt7612u_keep_detached, + (void *)mt7612u_set_channel, + (void *)mt7612u_set_txpower, + (void *)mt7612u_set_chainmask, + (void *)mt7612u_start, + (void *)mt7612u_stop, + (void *)mt7612u_tx, + (void *)mt7612u_rx_start, + (void *)mt7612u_rx_stop, + (void *)mt7612u_set_monitor_rx, + (void *)mt7612u_send_packet, + (void *)mt7612u_send_packets, + (void *)mt7612u_set_ack_responder, + (void *)mt7612u_clear_ack_responder, + (void *)mt7612u_get_stats, + (void *)mt7612u_link_stats_start, + (void *)mt7612u_link_stats, + (void *)mt7612u_read_tsf, + (void *)mt7612u_write_tsf, + (void *)mt7612u_get_caps, + (void *)mt7612u_asic_version, + (void *)mt7612u_mac_addr, +}; + +int main(void) +{ + size_t n = sizeof api / sizeof api[0]; + + for (size_t i = 0; i < n; i++) + if (!api[i]) return 1; + printf("api_link: %zu public entry points resolved\n", n); + return 0; +} diff --git a/src/mt7612u/tests/field_macros.c b/src/mt7612u/tests/field_macros.c new file mode 100644 index 00000000..0192d48c --- /dev/null +++ b/src/mt7612u/tests/field_macros.c @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * FIELD_PREP/FIELD_GET rest on MT_CTZ, which replaced __builtin_ctz so the + * expression stays constant-foldable on compilers that lack that builtin. + * This checks the replacement against the builtin over every mask the driver + * can form - all 32 single-bit masks and all 528 contiguous GENMASK(h, l) + * ranges - rather than over the handful the code happens to use today. + * + * It also asserts that MT_CTZ is usable where the builtin was: in a static + * initialiser. That is the property that ruled out MSVC's _BitScanForward, + * and a runtime-only check would not notice its loss. + */ +#include +#include "internal.h" + +/* If MT_CTZ ever stops being a constant expression, this fails to compile. */ +static const uint32_t constant_folded[] = { + FIELD_PREP(GENMASK(15, 0), 0x2004), + FIELD_PREP(GENMASK(25, 20), 0x3f), + FIELD_PREP(BIT(26), 1), + FIELD_PREP(GENMASK(19, 18), 1), +}; + +int main(void) +{ + unsigned checked = 0; + int fails = 0; + + for (unsigned high = 0; high < 32; high++) { + for (unsigned low = 0; low <= high; low++) { + uint32_t mask = GENMASK(high, low); + unsigned want = (unsigned)__builtin_ctz(mask); + unsigned got = MT_CTZ(mask); + + checked++; + if (got != want) { + printf(" FAIL MT_CTZ(0x%08x) want %u got %u\n", + mask, want, got); + if (++fails > 8) return 1; + } + } + } + + /* Round-trip: what FIELD_PREP writes, FIELD_GET must read back. */ + for (unsigned high = 0; high < 32; high++) { + for (unsigned low = 0; low <= high; low++) { + uint32_t mask = GENMASK(high, low); + uint32_t width = high - low + 1; + uint32_t value = (width >= 32 ? 0xffffffffu + : (1u << width) - 1u) & 0xa5a5a5a5u; + + if (FIELD_GET(mask, FIELD_PREP(mask, value)) != value) { + printf(" FAIL round-trip mask 0x%08x value 0x%08x\n", + mask, value); + if (++fails > 8) return 1; + } + } + } + + if (constant_folded[0] != 0x2004u || constant_folded[2] != 0x04000000u) { + printf(" FAIL static initialiser values\n"); + fails++; + } + + printf("field_macros: %u masks checked, %s\n", checked, + fails ? "FAIL" : "PASS"); + return fails ? 1 : 0; +} diff --git a/src/mt7612u/tests/frame_shape.c b/src/mt7612u/tests/frame_shape.c new file mode 100644 index 00000000..3fd08262 --- /dev/null +++ b/src/mt7612u/tests/frame_shape.c @@ -0,0 +1,498 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * Frame-shape tests: 802.11 header length, and the RX L2-pad fold that + * depends on it. No hardware, no privileges - mt_rx_parse() only reads the + * device struct for the chainmask and the EEPROM gain terms. + * + * Both cases here are regressions, not hypotheticals: + * + * - hdrlen treated only RTS and PS-Poll as 16-byte control frames, leaving + * BlockAckReq, BlockAck and the two CF-End subtypes at 10. On TX that + * inserts the L2 pad ten bytes in, i.e. inside the frame. + * - the RX fold moved a fixed 24 bytes. L2PAD is only ever set when the + * header is *not* 4-aligned - 26 bytes (QoS) or 30 (4-address) - so the + * last two header bytes were left behind and overwritten by the pad. On + * a QoS frame those two bytes are the QoS Control field, so every TID + * and ack-policy read as zero. + */ +#include +#include +#include "internal.h" + +static int fails; + +static void expect_hdrlen(const char *what, uint8_t b0, uint8_t b1, int want) +{ + uint8_t fc[2] = { b0, b1 }; + int got = mt_hdrlen_from_fc(fc); + + if (got != want) { + printf(" FAIL %-28s fc=%02x%02x want %d got %d\n", + what, b0, b1, want, got); + fails++; + } +} + +static void test_hdrlen(void) +{ + printf("mt_hdrlen_from_fc:\n"); + + /* management */ + expect_hdrlen("beacon", 0x80, 0x00, 24); + expect_hdrlen("action +Order", 0xd0, 0x80, 28); + + /* control: 16 by default, 10 only for CTS and ACK */ + expect_hdrlen("BlockAckReq", 0x84, 0x00, 16); + expect_hdrlen("BlockAck", 0x94, 0x00, 16); + expect_hdrlen("PS-Poll", 0xa4, 0x00, 16); + expect_hdrlen("RTS", 0xb4, 0x00, 16); + expect_hdrlen("CTS", 0xc4, 0x00, 10); + expect_hdrlen("ACK", 0xd4, 0x00, 10); + expect_hdrlen("CF-End", 0xe4, 0x00, 16); + expect_hdrlen("CF-End+CF-Ack", 0xf4, 0x00, 16); + + /* data */ + expect_hdrlen("data 3-addr", 0x08, 0x00, 24); + expect_hdrlen("QoS data", 0x88, 0x00, 26); + expect_hdrlen("QoS data +Order", 0x88, 0x80, 30); + expect_hdrlen("data 4-addr", 0x08, 0x03, 30); + expect_hdrlen("QoS data 4-addr", 0x88, 0x03, 32); +} + +/* + * Build [FCE 4][RXWI 32][26-byte QoS header][2 pad][body] and parse it. + * The QoS Control field sits at header offset 24..25. + */ +static void test_rx_l2pad(void) +{ + struct mt7612u_dev d; + uint8_t buf[256]; + const uint8_t *frame = NULL; + struct mt7612u_rx_info info; + static const uint8_t body[] = "payload"; + const int hdrlen = 26, pad = 2; + const int mpdu = hdrlen + (int)sizeof body; + uint8_t *hdr; + uint32_t rxinfo = MT_RXINFO_L2PAD; + uint32_t ctl = FIELD_PREP(MT_RXWI_CTL_MPDU_LEN, (uint32_t)mpdu); + int n, len; + + printf("mt_rx_parse, L2 pad on a QoS frame:\n"); + + memset(&d, 0, sizeof d); + d.chainmask = 0x0202; + memset(buf, 0, sizeof buf); + + for (int i = 0; i < 4; i++) buf[MT_DMA_HDR_LEN + i] = (uint8_t)(rxinfo >> (8 * i)); + for (int i = 0; i < 4; i++) buf[MT_DMA_HDR_LEN + 4 + i] = (uint8_t)(ctl >> (8 * i)); + /* rate word: OFDM, index 0 - not what this test is about, but it must + * decode without tripping the VHT/HT index maths. */ + buf[MT_DMA_HDR_LEN + 10] = 0x00; + buf[MT_DMA_HDR_LEN + 11] = 0x20; + + hdr = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN; + hdr[0] = 0x88; /* QoS data */ + hdr[1] = 0x00; + memset(hdr + 4, 0xff, 6); /* addr1 */ + hdr[24] = 0x07; /* QoS Control: TID 7 ... */ + hdr[25] = 0x00; /* ... normal ack policy */ + memset(hdr + hdrlen, 0xaa, pad); /* the pad the MAC inserted */ + memcpy(hdr + hdrlen + pad, body, sizeof body); + n = MT_DMA_HDR_LEN + MT_RXWI_LEN + hdrlen + pad + (int)sizeof body; + + len = mt_rx_parse(&d, buf, n, &frame, &info); + + if (len != mpdu) { + printf(" FAIL length: want %d got %d\n", mpdu, len); + fails++; + return; + } + if (frame[24] != 0x07 || frame[25] != 0x00) { + printf(" FAIL QoS Control zeroed by the pad fold: %02x %02x\n", + frame[24], frame[25]); + fails++; + } + if (memcmp(frame + hdrlen, body, sizeof body) != 0) { + printf(" FAIL body misaligned after the fold\n"); + fails++; + } + if (frame[0] != 0x88) { + printf(" FAIL frame control lost: %02x\n", frame[0]); + fails++; + } + + /* + * Negative control. Redo the fold the old way - a fixed 24 bytes - and + * confirm it produces exactly the corruption described above. Without + * this, a test that merely passes proves nothing about what it caught. + */ + { + uint8_t again[256]; + uint8_t *h; + + memcpy(again, buf, sizeof again); + h = again + MT_DMA_HDR_LEN + MT_RXWI_LEN; + /* re-lay the pre-fold bytes, since mt_rx_parse mutated buf */ + memset(h, 0, hdrlen + pad + sizeof body); + h[0] = 0x88; + h[24] = 0x07; + h[25] = 0x00; + memset(h + hdrlen, 0xaa, pad); + memcpy(h + hdrlen + pad, body, sizeof body); + + memmove(h + 2, h, 24); /* the old, fixed-24 fold */ + if (h[2 + 24] == 0x07) { + printf(" FAIL negative control: the old fold preserved " + "QoS Control, so this test could not have caught it\n"); + fails++; + } + } +} + +/* + * Radiotap VHT bandwidth: the code names a width *and* a sub-channel within + * it, so "40 (20L)" is a 20 MHz frame. Mapping 1-3 to 40 and >=4 to 80 aired + * a requested 20-in-40 at 40 MHz. + */ +static void test_vht_bandwidth(void) +{ + /* code -> expected width, from the radiotap VHT bandwidth table */ + static const struct { uint8_t code; enum mt7612u_bw bw; const char *what; } cases[] = { + { 0, MT7612U_BW_20, "20" }, { 1, MT7612U_BW_40, "40" }, + { 2, MT7612U_BW_20, "40 (20L)" }, { 3, MT7612U_BW_20, "40 (20U)" }, + { 4, MT7612U_BW_80, "80" }, { 5, MT7612U_BW_40, "80 (40L)" }, + { 6, MT7612U_BW_40, "80 (40U)" }, { 7, MT7612U_BW_20, "80 (20LL)" }, + { 8, MT7612U_BW_20, "80 (20LU)" }, { 9, MT7612U_BW_20, "80 (20UL)" }, + { 10, MT7612U_BW_20, "80 (20UU)" }, + }; + /* radiotap: present = VHT(21) only; then 12 bytes of VHT at offset 8, + * 2-byte aligned. known = BANDWIDTH, bandwidth byte at VHT+3. */ + uint8_t buf[8 + 12 + 32]; + struct mt7612u_tx_rate r; + + printf("radiotap VHT bandwidth:\n"); + for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; i++) { + memset(buf, 0, sizeof buf); + buf[2] = 20; /* radiotap length */ + buf[4] = 0x00; buf[5] = 0x00; + buf[6] = 0x20; buf[7] = 0x00; /* present bit 21 = VHT */ + buf[8] = 0x40; /* known: BANDWIDTH */ + buf[11] = cases[i].code; /* VHT+3 = bandwidth */ + buf[12] = 0x10; /* mcs_nss: MCS1 NSS0 -> nss 1 */ + + if (mt_radiotap_parse(buf, sizeof buf, &r) != 20) { + printf(" FAIL code %u: header not parsed\n", cases[i].code); + fails++; + continue; + } + if (r.bw != cases[i].bw) { + printf(" FAIL code %2u %-10s want bw %d got %d\n", + cases[i].code, cases[i].what, (int)cases[i].bw, (int)r.bw); + fails++; + } + } +} + +/* + * MT_RATE_PHY is three bits, so 5-7 are representable and name no format. + * mt76 drops such a frame (mt76x02_mac_process_rate returns -EINVAL); before + * this was ported, one decoded through the default arm and was reported as + * the lowest legacy rate - a real-looking CCK frame rather than garbage. + */ +static void test_invalid_phy(void) +{ + struct mt7612u_dev d; + uint8_t buf[128]; + const uint8_t *frame = NULL; + struct mt7612u_rx_info info; + uint32_t ctl = FIELD_PREP(MT_RXWI_CTL_MPDU_LEN, 40u); + + printf("mt_rx_parse, invalid PHY in the rate word:\n"); + for (unsigned phy = 0; phy < 8; phy++) { + uint16_t rate = (uint16_t)FIELD_PREP(MT_RATE_PHY, phy); + int len; + + memset(&d, 0, sizeof d); + d.chainmask = 0x0202; + memset(buf, 0, sizeof buf); + for (int i = 0; i < 4; i++) + buf[MT_DMA_HDR_LEN + 4 + i] = (uint8_t)(ctl >> (8 * i)); + buf[MT_DMA_HDR_LEN + 10] = (uint8_t)(rate & 0xff); + buf[MT_DMA_HDR_LEN + 11] = (uint8_t)(rate >> 8); + buf[MT_DMA_HDR_LEN + MT_RXWI_LEN] = 0x08; /* data frame */ + + len = mt_rx_parse(&d, buf, MT_DMA_HDR_LEN + MT_RXWI_LEN + 40, + &frame, &info); + if (phy <= 4 && len != 40) { + printf(" FAIL phy %u is valid but was dropped\n", phy); + fails++; + } + if (phy > 4 && len != 0) { + printf(" FAIL phy %u names no format but decoded as rate %u\n", + phy, info.mcs); + fails++; + } + } +} + +/* + * Channel grouping. The hardware tunes the *centre* of a widened channel, so + * at 80 MHz control channel 36 tunes 42. Getting this wrong is silent: every + * register write succeeds and the part transmits, just not where it was asked + * to. Only a correctly tuned receiver or a spectrum analyser would show it, + * which is why it is pinned here instead. + */ +static void test_chan_group(void) +{ + static const struct { + uint8_t chan, bw, hw, idx, group; + } ok[] = { + /* 20 MHz: the control channel is the hardware channel. */ + { 36, MT7612U_BW_20, 36, 0, 0 }, + { 149, MT7612U_BW_20, 149, 0, 0 }, + { 1, MT7612U_BW_20, 1, 0, 0 }, /* 2.4 GHz is fine at 20 */ + + /* 40 MHz: centre is control +/- 2, and which side alternates. */ + { 36, MT7612U_BW_40, 38, 1, 0 }, + { 40, MT7612U_BW_40, 38, 3, 1 }, + { 149, MT7612U_BW_40, 151, 1, 0 }, + { 161, MT7612U_BW_40, 159, 3, 1 }, + + /* 80 MHz: all four control channels of a group tune one centre, + * and ch_group_index says which quarter the control channel is. */ + { 36, MT7612U_BW_80, 42, 0, 0 }, + { 40, MT7612U_BW_80, 42, 1, 1 }, + { 44, MT7612U_BW_80, 42, 2, 2 }, + { 48, MT7612U_BW_80, 42, 3, 3 }, + { 52, MT7612U_BW_80, 58, 0, 0 }, + { 64, MT7612U_BW_80, 58, 3, 3 }, + { 100, MT7612U_BW_80, 106, 0, 0 }, + { 112, MT7612U_BW_80, 106, 3, 3 }, + { 116, MT7612U_BW_80, 122, 0, 0 }, + { 128, MT7612U_BW_80, 122, 3, 3 }, + { 132, MT7612U_BW_80, 138, 0, 0 }, + { 144, MT7612U_BW_80, 138, 3, 3 }, + /* U-NII-3 is the case a naive centre formula gets wrong: the + * group starts at 149, not 148, so every channel in it carries + * a +1 that the integer divide has to absorb. */ + { 149, MT7612U_BW_80, 155, 0, 0 }, + { 153, MT7612U_BW_80, 155, 1, 1 }, + { 157, MT7612U_BW_80, 155, 2, 2 }, + { 161, MT7612U_BW_80, 155, 3, 3 }, + /* 2.4 GHz 40 MHz reaches centres 6-9, i.e. control 4-11. */ + { 4, MT7612U_BW_40, 6, 1, 0 }, + { 6, MT7612U_BW_40, 8, 1, 0 }, + { 8, MT7612U_BW_40, 6, 3, 1 }, + { 11, MT7612U_BW_40, 9, 3, 1 }, + }; + static const struct { uint8_t chan, bw; const char *why; } refused[] = { + /* 5.35-5.47 GHz: the arithmetic yields centres 74 and 90, which + * are not allocated. Tuning them is what the grid check stops. */ + { 68, MT7612U_BW_80, "centre 74 is not allocated" }, + { 96, MT7612U_BW_80, "centre 90 is not allocated" }, + /* Off-grid control channels. 38 is a legal 40 MHz *centre*, so a + * caller could pass it by mistake; at 80 MHz it names no group. */ + { 38, MT7612U_BW_80, "not a control channel of any 80 group" }, + { 42, MT7612U_BW_80, "42 is a centre, not a control channel" }, + { 34, MT7612U_BW_80, "below the 5 GHz 80 MHz grid" }, + /* The 165/169/173/177 group centres on 171 = 5855 MHz and spans + * to 5895, past the 5825 this driver's caps declare. mt76's own + * channel list has it; we refuse until the band is widened. */ + { 165, MT7612U_BW_80, "centre 171 is outside the declared band" }, + { 177, MT7612U_BW_80, "centre 171 is outside the declared band" }, + /* 40 MHz gets the same grid check as 80. Without it these tune + * silently: 254 wraps a uint8_t to centre 0, and 165 centres on + * 167 = 5835 MHz, outside the declared band. */ + { 254, MT7612U_BW_40, "uint8_t wrap: would centre on 0" }, + { 255, MT7612U_BW_40, "uint8_t wrap: would centre on 1" }, + { 165, MT7612U_BW_40, "centre 167 is outside the declared band" }, + { 15, MT7612U_BW_40, "not on the 5 GHz 40 MHz grid" }, + { 35, MT7612U_BW_40, "not on the 5 GHz 40 MHz grid" }, + /* 2.4 GHz: 1-3 would need a secondary at or below channel 0, + * 12-13 one above 13. Both are what the old wrap produced. */ + { 1, MT7612U_BW_40, "would centre on 255 after the wrap" }, + { 2, MT7612U_BW_40, "would centre on 0 after the wrap" }, + { 13, MT7612U_BW_40, "would need channel 15" }, + { 6, MT7612U_BW_80, "no 80 MHz in 2.4 GHz at all" }, + { 14, MT7612U_BW_80, "no 80 MHz in 2.4 GHz at all" }, + }; + unsigned i; + + printf("mt_chan_group:\n"); + for (i = 0; i < sizeof ok / sizeof ok[0]; i++) { + uint8_t hw = 0xff, idx = 0xff, group = 0xff; + + if (mt_chan_group(ok[i].chan, ok[i].bw, &hw, &idx, &group)) { + printf(" FAIL ch %3u bw %u refused, should be accepted\n", + ok[i].chan, ok[i].bw); + fails++; + continue; + } + if (hw != ok[i].hw || idx != ok[i].idx || group != ok[i].group) { + printf(" FAIL ch %3u bw %u: want hw %u idx %u group %u, " + "got hw %u idx %u group %u\n", + ok[i].chan, ok[i].bw, ok[i].hw, ok[i].idx, + ok[i].group, hw, idx, group); + fails++; + } + } + + for (i = 0; i < sizeof refused / sizeof refused[0]; i++) { + uint8_t hw = 0x5a, idx = 0x5a, group = 0x5a; + + if (!mt_chan_group(refused[i].chan, refused[i].bw, &hw, &idx, &group)) { + printf(" FAIL ch %3u bw %u accepted (%s), tuned hw %u\n", + refused[i].chan, refused[i].bw, refused[i].why, hw); + fails++; + continue; + } + /* A refusal must not write the outputs: a caller that checks the + * return but reuses the buffer would otherwise tune whatever the + * last accepted call left behind. */ + if (hw != 0x5a || idx != 0x5a || group != 0x5a) { + printf(" FAIL ch %3u bw %u refused but wrote its outputs\n", + refused[i].chan, refused[i].bw); + fails++; + } + } + + /* Negative control: the 80 MHz centre must not be the control channel. + * If this ever passes, the maths has degenerated to a pass-through and + * every positive case above would still look right at 20 MHz. */ + { + uint8_t hw = 0; + + if (!mt_chan_group(36, MT7612U_BW_80, &hw, NULL, NULL) && hw == 36) { + printf(" FAIL negative control: 80 MHz returned the control " + "channel, so this test could not detect a missing centre\n"); + fails++; + } + } +} + +/* + * Radiotap HT bandwidth. The MCS field's `known` byte declares bandwidth with + * HAVE_BW (0x01); HAVE_MCS (0x02) declares the MCS index. Gating the width on + * 0x02 silently narrowed a requested 40 MHz frame to 20 whenever a caller + * declared bandwidth without an MCS index - legal radiotap, and invisible + * except on air. The VHT half of this bug was caught in review; this is the + * HT twin that came with it. + */ +static void test_ht_bandwidth(void) +{ + static const struct { + uint8_t known, flags; enum mt7612u_bw bw; const char *what; + } cases[] = { + /* HAVE_BW alone is enough to select the width. */ + { 0x01, 0x01, MT7612U_BW_40, "HAVE_BW, bw=40" }, + { 0x01, 0x00, MT7612U_BW_20, "HAVE_BW, bw=20" }, + { 0x01, 0x02, MT7612U_BW_20, "HAVE_BW, bw=20L" }, + { 0x01, 0x03, MT7612U_BW_20, "HAVE_BW, bw=20U" }, + /* Both declared: still 40. */ + { 0x03, 0x01, MT7612U_BW_40, "HAVE_BW|HAVE_MCS, bw=40" }, + /* Bandwidth NOT declared: the field is meaningless, stay at 20 + * even though the bits happen to read 40. */ + { 0x02, 0x01, MT7612U_BW_20, "HAVE_MCS only, bw bits 40" }, + { 0x00, 0x01, MT7612U_BW_20, "nothing declared" }, + }; + uint8_t buf[8 + 3 + 32]; + struct mt7612u_tx_rate r; + unsigned i; + + printf("radiotap HT bandwidth:\n"); + for (i = 0; i < sizeof cases / sizeof cases[0]; i++) { + memset(buf, 0, sizeof buf); + buf[2] = 11; /* radiotap length: 8 + 3 */ + buf[6] = 0x08; /* present bit 19 = MCS */ + buf[8] = cases[i].known; + buf[9] = cases[i].flags; + buf[10] = 0; /* MCS 0 */ + + if (mt_radiotap_parse(buf, sizeof buf, &r) != 11) { + printf(" FAIL %-24s header not parsed\n", cases[i].what); + fails++; + continue; + } + if (r.bw != cases[i].bw) { + printf(" FAIL %-24s want bw %d got %d\n", + cases[i].what, (int)cases[i].bw, (int)r.bw); + fails++; + } + } + + /* A declared field running past the declared header length is + * malformed. This used to return the header length, which both + * injection entry points read as success and then transmitted at + * whatever defaults had accumulated. */ + { + memset(buf, 0, sizeof buf); + buf[2] = 9; /* claims 9 bytes: 8 + 1 */ + buf[6] = 0x08; /* but declares MCS, which needs 3 */ + if (mt_radiotap_parse(buf, sizeof buf, &r) > 0) { + printf(" FAIL malformed header accepted (MCS field runs " + "past the declared length)\n"); + fails++; + } + } +} + + +/* + * The async ring must carry whatever the TX builders produce. + * + * MT_TX_BUFSZ was 2048 while mt_tx_raw() built up to MT_TX_BUF_MAX (4096) and + * mt7612u_send_packets() up to MT_USB_AGG_BUF (16 KB), and both route through + * the ring whenever an RX loop is running - the normal integrated shape. So a + * frame over ~2 KB was refused after being built, and every multi-frame batch + * reported zero accepted. It never showed on the bench because the sync path + * has no ring at all. + * + * This is a size relationship, not a transfer: it holds without hardware, and + * it is the thing that must never regress. + */ +static void test_tx_ring_ceiling(void) +{ + struct mt_async probe; + + printf("async ring ceiling:\n"); + + if (MT_TX_BUFSZ < MT_TX_BUF_MAX) { + printf(" FAIL ring slot %d < single-frame builder %d\n", + MT_TX_BUFSZ, MT_TX_BUF_MAX); + fails++; + } + if (MT_TX_BUFSZ < MT_USB_AGG_BUF) { + printf(" FAIL ring slot %d < aggregate builder %d\n", + MT_TX_BUFSZ, MT_USB_AGG_BUF); + fails++; + } + /* A full 16 KB aggregate must fit one slot with nothing left over to + * split, which is what mt_async_tx_submit() length-checks against. */ + if (sizeof probe.tx_buf[0] < (size_t)MT_USB_AGG_BUF) { + printf(" FAIL slot storage %zu < aggregate %d\n", + sizeof probe.tx_buf[0], MT_USB_AGG_BUF); + fails++; + } + /* Negative control: if the ring were still 2048 the first two checks + * would have to fire. Assert the constants really are what the + * builders use, so this cannot pass by both sides shrinking together. */ + if (MT_USB_AGG_BUF != 16384 || MT_TX_BUF_MAX != 4096) { + printf(" FAIL builder ceilings moved (agg %d, single %d) - " + "re-check this test still means what it says\n", + MT_USB_AGG_BUF, MT_TX_BUF_MAX); + fails++; + } +} + +int main(void) +{ + test_hdrlen(); + test_tx_ring_ceiling(); + test_invalid_phy(); + test_rx_l2pad(); + test_chan_group(); + test_vht_bandwidth(); + test_ht_bandwidth(); + printf("frame_shape: %s\n", fails ? "FAIL" : "PASS"); + return fails ? 1 : 0; +} diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c new file mode 100644 index 00000000..bfc09164 --- /dev/null +++ b/src/mt7612u/tools/bringup.c @@ -0,0 +1,2312 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * MT7612U bringup harness. One subcommand per gate (see src/mt7612u/README.md), so each + * stage is independently runnable on hardware. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../internal.h" + + +static struct mt7612u_dev dev; + +static double now_ms(void) +{ + struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec * 1000.0 + t.tv_nsec / 1e6; +} + +/* --- interruptible waits -------------------------------------------------- + * + * A gate that hangs on this part hangs hard: the thread blocks inside a USB + * ioctl in uninterruptible sleep, where SIGKILL does not reach it and Ctrl-C + * does nothing. + * + * The defence against that is the exclusive per-adapter lock in usb.c, which + * refuses a second opener and so removes the cause. A watchdog thread lived + * here for one commit and was removed: _exit() cannot reap a thread already + * blocked in an uninterruptible ioctl, so against the failure that motivated + * it the watchdog could only print a message and then fail to exit. Keeping + * it would have been complexity that reads like protection without being any. + * + * What is kept is the part that does work: signals set a flag every wait loop + * polls, so an interrupt unwinds through the normal teardown - MAC stopped, + * RX ring torn down, lock released - instead of leaving the receiver running. + */ +static volatile sig_atomic_t g_stop; + +static void on_signal(int sig) { (void)sig; g_stop = 1; } + +/* Interruptible sleep: returns 1 if the caller should keep going. */ +static int wait_ms(double ms) +{ + double t0 = now_ms(); + + while (now_ms() - t0 < ms) { + if (g_stop) return 0; + mt_usleep(50000); + } + return !g_stop; +} + + +static int gate_regs(void) +{ + int fail = 0; + + printf("MT_ASIC_VERSION = 0x%08x (chip %04x rev %04x)\n", + dev.rev, dev.rev >> 16, dev.rev & 0xffff); + printf("MT_MAC_CSR0 = 0x%08x\n", mt_rr(&dev, MT_MAC_CSR0)); + printf("MT_WLAN_FUN_CTRL = 0x%08x (bit0 WLAN_EN, bit1 CLK_EN)\n", + mt_rr(&dev, MT_WLAN_FUN_CTRL)); + printf("MT_MCU_COM_REG0 = 0x%08x (bit0 fw running, bit1 host ack)\n", + mt_rr(&dev, MT_MCU_COM_REG0)); + printf("MT_MCU_CLOCK_CTL = 0x%08x (bit0 ROM patch applied)\n", + mt_rr(&dev, MT_MCU_CLOCK_CTL)); + printf("MT_MAC_SYS_CTRL = 0x%08x\n", mt_rr(&dev, MT_MAC_SYS_CTRL)); + printf("MT_USB_U3DMA_CFG = 0x%08x (CFG space)\n", + mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG))); + + if (dev.rev != 0x76120044) { + printf("GATE A: FAIL - expected MT_ASIC_VERSION 0x76120044\n"); + return 1; + } + + /* Two write round-trips, one per address space, so a failure says which + * side broke. MT_TX_RTS_CFG is the MAC-space choice because mt76's own + * mac_stop does read/modify/restore on it, so it is proven R/W. + * + * Do NOT use MT_MAC_ADDR_DW1's U2ME_MASK here: bits 23:16 of that + * register are write-only on this silicon - the low 16 bits take a + * write and read back, the U2ME byte always reads 0. Probing with it + * reports a working write path as broken. */ + { + uint32_t o = mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG)); + uint32_t w = (o & ~MT_USB_DMA_CFG_RX_BULK_AGG_TOUT) | + FIELD_PREP(MT_USB_DMA_CFG_RX_BULK_AGG_TOUT, 0x33); + uint32_t r; + mt_wr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG), w); + r = mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG)); + mt_wr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG), o); + printf("\nCFG-space write 0x%08x -> read 0x%08x -> restore 0x%08x %s\n", + w, r, mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG)), r == w ? "OK" : "FAIL"); + fail |= (r != w); + } + { + uint32_t o = mt_rr(&dev, MT_TX_RTS_CFG); + uint32_t w = (o & ~MT_TX_RTS_CFG_RETRY_LIMIT) | + FIELD_PREP(MT_TX_RTS_CFG_RETRY_LIMIT, 0x2b); + uint32_t r, back; + mt_wr(&dev, MT_TX_RTS_CFG, w); + r = mt_rr(&dev, MT_TX_RTS_CFG); + mt_wr(&dev, MT_TX_RTS_CFG, o); + back = mt_rr(&dev, MT_TX_RTS_CFG); + printf("MAC-space write 0x%08x -> read 0x%08x -> restore 0x%08x %s\n", + w, r, back, (r == w && back == o) ? "OK" : "FAIL"); + fail |= (r != w) || (back != o); + } + + /* EEPROM read path, and the MAC it holds. */ + { + uint8_t mac[6]; + for (unsigned i = 0; i < 8; i += 4) { + uint32_t v = mt_rr(&dev, EEP_ADDR(MT_EE_MAC_ADDR + i)); + for (unsigned b = 0; b < 4 && i + b < 6; b++) + mac[i + b] = (v >> (8 * b)) & 0xff; + } + printf("\nEEPROM MAC (0x004) = %02x:%02x:%02x:%02x:%02x:%02x\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + if (mac[0] == 0xff || (mac[0] | mac[1] | mac[2]) == 0) { + printf("GATE A: FAIL - EEPROM MAC looks unprogrammed\n"); + fail = 1; + } + } + + printf("\nGATE A: %s\n", fail ? "FAIL" : "PASS"); + return fail; +} + +/* Gate B: MCU transport + ROM patch + firmware, then a live MCU round-trip. */ +static int gate_fw(const char *fw_dir) +{ + uint32_t clk, com0; + + printf("before load: MT_MCU_CLOCK_CTL=0x%08x MT_MCU_COM_REG0=0x%08x\n", + mt_rr(&dev, MT_MCU_CLOCK_CTL), mt_rr(&dev, MT_MCU_COM_REG0)); + + if (mt_eeprom_init(&dev)) + return 1; + + if (mt_fw_init(&dev, fw_dir)) { + printf("GATE B: FAIL - firmware load failed\n"); + return 1; + } + + clk = mt_rr(&dev, MT_MCU_CLOCK_CTL); + com0 = mt_rr(&dev, MT_MCU_COM_REG0); + printf("after load: MT_MCU_CLOCK_CTL=0x%08x (patch bit0=%u) " + "MT_MCU_COM_REG0=0x%08x (fw bit0=%u)\n", + clk, clk & 1, com0, com0 & 1); + + if (!(clk & 1) || !(com0 & 1)) { + printf("GATE B: FAIL - status bits not set\n"); + return 1; + } + + /* Follow the kernel's own post-firmware order (mt76x2u_mcu_init): + * Q_SELECT then RADIO_ON, neither of which waits for a response. */ + if (mt_mcu_function_select(&dev, Q_SELECT, 1)) { + printf("GATE B: FAIL - Q_SELECT bulk-out failed\n"); + return 1; + } + if (mt_mcu_set_radio_state(&dev, 1)) { + printf("GATE B: FAIL - RADIO_ON bulk-out failed\n"); + return 1; + } + printf("Q_SELECT + RADIO_ON sent (neither waits, as in mt76)\n"); + + /* The status bits alone are not proof. CMD_LOAD_CR is the only command + * the kernel waits on during probe, so it is the one known-good + * round-trip: out on EP 8, matched by sequence on EP 5. + * NOTE: do not use GET_FW_VERSION - it is declared in mt76's enum and + * called nowhere, and the firmware does not answer it. */ + if (mt_mcu_load_cr(&dev, MT_RF_BBP_CR, 0, 0)) { + printf("GATE B: FAIL - MCU round-trip (CMD_LOAD_CR) failed\n"); + return 1; + } + printf("MCU round-trip OK (CMD_LOAD_CR acked with matching seq)\n"); + + printf("\nGATE B: PASS\n"); + return 0; +} + +/* Gate C: full power-on + firmware + MAC/PHY init, with an oracle-diff log. */ +static int gate_init(const char *fw_dir) +{ + uint32_t clk0, com0, clk1, com1, clk2, com2; + + clk0 = mt_rr(&dev, MT_MCU_CLOCK_CTL); + com0 = mt_rr(&dev, MT_MCU_COM_REG0); + printf("state on entry: CLOCK_CTL=0x%08x COM_REG0=0x%08x\n", clk0, com0); + + /* Transition test. A gate that only checks "bit is set at the end" + * passes on stale state from a previous run, so force the bits down + * first and require them to come back up. */ + mt_power_cycle(&dev); + clk1 = mt_rr(&dev, MT_MCU_CLOCK_CTL); + com1 = mt_rr(&dev, MT_MCU_COM_REG0); + printf("after reset+power: CLOCK_CTL=0x%08x COM_REG0=0x%08x " + "(patch bit0=%u, fw bit0=%u)\n", clk1, com1, clk1 & 1, com1 & 1); + + if (mt_eeprom_init(&dev)) + return 1; + + dev.wrlog = fopen("wrlog.txt", "w"); + if (!dev.wrlog) + printf("warning: could not open wrlog.txt for the oracle diff\n"); + + if (mt_init_hardware(&dev, fw_dir)) { + printf("GATE C: FAIL - init_hardware failed\n"); + return 1; + } + + clk2 = mt_rr(&dev, MT_MCU_CLOCK_CTL); + com2 = mt_rr(&dev, MT_MCU_COM_REG0); + printf("after full init: CLOCK_CTL=0x%08x COM_REG0=0x%08x " + "(patch bit0=%u, fw bit0=%u)\n", clk2, com2, clk2 & 1, com2 & 1); + printf("MT_MAC_CSR0=0x%08x MT_MAC_SYS_CTRL=0x%08x MT_MAC_STATUS=0x%08x\n", + mt_rr(&dev, MT_MAC_CSR0), mt_rr(&dev, MT_MAC_SYS_CTRL), + mt_rr(&dev, MT_MAC_STATUS)); + printf("MT_WPDMA_GLO_CFG=0x%08x (TX/RX busy bits must be 0)\n", + mt_rr(&dev, MT_WPDMA_GLO_CFG)); + + if (!(clk2 & 1) || !(com2 & 1)) { + printf("GATE C: FAIL - firmware status bits not set after init\n"); + return 1; + } + if (mt_rr(&dev, MT_WPDMA_GLO_CFG) & + (MT_WPDMA_GLO_CFG_TX_DMA_BUSY | MT_WPDMA_GLO_CFG_RX_DMA_BUSY)) { + printf("GATE C: FAIL - WPDMA still busy\n"); + return 1; + } + + printf("\nwrote %s for the oracle diff\n", "wrlog.txt"); + printf("GATE C: PASS%s\n", + (com1 & 1) ? " (NOTE: reset did not clear the fw bit - see below)" : ""); + if (com1 & 1) + printf(" The COM_REG0 fw bit survived reset+power_on, so \"bit set at\n" + " the end\" is not by itself proof of a fresh load. The MCU\n" + " round-trip in Gate B is the check that cannot pass on stale state.\n"); + return 0; +} + +/* Gate D: full init, then set one fixed 5 GHz channel at 20 MHz. */ +static int gate_chan(uint8_t chan, const char *fw_dir) +{ + if (mt_eeprom_init(&dev)) + return 1; + + dev.wrlog = fopen("wrlog.txt", "w"); + dev.mculog = fopen("mculog.txt", "w"); + + if (mt_init_hardware(&dev, fw_dir)) { + printf("GATE D: FAIL - init_hardware failed\n"); + return 1; + } + printf("init complete, setting channel %u @ 20 MHz\n", chan); + + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) { + printf("GATE D: FAIL - set_channel failed\n"); + return 1; + } + + printf("MT_TX_BAND_CFG = 0x%08x (bit1 5G, bit2 2G)\n", + mt_rr(&dev, MT_TX_BAND_CFG)); + printf("MT_BBP(CORE,1) = 0x%08x (BW field 4:3 == 0 for 20 MHz)\n", + mt_rr(&dev, MT_BBP(CORE, 1))); + printf("MT_BBP(AGC,0) = 0x%08x\n", mt_rr(&dev, MT_BBP(AGC, 0))); + printf("MT_EXT_CCA_CFG = 0x%08x\n", mt_rr(&dev, MT_EXT_CCA_CFG)); + printf("MT_TX_ALC_CFG_0 = 0x%08x\n", mt_rr(&dev, MT_TX_ALC_CFG_0)); + printf("MT_TX_PWR_CFG_0 = 0x%08x\n", mt_rr(&dev, MT_TX_PWR_CFG_0)); + + if (FIELD_GET(MT_BBP_CORE_R1_BW, mt_rr(&dev, MT_BBP(CORE, 1))) != 0) { + printf("GATE D: FAIL - BBP CORE R1 bandwidth is not 20 MHz\n"); + return 1; + } + if (!(mt_rr(&dev, MT_TX_BAND_CFG) & MT_TX_BAND_CFG_5G)) { + printf("GATE D: FAIL - 5 GHz band not selected\n"); + return 1; + } + + printf("\nwrote mculog.txt - compare against the kernel ch%u stream\n", chan); + printf("GATE D: PASS\n"); + return 0; +} + +/* 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) +{ + /* A plain 3-address data frame: broadcast DA, a source MAC chosen to be + * unmistakable in a monitor capture, and a magic payload with a counter. */ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + uint8_t frame[64]; + struct mt7612u_tx_rate rate = { + .phy = (enum mt7612u_phy)phy, .mcs = (uint8_t)mcs, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1, .power_adj = 0, + }; + /* Indexed with (phy & 7): MT_RATE_PHY is three bits, so 5-7 are + * representable and named nothing. Five entries read past the end. */ + const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT", + "?5", "?6", "?7" }; + int sent = 0; + + if (mt_eeprom_init(&dev)) + return 1; + if (mt_init_hardware(&dev, NULL)) { + printf("GATE E: FAIL - init_hardware failed\n"); return 1; + } + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) { + 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)) { + printf("GATE E: FAIL - mac_start failed\n"); return 1; + } + printf("MAC started: MT_MAC_SYS_CTRL=0x%08x (bit2 TX, bit3 RX)\n", + mt_rr(&dev, MT_MAC_SYS_CTRL)); + + memset(frame, 0, sizeof frame); + frame[0] = 0x08; frame[1] = 0x00; /* data, ToDS=0 FromDS=0 */ + memset(frame + 4, 0xff, 6); /* addr1 = broadcast */ + memcpy(frame + 10, src, 6); /* addr2 = source */ + memcpy(frame + 16, src, 6); /* addr3 = bssid */ + memcpy(frame + 24, "MT7612U-HAL ", 12); + + printf("injecting %d frames on ch%u, %s idx %d, no-ACK, rate word 0x%04x\n", + count, chan, phy_name[phy & 7], mcs, mt_tx_rate_word(&rate)); + printf("source MAC %02x:%02x:%02x:%02x:%02x:%02x - grep the witness for it\n", + src[0], src[1], src[2], src[3], src[4], src[5]); + + for (int i = 0; i < count; i++) { + frame[36] = (uint8_t)i; + frame[37] = (uint8_t)(i >> 8); + /* sequence number, so the witness can see distinct frames */ + frame[22] = (uint8_t)((i & 0xf) << 4); + frame[23] = (uint8_t)(i >> 4); + if (mt7612u_tx(&dev, frame, 40, &rate) == 0) + sent++; + mt_usleep(2000); + } + + printf("submitted %d/%d frames\n", sent, count); + printf("MT_MAC_STATUS=0x%08x MT_TX_STA_CNT0=0x%08x\n", + mt_rr(&dev, MT_MAC_STATUS), mt_rr(&dev, 0x1710)); + mt_mac_stop(&dev); + + if (sent != count) { printf("GATE E: FAIL - some submissions failed\n"); return 1; } + printf("\nGATE E: frames submitted. PASS/FAIL is decided by the witness.\n"); + return 0; +} + +/* Gate F: monitor RX. Decode rate/BW and per-chain RSSI from the RXWI. */ +static int gate_rx(uint8_t chan, int want) +{ + 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. */ + const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT", + "?5", "?6", "?7" }; + static const char *bw_name[] = { "20", "40", "80", "?" }; + uint8_t buf[4096]; + int got = 0, empty = 0; + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) { + printf("GATE F: FAIL - init_hardware failed\n"); return 1; + } + 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)) { + printf("GATE F: FAIL - mac_start failed\n"); return 1; + } + /* Monitor: drop only CRC and PHY errors, accept everything else. The + * initvals value 0x15f97 drops a great deal more than that. */ + mt_wr(&dev, MT_RX_FILTR_CFG, + MT_RX_FILTR_CFG_CRC_ERR | MT_RX_FILTR_CFG_PHY_ERR); + printf("listening on ch%u\n", chan); + printf(" MT_RX_FILTR_CFG = 0x%08x\n", mt_rr(&dev, MT_RX_FILTR_CFG)); + printf(" MT_MAC_SYS_CTRL = 0x%08x (bit2 TX, bit3 RX)\n", + mt_rr(&dev, MT_MAC_SYS_CTRL)); + printf(" MT_USB_U3DMA_CFG = 0x%08x (bit22 RX_BULK_EN)\n", + mt_rr(&dev, CFG_ADDR(MT_USB_U3DMA_CFG))); + 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)); + + while (got < want && empty < 200) { + struct mt7612u_rx_info info; + const uint8_t *f = NULL; + int len = mt_rx_one(&dev, buf, sizeof buf, &f, &info, 50); + + if (len <= 0) { empty++; continue; } + got++; + if (got <= 20 || got % 50 == 0) + printf(" #%-4d len=%-5d %-5s mcs=%-2u nss=%u bw=%-2s " + "sgi=%u ldpc=%u stbc=%u rssi=[%d,%d] sa=%02x:%02x:%02x:%02x:%02x:%02x\n", + got, len, phy_name[info.phy & 7], info.mcs, info.nss, + bw_name[info.bw & 3], info.sgi, info.ldpc, info.stbc, + info.rssi[0], info.rssi[1], + len > 15 ? f[10] : 0, len > 15 ? f[11] : 0, + len > 15 ? f[12] : 0, len > 15 ? f[13] : 0, + len > 15 ? f[14] : 0, len > 15 ? f[15] : 0); + } + + printf("\nreceived %d frames\n", got); + mt_mac_stop(&dev); + if (got == 0) { + printf("GATE F: FAIL - no frames received\n"); + return 1; + } + printf("GATE F: PASS\n"); + return 0; +} + +/* How expensive is a channel change? Decides whether FHSS is on the table. */ +static int gate_hop(void) +{ + static const uint8_t chans[] = { 149, 153, 157, 161, 149, 157, 153, 161 }; + double t0, full = 0, fast = 0; + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + if (mt_set_channel(&dev, 149, MT7612U_BW_20)) return 1; + + for (unsigned i = 0; i < sizeof chans; i++) { + t0 = now_ms(); + if (mt_set_channel_ex(&dev, chans[i], MT7612U_BW_20, 0)) return 1; + full += now_ms() - t0; + } + for (unsigned i = 0; i < sizeof chans; i++) { + t0 = now_ms(); + if (mt_set_channel_ex(&dev, chans[i], MT7612U_BW_20, 1)) return 1; + fast += now_ms() - t0; + } + printf("channel switch, mean of %zu:\n", sizeof chans); + printf(" full (with firmware calibration burst): %6.2f ms\n", full / sizeof chans); + printf(" fast (calibration skipped) : %6.2f ms\n", fast / sizeof chans); + printf("\nfor reference, devourer on Realtek hops in ~0.5-2.5 ms\n"); + mt_mac_stop(&dev); + return 0; +} + +/* + * Gate G, the per-frame rate-control check: + * 1. alternate MCS0/MCS7 frame by frame - the witness must see the rate the + * frame's own index calls for. Correlating on the index rather than + * demanding an unbroken alternating sequence keeps a lost frame from + * failing a working test. + * 2. make the hardware rate LUT disagree with txwi.rate and see which airs, + * with a positive control that sets MT_TXWI_FLAGS_TX_RATE_LUT. + */ +static int gate_g(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + uint8_t frame[64]; + struct mt7612u_tx_rate mcs0 = { .phy = MT7612U_PHY_HT, .mcs = 0, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + struct mt7612u_tx_rate mcs7 = { .phy = MT7612U_PHY_HT, .mcs = 7, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + struct mt7612u_tx_rate ofdm6 = { .phy = MT7612U_PHY_OFDM, .mcs = 0, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + uint32_t lut; + long sent_alt = 0, sent_lut[2] = { 0, 0 }; + + 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; + + memset(frame, 0, sizeof frame); + frame[0] = 0x08; + memset(frame + 4, 0xff, 6); + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + memcpy(frame + 24, "MT7612U-HAL ", 12); + /* body[] at the witness starts at offset 24: [0..11] magic, [12] tag, + * [13..14] index. */ + + printf("test 1: per-frame alternation, HT MCS0 (rate word 0x%04x) / " + "MCS7 (0x%04x)\n", mt_tx_rate_word(&mcs0), mt_tx_rate_word(&mcs7)); + for (int i = 0; i < count; i++) { + frame[36] = 'T'; + frame[37] = (uint8_t)i; + frame[38] = (uint8_t)(i >> 8); + if (mt7612u_tx(&dev, frame, 40, (i & 1) ? &mcs7 : &mcs0) == 0) + sent_alt++; + mt_usleep(2000); + } + /* Report what actually went out, not what was asked for. Printing the + * requested count and returning 0 regardless made this gate pass even + * if every single submit failed - and then handed the witness an + * experiment that never aired. */ + printf(" sent %ld/%d frames, even index = MCS0, odd = MCS7\n", + sent_alt, count); + + /* Load WCID 1's hardware rate LUT with OFDM 6 Mbps, then transmit + * HT MCS7 frames that point at it. */ + lut = FIELD_PREP(MT_WCID_TX_INFO_RATE, mt_tx_rate_word(&ofdm6)) | + FIELD_PREP(MT_WCID_TX_INFO_NSS, 1) | MT_WCID_TX_INFO_SET; + mt_wr(&dev, MT_WCID_TX_RATE(1), lut); + mt_wr(&dev, MT_WCID_TX_RATE(1) + 4, 0); + printf("\ntest 2: WCID 1 rate LUT = 0x%08x (OFDM 6 Mbps), " + "txwi.rate = HT MCS7\n", lut); + printf(" read back MT_WCID_TX_RATE(1) = 0x%08x\n", + mt_rr(&dev, MT_WCID_TX_RATE(1))); + + for (int arm = 0; arm < 2; arm++) { + long ok = 0; + + for (int i = 0; i < 150; i++) { + frame[36] = arm ? 'B' : 'A'; + frame[37] = (uint8_t)i; + frame[38] = 0; + if (mt_tx_raw(&dev, frame, 40, &mcs7, 1, arm) == 0) + ok++; + mt_usleep(2000); + } + printf(" arm %c: wcid=1, TX_RATE_LUT flag %s -> %ld/150 frames\n", + arm ? 'B' : 'A', arm ? "SET" : "clear", ok); + sent_lut[arm] = ok; + } + + mt_mac_stop(&dev); + /* An arm that aired nothing is not a result the witness can rule on: + * "no frames decoded" would read as a negative finding rather than as + * a transmitter that never spoke. Fail loudly instead. */ + if (sent_alt == 0 || sent_lut[0] == 0 || sent_lut[1] == 0) { + printf("\nGATE g: FAIL - an arm submitted no frames " + "(alt %ld, lut A %ld, lut B %ld); the witness has nothing " + "to rule on\n", sent_alt, sent_lut[0], sent_lut[1]); + return 1; + } + printf("\nGate G frames sent. The witness decides.\n"); + return 0; +} + +static double cpu_ms(void) +{ + struct rusage r; + getrusage(RUSAGE_SELF, &r); + return r.ru_utime.tv_sec * 1000.0 + r.ru_utime.tv_usec / 1000.0 + + r.ru_stime.tv_sec * 1000.0 + r.ru_stime.tv_usec / 1000.0; +} + +/* Sustained TX: synchronous path vs the async ring, same frame and rate. */ +/* + * Largest MPDU the part will actually put on air. Not a throughput test: one + * burst per size, and the witness decides which sizes arrived. + * + * Worth measuring because the ceiling in this port was a buffer constant, not + * a number anyone had checked, and because the two public TX entry points did + * not agree on it - mt7612u_tx() refused above MT_TX_BUF_MAX - 32 while + * mt7612u_send_packets() bounded only against the 16 KB aggregate buffer. + * 802.11 puts the non-A-MSDU MPDU ceiling at 2304, which is the interesting + * boundary; sizes above it are here to see whether the MAC or the USB path + * objects first. + */ +/* Defined below with the other RX callbacks; gate_mtu needs it to keep the + * receiver drained during its async pass. */ +static void drain_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info); + +static int gate_mtu(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static const int sizes[] = { + 200, 1000, 1500, 2000, 2304, 3000, 3836, 3837, 4000, 4064, 4065, + }; + static uint8_t frame[8192]; + struct mt7612u_tx_rate rate = { .phy = MT7612U_PHY_HT, .mcs = 7, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + unsigned k; int short_arms = 0; /* sizes where the two TX paths disagreed */ + + + if (count <= 0 || count > 1000) count = 60; + 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; + + memset(frame, 0, sizeof frame); + frame[0] = 0x08; /* data, 3-address */ + memset(frame + 4, 0xff, 6); /* broadcast */ + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + memcpy(frame + 24, "MT7612U-HAL ", 12); + + printf("ch%u, HT MCS7 20 MHz, %d frames per size, BOTH TX paths.\n" + "'accepted' is what this driver submitted; the witness reports\n" + "which lengths actually decoded.\n\n", chan, count); + /* Two passes on purpose. mt_tx_raw() takes the async ring whenever one + * is running and the synchronous bulk otherwise, and those had + * different ceilings: the ring refused above 2048 while the builder + * produced up to 4096. Measuring only the sync path is what hid that, + * so the sweep now reports both and a divergence is visible in the + * table rather than in an integration months later. */ + printf(" %-6s %-10s %-10s %s\n", "bytes", "sync", "async", "note"); + + for (k = 0; k < sizeof sizes / sizeof sizes[0]; k++) { + int len = sizes[k]; + long ok_sync = 0, ok_async = 0; + unsigned long drained = 0; + int i, pass; + + if ((size_t)len > sizeof frame) continue; + /* Tag the payload with the size so the witness can bucket by what + * was ASKED for, not only by what arrived. */ + frame[36] = (uint8_t)(len & 0xff); + frame[37] = (uint8_t)(len >> 8); + + for (pass = 0; pass < 2; pass++) { + long *ok = pass ? &ok_async : &ok_sync; + + /* Pass 1 brings up the RX ring, which is what makes + * mt_tx_raw() take the async path. The receiver must be + * drained or the chip wedges below USB level, hence a + * real callback rather than a null one. */ + if (pass && mt7612u_rx_start(&dev, drain_cb, &drained)) { + printf(" %-6d rx_start failed - async pass skipped\n", len); + break; + } + for (i = 0; i < count; i++) { + frame[38] = (uint8_t)i; + if (mt7612u_tx(&dev, frame, (size_t)len, &rate) == 0) + (*ok)++; + mt_usleep(1500); + } + if (pass) mt7612u_rx_stop(&dev); + } + + printf(" %-6d %ld/%-8d %ld/%-8d %s\n", len, ok_sync, count, + ok_async, count, + (ok_sync == 0 && ok_async == 0) ? "refused by this driver" : + (ok_sync != ok_async) ? "PATHS DISAGREE" : + (len > 2304 ? "above the 802.11 MPDU ceiling" : "")); + if (ok_sync != ok_async) short_arms++; + mt_usleep(120000); + } + + mt_mac_stop(&dev); + if (short_arms) { + printf("\nGATE mtu: FAIL - %d size(s) where the sync and async TX\n" + "paths disagreed. One public API must not have two ceilings.\n", + short_arms); + return 1; + } + printf("\nThe largest size with a non-zero witness count is the answer.\n" + "A size this driver accepted but the witness never saw was\n" + "submitted and dropped somewhere below - that is the real limit.\n"); + return 0; +} + +static int gate_soak(uint8_t chan, int secs, int framelen) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static uint8_t frame[2048]; + struct mt7612u_tx_rate rate = { .phy = MT7612U_PHY_HT, .mcs = 7, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + double t0, wall, c0, cpu, smax, ssum; + long n; + + if (framelen < 40 || framelen > 1500) framelen = 1400; + 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; + + memset(frame, 0, sizeof frame); + frame[0] = 0x08; + memset(frame + 4, 0xff, 6); + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + memcpy(frame + 24, "MT7612U-HAL ", 12); + + printf("soak: %d s per arm, %d-byte frames, HT MCS7 20 MHz, no-ACK\n\n", + secs, framelen); + + /* --- synchronous --- */ + n = 0; t0 = now_ms(); c0 = cpu_ms(); smax = 0; ssum = 0; + while (now_ms() - t0 < secs * 1000.0) { + double s0 = now_ms(), s1; + frame[36] = (uint8_t)n; frame[37] = (uint8_t)(n >> 8); + if (mt7612u_tx(&dev, frame, (size_t)framelen, &rate) == 0) n++; + s1 = now_ms() - s0; + ssum += s1; if (s1 > smax) smax = s1; + } + wall = now_ms() - t0; cpu = cpu_ms() - c0; + printf(" sync : %7ld frames %8.0f fps %6.2f Mbit/s cpu %5.1f%% " + "submit mean %.3f ms max %.1f ms\n", + n, n * 1000.0 / wall, n * framelen * 8.0 / wall / 1000.0, + 100.0 * cpu / wall, ssum / (n ? n : 1), smax); + + /* --- async ring --- */ + if (mt_async_start(&dev, NULL, NULL)) { printf("async start failed\n"); return 1; } + n = 0; t0 = now_ms(); c0 = cpu_ms(); smax = 0; ssum = 0; + while (now_ms() - t0 < secs * 1000.0) { + double s0 = now_ms(), s1; + frame[36] = (uint8_t)n; frame[37] = (uint8_t)(n >> 8); + if (mt7612u_tx(&dev, frame, (size_t)framelen, &rate) == 0) n++; + s1 = now_ms() - s0; + ssum += s1; if (s1 > smax) smax = s1; + } + wall = now_ms() - t0; cpu = cpu_ms() - c0; + printf(" async: %7ld frames %8.0f fps %6.2f Mbit/s cpu %5.1f%% " + "submit mean %.3f ms max %.1f ms\n", + n, n * 1000.0 / wall, n * framelen * 8.0 / wall / 1000.0, + 100.0 * cpu / wall, ssum / (n ? n : 1), smax); + { + struct mt_async_stats st; + + mt_async_stats(&dev, &st); + printf(" submitted=%llu completed=%llu errors=%llu\n", + (unsigned long long)st.tx_submitted, + (unsigned long long)st.tx_done, + (unsigned long long)st.tx_err); + } + mt_async_stop(&dev); + + /* Below saturation the pool is never full, so submit returns as soon as + * the transfer is queued instead of waiting for the wire. That is what + * the ring actually buys a caller that has other work to do. */ + printf("\n paced to ~800 fps (well under the %0.0f fps air ceiling):\n", + n * 1000.0 / wall); + for (int arm = 0; arm < 2; arm++) { + if (arm && mt_async_start(&dev, NULL, NULL)) return 1; + n = 0; smax = 0; ssum = 0; t0 = now_ms(); c0 = cpu_ms(); + while (now_ms() - t0 < secs * 1000.0) { + double s0 = now_ms(), s1; + frame[36] = (uint8_t)n; + if (mt7612u_tx(&dev, frame, (size_t)framelen, &rate) == 0) n++; + s1 = now_ms() - s0; + ssum += s1; if (s1 > smax) smax = s1; + mt_usleep(1250); + } + wall = now_ms() - t0; cpu = cpu_ms() - c0; + printf(" %-5s %6ld frames %5.0f fps cpu %4.1f%% " + "submit mean %.3f ms max %.1f ms\n", + arm ? "async" : "sync", n, n * 1000.0 / wall, + 100.0 * cpu / wall, ssum / (n ? n : 1), smax); + if (arm) mt_async_stop(&dev); + } + + printf("\n MT_TX_STA_CNT0 = 0x%08x\n", mt_rr(&dev, 0x1710)); + mt_mac_stop(&dev); + return 0; +} + +/* + * Written by the libusb event thread, read by the gate while that thread is + * still running - gate_arx() and gate_duplex() both print before calling + * mt7612u_rx_stop(). Plain increments there are a data race, so the displayed + * rate and the duplex pass/fail verdict could be built from torn counts. + * Relaxed atomics: these are counters, nothing orders anything else off them, + * and this is the RX hot path in a throughput gate. + */ +struct arx_ctx { _Atomic unsigned long n; _Atomic unsigned long by_phy[8]; }; +static void arx_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + struct arx_ctx *c = user; + (void)frame; (void)len; + atomic_fetch_add_explicit(&c->n, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&c->by_phy[info->phy & 7], 1, + memory_order_relaxed); +} + +/* Async RX ring: the callback path StartRxLoop needs. */ +static int gate_arx(uint8_t chan, int secs) +{ + 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. */ + const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT", + "?5", "?6", "?7" }; + struct arx_ctx ctx = { 0 }; + double t0; + + 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; + } + t0 = now_ms(); + wait_ms(secs * 1000.0); + { + struct mt_async_stats st; + /* Actual elapsed, not the requested duration: an interrupt now + * unwinds through here, and dividing by the request would report + * a rate the run never achieved. */ + 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 " + "rx_invalid=%llu rx_dropped=%llu\n", + chan, el, ctx.n, ctx.n / (el > 0 ? el : 1), + (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); + mt_mac_stop(&dev); + return ctx.n ? 0 : 1; +} + +/* + * Concurrent TX and RX on one claimed handle - the InitWrite + StartRxLoop + + * send_packet shape a bidirectional link consumer uses. + * + * RX is expected to be ~0 while this runs, and that is the point rather than a + * fault: the radio is half duplex and this gate saturates TX, so what it + * proves is that a TX flood does not wedge the receiver or raise errors - + * tx_err and rx_err both stay 0 and the RX ring is still healthy afterwards. + * `arx` with a peer transmitting is the gate that measures receive. + */ +#define RECOVER_S 3.0 /* post-flood listen window */ +static int gate_duplex(uint8_t chan, int secs) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static uint8_t frame[2048]; + struct mt7612u_tx_rate rate = { .phy = MT7612U_PHY_HT, .mcs = 7, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + struct arx_ctx ctx = { 0 }; + double t0, wall; + long n = 0; + + 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; + memset(frame + 4, 0xff, 6); + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + memcpy(frame + 24, "MT7612U-HAL ", 12); + + if (mt7612u_rx_start(&dev, arx_cb, &ctx)) return 1; + + 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++; + } + wall = now_ms() - t0; + printf("duplex on ch%u for %.1f s:\n", chan, wall / 1000.0); + { + struct mt_async_stats st; + + mt_async_stats(&dev, &st); + printf(" TX %ld frames (%.0f fps) RX %lu frames (%.0f fps) " + "tx_err=%llu rx_err=%llu\n", + n, n * 1000.0 / wall, ctx.n, ctx.n * 1000.0 / wall, + (unsigned long long)st.tx_err, + (unsigned long long)st.rx_err); + } + /* + * The verdict used to be (n && ctx.n), which this gate's own setup + * cannot satisfy: the radio is half duplex and the loop above saturates + * TX, so RX during the flood is ~0 whether or not a peer is + * transmitting - verified both ways, and identically on the build from + * before the ring was resized, so it is not a regression. A gate that + * demands something its configuration cannot produce is as useless as + * one that cannot fail. + * + * What this CAN establish is that a TX flood does not harm the + * receiver. So: stop transmitting, leave the ring up, and require that + * frames arrive afterwards. That distinguishes "contention while + * transmitting", which is expected, from "the flood wedged RX", which + * is the failure worth catching - and it needs a peer, so it is stated + * rather than assumed. + */ + { + unsigned long before = atomic_load_explicit(&ctx.n, + memory_order_relaxed); + unsigned long after; + + 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); + 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); + mt_mac_stop(&dev); + + if (!n) { + printf("GATE duplex: FAIL - nothing transmitted\n"); + return 1; + } + if (after == before) { + printf("GATE duplex: FAIL - no frames received after TX stopped. " + "Either the flood wedged the receiver, or no peer was " + "transmitting; this gate needs one on the same channel.\n"); + return 1; + } + printf("GATE duplex: PASS - %ld frames out, receiver healthy after\n", n); + } + return 0; +} + +/* TX power: compare our EEPROM-derived registers against the values the + * kernel driver wrote for the same channel (captured in usbmon-bus2.txt). */ +static int gate_pwr(uint8_t chan) +{ + static const struct { uint32_t reg; uint32_t kernel_ch149; const char *n; } ref[] = { + { MT_TX_PWR_CFG_0, 0x04070606, "MT_TX_PWR_CFG_0" }, + { MT_TX_PWR_CFG_1, 0x04060202, "MT_TX_PWR_CFG_1" }, + { MT_TX_PWR_CFG_2, 0x04060101, "MT_TX_PWR_CFG_2" }, + { MT_TX_PWR_CFG_3, 0x04060101, "MT_TX_PWR_CFG_3" }, + { MT_TX_PWR_CFG_4, 0x00000101, "MT_TX_PWR_CFG_4" }, + { MT_TX_PWR_CFG_7, 0x00010002, "MT_TX_PWR_CFG_7" }, + { MT_TX_PWR_CFG_8, 0x00000001, "MT_TX_PWR_CFG_8" }, + { MT_TX_PWR_CFG_9, 0x00000001, "MT_TX_PWR_CFG_9" }, + { MT_TX_ALC_CFG_0, 0x2f2f171a, "MT_TX_ALC_CFG_0" }, + }; + int bad = 0; + + 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; + + printf("txpower_conf = %d (0.5 dB units = %d dBm), tssi=%d\n", + dev.txpower_conf, dev.txpower_conf / 2, mt_tssi_enabled(&dev)); + printf("target_power = %d, chain deltas = %d/%d\n\n", + dev.target_power, dev.target_power_delta[0], dev.target_power_delta[1]); + + printf("%-18s %-12s %-12s\n", "register", "ours", "kernel(ch149)"); + for (unsigned i = 0; i < sizeof ref / sizeof ref[0]; i++) { + uint32_t v = mt_rr(&dev, ref[i].reg); + int match = (chan == 149) ? (v == ref[i].kernel_ch149) : 1; + + printf(" %-16s 0x%08x 0x%08x %s\n", ref[i].n, v, + ref[i].kernel_ch149, + chan != 149 ? "(n/a, not ch149)" : (match ? "MATCH" : "*** DIFFER ***")); + if (!match) bad++; + } + + printf("\nper-rate table (0.5 dB units):\n cck "); + for (int i = 0; i < 4; i++) printf("%3d ", dev.rate_power.cck[i]); + printf("\n ofdm "); + for (int i = 0; i < 8; i++) printf("%3d ", dev.rate_power.ofdm[i]); + printf("\n ht "); + for (int i = 0; i < 16; i++) printf("%3d ", dev.rate_power.ht[i]); + printf("\n vht %3d %3d\n", dev.rate_power.vht[0], dev.rate_power.vht[1]); + + mt_mac_stop(&dev); + printf("\nGATE pwr: %s\n", bad ? "FAIL" : "PASS"); + return bad; +} + +/* + * A-MPDU. Three arms, same QoS-data frame and rate, distinguished by a tag + * byte in the payload so the witness can separate them: + * A no AMPDU flag (baseline) + * B AMPDU flag, QSEL_EDCA + * C AMPDU flag, QSEL_MGMT (what mt76 picks for aggregated TX) + * The observable is the witness's paggr / ppdu fields: a frame that arrived + * as part of an aggregate reports paggr=1. + */ +static int gate_ampdu(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static const uint8_t peer[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x02 }; + static uint8_t frame[128]; + struct mt7612u_tx_rate rate = { .phy = MT7612U_PHY_HT, .mcs = 7, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + static const struct { char tag; unsigned opts; const char *what; } arms[] = { + { 'A', 0, "no AMPDU (baseline)" }, + { 'B', MT_TXOPT_AMPDU, "AMPDU + QSEL_EDCA" }, + { 'C', MT_TXOPT_AMPDU | MT_TXOPT_QSEL_MGMT, "AMPDU + QSEL_MGMT" }, + }; + + 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; + + /* A real station-table entry: aggregation is a per-peer notion, and + * wcid 0xff (what the injector normally uses) names no peer. */ + mt_wcid_setup(&dev, 1, peer); + printf("WCID 1 = %02x:%02x:%02x:%02x:%02x:%02x\n", + peer[0], peer[1], peer[2], peer[3], peer[4], peer[5]); + + memset(frame, 0, sizeof frame); + frame[0] = 0x88; /* QoS Data */ + frame[1] = 0x00; + memcpy(frame + 4, peer, 6); /* addr1: unicast to the peer */ + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + /* QoS Control: TID 0, Ack Policy = No Ack (bits 6:5 = 01). Leaving this + * at Normal Ack makes the MAC retry every unicast frame against a peer + * that never answers, which costs ~50x throughput. */ + frame[24] = 0x20; frame[25] = 0x00; + memcpy(frame + 26, "MT7612U-HAL ", 12); + + if (mt_async_start(&dev, NULL, NULL)) return 1; + + for (unsigned a = 0; a < sizeof arms / sizeof arms[0]; a++) { + double t0 = now_ms(), wall; + + for (int i = 0; i < count; i++) { + frame[22] = (uint8_t)((i & 0xf) << 4); + frame[23] = (uint8_t)(i >> 4); + frame[38] = (uint8_t)arms[a].tag; + frame[39] = (uint8_t)i; + frame[40] = (uint8_t)(i >> 8); + /* back to back, no pacing - aggregation needs frames + * queued faster than the air drains them */ + mt_tx_raw(&dev, frame, 48, &rate, 1, arms[a].opts); + } + wall = now_ms() - t0; + printf(" arm %c: %-22s %5d frames %7.0f fps %6.2f Mbit/s\n", + arms[a].tag, arms[a].what, count, + count * 1000.0 / wall, count * 48 * 8.0 / wall / 1000.0); + mt_usleep(200000); + } + { + struct mt_async_stats st; + + mt_async_stats(&dev, &st); + printf(" tx_err=%llu\n", (unsigned long long)st.tx_err); + } + + /* The bisect above showed unicast is what collapses throughput (the MAC + * arms an ACK timeout for a peer that never answers), so measure the + * aggregation payoff on broadcast, where the link actually runs. + * Aggregation amortises preamble+IFS, so it should matter far more at + * small frame sizes than at 1400 bytes. */ + printf("\nA-MPDU payoff on broadcast QoS, HT MCS7, 3 s per cell:\n"); + printf(" %-6s %-7s %8s %10s\n", "bytes", "ampdu", "fps", "Mbit/s"); + { + static const int sizes[] = { 200, 1400 }; + static const char tags[2][2] = { { 'D', 'E' }, { 'F', 'G' } }; + + for (unsigned z = 0; z < 2; z++) { + for (int agg = 0; agg < 2; agg++) { + double t0, wall; + long n = 0; + + memset(frame, 0, sizeof frame); + frame[0] = 0x88; /* QoS data */ + memset(frame + 4, 0xff, 6); /* broadcast */ + memcpy(frame + 10, src, 6); + memcpy(frame + 16, src, 6); + frame[24] = 0x20; frame[25] = 0; /* TID 0, No Ack */ + memcpy(frame + 26, "MT7612U-HAL ", 12); + frame[38] = (uint8_t)tags[z][agg]; + + t0 = now_ms(); + while (now_ms() - t0 < 3000.0) { + frame[22] = (uint8_t)((n & 0xf) << 4); + frame[23] = (uint8_t)(n >> 4); + if (mt_tx_raw(&dev, frame, (size_t)sizes[z], &rate, 1, + agg ? (MT_TXOPT_AMPDU | MT_TXOPT_QSEL_MGMT) : 0) == 0) + n++; + } + wall = now_ms() - t0; + printf(" %-6d %-7s %8.0f %10.2f (tag %c)\n", + sizes[z], agg ? "on" : "off", + n * 1000.0 / wall, + n * sizes[z] * 8.0 / wall / 1000.0, tags[z][agg]); + } + } + } + mt_async_stop(&dev); + mt_mac_stop(&dev); + printf("\nA-MPDU frames sent. The witness paggr/ppdu fields decide.\n"); + return 0; +} + +/* Somebody has to read EP 4 whenever MAC RX is on; this gate does not care + * what arrives, only that the endpoint keeps being drained. */ +static void drain_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + (void)frame; (void)len; (void)info; + atomic_fetch_add_explicit((_Atomic unsigned long *)user, 1, + memory_order_relaxed); +} + +/* Capability descriptor, TSF and 40 MHz. */ +static int gate_caps(uint8_t chan) +{ + struct mt7612u_caps c; + unsigned long drained = 0; + uint64_t t1, t2; + int64_t delta; + int bad = 0; + + 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; + /* The RX ring must be draining EP 4 *before* the receiver is enabled. + * This gate then sits through two 200 ms sleeps and a channel switch; + * 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; } + + mt7612u_get_caps(&dev, &c); + printf("caps: %s rev 0x%08x %dTx%dRx bw_mask 0x%02x (20%s%s)\n", + c.chip_name, c.rev, c.nss_tx, c.nss_rx, c.bw_mask, + (c.bw_mask & 2) ? "/40" : "", (c.bw_mask & 4) ? "/80" : ""); + printf(" 5 GHz %u-%u MHz, 2.4 GHz %u-%u MHz\n", + c.band_5g_min_mhz, c.band_5g_max_mhz, + c.band_2g_min_mhz, c.band_2g_max_mhz); + printf(" ampdu_tx=%u per_chain_rssi=%u narrowband=%u fast_retune=%u\n", + c.ampdu_tx, c.per_chain_rssi, c.narrowband, c.fast_retune); + printf(" max MPDU: tx %u rx %u (rx is MT_MAX_LEN_CFG 0x%03x on air,\n" + " less the 4-byte FCS)\n", + c.max_mpdu_tx, c.max_mpdu_rx, + mt_rr(&dev, MT_MAX_LEN_CFG) & 0xfff); + + printf("\nRX gain from EEPROM: rssi_offset=[%d,%d] lna_gain=%d " + "high_gain=[%d,%d] mcu_gain=0x%08x\n", + dev.cal.rssi_offset[0], dev.cal.rssi_offset[1], dev.cal.lna_gain, + dev.cal.high_gain[0], dev.cal.high_gain[1], dev.cal.mcu_gain); + printf(" raw EEPROM: LNA_GAIN=0x%04x RSSI_OFF_5G_0=0x%04x " + "RSSI_OFF_5G_1=0x%04x GRP4_5_RX_HIGH_GAIN=0x%04x\n", + mt_ee(&dev, MT_EE_LNA_GAIN), mt_ee(&dev, MT_EE_RSSI_OFFSET_5G_0), + mt_ee(&dev, MT_EE_RSSI_OFFSET_5G_1), + mt_ee(&dev, MT_EE_RF_5G_GRP4_5_RX_HIGH_GAIN)); + printf(" -> all-zero correction is CORRECT here: this EEPROM has no gain\n" + " calibration programmed, and mcu_gain 0x%08x is exactly what the\n" + " kernel sent in CMD_INIT_GAIN_OP for the same channel.\n", + dev.cal.mcu_gain); + + /* TSF: the register names suggest DW0 is the low word but mt76 reads + * DW0 as the high one. Rather than trust either reading, sleep a known + * 200 ms and require the clock to have advanced by that much. */ + { + uint32_t a0 = mt_rr(&dev, MT_TSF_TIMER_DW0); + uint32_t a1 = mt_rr(&dev, MT_TSF_TIMER_DW1); + uint32_t b0, b1; + int64_t d_hi0, d_lo0; + + mt_usleep(200000); + b0 = mt_rr(&dev, MT_TSF_TIMER_DW0); + b1 = mt_rr(&dev, MT_TSF_TIMER_DW1); + + d_hi0 = (int64_t)((((uint64_t)b0 << 32) | b1) - (((uint64_t)a0 << 32) | a1)); + d_lo0 = (int64_t)((((uint64_t)b1 << 32) | b0) - (((uint64_t)a1 << 32) | a0)); + + printf("\nTSF raw: DW0 %08x -> %08x DW1 %08x -> %08x\n", a0, b0, a1, b1); + printf(" as (DW0<<32)|DW1 : delta %lld us\n", (long long)d_hi0); + printf(" as (DW1<<32)|DW0 : delta %lld us\n", (long long)d_lo0); + printf(" over a 200000 us sleep -> DW%d is the low word\n", + (d_lo0 > 150000 && d_lo0 < 400000) ? 0 : 1); + + t1 = mt7612u_read_tsf(&dev); + mt_usleep(200000); + t2 = mt7612u_read_tsf(&dev); + delta = (int64_t)(t2 - t1); + printf(" mt7612u_read_tsf(): delta %lld us %s\n", (long long)delta, + (delta > 150000 && delta < 400000) ? "OK" : "*** WRONG ORDER ***"); + if (delta < 150000 || delta > 400000) bad++; + } + + /* 40 MHz */ + printf("\n40 MHz on ch%u:\n", chan); + if (mt_set_channel(&dev, chan, MT7612U_BW_40)) { + printf(" set_channel(40 MHz) FAILED\n"); + bad++; + } else { + uint32_t core1 = mt_rr(&dev, MT_BBP(CORE, 1)); + uint32_t agc0 = mt_rr(&dev, MT_BBP(AGC, 0)); + unsigned bwf = FIELD_GET(MT_BBP_CORE_R1_BW, core1); + + printf(" MT_BBP(CORE,1)=0x%08x BW field=%u (2 = 40 MHz)\n", core1, bwf); + printf(" MT_BBP(AGC,0)=0x%08x AGC BW=%u (3 = 40 MHz)\n", + agc0, FIELD_GET(MT_BBP_AGC_R0_BW, agc0)); + if (bwf != 2) { printf(" *** BBP not in 40 MHz ***\n"); bad++; } + + /* Registers saying 40 MHz is not the same as 40 MHz on air. + * Transmit at bw=40 and let the witness report the width. */ + { + static const uint8_t src[6] = { 0x02,0x4d,0x54,0x76,0x12,0x01 }; + uint8_t f[64]; + struct mt7612u_tx_rate r40 = { .phy = MT7612U_PHY_HT, .mcs = 7, + .nss = 1, .bw = MT7612U_BW_40, + .no_ack = 1 }; + memset(f, 0, sizeof f); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + f[36] = 'W'; + for (int i = 0; i < 300; i++) { + f[22] = (uint8_t)((i & 0xf) << 4); + f[23] = (uint8_t)(i >> 4); + mt7612u_tx(&dev, f, 40, &r40); + mt_usleep(2000); + } + printf(" sent 300 frames at bw=40, tag W - witness reports the width\n"); + } + } + + mt_async_stop(&dev); + 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"); + return bad; +} + +/* + * Hardware ACK responder, using devourer's own methodology: an unACKed + * unicast frame is retransmitted, and a retransmission carries the Retry bit + * in frame control. So the observable is our own RX - count frames from the + * stimulus transmitter, split by the Retry bit, with the responder off and + * then on. If the MAC is ACKing, the retry copies collapse. + * + * The RX filter must keep MT_RX_FILTR_CFG_DUP clear or the hardware drops the + * duplicates this test is counting. + */ +/* Same event-thread/gate split as arx_ctx above. */ +struct ack_ctx { _Atomic unsigned long to_us, retry_to_us, other; }; + +static const uint8_t g_ack_mac[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0xaa }; + +static void ack_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + struct ack_ctx *c = user; + const uint8_t *f = frame; + + (void)info; + if (len < 16) return; + if (memcmp(f + 4, g_ack_mac, 6) != 0) { + atomic_fetch_add_explicit(&c->other, 1, memory_order_relaxed); + return; + } + atomic_fetch_add_explicit(&c->to_us, 1, memory_order_relaxed); + if (f[1] & 0x08) /* FC Retry bit */ + atomic_fetch_add_explicit(&c->retry_to_us, 1, memory_order_relaxed); +} + +static int gate_ack(uint8_t chan, int secs, int arm) +{ + struct ack_ctx off = { 0, 0, 0 }; + + 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; + /* 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; } + /* 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); + + printf("responder address %02x:%02x:%02x:%02x:%02x:%02x on ch%u\n", + g_ack_mac[0], g_ack_mac[1], g_ack_mac[2], g_ack_mac[3], + g_ack_mac[4], g_ack_mac[5], chan); + printf("stimulus expected from the other radio:\n" + " DEVOURER_TX_QOS_DATA=1 DEVOURER_TX_RA=02:4d:54:76:12:aa txdemo\n\n"); + + /* One arm per invocation, so the two conditions are cleanly separated + * in the stimulus radio's own capture rather than by timestamp windows. */ + if (arm) { + if (mt7612u_set_ack_responder(&dev, g_ack_mac)) { + printf("GATE ack: FAIL - could not arm\n"); + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + return 1; + } + printf("responder ARMED (MT_MAC_ADDR_DW0=0x%08x, AUTO_RSP_CFG=0x%08x)\n", + mt_rr(&dev, MT_MAC_ADDR_DW0), mt_rr(&dev, MT_AUTO_RSP_CFG)); + } else { + printf("responder NOT armed (control arm)\n"); + } + + /* Validate before the cast: `secs` is signed and came from argv, and + * (unsigned)(-1) * 1000000 is roughly 49 days with the receiver left + * running - an RX path enabled and undrained for that long is the wedge + * this port documents as replug-only. */ + if (secs <= 0 || secs > 3600) { + printf("GATE ack: FAIL - listen duration %d out of range (1..3600 s)\n", + secs); + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + return 1; + } + printf("listening %d s ...\n", secs); + /* 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)) { + printf("GATE ack: interrupted\n"); + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + return 1; + } + mt7612u_rx_stop(&dev); + printf(" stimulus frames addressed to the responder MAC: %lu (retries %lu)\n", + off.to_us, off.retry_to_us); + + if (arm) { + mt7612u_clear_ack_responder(&dev); + printf("cleared; MT_MAC_ADDR_DW0 back to 0x%08x\n", + mt_rr(&dev, MT_MAC_ADDR_DW0)); + } + mt_mac_stop(&dev); + + if (!off.to_us) { + printf("\nINCONCLUSIVE - the stimulus never reached us.\n"); + return 1; + } + printf("\nstimulus confirmed. The ACKs (if any) are counted on the\n" + "stimulus radio, which receives concurrently.\n"); + return 0; +} + + +/* + * Every RXWI byte against ambient traffic, bucketed by received level. + * + * The power sweep answered "does this byte track our transmitter". This asks + * the two questions that one could not: does a byte vary with received level + * across a much wider span than our own saturated link covers, and does a + * byte that looks constant differ between a clean channel and an interfered + * one. A noise floor would be flat within a channel and move between them. + */ +static struct { unsigned long n; long sum[20]; long mn[20], mx[20]; } g_rxb[6]; +static const int g_rxb_edge[6] = { -100, -80, -70, -60, -50, 0 }; + +static void rxbytes_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + uint8_t bytes[20]; + int band = 0; + + (void)user; (void)frame; + if (len < 16) return; + for (int i = 0; i < 6; i++) + if (info->rssi[0] <= g_rxb_edge[i]) { band = i; break; } + + for (int i = 0; i < 4; i++) bytes[i] = (uint8_t)info->rssi[i]; + for (int w = 0; w < 4; w++) + for (int b = 0; b < 4; b++) + bytes[4 + w * 4 + b] = (uint8_t)(info->bbp[w] >> (8 * b)); + + if (!g_rxb[band].n) + for (int i = 0; i < 20; i++) { g_rxb[band].mn[i] = 999; g_rxb[band].mx[i] = -999; } + g_rxb[band].n++; + for (int i = 0; i < 20; i++) { + long v = bytes[i]; + + g_rxb[band].sum[i] += v; + if (v < g_rxb[band].mn[i]) g_rxb[band].mn[i] = v; + if (v > g_rxb[band].mx[i]) g_rxb[band].mx[i] = v; + } +} + +static int gate_rxbytes(uint8_t chan, int secs) +{ + static const char *nm[20] = { + "rssi[0]", "rssi[1]", "rssi[2]", "rssi[3]", + "bbp0.b0", "bbp0.b1", "bbp0.b2", "bbp0.b3", + "bbp1.b0", "bbp1.b1", "bbp1.b2", "bbp1.b3", + "bbp2.b0", "bbp2.b1", "bbp2.b2", "bbp2.b3", + "bbp3.b0", "bbp3.b1", "bbp3.b2", "bbp3.b3", + }; + struct mt7612u_link_stats st; + + memset(g_rxb, 0, sizeof g_rxb); + 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 (mt7612u_rx_start(&dev, rxbytes_cb, NULL)) return 1; + if (mt_mac_start(&dev, 1)) return 1; + mt7612u_set_monitor_rx(&dev, 0); + mt7612u_link_stats_start(&dev); + + wait_ms(secs * 1000.0); + mt7612u_link_stats(&dev, &st); + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + + printf("ch%u, %d s ambient. false CCA this interval: %u (mt76 calls >800 " + "interfered, <10 clean)\n", chan, secs, st.rx_false_cca); + { + unsigned long tot = 0, nv = 0; + long rs = 0, ns = 0, ss = 0; + + for (int b = 0; b < 6; b++) { + if (!g_rxb[b].n) continue; + tot += g_rxb[b].n; + rs += g_rxb[b].sum[0]; + ns += g_rxb[b].sum[2]; + } + if (tot) { + double r = rs / (double)tot - 256, n = ns / (double)tot - 256; + + (void)nv; (void)ss; + printf(" mean rssi %.1f dBm, noise %.1f dBm -> SNR %.1f dB%s\n\n", + r, n, r - n, + n < -100 ? " (noise below thermal: no valid estimate)" : ""); + } + } + printf(" %-8s", "byte"); + for (int b = 0; b < 6; b++) if (g_rxb[b].n) printf(" <=%-4d", g_rxb_edge[b]); + printf(" min max\n"); + for (int i = 0; i < 20; i++) { + long mn = 999, mx = -999; + + printf(" %-8s", nm[i]); + for (int b = 0; b < 6; b++) { + if (!g_rxb[b].n) continue; + printf(" %7.1f", g_rxb[b].sum[i] / (double)g_rxb[b].n); + if (g_rxb[b].mn[i] < mn) mn = g_rxb[b].mn[i]; + if (g_rxb[b].mx[i] > mx) mx = g_rxb[b].mx[i]; + } + printf(" %4ld %4ld\n", mn, mx); + } + printf("\n frames per band:"); + for (int b = 0; b < 6; b++) if (g_rxb[b].n) printf(" %lu", g_rxb[b].n); + printf("\n"); + return 0; +} + +/* + * The MAC's MIB counters, sampled once a second. + * + * This is where this part's link reporting actually lives. The RX descriptor + * carries RSSI and nothing else - `bbp_rxinfo[4]`, which mt76 declares and + * never reads, is two words of zero plus a duplicate of the same two RSSI + * values - so there is no per-frame SNR or EVM. What there is instead is + * per-interval: channel occupancy, four classes of receive error, a false-CCA + * count that is the interference signal, and the A-MPDU length histogram. + * + * Read-and-clear, so each line is the second that just passed. + */ +static unsigned long linkstat_drained; + +static int gate_linkstat(uint8_t chan, int secs, int with_rx) +{ + struct mt7612u_link_stats st; + + 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; + /* The receiver has to be ON for any of the RX error classes or the + * busy timer to count anything, and the ring has to be draining before + * the receiver is enabled. Getting this wrong reads as "the counters + * are dead" rather than as a harness bug. */ + if (with_rx) { + if (mt7612u_rx_start(&dev, drain_cb, &linkstat_drained)) return 1; + } + if (mt_mac_start(&dev, with_rx)) return 1; + if (with_rx) mt7612u_set_monitor_rx(&dev, 0); + mt7612u_link_stats_start(&dev); + + printf("ch%u, receiver %s, %d samples of 1 s (read-and-clear)\n\n", + chan, with_rx ? "ON" : "off", secs); + printf(" %5s %9s %9s %6s %5s %5s %8s %5s %5s %5s %4s\n", + "s", "busy", "idle", "busy%", "crc", "phy", "falseCCA", "plcp", "dup", "ovf", + "temp"); + for (int i = 0; i < secs; i++) { + double busy_pct; + + if (!wait_ms(1000.0)) break; + if (mt7612u_link_stats(&dev, &st)) 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", + i, st.ch_busy, st.ch_idle, busy_pct, + st.rx_crc_err, st.rx_phy_err, st.rx_false_cca, + st.rx_plcp_err, st.rx_dup_err, st.rx_overflow, st.temp_c); + } + + { + int any = 0; + + for (int i = 0; i < 32; i++) if (st.agg_cnt[i]) any = 1; + printf("\n A-MPDU length histogram (last second): %s", + any ? "" : "all zero - nothing aggregated\n"); + if (any) { + for (int i = 0; i < 32; i++) + if (st.agg_cnt[i]) printf("[%d]=%u ", i + 1, st.agg_cnt[i]); + printf("\n"); + } + } + if (with_rx) { + mt7612u_rx_stop(&dev); + printf(" %lu frames reached the ring over the run\n", linkstat_drained); + } + mt_mac_stop(&dev); + return 0; +} + +/* --- MT7612U -> MT7612U link, and what the baseband reports per frame --- + * + * Two adapters, one transmitting at a swept TX power and one receiving. It + * answers three separate questions at once, which is why the sweep is a + * power sweep and not a fixed level: + * + * 1. Does this port's TX and RX work against each other end to end? + * 2. Does mt7612u_set_txpower() move *radiated* power? Everything so far + * compared registers against the kernel's, which is not the same claim. + * 3. RXWI bytes 16-31 are `bbp_rxinfo[4]`, which mt76 declares and never + * reads, and mt76x02 has no SNR or EVM anywhere. If any of those bytes + * is a link-quality metric it must move with the transmitter's power; + * if none of them does, they are not one. + * + * The receiver is NOT an independent instrument - it runs this same decode + * path - so this measures the link and the descriptor, not our correctness. + */ +static int gate_linktx(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static const int powers[] = { 0, 4, 8, 12, 16, 20, 24, 30 }; + struct mt7612u_tx_rate r = { .phy = MT7612U_PHY_HT, .mcs = 2, .nss = 1, + .bw = MT7612U_BW_20, .no_ack = 1 }; + uint8_t f[64]; + + 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; + + memset(f, 0, sizeof f); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + + printf("TX on ch%u, HT MCS2 1SS 20 MHz, %d frames per power step\n", + chan, count); + for (unsigned i = 0; i < sizeof powers / sizeof powers[0]; i++) { + long sent = 0; + + if (mt7612u_set_txpower(&dev, powers[i])) { + printf(" %2d dBm REFUSED\n", powers[i]); + continue; + } + f[36] = (uint8_t)powers[i]; + for (int n = 0; n < count; n++) { + f[22] = (uint8_t)((n & 0xf) << 4); + f[23] = (uint8_t)(n >> 4); + f[37] = (uint8_t)n; + if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; + mt_usleep(1200); + } + printf(" %2d dBm sent %ld/%d\n", powers[i], sent, count); + mt_usleep(120000); + } + mt_mac_stop(&dev); + return 0; +} + +/* Every byte the RXWI offers past the two RSSI values mt76 reads, averaged. + * 4 rssi bytes (mt76 uses only [0] and [1]; [2] and [3] are read by nobody, + * and on the legacy Ralink RXWI those slots were SNR0/SNR1) plus the 16 bytes + * of bbp_rxinfo. Signed and unsigned means both, because an SNR would be a + * small positive number and an RSSI a negative one. */ +struct link_bucket { unsigned long n; long b_sum[20]; long b_min[20], b_max[20]; }; +static struct link_bucket g_link[32]; +static int g_link_pw[32]; +static int g_link_n; + +static void linkrx_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + const uint8_t *f = frame; + int slot = -1, pw; + uint8_t bytes[20]; + + (void)user; + if (len < 40) return; + if (memcmp(f + 10, "\x02\x4d\x54\x76\x12\x01", 6)) return; + if (memcmp(f + 24, "MT7612U-HAL ", 12)) return; + pw = f[36]; + for (int i = 0; i < g_link_n; i++) + if (g_link_pw[i] == pw) { slot = i; break; } + if (slot < 0) { + if (g_link_n >= 32) return; + slot = g_link_n++; + g_link_pw[slot] = pw; + for (int i = 0; i < 20; i++) { + g_link[slot].b_min[i] = 999; + g_link[slot].b_max[i] = -999; + } + } + + for (int i = 0; i < 4; i++) bytes[i] = (uint8_t)info->rssi[i]; + for (int w = 0; w < 4; w++) + for (int b = 0; b < 4; b++) + bytes[4 + w * 4 + b] = (uint8_t)(info->bbp[w] >> (8 * b)); + + g_link[slot].n++; + for (int i = 0; i < 20; i++) { + long v = bytes[i]; + + g_link[slot].b_sum[i] += v; + if (v < g_link[slot].b_min[i]) g_link[slot].b_min[i] = v; + if (v > g_link[slot].b_max[i]) g_link[slot].b_max[i] = v; + } +} + +static int gate_linkrx(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 (mt7612u_rx_start(&dev, linkrx_cb, NULL)) return 1; + if (mt_mac_start(&dev, 1)) 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); + mt_mac_stop(&dev); + + { + static const char *nm[20] = { + "rssi[0]", "rssi[1]", "rssi[2]", "rssi[3]", + "bbp0.b0", "bbp0.b1", "bbp0.b2", "bbp0.b3", + "bbp1.b0", "bbp1.b1", "bbp1.b2", "bbp1.b3", + "bbp2.b0", "bbp2.b1", "bbp2.b2", "bbp2.b3", + "bbp3.b0", "bbp3.b1", "bbp3.b2", "bbp3.b3", + }; + + printf("\nmean of every candidate byte, per requested tx power\n"); + printf(" %-8s", "byte"); + for (int i = 0; i < g_link_n; i++) printf(" %7d", g_link_pw[i]); + printf(" span as int8\n"); + for (int b = 0; b < 20; b++) { + double lo = 1e9, hi = -1e9; + + printf(" %-8s", nm[b]); + for (int i = 0; i < g_link_n; i++) { + double m = g_link[i].b_sum[b] / (double)g_link[i].n; + + if (m < lo) lo = m; + if (m > hi) hi = m; + printf(" %7.1f", m); + } + printf(" %5.1f %6.1f\n", hi - lo, + g_link[0].b_sum[b] / (double)g_link[0].n > 127 + ? g_link[0].b_sum[b] / (double)g_link[0].n - 256 + : g_link[0].b_sum[b] / (double)g_link[0].n); + } + printf("\n frames per level:"); + for (int i = 0; i < g_link_n; i++) printf(" %lu", g_link[i].n); + printf("\n"); + } + printf("\nA byte whose span is ~0 across a 30 dB sweep carries no level or\n" + "quality information. One that tracks and stays a small positive\n" + "number is an SNR candidate; one that tracks and reads negative as\n" + "int8 is another copy of RSSI.\n"); + return g_link_n ? 0 : 1; +} + +/* + * Does STBC actually put the stream on both antennas, and does the second + * chain radiate without it? + * + * The coding gate proves the STBC bit reaches the air and the receiver + * decodes the frame as STBC. It says nothing about radiated power, and there + * is a real confound: a chip may already drive the second chain with cyclic + * delay diversity on a one-stream frame, in which case "STBC off" is not + * "one antenna". + * + * Phase 1 alternates STBC off/on frame by frame at one rate. Nothing is + * reconfigured between them - only one bit of the rate word changes - so + * ambient drift, distance and AGC state cancel. + * + * Phase 2 needs a chainmask change, which costs a channel re-set, so it runs + * in blocks and repeats the sequence twice: if the two passes disagree, the + * difference is drift and not the chainmask. + */ +static int gate_diversity(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + uint8_t f[64]; + + 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; + + memset(f, 0, sizeof f); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + + printf("HT MCS2, 1 spatial stream, 20 MHz, ch%u\n", chan); + printf("phase 1: STBC off/on alternating frame by frame (tags A / B)\n"); + { + struct mt7612u_tx_rate off = { .phy = MT7612U_PHY_HT, .mcs = 2, + .nss = 1, .bw = MT7612U_BW_20, + .no_ack = 1 }; + struct mt7612u_tx_rate on = off; + long n_off = 0, n_on = 0; + + on.stbc = 1; + printf(" rate word off 0x%04x on 0x%04x (one bit apart)\n", + mt_tx_rate_word(&off), mt_tx_rate_word(&on)); + for (int i = 0; i < count; i++) { + int stbc = i & 1; + + f[22] = (uint8_t)((i & 0xf) << 4); + f[23] = (uint8_t)(i >> 4); + f[36] = stbc ? 'B' : 'A'; + f[37] = (uint8_t)i; + if (mt7612u_tx(&dev, f, 44, stbc ? &on : &off) == 0) { + if (stbc) n_on++; else n_off++; + } + mt_usleep(1500); + } + printf(" submitted %ld off, %ld on\n", n_off, n_on); + } + + printf("phase 2: 1T1R vs 2T2R, STBC off, two passes (tags C / D)\n"); + for (int pass = 0; pass < 2; pass++) { + for (int two = 0; two < 2; two++) { + struct mt7612u_tx_rate r = { .phy = MT7612U_PHY_HT, .mcs = 2, + .nss = 1, .bw = MT7612U_BW_20, + .no_ack = 1 }; + long sent = 0; + + if (mt7612u_set_chainmask(&dev, two ? 0x0202 : 0x0101)) + return 1; + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; + printf(" pass %d chainmask 0x%04x txwi[17]=0x%02x\n", pass, + dev.chainmask, ((dev.chainmask & 0xf) > 1) ? 0x13 : 0); + f[36] = two ? 'D' : 'C'; + for (int i = 0; i < count; i++) { + f[22] = (uint8_t)((i & 0xf) << 4); + f[23] = (uint8_t)(i >> 4); + f[37] = (uint8_t)i; + if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; + mt_usleep(1500); + } + printf(" submitted %ld/%d\n", sent, count); + } + } + mt7612u_set_chainmask(&dev, 0x0202); + + mt_mac_stop(&dev); + printf("\nWitness RSSI per tag decides. A vs B is the STBC question with\n" + "nothing else changed; C vs D is whether the second chain radiates\n" + "at all without STBC.\n"); + return 0; +} + +/* + * The three modulation flags in the rate word: LDPC, STBC and short GI. + * + * Each frame carries both the DESC_RATE it should air at and the flag bits it + * should carry, so the witness compares the frame against its own claim + * rather than against an arm table. + * + * One arm is a deliberate negative control: STBC is requested at two spatial + * streams, where mt_tx_rate_word() refuses to set it because mt76 refuses too + * (STBC on this MAC is a 1SS feature). The frame must air with stbc clear. An + * arm that only ever asks for things that work cannot tell a working encoder + * from one that sets every bit it is handed. + */ +/* bw is the MT7612U_BW_* enum: 0 = 20, 1 = 40, 2 = 80. */ +static int gate_coding(uint8_t chan, int count, int bw) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static const struct { enum mt7612u_phy phy; uint8_t mcs, nss; int base; } + rates[] = { + { MT7612U_PHY_HT, 3, 1, 15 }, + { MT7612U_PHY_HT, 7, 1, 19 }, + { MT7612U_PHY_HT, 11, 2, 23 }, + { MT7612U_PHY_VHT, 3, 1, 47 }, + { MT7612U_PHY_VHT, 7, 1, 51 }, + { MT7612U_PHY_VHT, 3, 2, 57 }, + { MT7612U_PHY_VHT, 7, 2, 61 }, + }; + uint8_t f[64]; + int arms = 0; + int short_arms = 0; /* arms that aired fewer frames than asked */ + uint8_t hw_chan = chan; + + 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; + + 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", + chan, hw_chan, 20 << bw, count); + printf(" %-16s %-10s %-9s %s\n", "rate", "asked", "rate word", "expect on air"); + + memset(f, 0, sizeof f); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + + for (unsigned i = 0; i < sizeof rates / sizeof rates[0]; i++) { + /* 802.11n has no 80 MHz, so an HT arm at that width would emit a + * rate word that names no real format. gate_sweep and gate_vht + * skip the same pairing. */ + if (bw == MT7612U_BW_80 && rates[i].phy != MT7612U_PHY_VHT) { + printf(" %-16s skipped: 802.11n has no 80 MHz\n", "HT"); + continue; + } + for (int coding = 0; coding < 8; coding++) { + struct mt7612u_tx_rate r = { + .phy = rates[i].phy, .mcs = rates[i].mcs, + .nss = rates[i].nss, + .bw = (uint8_t)bw, + .sgi = (coding & 4) ? 1u : 0u, + .ldpc = (coding & 1) ? 1u : 0u, + .stbc = (coding & 2) ? 1u : 0u, + .no_ack = 1, + }; + uint16_t word = mt_tx_rate_word(&r); + /* What the encoder actually committed to, which is what + * the air must show - not what was asked for. */ + int on_air = ((word & MT_RATE_LDPC) ? 1 : 0) | + ((word & MT_RATE_STBC) ? 2 : 0) | + ((word & MT_RATE_SGI) ? 4 : 0); + char asked[16], expect[24]; + long sent = 0; + + snprintf(asked, sizeof asked, "%s%s%s", + (coding & 1) ? "L" : "-", (coding & 2) ? "S" : "-", + (coding & 4) ? "G" : "-"); + snprintf(expect, sizeof expect, "rate %d %s%s%s", + rates[i].base, + (on_air & 1) ? "L" : "-", (on_air & 2) ? "S" : "-", + (on_air & 4) ? "G" : "-"); + printf(" %s MCS%-2d %dSS %-10s 0x%04x %s%s\n", + rates[i].phy == MT7612U_PHY_HT ? "HT " : "VHT", + rates[i].mcs, rates[i].nss, asked, word, expect, + (coding & 2) && rates[i].nss > 1 ? " <- STBC refused at 2SS" : ""); + + f[36] = (uint8_t)rates[i].base; + f[38] = (uint8_t)on_air; + for (int n = 0; n < count; n++) { + f[22] = (uint8_t)((n & 0xf) << 4); + f[23] = (uint8_t)(n >> 4); + f[37] = (uint8_t)n; + if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; + mt_usleep(1200); + } + if (sent != count) { + printf(" submitted only %ld/%d\n", sent, count); + short_arms++; + } + arms++; + mt_usleep(80000); + } + } + + mt_mac_stop(&dev); + printf("\n%d arms swept. Payload offset 12 is the expected DESC_RATE,\n" + "offset 14 the expected LDPC|STBC|SGI bits.\n", arms); + if (short_arms) { + printf("GATE coding: FAIL - %d arm(s) submitted fewer frames than " + "asked; the witness cannot rule on an arm that did not air\n", + short_arms); + return 1; + } + return 0; +} + +/* + * Full rate-ladder sweep: every HT MCS 0-15 and every legal VHT MCS at both + * stream counts, at whichever width the caller picks. + * + * Each frame carries its own expected DESC_RATE code in the payload, so the + * check is "did this frame air at the rate it says it should have" rather + * than an arm table the analysis has to agree with separately. A frame that + * airs at the wrong rate indicts itself. + * + * VHT MCS9 is not legal at 20 MHz for one or two streams, so it is skipped + * there and included at 40. + */ +/* bw is the MT7612U_BW_* enum: 0 = 20, 1 = 40, 2 = 80. */ +static int gate_sweep(uint8_t chan, int count, int bw) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + uint8_t f[64]; + int arms = 0; + int short_arms = 0; /* arms that aired fewer frames than asked */ + uint8_t hw_chan = chan; + + 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; + + /* 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 + * the control channel with the wrong centre hears nothing. */ + mt_chan_group(chan, (uint8_t)bw, &hw_chan, NULL, NULL); + printf("ch%u (hw centre %u) at %d MHz, chainmask 0x%04x, %d frames per rate\n\n", + chan, hw_chan, 20 << bw, dev.chainmask, count); + printf(" %-18s %-9s %s\n", "rate", "rate word", "expected DESC_RATE"); + + memset(f, 0, sizeof f); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + + for (int phase = 0; phase < 2; phase++) { + /* HT has no 80 MHz: 802.11n stops at 40, and 80 is a VHT-only + * width. A rate word naming PHY=HT with BW=80 is not a wide HT + * frame, it is an unspecified one, so the HT ladder is skipped + * rather than swept at a width it cannot mean. */ + int last_mcs = phase == 0 ? 15 : (bw ? 9 : 8); + + if (phase == 0 && bw == MT7612U_BW_80) { + printf(" (HT ladder skipped: 802.11n has no 80 MHz)\n"); + continue; + } + + for (int mcs = 0; mcs <= last_mcs; mcs++) { + for (int nss = 1; nss <= 2; nss++) { + struct mt7612u_tx_rate r = { + .bw = (uint8_t)bw, + .no_ack = 1, + }; + char what[32]; + int expect; + long sent = 0; + + if (phase == 0) { + /* HT folds the stream count into the MCS + * number, so it is one ladder, not two. */ + if (nss == 2) continue; + r.phy = MT7612U_PHY_HT; + r.mcs = (uint8_t)mcs; + r.nss = (uint8_t)(1 + (mcs >> 3)); + expect = 12 + mcs; + snprintf(what, sizeof what, "HT MCS%-2d %dSS", mcs, r.nss); + } else { + r.phy = MT7612U_PHY_VHT; + r.mcs = (uint8_t)mcs; + r.nss = (uint8_t)nss; + expect = 44 + (nss - 1) * 10 + mcs; + snprintf(what, sizeof what, "VHT MCS%-2d %dSS", mcs, nss); + } + + printf(" %-18s 0x%04x %d\n", what, + mt_tx_rate_word(&r), expect); + f[36] = (uint8_t)expect; + for (int i = 0; i < count; i++) { + f[22] = (uint8_t)((i & 0xf) << 4); + f[23] = (uint8_t)(i >> 4); + f[37] = (uint8_t)i; + if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; + mt_usleep(1200); + } + if (sent != count) { + printf(" submitted only %ld/%d\n", sent, count); + short_arms++; + } + arms++; + mt_usleep(100000); + } + } + } + + mt_mac_stop(&dev); + printf("\n%d rates swept at %d MHz. Each frame carries its own expected\n" + "DESC_RATE at payload offset 12; the witness compares the two.\n" + "The witness must be listening at the same width - a 20 MHz\n" + "receiver decodes none of a 40 or 80 MHz frame.\n", arms, 20 << bw); + if (short_arms) { + printf("GATE sweep: FAIL - %d rate(s) submitted fewer frames than " + "asked\n", short_arms); + return 1; + } + return 0; +} + +/* + * VHT and two spatial streams on air. + * + * The rate word encodes both and the RX path decodes both, but until now + * neither had been transmitted - docs/mt7612u.md listed them as unexercised. + * Each arm carries its own tag byte so the witness attributes frames by + * content rather than by timestamp, and each has one expected DESC_RATE code + * at the witness: HT is 12+mcs, VHT 1SS is 44+mcs, VHT 2SS is 54+mcs. A + * stream count that silently collapsed to one would land on the 1SS codes, + * which is exactly the failure this is looking for. + * + * VHT MCS9 is not a legal rate at 20 MHz for one or two streams, so it only + * appears in the 40 MHz arms. + */ +/* bw is the MT7612U_BW_* enum: 0 = 20, 1 = 40, 2 = 80. */ +static int gate_vht(uint8_t chan, int count, int bw) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + static const struct { + char tag; enum mt7612u_phy phy; uint8_t mcs, nss; int wide_only; + const char *what; int expect; + } arms[] = { + { 'P', MT7612U_PHY_HT, 7, 1, 0, "HT MCS7 1SS", 19 }, + { 'Q', MT7612U_PHY_HT, 15, 2, 0, "HT MCS15 2SS", 27 }, + { 'R', MT7612U_PHY_VHT, 0, 1, 0, "VHT MCS0 1SS", 44 }, + { 'S', MT7612U_PHY_VHT, 7, 1, 0, "VHT MCS7 1SS", 51 }, + { 'T', MT7612U_PHY_VHT, 8, 1, 0, "VHT MCS8 1SS", 52 }, + { 'U', MT7612U_PHY_VHT, 0, 2, 0, "VHT MCS0 2SS", 54 }, + { 'V', MT7612U_PHY_VHT, 7, 2, 0, "VHT MCS7 2SS", 61 }, + { 'X', MT7612U_PHY_VHT, 8, 2, 0, "VHT MCS8 2SS", 62 }, + { 'Y', MT7612U_PHY_VHT, 9, 1, 1, "VHT MCS9 1SS", 53 }, + { 'Z', MT7612U_PHY_VHT, 9, 2, 1, "VHT MCS9 2SS", 63 }, + }; + uint8_t f[64]; + int short_arms = 0; /* arms that aired fewer frames than asked */ + uint8_t hw_chan = chan; + + 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; + + mt_chan_group(chan, (uint8_t)bw, &hw_chan, NULL, NULL); + printf("chainmask 0x%04x -> %d spatial streams, txwi[17]=0x%02x\n", + dev.chainmask, (dev.chainmask & 0xf) > 1 ? 2 : 1, + ((dev.chainmask & 0xf) > 1) ? 0x13 : 0); + printf("ch%u (hw centre %u) at %d MHz, %d frames per arm\n\n", + chan, hw_chan, 20 << bw, count); + printf(" tag %-16s rate word expected witness DESC_RATE\n", "arm"); + + memset(f, 0, sizeof f); + f[0] = 0x08; /* data, 3-address */ + memset(f + 4, 0xff, 6); /* broadcast */ + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + + for (unsigned a = 0; a < sizeof arms / sizeof arms[0]; a++) { + struct mt7612u_tx_rate r = { .phy = arms[a].phy, .mcs = arms[a].mcs, + .nss = arms[a].nss, + .bw = (uint8_t)bw, + .no_ack = 1 }; + long sent = 0; + + /* MCS9 has no 20 MHz encoding at 1 or 2 streams. */ + if (arms[a].wide_only && bw == MT7612U_BW_20) continue; + /* HT is a 20/40-only PHY: 80 MHz is VHT-defined. */ + if (arms[a].phy != MT7612U_PHY_VHT && bw == MT7612U_BW_80) continue; + + printf(" %c %-16s 0x%04x %d\n", arms[a].tag, arms[a].what, + mt_tx_rate_word(&r), arms[a].expect); + f[36] = (uint8_t)arms[a].tag; + for (int i = 0; i < count; i++) { + f[22] = (uint8_t)((i & 0xf) << 4); + f[23] = (uint8_t)(i >> 4); + f[37] = (uint8_t)i; + f[38] = (uint8_t)(i >> 8); + if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; + mt_usleep(1500); + } + printf(" submitted %ld/%d\n", sent, count); + if (sent != count) short_arms++; + mt_usleep(150000); + } + + mt_mac_stop(&dev); + printf("\nFrames submitted. The witness decides: each tag must appear at\n" + "its expected DESC_RATE. A 2SS arm landing on a 1SS code means the\n" + "second stream did not go out.\n"); + if (short_arms) { + printf("GATE vht: FAIL - %d arm(s) submitted fewer frames than " + "asked\n", short_arms); + return 1; + } + return 0; +} + +/* + * The two radiotap entry points: send_packet (one framed MPDU) and + * send_packets (several, chained into one bulk-OUT transfer via + * MT_TXD_INFO_NEXT_VLD). Tag A = singular, tag B = aggregated. + */ +static int gate_rtap(uint8_t chan, int count) +{ + static const uint8_t src[6] = { 0x02, 0x4d, 0x54, 0x76, 0x12, 0x01 }; + /* radiotap: present = MCS | TX_FLAGS, then tx_flags(2), mcs(3) */ + static const uint8_t rtap[] = { + 0x00, 0x00, 0x0d, 0x00, /* ver, pad, len 13 */ + 0x00, 0x80, 0x08, 0x00, /* present: TX_FLAGS(15) MCS(19) */ + 0x08, 0x00, /* TX_FLAGS = NOACK */ + 0x1f, 0x00, 0x07, /* MCS: known, flags, index 7 */ + }; + uint8_t pkt[13 + 64]; + struct mt7612u_tx_view views[16]; + uint8_t bufs[16][13 + 64]; + double t0, wall; + long n = 0; + size_t acc; + + 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; + + memcpy(pkt, rtap, sizeof rtap); + { + uint8_t *f = pkt + sizeof rtap; + + memset(f, 0, 64); + f[0] = 0x08; + memset(f + 4, 0xff, 6); + memcpy(f + 10, src, 6); + memcpy(f + 16, src, 6); + memcpy(f + 24, "MT7612U-HAL ", 12); + } + + /* Round-trip the parser first: what did it make of that header? */ + { + struct mt7612u_tx_rate r; + int rl = mt_radiotap_parse(pkt, sizeof pkt, &r); + + printf("radiotap parse: hdrlen=%d -> phy=%d mcs=%u nss=%u bw=%d " + "sgi=%u ldpc=%u stbc=%u no_ack=%u (rate word 0x%04x)\n", + rl, r.phy, r.mcs, r.nss, r.bw, r.sgi, r.ldpc, r.stbc, + r.no_ack, mt_tx_rate_word(&r)); + if (rl != 13 || r.phy != MT7612U_PHY_HT || r.mcs != 7 || !r.no_ack) { + printf("GATE rtap: FAIL - parser did not decode the header\n"); + return 1; + } + } + + /* Tag A: send_packet, one frame per call. */ + pkt[sizeof rtap + 36] = 'A'; + t0 = now_ms(); + for (int i = 0; i < count; i++) { + pkt[sizeof rtap + 22] = (uint8_t)((i & 0xf) << 4); + pkt[sizeof rtap + 23] = (uint8_t)(i >> 4); + if (mt7612u_send_packet(&dev, pkt, sizeof rtap + 40) == 0) n++; + } + wall = now_ms() - t0; + printf("send_packet : %ld frames, %.0f fps\n", n, n * 1000.0 / wall); + + /* Tag B: send_packets, 16 per call -> one bulk transfer per 16 frames. */ + for (int k = 0; k < 16; k++) { + memcpy(bufs[k], pkt, sizeof rtap + 40); + bufs[k][sizeof rtap + 36] = 'B'; + views[k].data = bufs[k]; + views[k].len = sizeof rtap + 40; + } + acc = 0; + t0 = now_ms(); + for (int i = 0; i < count / 16; i++) { + for (int k = 0; k < 16; k++) { + bufs[k][sizeof rtap + 22] = (uint8_t)(((i * 16 + k) & 0xf) << 4); + bufs[k][sizeof rtap + 23] = (uint8_t)((i * 16 + k) >> 4); + } + acc += mt7612u_send_packets(&dev, views, 16); + } + wall = now_ms() - t0; + printf("send_packets: %zu frames in %d transfers (16/transfer), %.0f fps\n", + acc, count / 16, acc * 1000.0 / wall); + + mt_mac_stop(&dev); + printf("\nWitness decides: tag A must appear (send_packet works) and tag B\n" + "must appear (USB chaining via NEXT_VLD actually airs).\n"); + return 0; +} + +int main(int argc, char **argv) +{ + const char *err = NULL, *cmd = argc > 1 ? argv[1] : "regs"; + int rc; + /* The width argument that sweep/coding/vht share in argv[4]. Validated + * here rather than inside them, because they format it as `20 << bw`, + * which for a negative or large argv value is undefined rather than + * merely wrong. Other gates give argv[4] a different meaning, so the + * check is scoped to the three that read it as a width. */ + int want_bw = argc > 4 ? atoi(argv[4]) : 0; + + if (!strcmp(cmd, "sweep") || !strcmp(cmd, "coding") || !strcmp(cmd, "vht")) { + if (want_bw < MT7612U_BW_20 || want_bw > MT7612U_BW_80) { + fprintf(stderr, + "bad bandwidth '%s': 0 = 20 MHz, 1 = 40, 2 = 80\n", + argv[4]); + return 2; + } + /* A non-positive count sweeps every arm with zero frames and + * then reports success, which is the same "structurally + * guaranteed pass" the shortfall check above exists to stop. */ + if (argc > 3 && atoi(argv[3]) <= 0) { + fprintf(stderr, "bad frame count '%s': must be positive\n", + argv[3]); + return 2; + } + } + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + if (mt_open(&dev, &err)) { + fprintf(stderr, "open failed: %s\n", err ? err : "?"); + return 1; + } + + if (!strcmp(cmd, "regs")) { + rc = gate_regs(); + } else if (!strcmp(cmd, "rtap")) { + rc = gate_rtap(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 400); + } else if (!strcmp(cmd, "ack")) { + rc = gate_ack(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 6, + argc > 4 ? atoi(argv[4]) : 0); + } else if (!strcmp(cmd, "caps")) { + rc = gate_caps(argc > 2 ? (uint8_t)atoi(argv[2]) : 149); + } else if (!strcmp(cmd, "rxbytes")) { + rc = gate_rxbytes(argc > 2 ? (uint8_t)atoi(argv[2]) : 1, + argc > 3 ? atoi(argv[3]) : 15); + } else if (!strcmp(cmd, "linkstat")) { + rc = gate_linkstat(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 10, + argc > 4 ? atoi(argv[4]) : 0); + } else if (!strcmp(cmd, "linktx")) { + rc = gate_linktx(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 400); + } else if (!strcmp(cmd, "linkrx")) { + rc = gate_linkrx(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 30); + } else if (!strcmp(cmd, "diversity")) { + rc = gate_diversity(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 600); + } else if (!strcmp(cmd, "coding")) { + rc = gate_coding(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 100, want_bw); + } else if (!strcmp(cmd, "mtu")) { + rc = gate_mtu(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 60); + } else if (!strcmp(cmd, "sweep")) { + rc = gate_sweep(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 120, want_bw); + } else if (!strcmp(cmd, "vht")) { + rc = gate_vht(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 300, want_bw); + } else if (!strcmp(cmd, "ampdu")) { + rc = gate_ampdu(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 400); + } else if (!strcmp(cmd, "pwr")) { + rc = gate_pwr(argc > 2 ? (uint8_t)atoi(argv[2]) : 149); + } else if (!strcmp(cmd, "soak")) { + rc = gate_soak(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 5, + argc > 4 ? atoi(argv[4]) : 1400); + } else if (!strcmp(cmd, "duplex")) { + rc = gate_duplex(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + 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); + } else if (!strcmp(cmd, "gateg")) { + rc = gate_g(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 300); + } else if (!strcmp(cmd, "hop")) { + rc = gate_hop(); + } else if (!strcmp(cmd, "rx")) { + rc = gate_rx(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 40); + } else if (!strcmp(cmd, "tx")) { + rc = gate_tx(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? atoi(argv[3]) : 200, + argc > 4 ? atoi(argv[4]) : MT7612U_PHY_OFDM, + argc > 5 ? atoi(argv[5]) : 0); + } else if (!strcmp(cmd, "chan")) { + rc = gate_chan(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, + argc > 3 ? argv[3] : NULL); + } else if (!strcmp(cmd, "init")) { + rc = gate_init(argc > 2 ? argv[2] : NULL); + } 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 [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; + } + + mt_close(&dev); + return rc; +} diff --git a/src/mt7612u/tx.c b/src/mt7612u/tx.c new file mode 100644 index 00000000..58e10501 --- /dev/null +++ b/src/mt7612u/tx.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * TX path: build the 20-byte TXWI and the 4-byte TXINFO, then submit one bulk + * transfer. That is the whole of it - no firmware involvement, no per-station + * rate table. Ported from mt76/mt76x02_usb_core.c and mt76x02_mac.c. + * + * Wire layout (INVESTIGATION.md §7): + * [TXINFO 4B][TXWI 20B][802.11 hdr][pad][payload][pad to 4][4B zero] + */ +#include +#include "internal.h" + + +static void put_le16(uint8_t *p, uint16_t v) { p[0] = v & 0xff; p[1] = v >> 8; } +static void put_le32(uint8_t *p, uint32_t v) +{ + p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; + p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff; +} + +/* The 16-bit per-packet rate word. Everything the PHY needs is in here, and + * because MT_TXWI_FLAGS_TX_RATE_LUT is never set the MAC uses it verbatim. */ +uint16_t mt_tx_rate_word(const struct mt7612u_tx_rate *r) +{ + uint32_t idx, word; + + switch (r->phy) { + case MT7612U_PHY_VHT: + /* index is NSS-1 in bits 5:4, MCS in bits 3:0 */ + idx = FIELD_PREP(MT_RATE_VHT_IDX, r->mcs) | + FIELD_PREP(MT_RATE_VHT_NSS, (uint32_t)(r->nss ? r->nss - 1 : 0)); + break; + case MT7612U_PHY_HT: + case MT7612U_PHY_HT_GF: + /* HT folds NSS into the MCS number: nss = 1 + (idx >> 3) */ + idx = r->mcs; + break; + default: + idx = r->mcs; + break; + } + + word = FIELD_PREP(MT_RATE_INDEX, idx) | + FIELD_PREP(MT_RATE_PHY, (uint32_t)r->phy) | + FIELD_PREP(MT_RATE_BW, (uint32_t)r->bw); + if (r->sgi) word |= MT_RATE_SGI; + if (r->ldpc) word |= MT_RATE_LDPC; + /* mt76 only sets STBC at a single spatial stream, and so do we. */ + if (r->stbc && r->nss <= 1) word |= MT_RATE_STBC; + + return (uint16_t)word; +} + +/* + * Low-level submit. wcid selects a hardware rate-LUT entry (0xff = none) and + * use_rate_lut sets MT_TXWI_FLAGS_TX_RATE_LUT, which is the bit mt76 defines + * and never sets. Exposed so the Gate-G control can make the LUT and the + * descriptor disagree and see which one airs. + */ +/* + * ieee80211_hdrlen(), ported from the kernel's net/mac80211 helper - the same + * function mt76 reaches through ieee80211_get_hdrlen_from_skb() on both the + * TX (mt76_insert_hdr_pad) and RX (mt76x02_remove_hdr_pad) sides. + * + * Enumerating "the control frames that are 16 bytes" gets this wrong: the + * default for control frames is 16, and only CTS and ACK are 10. Listing + * RTS and PS-Poll as the 16-byte cases leaves BlockAckReq, BlockAck and both + * CF-End subtypes at 10, which inserts the L2 pad *inside* the frame. + */ +int mt_hdrlen_from_fc(const uint8_t *f) +{ + unsigned fc = (unsigned)f[0] | ((unsigned)f[1] << 8); + unsigned type = (fc >> 2) & 3; + int len = 24; + + if (type == 2) { /* data */ + if ((fc & 0x0300) == 0x0300) + len = 30; /* 4-address */ + if (fc & 0x0080) { /* QoS subtype bit */ + len += 2; /* QoS Control */ + if (fc & 0x8000) /* Order -> HT Control */ + len += 4; + } + return len; + } + if (type == 0) /* management */ + return (fc & 0x8000) ? 28 : 24; /* Order -> HT Control */ + if (type == 1) /* control */ + return ((fc & 0x00e0) == 0x00c0) ? 10 : 16; /* CTS, ACK */ + return len; +} + +/* mt76x02_mac_wcid_setup(): a station-table entry, needed before the + * hardware will treat frames as belonging to a peer. */ +void mt_wcid_setup(struct mt7612u_dev *d, uint8_t idx, const uint8_t *mac) +{ + uint8_t addr[8] = { 0 }; + + mt_wr(d, MT_WCID_ATTR(idx), 0); + if (mac) memcpy(addr, mac, 6); + if (idx < 128) mt_wr_copy(d, MT_WCID_ADDR(idx), addr, 8); +} + +/* + * Build one [TXINFO][TXWI][802.11][pad to 4] block into `buf`, returning its + * length. `next_vld` sets MT_TXD_INFO_NEXT_VLD, which tells the TXDMA another + * block follows in the same bulk transfer - the hook USB aggregation hangs on. + * mt76 never sets this bit; the packing in send_packets is the one thing here + * that is not a port. `trailer` appends the 4-byte zero terminator, which only + * the last block in a transfer carries. + */ +int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, + const void *frame, size_t len, + const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts, + int next_vld, int trailer) +{ + uint8_t *txwi = buf + 4; + uint32_t info; + int hdrlen, hdr_pad = 0, body, padded, pad, total, rc, n = 0; + const uint8_t *f = frame; + + if (len < 10 || len + 32 > bufsz) { ERR("bad frame length %zu", len); return -1; } + + /* mt76_insert_hdr_pad(): 2 bytes after the header when the 802.11 + * header is not a multiple of 4, so the body stays 4-aligned. The MAC + * strips it. A 3-address data header is 24 bytes, so normally none. */ + hdrlen = mt_hdrlen_from_fc(f); + if (hdrlen > (int)len) hdrlen = (int)len; + if (hdrlen % 4) hdr_pad = 2; + + memset(txwi, 0, MT_TXWI_LEN); + { + uint16_t fl = (opts & MT_TXOPT_RATE_LUT) ? MT_TXWI_FLAGS_TX_RATE_LUT : 0; + + if (opts & MT_TXOPT_AMPDU) + fl |= MT_TXWI_FLAGS_AMPDU | + FIELD_PREP(MT_TXWI_FLAGS_MPDU_DENSITY, 4); + put_le16(txwi + 0, fl); + } + /* "A frame may narrow below the channel but never widen it" was only + * ever a comment; enforce it here, where the tuned width is known. The + * BBP is configured for one width at a time, so a rate word asking for + * a wider frame than the channel was tuned to does not give a wider + * frame - it gives an unspecified one. Clamping rather than dropping + * keeps a caller that set its TX mode before its channel on the air, + * which is an ordering the C++ layer allows. Warn once: per frame would + * flood a TX loop. */ + if (d->chan && rate->bw > d->bw) { + struct mt7612u_tx_rate narrowed = *rate; + + if (!d->bw_clamp_warned) { + d->bw_clamp_warned = 1; + WARN("rate asks for bw %u on a bw %u channel; narrowing " + "to the channel width (further occurrences silent)", + rate->bw, d->bw); + } + narrowed.bw = d->bw; + put_le16(txwi + 2, mt_tx_rate_word(&narrowed)); + } else { + put_le16(txwi + 2, mt_tx_rate_word(rate)); /* rate */ + } + /* ack_ctl bit0 REQ: set it only when an ACK is wanted. Leaving it + * clear is how a frame becomes no-ACK, per packet. */ + txwi[4] = rate->no_ack ? 0 : MT_TXWI_ACK_CTL_REQ; + if (opts & MT_TXOPT_AMPDU) + txwi[4] |= FIELD_PREP(MT_TXWI_ACK_CTL_BA_WINDOW, 63); + txwi[5] = wcid; /* 0xff = none */ + put_le16(txwi + 6, (uint16_t)len); /* len_ctl: true MPDU len */ + put_le32(txwi + 8, 0); /* iv */ + put_le32(txwi + 12, 0); /* eiv */ + txwi[16] = 0; /* aid */ + /* nstreams > 1 and rev >= E4 -> 0x13, as mt76x02_mac_write_txwi(). */ + txwi[17] = ((d->chainmask & 0xf) > 1) ? 0x13 : 0; + /* mt76x02_mac_write_txwi(): the 4-bit trim is derived from the per-rate + * ceiling and the chain target power. rate->power_adj overrides it when + * the caller wants explicit control. */ + { + int8_t adj = rate->power_adj; + + if (!adj) { + int8_t max_adj = mt_tx_get_max_txpwr_adj(d, rate); + adj = mt_tx_get_txpwr_adj(d, d->txpower_conf, max_adj); + } + txwi[18] = FIELD_PREP(MT_TX_PWR_ADJ, (uint32_t)(adj & 0xf)); + } + txwi[19] = 0; /* pktid */ + + /* frame, with the header pad inserted if needed */ + memcpy(buf + 4 + MT_TXWI_LEN, f, (size_t)hdrlen); + if (hdr_pad) + memset(buf + 4 + MT_TXWI_LEN + hdrlen, 0, 2); + body = (int)len - hdrlen; + if (body > 0) + memcpy(buf + 4 + MT_TXWI_LEN + hdrlen + hdr_pad, f + hdrlen, (size_t)body); + + padded = (int)len + hdr_pad; + + info = FIELD_PREP(MT_TXD_INFO_LEN, (uint32_t)((MT_TXWI_LEN + padded + 3) & ~3)) | + FIELD_PREP(MT_TXD_INFO_DPORT, WLAN_PORT) | + FIELD_PREP(MT_TXD_INFO_QSEL, + (opts & MT_TXOPT_QSEL_MGMT) ? MT_QSEL_MGMT : MT_QSEL_EDCA) | + MT_TXD_INFO_80211 | + MT_TXD_INFO_WIV; /* no hardware IV insertion - unencrypted */ + if (next_vld) + info |= MT_TXD_INFO_NEXT_VLD; + put_le32(buf, info); + + { + int upto = 4 + MT_TXWI_LEN + padded; + + pad = ((upto + 3) & ~3) - upto + (trailer ? 4 : 0); + memset(buf + upto, 0, (size_t)pad); + total = upto + pad; + } + (void)rc; (void)n; + return total; +} + +int mt_tx_raw(struct mt7612u_dev *d, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate, uint8_t wcid, unsigned opts) +{ + uint8_t buf[MT_TX_BUF_MAX]; + int total = mt_tx_build(d, buf, sizeof buf, frame, len, rate, wcid, opts, 0, 1); + int rc, n = 0; + + if (total < 0) return -1; + + /* Async pool when one is running - it only blocks when every slot is in + * flight. Otherwise fall back to a synchronous transfer. */ + if (d->a) + return mt_async_tx_submit(d, buf, total); + + rc = mt_bulk(d, MT_EP_OUT_AC_BE, buf, total, &n, 500); + if (rc) { ERR("tx bulk out: %s", libusb_error_name(rc)); return -1; } + if (n != total) { ERR("tx short write %d/%d", n, total); return -1; } + return 0; +} + +int mt7612u_tx(struct mt7612u_dev *d, const void *frame, size_t len, + const struct mt7612u_tx_rate *rate) +{ + return mt_tx_raw(d, frame, len, rate, 0xff, 0); +} diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c new file mode 100644 index 00000000..88625df7 --- /dev/null +++ b/src/mt7612u/usb.c @@ -0,0 +1,599 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * libusb transport for MT7612U. Replaces mt76/usb.c's vendor-request and URB + * plumbing; the wire encoding is identical (verified against usbmon, see + * ../../INVESTIGATION.md §11). + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +/* See the LOG/WARN/ERR contract in internal.h. The whole line is formatted + * first and emitted with one fwrite + fflush: two stdio calls could interleave + * with a line from the libusb event thread, and an unflushed stderr can stall + * a piped consumer mid-bring-up. Truncation is silent and deliberate - a + * diagnostic is not worth a heap allocation on a path that may already be + * failing. */ +void mt_diag(char level, const char *fmt, ...) +{ + char line[512]; + int n; + va_list ap; + + n = snprintf(line, sizeof line, "devourer [%c] mt7612u: ", level); + if (n < 0 || (size_t)n >= sizeof line) + return; + va_start(ap, fmt); + n += vsnprintf(line + n, sizeof line - (size_t)n - 1, fmt, ap); + va_end(ap); + if (n < 0) + return; + /* vsnprintf returns what it WOULD have written, so clamp before using + * it as a length - otherwise a truncated line writes past the buffer. */ + if ((size_t)n > sizeof line - 2) + n = (int)(sizeof line - 2); + line[n++] = '\n'; + + fwrite(line, 1, (size_t)n, stderr); + fflush(stderr); +} + +#define REQ_IN (LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE) +#define REQ_OUT (LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE) +/* mt76's MT_VEND_REQ_TOUT_MS / MT_VEND_REQ_MAX_RETRY. The product of the two + * is the worst-case cost of one register access, so it bounds every poll + * loop below - which is why the timeout is 300 ms and not something longer. */ +#define CTRL_TIMEOUT_MS 300 +#define VEND_RETRIES 10 + +void mt_usleep(unsigned us) +{ + struct timespec ts = { .tv_sec = us / 1000000, .tv_nsec = (us % 1000000) * 1000 }; + nanosleep(&ts, NULL); +} + +/* Held for the process lifetime; flock releases it on any exit. */ +static int g_lock_fd = -1; + +static uint64_t now_us(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)(ts.tv_nsec / 1000); +} + +int mt_vendor_req(struct mt7612u_dev *d, uint8_t req, uint8_t type, + uint16_t val, uint16_t idx, void *buf, size_t len) +{ + int rc = LIBUSB_ERROR_OTHER; + + 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; + mt_usleep(5000); + } + ERR("vendor req %02x idx %04x failed: %s", req, idx, libusb_error_name(rc)); + return rc; +} + +/* Address bits 31:30 select the space, exactly as mt76's __mt76u_rr/wr do. */ +static uint8_t rd_req(uint32_t addr) +{ + if (addr & MT_VEND_TYPE_EEPROM) return MT_VEND_READ_EEPROM; + if (addr & MT_VEND_TYPE_CFG) return MT_VEND_READ_CFG; + return MT_VEND_MULTI_READ; +} + +static uint8_t wr_req(uint32_t addr) +{ + if (addr & MT_VEND_TYPE_CFG) return MT_VEND_WRITE_CFG; + return MT_VEND_MULTI_WRITE; +} + +/* + * A register read that reports failure separately from the value. This matters + * because 0xffffffff is a legitimate read on this part - MT_MAC_CSR0 returns + * it while the core is still coming up - so it cannot double as a sentinel. + * Returns 0 and fills *val on success, -1 on a transport failure. + */ +int mt_rr_chk(struct mt7612u_dev *d, uint32_t addr, uint32_t *val) +{ + uint8_t req = rd_req(addr), b[4] = { 0 }; + uint32_t a = addr & ~MT_VEND_TYPE_MASK; + + if (mt_vendor_req(d, req, REQ_IN, (uint16_t)(a >> 16), (uint16_t)a, + b, sizeof b) != (int)sizeof b) { + d->io_err++; + return -1; + } + *val = (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); + return 0; +} + +/* Convenience form for the places that genuinely cannot act on a failure + * (logging, one-shot identity reads). It still bumps d->io_err, so a caller + * that cares can notice after the fact. */ +uint32_t mt_rr(struct mt7612u_dev *d, uint32_t addr) +{ + uint32_t v; + + return mt_rr_chk(d, addr, &v) ? ~0u : v; +} + +void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val) +{ + uint8_t req = wr_req(addr), b[4]; + uint32_t a = addr & ~MT_VEND_TYPE_MASK; + + b[0] = val & 0xff; b[1] = (val >> 8) & 0xff; + b[2] = (val >> 16) & 0xff; b[3] = (val >> 24) & 0xff; + if (mt_vendor_req(d, req, REQ_OUT, (uint16_t)(a >> 16), (uint16_t)a, + b, sizeof b) != (int)sizeof b) + d->io_err++; + + /* Oracle-diff log: same shape decode.py renders from usbmon. */ + if (d->wrlog) + fprintf(d->wrlog, "req=0x%02x addr=0x%04x data=%02x%02x%02x%02x\n", + req, (unsigned)(a & 0xffff), b[0], b[1], b[2], b[3]); +} + +/* + * Read-modify-write. A failed read MUST NOT be written back: mt_rr's ~0u would + * turn the operation into "set every bit", and the addresses this is used on + * (MT_WLAN_FUN_CTRL, MT_MAC_SYS_CTRL, the BBP AGC block) are exactly the ones + * where that is destructive. Returns 0 on success, -1 if nothing was written. + */ +/* + * Register-I/O failures accumulate per device rather than being returned from + * every accessor. + * + * mt_wr() discarded mt_vendor_req()'s result entirely, so a write that + * exhausted its retries mid-bring-up left the hardware partly configured while + * the public call still returned success. Threading a status through every + * writer would touch several hundred call sites in an initialisation sequence + * that is deliberately a verbatim port of mt76's, and that churn would bury + * the thing it is meant to protect. + * + * So this follows the shape the reads already had - mt_rr_chk() has always + * bumped this same counter. Writes stay best-effort at the call site, and a + * SEQUENCE checks the accumulator at its boundary: mt_io_clear() on entry, + * mt_io_errors() on exit, and the whole setup fails if any access failed. + * Optional or diagnostic writes stay best-effort by not being bracketed. + */ +void mt_io_clear(struct mt7612u_dev *d) { d->io_err = 0; } +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 + * than at a sequence boundary. */ +int mt_wr_chk(struct mt7612u_dev *d, uint32_t addr, uint32_t val) +{ + unsigned before = d->io_err; + + mt_wr(d, addr, val); + return d->io_err == before ? 0 : -1; +} + +int mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val) +{ + uint32_t cur; + + if (mt_rr_chk(d, addr, &cur)) { + ERR("rmw 0x%05x skipped: read failed", addr & ~MT_VEND_TYPE_MASK); + return -1; + } + mt_wr(d, addr, (cur & ~mask) | val); + return 0; +} + +/* + * Poll against a real deadline rather than a count of sleeps. One register + * access can itself cost up to VEND_RETRIES * CTRL_TIMEOUT_MS, so counting + * iterations would let a caller asking for 200 ms block for seconds. + */ +int mt_poll(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, + uint32_t val, int timeout_us) +{ + uint64_t deadline = now_us() + (uint64_t)(timeout_us < 0 ? 0 : timeout_us); + + for (;;) { + uint32_t cur; + + if (mt_rr_chk(d, addr, &cur)) { + ERR("poll 0x%05x aborted: read failed", + addr & ~MT_VEND_TYPE_MASK); + return 0; + } + if ((cur & mask) == val) + return 1; + if (now_us() >= deadline) + return 0; + mt_usleep(1000); + } +} + +void mt_single_wr(struct mt7612u_dev *d, uint8_t req, uint16_t off, uint32_t val) +{ + if (mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val & 0xffff), off, NULL, 0) < 0) + d->io_err++; + if (mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val >> 16), + (uint16_t)(off + 2), NULL, 0) < 0) + d->io_err++; +} + +int mt_bulk(struct mt7612u_dev *d, uint8_t ep, void *buf, int len, + int *xfered, unsigned timeout_ms) +{ + int n = 0; + int rc = libusb_bulk_transfer(d->h, ep, (unsigned char *)buf, len, + &n, timeout_ms); + if (xfered) *xfered = n; + return rc; +} + +/* mt76x02_wait_for_mac(): MAC_CSR0 reads 0 or ~0 until the core is alive. + * Both are legitimate values here, which is why this uses the checked read - + * a transport failure is a different condition from "still coming up". */ +int mt_wait_for_mac(struct mt7612u_dev *d) +{ + for (int i = 0; i < 500; i++) { + uint32_t v; + + if (!mt_rr_chk(d, MT_MAC_CSR0, &v) && v != 0 && v != ~0u) + return 1; + mt_usleep(5000); + } + return 0; +} + +/* Seed the TX power and confirm this really is an MT7612 before anything + * writes to it. Shared by both open paths. */ +static int mt_identify(struct mt7612u_dev *d, const char **err) +{ + /* 0.5 dB units, as mt76's txpower_conf = power_level * 2. 20 dBm is a + * conservative seed; mt7612u_set_txpower() overrides it. */ + if (!d->txpower_conf) + d->txpower_conf = 40; + + d->rev = mt_rr(d, MT_ASIC_VERSION); + if ((d->rev >> 16) != 0x7612) { + if (err) *err = "not an MT7612 (unexpected MT_ASIC_VERSION)"; + return -1; + } + return 0; +} + +/* + * Adopt a handle the caller opened, reset and claimed interface 0 on. No + * 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. + */ +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; } + d->h = h; + d->ctx = ctx; + d->owns_handle = 0; + d->kernel_was_attached = 0; + return mt_identify(d, err); +} + +/* + * Open one MT7612U, honouring MT7612U_DEV when more than one is attached. + * + * libusb_open_device_with_vid_pid() returns whichever matching device + * enumerates first, which is fine with one adapter and silently ambiguous + * 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. + */ +/* + * Exclusive per-adapter lock - the same lock devourer's own UsbDeviceLock + * takes, deliberately byte-identical in key and path so the two contend. + * + * Two consumers on one MT7612U is not a race that resolves badly, it is the + * one failure neither driver recovers from: the loser blocks inside a USB + * ioctl in uninterruptible sleep, where SIGKILL does not reach it. + * src/UsbDeviceLock.h describes the same symptom in the same words, which is + * why this mirrors it rather than inventing a second scheme. + * + * That mirroring is the whole point. A lock file of our own would make + * `bringup` and `rxdemo` invisible to each other and reproduce the wedge + * across the two tools, which is exactly the case this is meant to stop: + * key bus + USB port path, e.g. "3-1.4", with UsbDeviceLock's + * "-a
" fallback when the backend reports no port path + * path "/tmp" + "/devourer-usb-" + key + ".lock" (UsbDeviceLock's default) + * flags O_CREAT|O_RDWR|O_NOFOLLOW, 0666, then flock(LOCK_EX|LOCK_NB) + * + * Fail-open vs fail-closed follows UsbDeviceLock too: genuine contention + * refuses, while an infrastructure failure (read-only tmpdir) warns and + * proceeds, so a quirky environment never bricks an otherwise-working open. + * + * flock is released by the kernel on process death however it arrives, so + * there are no stale locks to clean up - which matters precisely because + * these processes sometimes die badly. + */ +static void adapter_key(libusb_device *dev, char *out, size_t n) +{ + uint8_t ports[8]; + int np = libusb_get_port_numbers(dev, ports, sizeof ports); + int off = snprintf(out, n, "%u", libusb_get_bus_number(dev)); + + if (np <= 0) { + snprintf(out + off, n - (size_t)off, "-a%u", + libusb_get_device_address(dev)); + return; + } + for (int i = 0; i < np && off > 0 && (size_t)off < n; i++) + off += snprintf(out + off, n - (size_t)off, "%s%u", + i ? "." : "-", ports[i]); +} + +/* Returns a held fd, -1 to proceed unlocked (infrastructure failure), or + * -2 when another process holds the adapter and the caller must refuse. */ +static int lock_adapter(libusb_device *dev, const char **err) +{ + /* "/tmp" literally, and deliberately NOT getenv("TMPDIR"): the whole + * point of this lock is to contend with the C++ UsbDeviceLock, and that + * one takes its directory from DeviceConfig usb.lock_dir defaulting to + * "/tmp" (UsbDeviceLock.cpp:101) without ever consulting the + * environment. Reading TMPDIR here would put the two on different files + * whenever it is set, and the exclusion would lapse silently - which + * costs exactly what the comment below describes. A caller that sets a + * non-default usb.lock_dir is out of reach of this harness either way. */ + const char *dir = "/tmp"; + char key[64], path[256]; + int fd; + + adapter_key(dev, key, sizeof key); + snprintf(path, sizeof path, "%s/devourer-usb-%s.lock", dir, key); + + /* O_NOFOLLOW: the path is world-writable and predictable, so a symlink + * planted there must not redirect the open. UsbDeviceLock does the same. */ + fd = open(path, O_CREAT | O_RDWR | O_NOFOLLOW | O_CLOEXEC, 0666); + if (fd < 0) { + WARN("cannot open lock file %s (%s) - proceeding without " + "exclusivity", path, strerror(errno)); + return -1; + } + if (flock(fd, LOCK_EX | LOCK_NB) == 0) + return fd; + close(fd); + if (errno == EWOULDBLOCK || errno == EAGAIN) { + if (err) + *err = "adapter is already open in another process " + "(devourer or bringup); a second opener would wedge it " + "beyond SIGKILL"; + return -2; + } + WARN("cannot lock %s (%s) - proceeding without exclusivity", + path, strerror(errno)); + return -1; +} + +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); + int matches = 0; + + if (n < 0) { + if (err) *err = "libusb_get_device_list failed"; + return NULL; + } + + for (ssize_t i = 0; i < n; i++) { + struct libusb_device_descriptor desc; + uint8_t ports[8]; + char id[32]; + int np, off; + + if (libusb_get_device_descriptor(list[i], &desc)) + continue; + if (desc.idVendor != MT7612U_VID || desc.idProduct != MT7612U_PID) + continue; + + np = libusb_get_port_numbers(list[i], ports, sizeof ports); + off = snprintf(id, sizeof id, "%u", libusb_get_bus_number(list[i])); + for (int j = 0; j < np && off > 0 && off < (int)sizeof id; j++) + off += snprintf(id + off, sizeof id - (size_t)off, "%s%u", + j ? "." : "-", ports[j]); + + if (!sel || !*sel) { + LOG("MT7612U at %s%s", id, matches ? "" : " <- selected (first)"); + } else if (!strcmp(sel, id)) { + LOG("MT7612U at %s <- selected by MT7612U_DEV", id); + } else { + char idx[8]; + + snprintf(idx, sizeof idx, "%d", matches); + if (strcmp(sel, idx)) { matches++; continue; } + LOG("MT7612U at %s <- selected by MT7612U_DEV index %d", id, matches); + } + + if (!h) { + int lk = lock_adapter(list[i], err); + + if (lk == -2) { /* held by someone else */ + libusb_free_device_list(list, 1); + return NULL; + } + g_lock_fd = lk; + if (libusb_open(list[i], &h)) { + /* Only mt_close() releases the lock, and a failed + * mt_open() never reaches it - so holding it here + * made the process collide with its own stale lock + * on the very next retry. Release what this + * iteration took. */ + h = NULL; + if (g_lock_fd >= 0) { close(g_lock_fd); g_lock_fd = -1; } + } + } + matches++; + if (h && sel && *sel) + break; + } + + if (matches > 1 && (!sel || !*sel)) + WARN("%d MT7612U adapters attached and MT7612U_DEV is unset - " + "using the first. Set MT7612U_DEV= to be explicit.", + matches); + libusb_free_device_list(list, 1); + if (!h && err) + *err = matches ? "MT7612U found but could not be opened (try sudo)" + : "MT7612U not found"; + return h; +} + +int mt_open(struct mt7612u_dev *d, const char **err) +{ + int rc; + + if (libusb_init(&d->ctx)) { if (err) *err = "libusb_init failed"; return -1; } + d->owns_handle = 1; + + d->h = open_selected(d->ctx, err); + if (!d->h) { + libusb_exit(d->ctx); d->ctx = NULL; + return -1; + } + + d->kernel_was_attached = libusb_kernel_driver_active(d->h, 0) == 1; + if (d->kernel_was_attached) { + rc = libusb_detach_kernel_driver(d->h, 0); + if (rc) { + if (err) *err = "could not detach mt76x2u"; + goto fail; + } + LOG("detached kernel driver from interface 0"); + } + + /* A USB port reset before claiming. Without it the chip keeps whatever + * FCE/DMA state the previous run left behind, and the next firmware + * upload times out mid-chunk - reproducible after a few init cycles. + * devourer does the same thing on open for the same reason. */ + rc = libusb_reset_device(d->h); + if (rc == LIBUSB_ERROR_NOT_FOUND) { + /* Re-enumerated under a new address: reopen and re-detach. */ + libusb_close(d->h); + mt_usleep(200000); + d->h = open_selected(d->ctx, NULL); + if (!d->h) { + if (err) *err = "device vanished after USB reset"; + libusb_exit(d->ctx); d->ctx = NULL; + return -1; + } + if (libusb_kernel_driver_active(d->h, 0) == 1) + libusb_detach_kernel_driver(d->h, 0); + } else if (rc) { + WARN("USB reset returned %s", libusb_error_name(rc)); + } + + rc = libusb_claim_interface(d->h, 0); + if (rc) { + if (err) *err = "could not claim interface 0 (another process using it?)"; + goto fail; + } + + if (mt_identify(d, err)) { + libusb_release_interface(d->h, 0); + goto fail; + } + return 0; + +fail: + if (d->kernel_was_attached) + libusb_attach_kernel_driver(d->h, 0); + libusb_close(d->h); d->h = NULL; + libusb_exit(d->ctx); d->ctx = NULL; + return -1; +} + +void mt_close(struct mt7612u_dev *d) +{ + if (d->wrlog) { fclose(d->wrlog); d->wrlog = NULL; } + if (d->mculog) { fclose(d->mculog); d->mculog = NULL; } + if (d->transfers_stranded) { + /* Deliberately leaks the handle and context. Transfers submitted + * on them are still owned by libusb with no event thread left to + * complete them; releasing the interface or closing underneath + * that is undefined, and a leaked handle on a process that is + * already tearing down is the cheaper failure. */ + ERR("close: transfers still owned by libusb - leaking the USB " + "handle and context rather than closing underneath them"); + d->h = NULL; + d->ctx = NULL; + if (g_lock_fd >= 0) { close(g_lock_fd); g_lock_fd = -1; } + return; + } + if (d->h) { + if (d->owns_handle) { + libusb_release_interface(d->h, 0); + if (d->kernel_was_attached && !d->keep_detached) { + if (libusb_attach_kernel_driver(d->h, 0) == 0) + LOG("reattached kernel driver"); + } + libusb_close(d->h); + } + d->h = NULL; + } + if (d->ctx && d->owns_handle) libusb_exit(d->ctx); + d->ctx = NULL; + if (g_lock_fd >= 0) { close(g_lock_fd); g_lock_fd = -1; } +} + +/* Block write, as mt76u_copy(): one MULTI_WRITE per batch, wValue 0. + * The kernel uses this for the WCID address table (8 B) and the shared-key + * table (32 B) - 192 transfers that would otherwise be ~700 4-byte writes. */ +void mt_wr_copy(struct mt7612u_dev *d, uint32_t offset, const void *data, int len) +{ + const uint8_t *p = data; + uint8_t buf[64]; + + /* The hardware wants whole 32-bit words, but only `len` bytes belong to + * the caller. Round the *transfer* up and zero-fill the tail; rounding + * `len` up instead reads past the end of the caller's buffer. */ + for (int i = 0; i < len; ) { + int n = len - i, xfer; + + if (n > (int)sizeof buf) n = (int)sizeof buf; + xfer = (n + 3) & ~3; + memset(buf, 0, (size_t)xfer); + memcpy(buf, p + i, (size_t)n); + if (mt_vendor_req(d, MT_VEND_MULTI_WRITE, REQ_OUT, 0, + (uint16_t)(offset + i), buf, (size_t)xfer) < 0) + return; + i += n; + } +} + +/* --- public lifecycle helpers that belong with the transport --- */ + +void mt7612u_keep_detached(struct mt7612u_dev *d, int keep) +{ + if (d) d->keep_detached = keep; +} + +uint32_t mt7612u_asic_version(const struct mt7612u_dev *d) +{ + return d ? d->rev : 0; +} + +const uint8_t *mt7612u_mac_addr(const struct mt7612u_dev *d) +{ + return d ? d->macaddr : NULL; +} diff --git a/tools/extract_mt7612u_tables.py b/tools/extract_mt7612u_tables.py new file mode 100755 index 00000000..bac92b4b --- /dev/null +++ b/tools/extract_mt7612u_tables.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Extract the pinned MT7612U MAC init-value table. + +mt76 writes these as symbolic register names plus four FIELD_PREP macros, so a +transcription is a chance to typo a register address into a plausible-looking +one. This resolves the names against mt76x02_regs.h and evaluates the macros, +so the checked-in header is mechanical rather than hand-copied. + +Unlike the Realtek extractors here, the source is C initialiser syntax rather +than a vendor parameter blob, so the symbol table and the small expression +evaluator below are the bulk of the work. Both are deliberately narrow: they +understand only the constructs these definitions actually use, and raise on +anything else instead of guessing. +""" + +from __future__ import annotations + +import argparse +import hashlib +import re +from pathlib import Path + +UPSTREAM = "openwrt/mt76 commit be5ce79" +DEFAULT_ROOT = "reference/mt76" +SUBMODULE_HINT = ( + f"{DEFAULT_ROOT} is a pinned git submodule ({UPSTREAM}); fetch it with\n" + f" git submodule update --init {DEFAULT_ROOT}" +) +OUTPUT_H = "src/mt7612u/initvals.h" + +SOURCES = { + "init": ( + "mt76x2/init.c", + "d356f6d90cb2171272a4885fb4d5f4e9a607f06ec084babb9c5025b8f3f83e77", + ), + "regs": ( + "mt76x02_regs.h", + "a75b36645b29e69db0627d9a16f776cad0e4000b6944a31183ef644d0398aefb", + ), +} + +# The two arrays mt76_write_mac_initvals() writes, in the order it writes them. +ARRAYS = ("vals", "prot_vals") + +# Expected shape of the result, so a silent parse regression cannot pass. +EXPECTED_COUNT = 60 +EXPECTED_SHA256 = "ba381217626e876b27a2aa9150440ff7c6af86740a3c64afbb36e81347df3719" + + +# --- a very small C constant-expression evaluator ------------------------- + +_DEFINE = re.compile(r"^\s*#\s*define\s+(MT_[A-Za-z0-9_]+)\s+(.+?)\s*(?:/\*.*)?$") + + +def _strip_comments(text: str) -> str: + text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S) + return re.sub(r"//[^\n]*", " ", text) + + +def load_symbols(regs_text: str) -> dict[str, str]: + """Object-like MT_* defines only. Function-like ones (MT_BBP(x, y)) are + not constants and are never referenced by the tables below.""" + symbols: dict[str, str] = {} + text = regs_text.replace("\\\n", " ") + for line in text.splitlines(): + match = _DEFINE.match(line) + if not match: + continue + name, body = match.group(1), match.group(2).strip() + if name.endswith("(") or "(" in line.split(name, 1)[0]: + continue + symbols[name] = body + return symbols + + +def _ffs(mask: int) -> int: + if mask == 0: + raise SystemExit("FIELD_PREP with a zero mask") + return (mask & -mask).bit_length() - 1 + + +def _split_args(text: str) -> list[str]: + args, depth, start = [], 0, 0 + for index, char in enumerate(text): + if char == "(": + depth += 1 + elif char == ")": + depth -= 1 + elif char == "," and depth == 0: + args.append(text[start:index]) + start = index + 1 + args.append(text[start:]) + return [arg.strip() for arg in args] + + +def _match_call(expr: str, name: str) -> tuple[str, int] | None: + """If expr starts with name(...), return its argument text and the index + just past the closing parenthesis.""" + if not expr.startswith(name + "("): + return None + depth, index = 0, len(name) + while index < len(expr): + if expr[index] == "(": + depth += 1 + elif expr[index] == ")": + depth -= 1 + if depth == 0: + return expr[len(name) + 1:index], index + 1 + index += 1 + raise SystemExit(f"unbalanced parentheses in {expr!r}") + + +def evaluate(expr: str, symbols: dict[str, str], depth: int = 0) -> int: + """Evaluate one C constant expression built from the constructs these + tables use: integer literals, MT_* symbols, BIT, GENMASK, FIELD_PREP, + parentheses and | + << & ~. Anything else is an error, not a guess.""" + if depth > 32: + raise SystemExit(f"macro recursion too deep at {expr!r}") + + expr = _strip_comments(expr).strip() + while expr.endswith(","): + expr = expr[:-1].strip() + + # Rewrite the calls we understand into plain Python, innermost first. + out, index = [], 0 + while index < len(expr): + rest = expr[index:] + for name in ("FIELD_PREP", "GENMASK", "BIT"): + call = _match_call(rest, name) + if call is None: + continue + args_text, consumed = call + args = [evaluate(a, symbols, depth + 1) for a in _split_args(args_text)] + if name == "BIT": + value = 1 << args[0] + elif name == "GENMASK": + high, low = args + value = ((1 << (high - low + 1)) - 1) << low + else: + mask, field = args + value = (field << _ffs(mask)) & mask + out.append(f"({value})") + index += consumed + break + else: + # Numbers first: "0x0400" would otherwise tokenise as 0 followed + # by an identifier "x0400". + number = re.match(r"0[xX][0-9a-fA-F]+[uUlL]*|\d+[uUlL]*", rest) + if number: + literal = number.group(0).rstrip("uUlL") + out.append(f"({int(literal, 0)})") + index += len(number.group(0)) + continue + match = re.match(r"[A-Za-z_][A-Za-z0-9_]*", rest) + if match: + name = match.group(0) + if name not in symbols: + raise SystemExit(f"unknown symbol {name!r} in {expr!r}") + out.append(f"({evaluate(symbols[name], symbols, depth + 1)})") + index += len(name) + else: + out.append(rest[0]) + index += 1 + + python = "".join(out) + python = re.sub(r"\b(0[xX][0-9a-fA-F]+|\d+)[uUlL]+\b", r"\1", python) + if not re.fullmatch(r"[0-9xXa-fA-F()|+\-*<>&~^\s]*", python): + raise SystemExit(f"refusing to evaluate {expr!r} -> {python!r}") + try: + return int(eval(python, {"__builtins__": {}}, {})) & 0xFFFFFFFF + except Exception as exc: # noqa: BLE001 - report the expression, not a trace + raise SystemExit(f"could not evaluate {expr!r}: {exc}") from exc + + +# --- extraction ----------------------------------------------------------- + +def local_macros(function_text: str) -> dict[str, str]: + """The DEFAULT_PROT_CFG_* macros are defined inside the function and are + not visible in the register header.""" + macros = {} + text = function_text.replace("\\\n", " ") + for match in re.finditer(r"^\s*#\s*define\s+(DEFAULT_\w+)\s+(.+)$", text, re.M): + macros[match.group(1)] = match.group(2).strip() + return macros + + +def extract_array(function_text: str, name: str) -> list[tuple[str, str]]: + match = re.search( + rf"\b{name}\[\]\s*=\s*\{{(.*?)\n\t\}};", function_text, re.S + ) + if not match: + raise SystemExit(f"could not find {name}[] in mt76_write_mac_initvals()") + body = _strip_comments(match.group(1)) + pairs = re.findall(r"\{\s*([^,]+?)\s*,\s*([^}]+?)\s*\}", body) + if not pairs: + raise SystemExit(f"{name}[] parsed to zero entries") + return [(reg.strip(), val.strip()) for reg, val in pairs] + + +def load(source_root: Path) -> list[tuple[int, int, str]]: + texts = {} + for key, (relative, expected_hash) in SOURCES.items(): + path = source_root / relative + if not path.exists(): + raise SystemExit(f"missing {path}\n{SUBMODULE_HINT}") + source = path.read_bytes() + actual_hash = hashlib.sha256(source).hexdigest() + if actual_hash != expected_hash: + raise SystemExit( + f"unexpected {relative} SHA-256: {actual_hash}; " + f"expected {expected_hash}" + ) + texts[key] = source.decode("utf-8") + + function = re.search( + r"void mt76_write_mac_initvals\(.*?\n\}", texts["init"], re.S + ) + if not function: + raise SystemExit("mt76_write_mac_initvals() not found in mt76x2/init.c") + function_text = function.group(0) + + symbols = load_symbols(texts["regs"]) + symbols.update(local_macros(function_text)) + + rows: list[tuple[int, int, str]] = [] + for array in ARRAYS: + for reg_expr, val_expr in extract_array(function_text, array): + reg = evaluate(reg_expr, symbols) + val = evaluate(val_expr, symbols) + # Keep the symbolic name as a comment when there is one, so the + # generated file stays readable against the register header. + label = reg_expr if reg_expr.startswith("MT_") else "" + rows.append((reg, val, label)) + return rows + + +def table_hash(rows: list[tuple[int, int, str]]) -> str: + digest = hashlib.sha256() + for reg, val, _ in rows: + digest.update(reg.to_bytes(4, "little")) + digest.update(val.to_bytes(4, "little")) + return digest.hexdigest() + + +def render_h(rows: list[tuple[int, int, str]]) -> str: + lines = [ + "/* SPDX-License-Identifier: BSD-3-Clause-Clear */\n" + "/* GENERATED - do not hand-edit.\n" + f" * Source: {UPSTREAM}, mt76_write_mac_initvals() in mt76x2/init.c,\n" + " * with register names and the DEFAULT_PROT_CFG_* macros resolved\n" + " * against mt76x02_regs.h.\n" + " * Regenerate: tools/extract_mt7612u_tables.py\n" + " * Verify: tools/extract_mt7612u_tables.py --check\n" + f" * Table SHA-256 (LE reg,val stream): {table_hash(rows)}\n" + " */\n" + "#ifndef MT7612U_INITVALS_H\n" + "#define MT7612U_INITVALS_H\n" + "\n" + "static const struct { uint32_t reg; uint32_t val; } " + "mt7612u_mac_initvals[] = {\n" + ] + for reg, val, label in rows: + comment = f" /* {label} */" if label else "" + lines.append(f"\t{{ 0x{reg:04x}, 0x{val:08x} }},{comment}\n") + lines.append("};\n#endif\n") + return "".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-root", type=Path) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + root = Path(__file__).resolve().parent.parent + source_root = args.source_root or root / DEFAULT_ROOT + rows = load(source_root) + + actual_hash = table_hash(rows) + if len(rows) != EXPECTED_COUNT or actual_hash != EXPECTED_SHA256: + raise SystemExit( + f"unexpected table: count={len(rows)} sha256={actual_hash}; " + f"expected count={EXPECTED_COUNT} sha256={EXPECTED_SHA256}" + ) + + output = render_h(rows) + # Explicit encoding + newline="": the generated file is a byte-defined + # artifact, so --check must compare identically on every platform. + if args.check: + path = root / OUTPUT_H + # NOT Path.read_text(newline=""): that keyword only exists on Python + # 3.13+, and this script has to run on whatever the CI image ships + # (3.12 today). open() has taken `newline` since forever. + if not path.exists(): + existing = None + else: + with open(path, encoding="utf-8", newline="") as fh: + existing = fh.read() + if existing != output: + raise SystemExit(f"stale generated output: {OUTPUT_H}") + verb = "verified" + else: + (root / OUTPUT_H).write_text(output, encoding="utf-8", newline="") + verb = "wrote" + print(f"{verb} {OUTPUT_H}: {len(rows)} register writes") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())