From 36d45ceaf390482d52eccd497e325c151dfe852b Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 15:03:28 +0200 Subject: [PATCH 01/12] mt7612u: a MediaTek backend, measured but not wired in ## Problem Every backend here is Realtek. A consumer scoping a fixed-channel, fixed-rate video link had an MT7612U on the bench and no way to drive it from this library. Whether MediaTek silicon belongs in this project at all is a scope question for the maintainers, and one worth answering against measurements rather than a proposal. ## Change Adds `src/mt7612u/`, a standalone C library plus a per-gate bringup harness, and `docs/mt7612u.md` carrying the measurements, the methods and the limits. `CMakeLists.txt` is untouched. There is no `IRtlDevice` implementation and no `WiFiDriver` dispatch, so nothing in the shipped library changes and CI sees a docs-and-new-directory diff. The subtree builds on its own with `make -C src/mt7612u`. What is measured on hardware, against an RTL8812AU witness running this project's own rxdemo/txdemo: - TX rate is authoritative: three rates match exactly, per-frame alternation is 229/229, and the hardware rate LUT cannot override the descriptor even with MT_TXWI_FLAGS_TX_RATE_LUT set. - TX power: all nine registers identical to what the kernel driver programs for the same channel. - A-MPDU works on injected frames (paggr 0/352 control vs 326/326 armed), lifting 34.03 to 44.55 Mbit/s at 1400 bytes and 7.01 to 15.50 at 200. This is the opposite of the Jaguar1 result in docs/aggregation.md. - Hardware ACK responder: 0 frames at the stimulus radio unarmed, 3500+ ACKs armed. - 40 MHz, TSF, monitor RX with per-chain RSSI, radiotap send_packet and send_packets with USB chaining via MT_TXD_INFO_NEXT_VLD. What it cannot do, measured: channel switch is 526 ms full / 48 ms with calibration skipped against 0.5-2.5 ms on the Realtek parts, because the RF plane lives behind the MCU. FHSS is out of reach. Narrowband 5/10 MHz has no encoding in the rate word. docs/mt7612u.md carries a Counterparts section: one unit, one witness generation, TX power verified against registers rather than radiated power, an RX gain path that has never done anything because this EEPROM has no calibration, no cold-boot test, and nothing in CI. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 257 ++++++ src/mt7612u/.gitignore | 4 + src/mt7612u/Makefile | 21 + src/mt7612u/README.md | 65 ++ src/mt7612u/async.c | 213 +++++ src/mt7612u/caps.c | 107 +++ src/mt7612u/eeprom.c | 276 ++++++ src/mt7612u/fw.c | 237 +++++ src/mt7612u/include/mt7612u/mt7612u.h | 171 ++++ src/mt7612u/init.c | 390 ++++++++ src/mt7612u/initvals.h | 70 ++ src/mt7612u/internal.h | 184 ++++ src/mt7612u/mcu.c | 164 ++++ src/mt7612u/phy.c | 435 +++++++++ src/mt7612u/radiotap.c | 235 +++++ src/mt7612u/regs.h | 385 ++++++++ src/mt7612u/rx.c | 111 +++ src/mt7612u/tools/bringup.c | 1181 +++++++++++++++++++++++++ src/mt7612u/tx.c | 211 +++++ src/mt7612u/usb.c | 232 +++++ 20 files changed, 4949 insertions(+) create mode 100644 docs/mt7612u.md create mode 100644 src/mt7612u/.gitignore create mode 100644 src/mt7612u/Makefile create mode 100644 src/mt7612u/README.md create mode 100644 src/mt7612u/async.c create mode 100644 src/mt7612u/caps.c create mode 100644 src/mt7612u/eeprom.c create mode 100644 src/mt7612u/fw.c create mode 100644 src/mt7612u/include/mt7612u/mt7612u.h create mode 100644 src/mt7612u/init.c create mode 100644 src/mt7612u/initvals.h create mode 100644 src/mt7612u/internal.h create mode 100644 src/mt7612u/mcu.c create mode 100644 src/mt7612u/phy.c create mode 100644 src/mt7612u/radiotap.c create mode 100644 src/mt7612u/regs.h create mode 100644 src/mt7612u/rx.c create mode 100644 src/mt7612u/tools/bringup.c create mode 100644 src/mt7612u/tx.c create mode 100644 src/mt7612u/usb.c diff --git a/docs/mt7612u.md b/docs/mt7612u.md new file mode 100644 index 0000000..620c288 --- /dev/null +++ b/docs/mt7612u.md @@ -0,0 +1,257 @@ +# 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 `## 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. + +## 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. + +## 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.** No `ctest` integration, 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. + +## 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/src/mt7612u/.gitignore b/src/mt7612u/.gitignore new file mode 100644 index 0000000..7310ca1 --- /dev/null +++ b/src/mt7612u/.gitignore @@ -0,0 +1,4 @@ +bringup +*.o +*.d +firmware/ diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile new file mode 100644 index 0000000..8258b68 --- /dev/null +++ b/src/mt7612u/Makefile @@ -0,0 +1,21 @@ +# Standalone. Not reachable from the project's CMakeLists.txt. +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 +SRCS = $(wildcard *.c) tools/bringup.c +OBJS = $(SRCS:.c=.o) +DEPS = $(OBJS:.o=.d) + +all: bringup +bringup: $(OBJS) + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) +%.o: %.c + $(CC) $(CFLAGS) $(DEPFLAGS) -I. -c -o $@ $< +clean: + rm -f $(OBJS) $(DEPS) bringup +-include $(DEPS) +.PHONY: all clean diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md new file mode 100644 index 0000000..840340f --- /dev/null +++ b/src/mt7612u/README.md @@ -0,0 +1,65 @@ +# src/mt7612u — MediaTek MT7612U, standalone + +**Not built by `CMakeLists.txt`.** This subtree does not implement +`IRtlDevice`, is not reachable from `WiFiDriver`, and adds nothing to the +library target. It builds on its own: + +```sh +make -C src/mt7612u # -> src/mt7612u/bringup +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 | + +## 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 +``` + +## 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. + +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 0000000..9e94936 --- /dev/null +++ b/src/mt7612u/async.c @@ -0,0 +1,213 @@ +/* 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" + +static void *evt_thread(void *arg) +{ + struct mt7612u_dev *d = arg; + struct timeval tv = { .tv_sec = 0, .tv_usec = 50000 }; + + while (d->a && d->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 = d->a; + + 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->rx_frames++; + if (a->cb) + a->cb(a->cb_user, frame, (size_t)len, &info); + } + } else if (t->status != LIBUSB_TRANSFER_CANCELLED) { + a->rx_err++; + } + + if (a->rx_active && t->status != LIBUSB_TRANSFER_CANCELLED) { + if (libusb_submit_transfer(t)) + a->rx_err++; + } else { + a->rx_inflight--; + } +} + +static void LIBUSB_CALL tx_done(struct libusb_transfer *t) +{ + struct mt_slot *s = t->user_data; + struct mt_async *a = s->d->a; + + if (t->status == LIBUSB_TRANSFER_COMPLETED && + t->actual_length == t->length) + a->tx_done_n++; + else + a->tx_err++; + + pthread_mutex_lock(&a->tx_lock); + a->tx_busy[s->idx] = 0; + a->tx_inflight--; + pthread_cond_signal(&a->tx_cv); + pthread_mutex_unlock(&a->tx_lock); +} + +int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) +{ + 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->tx_lock, NULL); + pthread_cond_init(&a->tx_cv, NULL); + + for (int i = 0; i < MT_TX_RING; i++) { + a->tx_slot[i].d = d; + 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].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; } + + if (cb) { + a->rx_active = 1; + 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; + } + a->rx_inflight++; + } + 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; +} + +void mt_async_stop(struct mt7612u_dev *d) +{ + struct mt_async *a = d->a; + + if (!a) return; + + a->rx_active = 0; + for (int i = 0; i < MT_RX_RING; i++) + if (a->rx[i]) libusb_cancel_transfer(a->rx[i]); + + /* Let the in-flight TX drain before tearing the event thread down. */ + pthread_mutex_lock(&a->tx_lock); + for (int spins = 0; a->tx_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->tx_cv, &a->tx_lock, &ts); + } + pthread_mutex_unlock(&a->tx_lock); + + for (int spins = 0; a->rx_inflight && spins < 100; spins++) + mt_usleep(10000); + + a->running = 0; + pthread_join(a->evt, NULL); + + 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->tx_lock); + pthread_cond_destroy(&a->tx_cv); + free(a); + d->a = NULL; +} + +/* + * 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; + + if (!a || len > MT_TX_BUFSZ) return -1; + + pthread_mutex_lock(&a->tx_lock); + for (;;) { + 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->tx_cv, &a->tx_lock); + } + a->tx_busy[idx] = 1; + a->tx_inflight++; + pthread_mutex_unlock(&a->tx_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); + if (libusb_submit_transfer(a->tx[idx])) { + pthread_mutex_lock(&a->tx_lock); + a->tx_busy[idx] = 0; + a->tx_inflight--; + pthread_mutex_unlock(&a->tx_lock); + a->tx_err++; + return -1; + } + a->tx_submitted++; + return 0; +} + +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; +} diff --git a/src/mt7612u/caps.c b/src/mt7612u/caps.c new file mode 100644 index 0000000..1d053f0 --- /dev/null +++ b/src/mt7612u/caps.c @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* TSF access and the static capability descriptor. */ +#include +#include "internal.h" + +/* + * DW0 is the LOW word. mt76's mt76x02u_restart_pre_tbtt_timer() assembles this + * as (dw0 << 32) | dw1, which is backwards - but the result there only ever + * feeds a dev_dbg() print, so the bug is never exercised and has survived. + * Copying it produced a clock that advanced by 8.6e14 "us" per 200 ms. + * Measured: (DW1 << 32) | DW0 gives 200159 us over a 200000 us sleep. + */ +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); + /* 80 MHz is silicon-capable but the width maths is not ported yet; + * advertise what this driver actually delivers, not what the part could. */ + c->bw_mask = 0x3; /* 20 and 40 MHz */ + 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. */ + { + 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 0000000..c3d61b1 --- /dev/null +++ b/src/mt7612u/eeprom.c @@ -0,0 +1,276 @@ +/* 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 here: unprogrammed EEPROM + * cells legitimately read as all-ones (this image is 0xff from 0x010 + * onward for a stretch). Validate the image below instead. */ + 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_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); + + 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 0000000..3e58bc5 --- /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 0000000..c5c7956 --- /dev/null +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -0,0 +1,171 @@ +/* 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 */ + uint8_t n_chains; + uint16_t mpdu_len; + uint16_t seq; +}; + +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); +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. 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. + */ +int mt7612u_set_channel(struct mt7612u_dev *dev, unsigned 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); + +int mt7612u_start(struct mt7612u_dev *dev); /* enable MAC TX+RX */ +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 excludes the RXWI. + * 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); + +/* + * 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); + +/* 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 */ + 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 0000000..06e6e99 --- /dev/null +++ b/src/mt7612u/init.c @@ -0,0 +1,390 @@ +/* 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 PLAN.md: trimming this sequence is a post-Gate-E activity, because a + * 95%-correct init answers every register read and still radiates nothing. + */ +#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)) + LOG("warning: 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); + mt_wr(d, MT_MAX_LEN_CFG, 0x2f00); + + 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)) + LOG("warning: 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) +{ + 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)) + LOG("warning: 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); + + return mt_mac_stop(d); +} diff --git a/src/mt7612u/initvals.h b/src/mt7612u/initvals.h new file mode 100644 index 0000000..c2d7355 --- /dev/null +++ b/src/mt7612u/initvals.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* GENERATED from openwrt/mt76 mt76x2/init.c mt76_write_mac_initvals() + * plus mt76x02_regs.h. Do not hand-edit: regenerate with the script in + * ../../PLAN.md Stage C if the reference tree moves. */ +#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 0000000..c695610 --- /dev/null +++ b/src/mt7612u/internal.h @@ -0,0 +1,184 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +#ifndef MT7612U_INTERNAL_H +#define MT7612U_INTERNAL_H + +#include +#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 +#define MT_TX_RING 32 +#define MT_RX_BUFSZ 4096 +#define MT_TX_BUFSZ 2048 +#define MT_USB_AGG_BUF 16384 /* one aggregated bulk-OUT transfer */ +#define MT_USB_AGG_MAX 32 /* frames chained per transfer */ + +struct mt7612u_dev; +struct mt_slot { struct mt7612u_dev *d; 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]; + pthread_mutex_t tx_lock; + pthread_cond_t tx_cv; + pthread_t evt; + volatile int running, rx_active; + volatile 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; +}; + +struct mt7612u_dev { + libusb_context *ctx; + libusb_device_handle *h; + int kernel_was_attached; + int keep_detached; + + 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; + 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; + + /* 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); +void mt_close(struct mt7612u_dev *d); +uint32_t mt_rr(struct mt7612u_dev *d, uint32_t addr); +void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val); +void mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val); +#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); +#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 --- */ +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); + +#define LOG(...) do { fprintf(stderr, "[mt7612u] " __VA_ARGS__); fputc('\n', stderr); } while (0) +#define ERR(...) do { fprintf(stderr, "[mt7612u] ERROR " __VA_ARGS__); fputc('\n', stderr); } while (0) + +#endif diff --git a/src/mt7612u/mcu.c b/src/mt7612u/mcu.c new file mode 100644 index 0000000..c81679a --- /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 0000000..015ca37 --- /dev/null +++ b/src/mt7612u/phy.c @@ -0,0 +1,435 @@ +/* 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); +} + +/* 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 (bw == 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. */ + int sec_above = (chan / 4) & 1; + + if (sec_above) { bw_index = 1; ch_group_index = 0; } + else { bw_index = 3; ch_group_index = 1; } + hw_chan = (uint8_t)(chan + 2 - ch_group_index * 4); + } else if (bw != MT7612U_BW_20) { + ERR("only 20 and 40 MHz are implemented"); + return -1; + } + + d->cal.channel_cal_done = fast; + d->chan = chan; + d->bw = bw; + + 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]); + + if (mt_mcu_set_channel(d, hw_chan, bw, bw_index, 0)) + return -1; + if (mt_mcu_init_gain(d, hw_chan, d->cal.mcu_gain, 1)) + return -1; + + /* rev >= E3: enable LDPC Rx */ + mt_set(d, MT_BBP(RXO, 13), BIT(10)); + + 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; + } + 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; +} diff --git a/src/mt7612u/radiotap.c b/src/mt7612u/radiotap.c new file mode 100644 index 0000000..5d2b44a --- /dev/null +++ b/src/mt7612u/radiotap.c @@ -0,0 +1,235 @@ +/* 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 + +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; + } +} + +/* + * 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); + if (off + size > rlen) + return (int)rlen; + 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: + r->power_adj = 0; /* absolute dBm is a device-level knob */ + 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)); + if ((known & 0x02) && ((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 = bwc == 0 ? MT7612U_BW_20 + : (bwc <= 3 ? MT7612U_BW_40 : MT7612U_BW_80); + 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; + size_t idx[MT_USB_AGG_MAX]; + + /* Pass 1: pick the frames that fit in one transfer. */ + while (i < count && n_in_buf < MT_USB_AGG_MAX) { + size_t need; + + if (!pkts[i].data || pkts[i].len < 8) { i++; continue; } + need = pkts[i].len + 32; + if (off + need + 4 > sizeof buf) break; + idx[n_in_buf++] = i; + off += need; /* upper bound; pass 2 uses the real size */ + i++; + } + if (!n_in_buf) break; + + /* Pass 2: build them back to back. 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++) { + struct mt7612u_tx_rate r; + const uint8_t *p = pkts[idx[j]].data; + size_t plen = pkts[idx[j]].len; + int last = (j + 1 == n_in_buf); + int rlen = mt_radiotap_parse(p, plen, &r); + int blk; + + if (rlen <= 0 || (size_t)rlen >= plen) continue; + blk = mt_tx_build(d, buf + off, sizeof buf - off, + p + rlen, plen - (size_t)rlen, &r, 0xff, 0, + !last, last); + if (blk < 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 0000000..4af8611 --- /dev/null +++ b/src/mt7612u/regs.h @@ -0,0 +1,385 @@ +/* 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)))) +/* Lowest set bit of a contiguous mask, for FIELD_PREP/GET. */ +#define _SHIFT(m) (__builtin_ctz(m)) +#define FIELD_PREP(m, v) (((uint32_t)(v) << _SHIFT(m)) & (m)) +#define FIELD_GET(m, v) (((uint32_t)(v) & (m)) >> _SHIFT(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) +#define MT_RX_STAT_1 0x1704 +#define MT_RX_STAT_1_CCA_ERRORS GENMASK(15, 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 0000000..51ef9c1 --- /dev/null +++ b/src/mt7612u/rx.c @@ -0,0 +1,111 @@ +/* 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. */ +static void 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); + 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; + } +} + +/* + * 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); + decode_rate(get_le16(rxwi + 10), info); + + /* 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); + + if (rxinfo & MT_RXINFO_L2PAD) + pad = 2; + + 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 by moving the header down over it. */ + if (pad && len > 24) { + memmove(buf + MT_DMA_HDR_LEN + MT_RXWI_LEN + 2, + buf + MT_DMA_HDR_LEN + MT_RXWI_LEN, 24); + *frame = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN + 2; + } + 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/tools/bringup.c b/src/mt7612u/tools/bringup.c new file mode 100644 index 0000000..567b71b --- /dev/null +++ b/src/mt7612u/tools/bringup.c @@ -0,0 +1,1181 @@ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ +/* + * MT7612U bringup harness. One subcommand per gate from PLAN.md, so each + * stage is independently runnable on hardware. + */ +#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; +} + +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, NULL)) { + 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, NULL)) { + 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, + }; + const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + 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 const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + 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, as PLAN.md actually specified it: + * 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; + + 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); + mt7612u_tx(&dev, frame, 40, (i & 1) ? &mcs7 : &mcs0); + mt_usleep(2000); + } + printf(" sent %d frames, even index = MCS0, odd = MCS7\n", 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++) { + for (int i = 0; i < 150; i++) { + frame[36] = arm ? 'B' : 'A'; + frame[37] = (uint8_t)i; + frame[38] = 0; + mt_tx_raw(&dev, frame, 40, &mcs7, 1, arm); + mt_usleep(2000); + } + printf(" arm %c: wcid=1, TX_RATE_LUT flag %s -> 150 frames\n", + arm ? 'B' : 'A', arm ? "SET" : "clear"); + } + + mt_mac_stop(&dev); + 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. */ +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); + printf(" submitted=%llu completed=%llu errors=%llu\n", + (unsigned long long)dev.a->tx_submitted, + (unsigned long long)dev.a->tx_done_n, + (unsigned long long)dev.a->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; +} + +struct arx_ctx { unsigned long n; 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; + c->n++; + c->by_phy[info->phy & 7]++; +} + +/* Async RX ring: the callback path StartRxLoop needs. */ +static int gate_arx(uint8_t chan, int secs) +{ + static const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + 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(); + while (now_ms() - t0 < secs * 1000.0) + mt_usleep(100000); + printf("async RX on ch%u for %d s: %lu frames (%.0f/s), rx_err=%llu\n", + chan, secs, ctx.n, ctx.n / (double)secs, + (unsigned long long)dev.a->rx_err); + for (int i = 0; i < 5; 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 waybeam-link uses. */ +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); + 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)dev.a->tx_err, + (unsigned long long)dev.a->rx_err); + mt7612u_rx_stop(&dev); + mt_mac_stop(&dev); + return (n && ctx.n) ? 0 : 1; +} + +/* 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); + } + printf(" tx_err=%llu\n", (unsigned long long)dev.a->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; +} + +/* Capability descriptor, TSF and 40 MHz. */ +static int gate_caps(uint8_t chan) +{ + struct mt7612u_caps c; + 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; + if (mt_mac_start(&dev, 1)) 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("\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_mac_stop(&dev); + 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. + */ +struct ack_ctx { 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) { c->other++; return; } + c->to_us++; + if (f[1] & 0x08) c->retry_to_us++; /* FC Retry bit */ +} + +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; + if (mt_mac_start(&dev, 1)) 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"); + 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"); + } + + if (mt7612u_rx_start(&dev, ack_cb, &off)) return 1; + printf("listening %d s ...\n", secs); + mt_usleep((unsigned)secs * 1000000u); + 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; +} + +/* + * 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; + + 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, "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"); + rc = 2; + } + + mt_close(&dev); + return rc; +} diff --git a/src/mt7612u/tx.c b/src/mt7612u/tx.c new file mode 100644 index 0000000..907d26a --- /dev/null +++ b/src/mt7612u/tx.c @@ -0,0 +1,211 @@ +/* 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" + +#define MT_TX_BUF_MAX 2048 + +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. + */ +/* 802.11 header length from the frame control field. QoS data carries two + * extra bytes, which pushes the header to 26 and so requires the L2 pad - + * and A-MPDU only ever carries QoS data, so getting this wrong breaks + * aggregation before the hardware ever sees it. */ +static int hdrlen_from_fc(const uint8_t *f) +{ + unsigned fc = (unsigned)f[0] | ((unsigned)f[1] << 8); + unsigned type = (fc >> 2) & 3, stype = (fc >> 4) & 0xf; + int len = 24; + + if (type == 1) /* control */ + return ((stype == 0xb) || (stype == 0xa)) ? 16 : 10; + if (type == 2) { /* data */ + if ((fc & 0x0300) == 0x0300) + len = 30; /* 4-address */ + if (stype & 0x08) + len += 2; /* QoS control */ + } + 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 = 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); + } + 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 0000000..f5d4c6c --- /dev/null +++ b/src/mt7612u/usb.c @@ -0,0 +1,232 @@ +/* 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 "internal.h" + +#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) +#define CTRL_TIMEOUT_MS 1000 +#define VEND_RETRIES 10 + +void mt_usleep(unsigned us) +{ + struct timespec ts = { .tv_sec = us / 1000000, .tv_nsec = (us % 1000000) * 1000 }; + nanosleep(&ts, NULL); +} + +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; +} + +uint32_t mt_rr(struct mt7612u_dev *d, uint32_t addr) +{ + 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) + return ~0u; + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +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; + mt_vendor_req(d, req, REQ_OUT, (uint16_t)(a >> 16), (uint16_t)a, b, sizeof b); + + /* 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]); +} + +void mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val) +{ + mt_wr(d, addr, (mt_rr(d, addr) & ~mask) | val); +} + +int mt_poll(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, + uint32_t val, int timeout_us) +{ + int elapsed = 0; + + do { + if ((mt_rr(d, addr) & mask) == val) + return 1; + mt_usleep(1000); + elapsed += 1000; + } while (elapsed < timeout_us); + return 0; +} + +void mt_single_wr(struct mt7612u_dev *d, uint8_t req, uint16_t off, uint32_t val) +{ + mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val & 0xffff), off, NULL, 0); + mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val >> 16), (uint16_t)(off + 2), NULL, 0); +} + +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. */ +int mt_wait_for_mac(struct mt7612u_dev *d) +{ + for (int i = 0; i < 500; i++) { + uint32_t v = mt_rr(d, MT_MAC_CSR0); + if (v != 0 && v != ~0u) + return 1; + mt_usleep(5000); + } + return 0; +} + +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->h = libusb_open_device_with_vid_pid(d->ctx, MT7612U_VID, MT7612U_PID); + if (!d->h) { + if (err) *err = "MT7612U not found or permission denied (try sudo)"; + 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 = libusb_open_device_with_vid_pid(d->ctx, MT7612U_VID, MT7612U_PID); + 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) { + LOG("warning: 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; + } + + /* 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)"; + 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->h) { + 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) { libusb_exit(d->ctx); d->ctx = NULL; } +} + +/* 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]; + + len = (len + 3) & ~3; + for (int i = 0; i < len; ) { + int n = len - i; + + if (n > (int)sizeof buf) n = (int)sizeof buf; + 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)n) < 0) + return; + i += n; + } +} From 2142a5a080d390113be0a93865cb774aee47e4c6 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:00:28 +0200 Subject: [PATCH 02/12] mt7612u: fix the defects under the measurements ## Problem Review of #412 found bugs that the hardware measurements sat on top of. Two of them change what the driver puts on air or hands to a caller, one is a use-after-free during teardown, and one made the "standalone library" claim false. ## Change Ten confirmed findings, each with the fix and its evidence. **The public header declared nine functions that had no definition anywhere** (`mt7612u_open/_close/_keep_detached/_set_channel/_set_chainmask/_start/ _stop/_asic_version/_mac_addr`). The bringup tool builds because it calls internals directly, so nothing caught it. They are now defined, and `tests/api_link.c` takes the address of all twenty public entry points while including only the public header, so the link fails if a declaration ever loses its definition again. `set_channel` also disagreed with the internal signature on `unsigned` vs `uint8_t`; it is `uint8_t` now. **The RX L2-pad fold moved a fixed 24 bytes.** L2PAD is only ever set when the header is not 4-aligned, i.e. 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, which means every TID and ack-policy reached the callback as zero. It now moves the real header length, as mt76x02_remove_hdr_pad() does. **Header length treated only RTS and PS-Poll as 16-byte control frames.** BlockAckReq, BlockAck and both CF-End subtypes are also 16, so on TX the L2 pad was inserted ten bytes in, inside the frame. Replaced with a port of ieee80211_hdrlen(): control frames are 16 by default and only CTS and ACK are 10. That is the same function mt76 reaches via ieee80211_get_hdrlen_from_skb() on both sides, so TX and RX now share one implementation instead of two that could drift. Both are covered by `tests/frame_shape.c`, which also carries a negative control: it redoes the old fixed-24 fold and asserts the QoS Control really is destroyed, so a passing test says something. Reverting either fix makes it fail - the RX one reports `QoS Control zeroed by the pad fold: aa aa`. **mt_async_stop() cancelled only RX, then freed every transfer.** On a wedged chip - the case this driver's own notes describe - TX URBs never complete, and libusb owns a submitted transfer until its callback runs. Now both rings are cancelled, both are waited on with the event thread still running, and if either still has transfers outstanding at the deadline the ring is deliberately leaked rather than freeing memory the kernel may still write into. Each slot names its own ring instead of dev->a, so a leaked ring's late completions cannot touch a replacement one. The `fail:` path no longer joins a zeroed pthread_t. `running`, `rx_active` and both inflight counters move under the mutex - volatile orders nothing and makes no read-modify-write atomic - and the statistics move with them behind mt_async_stats(), because the harness was reading them live off the struct. A caller blocked waiting for a TX slot now gives up when the ring stops. **mt_rr() returned ~0u on failure and mt_rmw() consumed it unchecked**, so one exhausted EP0 read wrote 0xFFFFFFFF | val into MT_WLAN_FUN_CTRL, MT_MAC_SYS_CTRL or the BBP AGC block. 0xffffffff is a real value on this part (MT_MAC_CSR0 reads it while the core comes up), so it cannot double as a sentinel: mt_rr_chk() reports failure separately, mt_rmw() refuses to write after a failed read, and mt_wait_for_mac() distinguishes "not ready" from "no transport". mt_poll() polls against a real deadline and aborts on a read failure - one register access can cost VEND_RETRIES * timeout, so counting sleeps let a caller asking for 200 ms block for seconds. The control timeout drops to mt76's own 300 ms. **mt_wr_copy() rounded len up and then memcpy'd from the caller's buffer**, over-reading whenever len % 4. Latent at the current call sites (8 and 32 bytes); it now rounds the transfer up and zero-fills instead. **Two bringup gates enabled MAC RX and never drained EP 4** - the exact wedge pattern documented three paragraphs above them, and one of them then sat through two 200 ms sleeps and a channel switch. Both start the ring before the receiver. mt7612u_start() enables RX only when an RX ring is already running, and the header says why. **Radiotap DBM_TX_POWER was parsed and silently dropped.** It now says so once per process, pointing at the two knobs that do work. **The mt76 TSF bug claim now carries its citation** - mt76x02_usb_core.c:155-158, where tsf = (u64)dw0 << 32 | dw1 feeds only the dev_dbg() on the next line, which is why the order has survived upstream. Also fixed while in send_packets(): the radiotap parse moved into the selection pass. A frame the build pass could still reject would break the chain it was building, since NEXT_VLD and the single trailing zero word are assigned by position - dropping whichever frame happened to be last left the transfer unterminated. ## Verification `make -C src/mt7612u check` - twenty public entry points resolved, frame_shape PASS. Both new tests were mutation-tested: removing one definition, and reverting each frame-shape fix, each makes them fail. No hardware and no privileges needed. Clean build, no warnings. `CMakeLists.txt` is still untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/.gitignore | 2 + src/mt7612u/Makefile | 30 ++++- src/mt7612u/README.md | 11 ++ src/mt7612u/async.c | 162 ++++++++++++++++++++------ src/mt7612u/caps.c | 21 +++- src/mt7612u/include/mt7612u/mt7612u.h | 16 ++- src/mt7612u/init.c | 55 +++++++++ src/mt7612u/internal.h | 33 +++++- src/mt7612u/phy.c | 30 +++++ src/mt7612u/radiotap.c | 79 +++++++++---- src/mt7612u/rx.c | 26 ++++- src/mt7612u/tests/api_link.c | 47 ++++++++ src/mt7612u/tests/frame_shape.c | 158 +++++++++++++++++++++++++ src/mt7612u/tools/bringup.c | 72 +++++++++--- src/mt7612u/tx.c | 38 +++--- src/mt7612u/usb.c | 117 ++++++++++++++++--- 16 files changed, 764 insertions(+), 133 deletions(-) create mode 100644 src/mt7612u/tests/api_link.c create mode 100644 src/mt7612u/tests/frame_shape.c diff --git a/src/mt7612u/.gitignore b/src/mt7612u/.gitignore index 7310ca1..7a95887 100644 --- a/src/mt7612u/.gitignore +++ b/src/mt7612u/.gitignore @@ -1,4 +1,6 @@ bringup +tests/api_link +tests/frame_shape *.o *.d firmware/ diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile index 8258b68..74c2320 100644 --- a/src/mt7612u/Makefile +++ b/src/mt7612u/Makefile @@ -6,16 +6,34 @@ CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter # struct at runtime and presents as a wild pointer, not a build problem. DEPFLAGS = -MMD -MP LDLIBS = -lusb-1.0 -lpthread -SRCS = $(wildcard *.c) tools/bringup.c +LIBSRCS = $(wildcard *.c) +TESTS = tests/api_link tests/frame_shape +SRCS = $(LIBSRCS) tools/bringup.c $(TESTS:=.c) OBJS = $(SRCS:.c=.o) +LIBOBJS = $(LIBSRCS:.c=.o) DEPS = $(OBJS:.o=.d) -all: bringup -bringup: $(OBJS) +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) + %.o: %.c - $(CC) $(CFLAGS) $(DEPFLAGS) -I. -c -o $@ $< + $(CC) $(CFLAGS) $(DEPFLAGS) -I. -Iinclude -c -o $@ $< + +# No hardware and no privileges needed: safe to run anywhere. +check: $(TESTS) + @for t in $(TESTS); do ./$$t || exit 1; done + clean: - rm -f $(OBJS) $(DEPS) bringup + rm -f $(OBJS) $(DEPS) bringup $(TESTS) -include $(DEPS) -.PHONY: all clean +.PHONY: all check clean diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md index 840340f..c6b8856 100644 --- a/src/mt7612u/README.md +++ b/src/mt7612u/README.md @@ -6,6 +6,7 @@ library target. It builds 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 ``` @@ -27,6 +28,16 @@ Measurements, methods and limits: [`../../docs/mt7612u.md`](../../docs/mt7612u.m | `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 | + +## 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*. ## Firmware diff --git a/src/mt7612u/async.c b/src/mt7612u/async.c index 9e94936..5d75eae 100644 --- a/src/mt7612u/async.c +++ b/src/mt7612u/async.c @@ -16,12 +16,29 @@ #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 (d->a && d->a->running) + while (locked_get(a, &a->running)) libusb_handle_events_timeout_completed(d->ctx, &tv, NULL); return NULL; } @@ -30,7 +47,8 @@ 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 = d->a; + struct mt_async *a = s->a; + int resubmit; if (t->status == LIBUSB_TRANSFER_COMPLETED) { const uint8_t *frame = NULL; @@ -38,38 +56,49 @@ static void LIBUSB_CALL rx_done(struct libusb_transfer *t) int len = mt_rx_parse(d, t->buffer, t->actual_length, &frame, &info); if (len > 0) { + 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); } - if (a->rx_active && t->status != LIBUSB_TRANSFER_CANCELLED) { - if (libusb_submit_transfer(t)) - a->rx_err++; - } else { - a->rx_inflight--; - } + 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->d->a; + 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++; - - pthread_mutex_lock(&a->tx_lock); a->tx_busy[s->idx] = 0; a->tx_inflight--; - pthread_cond_signal(&a->tx_cv); - pthread_mutex_unlock(&a->tx_lock); + pthread_cond_broadcast(&a->cv); + pthread_mutex_unlock(&a->lock); } int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) @@ -82,17 +111,19 @@ int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) d->a = a; a->cb = cb; a->cb_user = user; - pthread_mutex_init(&a->tx_lock, NULL); - pthread_cond_init(&a->tx_cv, NULL); + 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; @@ -100,9 +131,12 @@ int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) 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, @@ -111,7 +145,9 @@ int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) 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); @@ -125,41 +161,68 @@ int mt_async_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) 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]); - /* Let the in-flight TX drain before tearing the event thread down. */ - pthread_mutex_lock(&a->tx_lock); - for (int spins = 0; a->tx_inflight && spins < 200; spins++) { + /* 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->tx_cv, &a->tx_lock, &ts); + pthread_cond_timedwait(&a->cv, &a->lock, &ts); } - pthread_mutex_unlock(&a->tx_lock); + stuck_tx = a->tx_inflight; + stuck_rx = a->rx_inflight; + a->running = 0; + pthread_mutex_unlock(&a->lock); - for (int spins = 0; a->rx_inflight && spins < 100; spins++) - mt_usleep(10000); + if (a->evt_started) + pthread_join(a->evt, NULL); - a->running = 0; - pthread_join(a->evt, NULL); + d->a = NULL; + if (stuck_tx || stuck_rx) { + ERR("async stop: %d TX and %d RX transfers still in flight after 2 s " + "- leaking the ring rather than freeing memory libusb 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->tx_lock); - pthread_cond_destroy(&a->tx_cv); + pthread_mutex_destroy(&a->lock); + pthread_cond_destroy(&a->cv); free(a); - d->a = NULL; } /* @@ -169,35 +232,60 @@ void mt_async_stop(struct mt7612u_dev *d) int mt_async_tx_submit(struct mt7612u_dev *d, const uint8_t *buf, int len) { struct mt_async *a = d->a; - int idx = -1; + int idx = -1, rc; if (!a || len > MT_TX_BUFSZ) return -1; - pthread_mutex_lock(&a->tx_lock); + 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->tx_cv, &a->tx_lock); + pthread_cond_wait(&a->cv, &a->lock); } a->tx_busy[idx] = 1; a->tx_inflight++; - pthread_mutex_unlock(&a->tx_lock); + 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); - if (libusb_submit_transfer(a->tx[idx])) { - pthread_mutex_lock(&a->tx_lock); + rc = libusb_submit_transfer(a->tx[idx]); + + pthread_mutex_lock(&a->lock); + if (rc) { a->tx_busy[idx] = 0; a->tx_inflight--; - pthread_mutex_unlock(&a->tx_lock); a->tx_err++; - return -1; + pthread_cond_broadcast(&a->cv); + } else { + a->tx_submitted++; } - a->tx_submitted++; - return 0; + 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; + pthread_mutex_unlock(&a->lock); } int mt7612u_rx_start(struct mt7612u_dev *d, mt7612u_rx_cb cb, void *user) diff --git a/src/mt7612u/caps.c b/src/mt7612u/caps.c index 1d053f0..d895376 100644 --- a/src/mt7612u/caps.c +++ b/src/mt7612u/caps.c @@ -4,11 +4,22 @@ #include "internal.h" /* - * DW0 is the LOW word. mt76's mt76x02u_restart_pre_tbtt_timer() assembles this - * as (dw0 << 32) | dw1, which is backwards - but the result there only ever - * feeds a dev_dbg() print, so the bug is never exercised and has survived. - * Copying it produced a clock that advanced by 8.6e14 "us" per 200 ms. - * Measured: (DW1 << 32) | DW0 gives 200159 us over a 200000 us sleep. + * 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) { diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index c5c7956..d8964bc 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -85,10 +85,12 @@ void mt7612u_close(struct mt7612u_dev *dev); void mt7612u_keep_detached(struct mt7612u_dev *dev, int keep); /* - * Channel + width. 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. + * Channel + width. `chan` is an 802.11 channel number. 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. Only 20 and 40 MHz are + * implemented; 80 MHz is silicon-capable but the width maths is not ported. */ -int mt7612u_set_channel(struct mt7612u_dev *dev, unsigned chan, enum mt7612u_bw bw); +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); @@ -96,7 +98,13 @@ 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); -int mt7612u_start(struct mt7612u_dev *dev); /* enable MAC TX+RX */ +/* + * 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); /* diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index 06e6e99..d24344c 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -5,6 +5,7 @@ * See PLAN.md: trimming this sequence is a post-Gate-E activity, because a * 95%-correct init answers every register read and still radiates nothing. */ +#include #include #include "internal.h" #include "initvals.h" @@ -388,3 +389,57 @@ int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir) return mt_mac_stop(d); } + +/* --- the public lifecycle, as declared in include/mt7612u/mt7612u.h --- */ + +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; } + 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; +} + +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); +} diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index c695610..5737e26 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -45,7 +45,11 @@ struct mt7612u_cal { #define MT_USB_AGG_MAX 32 /* frames chained per transfer */ struct mt7612u_dev; -struct mt_slot { struct mt7612u_dev *d; int idx; }; +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]; @@ -53,11 +57,14 @@ struct mt_async { 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]; - pthread_mutex_t tx_lock; - pthread_cond_t tx_cv; + /* 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; - volatile int running, rx_active; - volatile int tx_inflight, rx_inflight; + 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; @@ -83,6 +90,8 @@ struct mt7612u_dev { struct mt_rate_power rate_power; struct mt7612u_cal cal; + unsigned io_err; /* EP0 transfers that exhausted their retries */ + /* Oracle-diff log: every EP0 write we emit, in order. */ uint8_t ack_saved_mac[6]; int ack_saved; @@ -94,9 +103,14 @@ struct mt7612u_dev { /* --- usb.c --- */ int mt_open(struct mt7612u_dev *d, 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); -void mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, 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); #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. */ @@ -141,6 +155,9 @@ 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 */ @@ -156,6 +173,10 @@ void mt_wcid_setup(struct mt7612u_dev *d, uint8_t idx, const uint8_t *mac); 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; +}; +void mt_async_stats(struct mt7612u_dev *d, struct mt_async_stats *out); 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); diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index 015ca37..aeeb167 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -433,3 +433,33 @@ int mt7612u_set_txpower(struct mt7612u_dev *d, int dbm) 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. + */ +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) { + ERR("set_channel: only 20 and 40 MHz are implemented (bw=%d)", (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 index 5d2b44a..005f721 100644 --- a/src/mt7612u/radiotap.c +++ b/src/mt7612u/radiotap.c @@ -33,6 +33,9 @@ static const struct { uint8_t align, size; } rt_field[] = { #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) { @@ -115,7 +118,19 @@ int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r) if (rd16(p) & RT_TX_FLAGS_NOACK) r->no_ack = 1; break; case RT_DBM_TX_POWER: - r->power_adj = 0; /* absolute dBm is a device-level knob */ + /* 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]; @@ -187,37 +202,57 @@ size_t mt7612u_send_packets(struct mt7612u_dev *d, while (i < count) { size_t off = 0, n_in_buf = 0, j; - size_t idx[MT_USB_AGG_MAX]; + struct { const uint8_t *mpdu; size_t len; struct mt7612u_tx_rate r; } + sel[MT_USB_AGG_MAX]; - /* Pass 1: pick the frames that fit in one transfer. */ + /* + * 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) { - size_t need; + 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; + if (off + need > sizeof buf) break; - if (!pkts[i].data || pkts[i].len < 8) { i++; continue; } - need = pkts[i].len + 32; - if (off + need + 4 > sizeof buf) break; - idx[n_in_buf++] = i; - off += need; /* upper bound; pass 2 uses the real size */ + 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: build them back to back. NEXT_VLD on every block except - * the last, and only the last carries the 4-byte zero trailer. */ + /* 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++) { - struct mt7612u_tx_rate r; - const uint8_t *p = pkts[idx[j]].data; - size_t plen = pkts[idx[j]].len; int last = (j + 1 == n_in_buf); - int rlen = mt_radiotap_parse(p, plen, &r); - int blk; - - if (rlen <= 0 || (size_t)rlen >= plen) continue; - blk = mt_tx_build(d, buf + off, sizeof buf - off, - p + rlen, plen - (size_t)rlen, &r, 0xff, 0, - !last, last); - if (blk < 0) break; + 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; diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c index 51ef9c1..631a97a 100644 --- a/src/mt7612u/rx.c +++ b/src/mt7612u/rx.c @@ -84,17 +84,33 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, 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 by moving the header down over it. */ - if (pad && len > 24) { - memmove(buf + MT_DMA_HDR_LEN + MT_RXWI_LEN + 2, - buf + MT_DMA_HDR_LEN + MT_RXWI_LEN, 24); - *frame = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN + 2; + /* + * 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; } diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c new file mode 100644 index 0000000..8180835 --- /dev/null +++ b/src/mt7612u/tests/api_link.c @@ -0,0 +1,47 @@ +/* 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_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_send_packet, + (void *)mt7612u_send_packets, + (void *)mt7612u_set_ack_responder, + (void *)mt7612u_clear_ack_responder, + (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/frame_shape.c b/src/mt7612u/tests/frame_shape.c new file mode 100644 index 0000000..5499532 --- /dev/null +++ b/src/mt7612u/tests/frame_shape.c @@ -0,0 +1,158 @@ +/* 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++; + } + } +} + +int main(void) +{ + test_hdrlen(); + test_rx_l2pad(); + 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 index 567b71b..5f307a9 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -543,10 +543,15 @@ static int gate_soak(uint8_t chan, int secs, int framelen) "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); - printf(" submitted=%llu completed=%llu errors=%llu\n", - (unsigned long long)dev.a->tx_submitted, - (unsigned long long)dev.a->tx_done_n, - (unsigned long long)dev.a->tx_err); + { + 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 @@ -608,9 +613,14 @@ static int gate_arx(uint8_t chan, int secs) t0 = now_ms(); while (now_ms() - t0 < secs * 1000.0) mt_usleep(100000); - printf("async RX on ch%u for %d s: %lu frames (%.0f/s), rx_err=%llu\n", - chan, secs, ctx.n, ctx.n / (double)secs, - (unsigned long long)dev.a->rx_err); + { + struct mt_async_stats st; + + mt_async_stats(&dev, &st); + printf("async RX on ch%u for %d s: %lu frames (%.0f/s), rx_err=%llu\n", + chan, secs, ctx.n, ctx.n / (double)secs, + (unsigned long long)st.rx_err); + } for (int i = 0; i < 5; i++) if (ctx.by_phy[i]) printf(" %-6s %lu\n", phy_name[i], ctx.by_phy[i]); mt7612u_rx_stop(&dev); @@ -653,11 +663,16 @@ static int gate_duplex(uint8_t chan, int secs) } wall = now_ms() - t0; printf("duplex on ch%u for %.1f s:\n", chan, wall / 1000.0); - 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)dev.a->tx_err, - (unsigned long long)dev.a->rx_err); + { + 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); + } mt7612u_rx_stop(&dev); mt_mac_stop(&dev); return (n && ctx.n) ? 0 : 1; @@ -779,7 +794,12 @@ static int gate_ampdu(uint8_t chan, int count) count * 1000.0 / wall, count * 48 * 8.0 / wall / 1000.0); mt_usleep(200000); } - printf(" tx_err=%llu\n", (unsigned long long)dev.a->tx_err); + { + 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 @@ -828,10 +848,20 @@ static int gate_ampdu(uint8_t chan, int count) 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; + (*(unsigned long *)user)++; +} + /* 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; @@ -839,7 +869,12 @@ static int gate_caps(uint8_t chan) 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; + /* 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", @@ -936,7 +971,9 @@ static int gate_caps(uint8_t chan) } } + 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; } @@ -975,7 +1012,10 @@ static int gate_ack(uint8_t chan, int secs, int arm) 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; + /* 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); @@ -991,6 +1031,7 @@ static int gate_ack(uint8_t chan, int secs, int arm) if (arm) { if (mt7612u_set_ack_responder(&dev, g_ack_mac)) { printf("GATE ack: FAIL - could not arm\n"); + mt7612u_rx_stop(&dev); mt_mac_stop(&dev); return 1; } @@ -1000,7 +1041,6 @@ static int gate_ack(uint8_t chan, int secs, int arm) printf("responder NOT armed (control arm)\n"); } - if (mt7612u_rx_start(&dev, ack_cb, &off)) return 1; printf("listening %d s ...\n", secs); mt_usleep((unsigned)secs * 1000000u); mt7612u_rx_stop(&dev); diff --git a/src/mt7612u/tx.c b/src/mt7612u/tx.c index 907d26a..0624259 100644 --- a/src/mt7612u/tx.c +++ b/src/mt7612u/tx.c @@ -58,24 +58,36 @@ uint16_t mt_tx_rate_word(const struct mt7612u_tx_rate *r) * and never sets. Exposed so the Gate-G control can make the LUT and the * descriptor disagree and see which one airs. */ -/* 802.11 header length from the frame control field. QoS data carries two - * extra bytes, which pushes the header to 26 and so requires the L2 pad - - * and A-MPDU only ever carries QoS data, so getting this wrong breaks - * aggregation before the hardware ever sees it. */ -static int hdrlen_from_fc(const uint8_t *f) +/* + * 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, stype = (fc >> 4) & 0xf; + unsigned type = (fc >> 2) & 3; int len = 24; - if (type == 1) /* control */ - return ((stype == 0xb) || (stype == 0xa)) ? 16 : 10; - if (type == 2) { /* data */ + if (type == 2) { /* data */ if ((fc & 0x0300) == 0x0300) - len = 30; /* 4-address */ - if (stype & 0x08) - len += 2; /* QoS control */ + 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; } @@ -113,7 +125,7 @@ int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, /* 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 = hdrlen_from_fc(f); + hdrlen = mt_hdrlen_from_fc(f); if (hdrlen > (int)len) hdrlen = (int)len; if (hdrlen % 4) hdr_pad = 2; diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index f5d4c6c..288177b 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -10,7 +10,10 @@ #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) -#define CTRL_TIMEOUT_MS 1000 +/* 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) @@ -19,6 +22,14 @@ void mt_usleep(unsigned us) nanosleep(&ts, NULL); } +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) { @@ -50,16 +61,35 @@ static uint8_t wr_req(uint32_t addr) return MT_VEND_MULTI_WRITE; } -uint32_t mt_rr(struct mt7612u_dev *d, uint32_t addr) +/* + * 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) - return ~0u; - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + 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) @@ -77,23 +107,48 @@ void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val) req, (unsigned)(a & 0xffff), b[0], b[1], b[2], b[3]); } -void mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val) +/* + * 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. + */ +int mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val) { - mt_wr(d, addr, (mt_rr(d, addr) & ~mask) | 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) { - int elapsed = 0; + uint64_t deadline = now_us() + (uint64_t)(timeout_us < 0 ? 0 : timeout_us); + + for (;;) { + uint32_t cur; - do { - if ((mt_rr(d, addr) & mask) == val) + 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); - elapsed += 1000; - } while (elapsed < timeout_us); - return 0; + } } void mt_single_wr(struct mt7612u_dev *d, uint8_t req, uint16_t off, uint32_t val) @@ -112,12 +167,15 @@ int mt_bulk(struct mt7612u_dev *d, uint8_t ep, void *buf, int len, return rc; } -/* mt76x02_wait_for_mac(): MAC_CSR0 reads 0 or ~0 until the core is alive. */ +/* 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 = mt_rr(d, MT_MAC_CSR0); - if (v != 0 && v != ~0u) + uint32_t v; + + if (!mt_rr_chk(d, MT_MAC_CSR0, &v) && v != 0 && v != ~0u) return 1; mt_usleep(5000); } @@ -218,15 +276,36 @@ void mt_wr_copy(struct mt7612u_dev *d, uint32_t offset, const void *data, int le const uint8_t *p = data; uint8_t buf[64]; - len = (len + 3) & ~3; + /* 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; + 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)n) < 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; +} From c64975dad1108f17e0e2d5db8aada44195489fd8 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:04:10 +0200 Subject: [PATCH 03/12] mt7612u: generate initvals.h instead of transcribing it ## Problem `src/mt7612u/initvals.h` was hand-copied from mt76 and pointed at a regeneration script in a PLAN.md that is not in this repository. The project rule is that tables come from `tools/extract_*.py` against a pinned `reference/` tree with a `--check` mode, so the checked-in artifact stays re-verifiable from a fresh checkout. ## Change Adds `reference/mt76` as a pinned shallow submodule (`openwrt/mt76` at `be5ce79`) and `tools/extract_mt7612u_tables.py`, following the `extract_8733b_*.py` shape: `UPSTREAM` string, per-source SHA-256, an `EXPECTED` count and table hash, and `--check` that byte-compares the checked-in header. The source here is C initialiser syntax rather than a vendor parameter blob, so the script carries a symbol table over `mt76x02_regs.h` and a small constant-expression evaluator for `BIT` / `GENMASK` / `FIELD_PREP`. Both are deliberately narrow - they understand only what these definitions use and raise on anything else rather than guessing. That is what lets the four `DEFAULT_PROT_CFG_*` macros, defined inside `mt76_write_mac_initvals()` itself, be computed rather than copied. `reference/mt76` is the one entry there that is not a Realtek vendor drop: the MediaTek parts have no out-of-tree vendor driver to mirror, mt76 is the mainline reference, and it is BSD-3-Clause-Clear rather than GPL-2-only, which is why `src/mt7612u/` can carry ported sequences at all. `reference/README.md` says so. ## Verification The generator reproduces the previously hand-typed table byte for byte - all sixty rows, register addresses and values, including the four computed protection-config words. So this also independently confirms the original transcription was correct rather than silently replacing it. `--check` is load-bearing: appending one newline to the header makes it exit 1 with `stale generated output`. No CI workflow checks out submodules, so the twenty builds are unaffected. `make -C src/mt7612u check` still passes. `CMakeLists.txt` still untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- .gitmodules | 5 + reference/README.md | 14 +- reference/mt76 | 1 + src/mt7612u/README.md | 16 +- src/mt7612u/initvals.h | 11 +- tools/extract_mt7612u_tables.py | 302 ++++++++++++++++++++++++++++++++ 6 files changed, 344 insertions(+), 5 deletions(-) create mode 160000 reference/mt76 create mode 100755 tools/extract_mt7612u_tables.py diff --git a/.gitmodules b/.gitmodules index b5781d3..91c08b9 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/reference/README.md b/reference/README.md index fd46bd9..495a356 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 0000000..be5ce79 --- /dev/null +++ b/reference/mt76 @@ -0,0 +1 @@ +Subproject commit be5ce7910521492d4a2e4ce7ee3843680a46c047 diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md index c6b8856..56ab99e 100644 --- a/src/mt7612u/README.md +++ b/src/mt7612u/README.md @@ -29,6 +29,7 @@ Measurements, methods and limits: [`../../docs/mt7612u.md`](../../docs/mt7612u.m | `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 @@ -68,7 +69,20 @@ rtap send_packet / send_packets hop channel-switch cost 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. +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 diff --git a/src/mt7612u/initvals.h b/src/mt7612u/initvals.h index c2d7355..942f3c4 100644 --- a/src/mt7612u/initvals.h +++ b/src/mt7612u/initvals.h @@ -1,7 +1,12 @@ /* SPDX-License-Identifier: BSD-3-Clause-Clear */ -/* GENERATED from openwrt/mt76 mt76x2/init.c mt76_write_mac_initvals() - * plus mt76x02_regs.h. Do not hand-edit: regenerate with the script in - * ../../PLAN.md Stage C if the reference tree moves. */ +/* 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 diff --git a/tools/extract_mt7612u_tables.py b/tools/extract_mt7612u_tables.py new file mode 100755 index 0000000..aa1ecc2 --- /dev/null +++ b/tools/extract_mt7612u_tables.py @@ -0,0 +1,302 @@ +#!/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 + if not path.exists() or path.read_text(encoding="utf-8", newline="") != 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()) From 2ac9b9ac0b8630efce2c67db2fb6dffcaab17997 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:07:06 +0200 Subject: [PATCH 04/12] mt7612u: portability fixes that stand on their own ## Problem Review of #412 asked for MSVC/Windows portability, which is first-class in this project. Three of the items are correctness issues independent of any compiler; the rest depend on how the subtree eventually joins the build. ## Change **`_SHIFT` was a reserved identifier.** Leading underscore followed by a capital is reserved to the implementation in every scope. Renamed. **`FIELD_PREP`/`FIELD_GET` no longer call `__builtin_ctz`.** MSVC has no such builtin, and the obvious substitute, `_BitScanForward`, is a function with an out-parameter and therefore cannot appear in a constant expression. These macros must stay constant expressions: `ext_cca_chan` in phy.c is a static table built from them. `MT_CTZ` isolates the low bit and binary-searches its position - a constant expression on every compiler, folded to one instruction under optimisation. **libusb include path.** `` is what the rest of this project includes, letting the build system supply the directory; the standalone Makefile has no pkg-config step and most distributions ship the header under `libusb-1.0/`. `__has_include` tries the project spelling first and falls back. ## What is deliberately not 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. A C shim written now would be deleted when the subtree joins the build, so those two files keep POSIX until then. They are the only two files involved, and the README says so. Likewise the Makefile stays a Makefile: the subtree is deliberately not reachable from `CMakeLists.txt` yet, so a CMake target now would be a target nothing builds. ## Verification `tests/field_macros` is new and checks the replacement against `__builtin_ctz` over every mask the driver can form - all 32 single-bit masks and all 528 contiguous `GENMASK(h, l)` ranges - plus a `FIELD_PREP`/ `FIELD_GET` round-trip on each. It carries a static initialiser built from `FIELD_PREP`, so if `MT_CTZ` ever stops being constant-foldable the test fails to compile rather than silently passing at runtime. Both libusb spellings build and pass: default, and with the header on the include path directly. `make -C src/mt7612u check`: 20 entry points, frame_shape PASS, field_macros 528 masks PASS. No warnings. `CMakeLists.txt` untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/Makefile | 5 ++- src/mt7612u/README.md | 22 ++++++++++ src/mt7612u/internal.h | 15 ++++++- src/mt7612u/regs.h | 30 ++++++++++++-- src/mt7612u/tests/field_macros | Bin 0 -> 243592 bytes src/mt7612u/tests/field_macros.c | 68 +++++++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 6 deletions(-) create mode 100755 src/mt7612u/tests/field_macros create mode 100644 src/mt7612u/tests/field_macros.c diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile index 74c2320..6be6182 100644 --- a/src/mt7612u/Makefile +++ b/src/mt7612u/Makefile @@ -7,7 +7,7 @@ CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter DEPFLAGS = -MMD -MP LDLIBS = -lusb-1.0 -lpthread LIBSRCS = $(wildcard *.c) -TESTS = tests/api_link tests/frame_shape +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) @@ -26,6 +26,9 @@ tests/api_link: $(LIBOBJS) tests/api_link.o 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 $@ $< diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md index 56ab99e..74b2210 100644 --- a/src/mt7612u/README.md +++ b/src/mt7612u/README.md @@ -40,6 +40,28 @@ 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` diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 5737e26..d336965 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -2,7 +2,20 @@ #ifndef MT7612U_INTERNAL_H #define MT7612U_INTERNAL_H -#include +/* 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 diff --git a/src/mt7612u/regs.h b/src/mt7612u/regs.h index 4af8611..339d0cc 100644 --- a/src/mt7612u/regs.h +++ b/src/mt7612u/regs.h @@ -14,10 +14,32 @@ #define BIT(n) (1u << (n)) #define GENMASK(h, l) (((~0u) - (1u << (l)) + 1) & (~0u >> (31 - (h)))) -/* Lowest set bit of a contiguous mask, for FIELD_PREP/GET. */ -#define _SHIFT(m) (__builtin_ctz(m)) -#define FIELD_PREP(m, v) (((uint32_t)(v) << _SHIFT(m)) & (m)) -#define FIELD_GET(m, v) (((uint32_t)(v) & (m)) >> _SHIFT(m)) + +/* + * 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) diff --git a/src/mt7612u/tests/field_macros b/src/mt7612u/tests/field_macros new file mode 100755 index 0000000000000000000000000000000000000000..5f675801cfe72d59e39eb9ac391d9a54f05b1d0c GIT binary patch literal 243592 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 +#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; +} From 081f952764e5746fbbce48b7fd050418744e101a Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:12:50 +0200 Subject: [PATCH 05/12] docs(mt7612u): record the offline tests and re-verify on hardware The three fix commits are re-measured on the same bench and the same RTL8812AU witness. Nothing regressed, and the A-MPDU figures reproduce the original PR numbers exactly. - `init` clean, so the 300 ms control timeout and the deadline-based `mt_poll` did not break bring-up. - `rtap`: tag A 401 frames, tag B 388 - `send_packet` and `send_packets` both still air, with `send_packets` still chaining 16 frames per USB transfer. - `ampdu`: 41 734 of 85 580 witnessed frames carry `paggr=1`. 6.26 -> 15.50 Mbit/s at 200 bytes, 34.05 -> 44.55 at 1400 - the same two figures the PR reported. - `caps`: PASS on three consecutive cycles with the RX ring now started before the receiver rather than after it. 40 MHz re-confirmed at 300/300 frames reporting `bw=1`, against a witness also at 40 MHz. TSF 200347 us over a 200000 us sleep. - `arx` on ch1: 278 ambient frames, `rx_err=0`, CCK/OFDM/HT all decoded through the rewritten L2-pad fold. The header-length fix has no on-air observable in these gates: it only changes control frames, and nothing here injects one. The unit test with its negative control is the evidence for that one. Counterparts updated: the offline tests and the generator's `--check` pass locally, but no workflow runs them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 42 +++++++++++++++++++++++++++++++++++++++--- src/mt7612u/.gitignore | 3 +++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 620c288..21b2abe 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -2,7 +2,24 @@ 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 `## Counterparts` before +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 | +| `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 @@ -208,6 +225,23 @@ 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 | +| `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. @@ -234,8 +268,10 @@ Stated because the numbers above are uniformly favourable. firmware-running bit. - **The 48 ms fast retune is our implementation, not the floor.** The floor is unmeasured. -- **Nothing here runs in CI.** No `ctest` integration, no sanitizer build, no - lifecycle soak of the kind the Realtek backends carry. +- **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. diff --git a/src/mt7612u/.gitignore b/src/mt7612u/.gitignore index 7a95887..1f87973 100644 --- a/src/mt7612u/.gitignore +++ b/src/mt7612u/.gitignore @@ -1,6 +1,9 @@ bringup tests/api_link tests/frame_shape +tests/field_macros *.o *.d firmware/ +wrlog.txt +mculog.txt From a9cd1566733b54b72fe6f9698bf21d34ad63b966 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:15:29 +0200 Subject: [PATCH 06/12] mt7612u: a radiotap VHT bandwidth code is a width *and* a sub-channel ## Problem `radiotap.c` mapped VHT bandwidth codes 1-3 to 40 MHz and >=4 to 80. The code names a channel width and the sub-channel used within it, so 2 is "40 (20L)" - a 20 MHz frame in the lower half of a 40 MHz channel, not a 40 MHz frame. A caller asking for 20-in-40 got 40 MHz on air, and 20-in-80 got 80. This contradicted the file's own HT branch, which reads the equivalent HT codes correctly - and this project's `src/ieee80211_radiotap.h:112` names them `IEEE80211_RADIOTAP_MCS_BW_20L` and `_20U`, which is the same semantics spelled out in the repository already. ## Change A table over the eleven codes this radio can express, mapping each to the width the frame is actually sent at. Codes 11 and above are 160 MHz and its sub-channels; the rate word has no 160 MHz encoding, so those log and fall back to 20 rather than silently narrowing. ## Verification `tests/frame_shape` gains the whole table, all eleven codes. Restoring the old expression fails eight of them, so the test is load-bearing rather than decorative. ## Elsewhere in this repository `src/jaguar1/RtlJaguarDevice.cpp:1097-1100` and `src/jaguar3/RtlJaguar3Device.cpp:1940-1943` carry the same `bw >= 1 && bw <= 3 -> CHANNEL_WIDTH_40`, `bw >= 4 && bw <= 10 -> CHANNEL_WIDTH_80` mapping, and are not touched here - a shipping backend's on-air behaviour is not something to change from inside a new-backend PR. Reported so it can be triaged separately. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 4 +-- src/mt7612u/radiotap.c | 41 +++++++++++++++++++++++++++-- src/mt7612u/tests/field_macros | Bin 243592 -> 244264 bytes src/mt7612u/tests/frame_shape.c | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 21b2abe..de9d178 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -9,7 +9,7 @@ running this project's own `rxdemo`/`txdemo`. Read `## Offline tests | 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 | +| `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 @@ -232,7 +232,7 @@ changed at once**, so that run does not attribute the wedge to one of them. | test | what it holds | |---|---| | `api_link` | takes the address of all 20 public entry points while including only the public header, so a declaration that loses its definition is a link error | -| `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 | +| `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 diff --git a/src/mt7612u/radiotap.c b/src/mt7612u/radiotap.c index 005f721..7d31f90 100644 --- a/src/mt7612u/radiotap.c +++ b/src/mt7612u/radiotap.c @@ -63,6 +63,44 @@ static int legacy_rate_index(uint8_t r500, enum mt7612u_phy *phy) } } +/* + * 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. @@ -157,8 +195,7 @@ int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r) if (flags & 0x04) r->sgi = 1; if (coding & 0x01) r->ldpc = 1; - r->bw = bwc == 0 ? MT7612U_BW_20 - : (bwc <= 3 ? MT7612U_BW_40 : MT7612U_BW_80); + r->bw = vht_bandwidth(bwc); break; } default: diff --git a/src/mt7612u/tests/field_macros b/src/mt7612u/tests/field_macros index 5f675801cfe72d59e39eb9ac391d9a54f05b1d0c..87ab6a14f7642cfa48a5bb492833fabe8af9d259 100755 GIT binary patch delta 25537 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 delta 24893 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 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++; + } + } +} + int main(void) { test_hdrlen(); test_rx_l2pad(); + test_vht_bandwidth(); printf("frame_shape: %s\n", fails ? "FAIL" : "PASS"); return fails ? 1 : 0; } From 62410fb5a832562bd766d5525da22f050b1018d8 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 17:21:39 +0200 Subject: [PATCH 07/12] mt7612u: untrack a test binary that slipped past .gitignore `tests/field_macros` was staged in 2ac9b9a before `.gitignore` learned about it, so a 300 KB build artifact went into the tree. The ignore entry landed one commit later and could not retroactively untrack it. The other two test binaries were never tracked; `git ls-files src/mt7612u/tests/` now lists sources only. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/tests/field_macros | Bin 244264 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 src/mt7612u/tests/field_macros diff --git a/src/mt7612u/tests/field_macros b/src/mt7612u/tests/field_macros deleted file mode 100755 index 87ab6a14f7642cfa48a5bb492833fabe8af9d259..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244264 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
t6tv=ed*2wR9rHxHhKO;LZlr54Lc)40W{#-m2n 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

xyHD?cw#*hkhT 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 qziRxa;a*@0JoUGBmVj1~b7A?_&!5D*6@A%a0i| Date: Sun, 6 Sep 2026 17:22:21 +0200 Subject: [PATCH 08/12] mt7612u: measure what the four bytes past MPDU_LEN actually are ## Problem `Packet::Data` in `src/RxPacket.h` is documented as the full 802.11 frame including the trailing FCS, and every Realtek parser here honours it. Whether MT7612U can is an integration question, and guessing at it is how a four-byte truncation ships. Every RX buffer does carry 4-7 bytes past `MPDU_LEN` - over 4263 ambient frames the tail was exactly 4 on 3375 of them and 5-7 on the rest, which reads precisely like a fixed 4-byte field plus USB 4-byte alignment. So the obvious move is to hand a consumer `len + 4` and call it the FCS. ## What it actually 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 before the negative was believed. 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`. mt76 never sets `RX_FLAG_INCLUDE_FCS` for this family either. The MAC strips the checksum and does not hand it up. ## Change Documentation only - the code was already correct, it just did not say what it could not do. `docs/mt7612u.md` gains the measurement under RX and a Counterparts entry; the public header's RX callback says the frame carries no FCS and why the trailing bytes are not one. This matters at the boundary: a consumer that trims four bytes because the `Packet::Data` contract invites it - as `tools/bf_report_decode.py` already does - would eat four bytes of payload off every frame. Whatever shape integration takes, the divergence gets declared rather than smoothed over. Also here: `tests/field_macros` was untracked in the previous commit after slipping past `.gitignore`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- docs/mt7612u.md | 27 +++++++++++++++++++++++++++ src/mt7612u/include/mt7612u/mt7612u.h | 7 +++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index de9d178..6361488 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -157,6 +157,31 @@ 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 @@ -274,6 +299,8 @@ Stated because the numbers above are uniformly favourable. 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 diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index d8964bc..cfa130f 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -115,8 +115,11 @@ 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 excludes the RXWI. - * Must not block and must not call back into the device. + * 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); From 7be0e4061b235e10ed87e93bfc0a8f816f2d85b8 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 20:00:55 +0200 Subject: [PATCH 09/12] mt7612u: the 40 MHz path tunes a channel it was never asked for mt76 never validates a control channel because it never derives one: mt76x2u_phy_set_channel() takes the segment centre from cfg80211's chandef, so an off-grid channel cannot reach it. This port takes a bare channel number and derives the centre from the standard pairing, which means an off-grid channel still produces *a* number -- and then transmits 40 MHz wide somewhere the caller did not ask for, with every register write succeeding and nothing saying so. Two cases were reachable straight through mt7612u_set_channel(), which checks only that the channel is non-zero: - the arithmetic ran in uint8_t. Control channel 254 computes 256, which truncates to 0, handing the MCU channel index 0 with the 5 GHz register set loaded; 255 gives 1. - in 2.4 GHz the pairing only ever reaches centres 6-9. Channel 1 computes -1, i.e. 255 after truncation. Channels 12 and 13 would need a secondary above channel 13. mt_chan40_centre() computes the centre in int and validates it against the centres that exist, which catches the off-grid case, both wraps and the out-of-band case together. The 5 GHz ceiling is 159 rather than 175 because centres 167 and 175 span past the 5825 MHz that mt7612u_caps declares. Pinned in tests/frame_shape.c, and mutation-tested: removing the validation makes nine cases fail. Restoring the uint8_t arithmetic does *not* make the suite fail, because no wrapped value aliases onto a legal centre -- the int is there so the refusal names 256 rather than reporting 0 and sending whoever reads it after the wrong bug. The comment says that rather than claiming a guard it does not provide. On air, unchanged: `bringup caps 149` still sends 300 frames at 40 MHz and the RTL8812AU witness still reports 300 at bw=1. Also: `make check` now depends on `bringup`. Nothing else compiled it, so a change that broke a gate shipped green -- which is exactly what happened while preparing this commit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/Makefile | 6 ++- src/mt7612u/internal.h | 1 + src/mt7612u/phy.c | 66 ++++++++++++++++++++++++++---- src/mt7612u/tests/frame_shape.c | 72 +++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile index 6be6182..e205d01 100644 --- a/src/mt7612u/Makefile +++ b/src/mt7612u/Makefile @@ -32,8 +32,10 @@ tests/field_macros: $(LIBOBJS) tests/field_macros.o %.o: %.c $(CC) $(CFLAGS) $(DEPFLAGS) -I. -Iinclude -c -o $@ $< -# No hardware and no privileges needed: safe to run anywhere. -check: $(TESTS) +# 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: diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index d336965..b344027 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -211,6 +211,7 @@ 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_chan40_centre(uint8_t chan, uint8_t *bw_index, uint8_t *ch_group); #define LOG(...) do { fprintf(stderr, "[mt7612u] " __VA_ARGS__); fputc('\n', stderr); } while (0) #define ERR(...) do { fprintf(stderr, "[mt7612u] ERROR " __VA_ARGS__); fputc('\n', stderr); } while (0) diff --git a/src/mt7612u/phy.c b/src/mt7612u/phy.c index aeeb167..83cf758 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -311,6 +311,59 @@ int8_t mt_tx_get_txpwr_adj(struct mt7612u_dev *d, int8_t txpwr, int8_t max_adj) return (int8_t)(v < -16 ? 8 : (v + 32) / 2); } +/* + * The 40 MHz centre channel, or -1 if this control channel cannot carry + * 40 MHz. mt76x2u_phy_set_channel()'s 40 MHz case, plus the validation it does + * not need: mt76 gets the segment centre from cfg80211's chandef, so an + * off-grid channel never reaches it. This API takes a bare channel number and + * derives the centre from the standard pairing - 36/44/149/157 take the upper + * half, 40/48/153/161 the lower - which means a channel that is not on the + * grid still produces *a* number, and the caller transmits 40 MHz wide + * somewhere it did not ask for. Every register write succeeds, so nothing + * says so. + * + * Two cases made that reachable through the public API: + * + * - the arithmetic ran in uint8_t, so control channel 254 computed 256 and + * truncated to 0, handing the MCU channel index 0 with the 5 GHz register + * set loaded. 255 gave 1. + * - in 2.4 GHz the pairing only ever reaches centres 6-9. Channel 1 computed + * -1, i.e. 255 after truncation; 12 and 13 would need a secondary above + * channel 13. + * + * Validating the centre against the ones that exist catches all of it at once, + * including both wraps - no wrapped value aliases onto a legal centre. The + * 5 GHz ceiling is channel 159 rather than 175 because centres 167 and 175 + * span past the band mt7612u_caps declares (5825 MHz); widen the declared band + * first if they are wanted. + */ +int mt_chan40_centre(uint8_t chan, uint8_t *bw_index, uint8_t *ch_group) +{ + static const uint8_t centre_5g[] = { + 38, 46, 54, 62, 102, 110, 118, 126, 134, 142, 151, 159, + }; + static const uint8_t centre_2g[] = { 6, 7, 8, 9 }; + const uint8_t *ok = chan > 14 ? centre_5g : centre_2g; + size_t n_ok = chan > 14 ? sizeof centre_5g / sizeof centre_5g[0] + : sizeof centre_2g / sizeof centre_2g[0]; + int group = ((chan / 4) & 1) ? 0 : 1; + int idx = group ? 3 : 1; + /* int, not uint8_t: this is the expression that wrapped. */ + int centre = chan + 2 - group * 4; + size_t i; + + for (i = 0; i < n_ok; i++) { + if (centre != ok[i]) + continue; + if (bw_index) *bw_index = (uint8_t)idx; + if (ch_group) *ch_group = (uint8_t)group; + return centre; + } + ERR("channel %u cannot carry 40 MHz: it would centre on %d, which is " + "not a usable 40 MHz centre channel in this band", chan, centre); + return -1; +} + /* 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) @@ -333,14 +386,11 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) uint8_t bw_index = 0, ch_group_index = 0, hw_chan = chan; if (bw == 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. */ - int sec_above = (chan / 4) & 1; - - if (sec_above) { bw_index = 1; ch_group_index = 0; } - else { bw_index = 3; ch_group_index = 1; } - hw_chan = (uint8_t)(chan + 2 - ch_group_index * 4); + int centre = mt_chan40_centre(chan, &bw_index, &ch_group_index); + + if (centre < 0) + return -1; + hw_chan = (uint8_t)centre; } else if (bw != MT7612U_BW_20) { ERR("only 20 and 40 MHz are implemented"); return -1; diff --git a/src/mt7612u/tests/frame_shape.c b/src/mt7612u/tests/frame_shape.c index a266135..a23061c 100644 --- a/src/mt7612u/tests/frame_shape.c +++ b/src/mt7612u/tests/frame_shape.c @@ -193,11 +193,83 @@ static void test_vht_bandwidth(void) } } +/* + * The 40 MHz centre channel. The hardware tunes the centre, not the control + * channel, and an off-grid control channel still produces *a* number: every + * register write succeeds and the part transmits 40 MHz wide somewhere the + * caller did not ask for. Two cases were reachable through the public API - + * control channel 254 computed 256 and truncated to 0, and 2.4 GHz channel 1 + * computed -1, i.e. 255. + */ +static void test_chan40_centre(void) +{ + static const struct { uint8_t chan; int centre; uint8_t idx, group; } ok[] = { + { 36, 38, 1, 0 }, { 40, 38, 3, 1 }, + { 44, 46, 1, 0 }, { 48, 46, 3, 1 }, + { 149, 151, 1, 0 }, { 153, 151, 3, 1 }, + { 157, 159, 1, 0 }, { 161, 159, 3, 1 }, + { 100, 102, 1, 0 }, { 128, 126, 3, 1 }, + /* 2.4 GHz reaches centres 6-9, i.e. control channels 4-11. */ + { 4, 6, 1, 0 }, { 6, 8, 1, 0 }, + { 8, 6, 3, 1 }, { 11, 9, 3, 1 }, + }; + static const struct { uint8_t chan; const char *why; } refused[] = { + { 254, "computes 256, which truncated to channel 0" }, + { 255, "computes 257, which truncated to channel 1" }, + { 1, "computes -1, which truncated to channel 255" }, + { 2, "computes 0" }, + { 13, "would need a secondary above channel 13" }, + { 165, "centre 167 is outside the declared band" }, + { 177, "centre 175 is outside the declared band" }, + { 15, "not on the 5 GHz 40 MHz grid" }, + { 35, "not on the 5 GHz 40 MHz grid" }, + }; + unsigned i; + + printf("mt_chan40_centre:\n"); + for (i = 0; i < sizeof ok / sizeof ok[0]; i++) { + uint8_t idx = 0xff, group = 0xff; + int got = mt_chan40_centre(ok[i].chan, &idx, &group); + + if (got != ok[i].centre || idx != ok[i].idx || group != ok[i].group) { + printf(" FAIL ch %3u: want centre %d idx %u group %u, " + "got %d idx %u group %u\n", ok[i].chan, ok[i].centre, + ok[i].idx, ok[i].group, got, idx, group); + fails++; + } + } + for (i = 0; i < sizeof refused / sizeof refused[0]; i++) { + uint8_t idx = 0x5a, group = 0x5a; + + if (mt_chan40_centre(refused[i].chan, &idx, &group) >= 0) { + printf(" FAIL ch %3u accepted (%s)\n", + refused[i].chan, refused[i].why); + fails++; + continue; + } + /* A refusal must not write the outputs: a caller that checks the + * return but reuses the buffer would tune what was left behind. */ + if (idx != 0x5a || group != 0x5a) { + printf(" FAIL ch %3u refused but wrote its outputs\n", + refused[i].chan); + fails++; + } + } + /* Negative control: the centre must differ from the control channel. If + * this passes, the maths degenerated to a pass-through and every case + * above would still look right. */ + if (mt_chan40_centre(36, NULL, NULL) == 36) { + printf(" FAIL negative control: 40 MHz returned the control channel\n"); + fails++; + } +} + int main(void) { test_hdrlen(); test_rx_l2pad(); test_vht_bandwidth(); + test_chan40_centre(); printf("frame_shape: %s\n", fails ? "FAIL" : "PASS"); return fails ? 1 : 0; } From eca657c27ea807686ef7028a74dc63935c06adf1 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 6 Sep 2026 20:52:30 +0200 Subject: [PATCH 10/12] =?UTF-8?q?mt7612u:=20act=20on=20the=20Qodo=20review?= =?UTF-8?q?=20=E2=80=94=20five=20real,=20two=20not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourteen findings; each was checked against the code and against mt76 rather than taken on trust. Fixed: - **Radiotap HT bandwidth gated on the wrong bit.** The MCS `known` byte declares bandwidth with HAVE_BW (0x01); HAVE_MCS is 0x02. We tested 0x02, so a header declaring bandwidth without an MCS index -- legal radiotap -- silently narrowed a requested 40 MHz frame to 20. This is the HT twin of the VHT bandwidth bug caught in review; the same slip, missed twice. Mutation-tested: restoring 0x02 fails two cases in both directions. - **A malformed radiotap header transmitted anyway.** A declared field running past the declared header length returned the header length, which both injection entry points read as success and then aired at whatever defaults had accumulated. Now refused. An unknown *trailing* present bit still stops cleanly -- that header is well formed, we just cannot read the rest of it. - **The table generator's --check crashed on anything before Python 3.13.** `Path.read_text(newline=...)` is 3.13-only; the documented command and the ctest cell would TypeError on the 3.12 that CI images ship. This one was invisible here precisely because this box runs 3.13. - **phy_name[phy & 7] indexed a five-entry array.** MT_RATE_PHY is three bits, so 5-7 are representable and read past the end -- reachable from command-line input and from a descriptor. (by_phy[] was already 8 and is fine.) - **gate_g reported frames it never sent.** It discarded every transmit return, printed the requested count and returned 0, so a run where every submit failed still handed the witness an experiment that never aired -- and "no frames decoded" would have read as a negative finding rather than as a silent transmitter. Now counts what actually went out and fails when an arm sent nothing. - The ACK gate cast an unchecked signed duration to unsigned and slept uninterruptibly; -1 meant roughly 49 days with the receiver running. Now the SIGINT-aware wait every other gate uses. Not defects, and now commented so the next reader does not re-derive them: - The 2.4 GHz VHT power double shift is byte-for-byte mt76's own `mt76x2_get_rate_power()`, and `rate_power_val` takes a u8 on both sides. Changing it would be a silent divergence from the reference this port is checked against. - `mt7612u_clear_ack_responder()` not clearing MT_AUTO_RSP_EN is correct: init writes MT_AUTO_RSP_CFG = 0x13 and the EN bit is BIT(0), so the gate is already on before any caller arms a responder. Clearing it would leave the device in a state its own init never produces. On air, unchanged: `bringup rtap 149 400` still gives tag A 400/400 and tag B 377/400 at rate 19, against the 401/388 recorded before. 60/60 ctest. Still open from the review and NOT addressed here: transfer teardown racing libusb ownership, mt_wr() discarding failures, EEPROM read errors reaching calibration data as 0xff, the harness counter data race, and the LOG/ERR format. They are real, but each is a design change rather than a fix, and they want the maintainer's view first. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/caps.c | 9 ++++- src/mt7612u/eeprom.c | 8 ++++ src/mt7612u/radiotap.c | 26 +++++++++++-- src/mt7612u/tests/frame_shape.c | 67 +++++++++++++++++++++++++++++++++ src/mt7612u/tools/bringup.c | 56 +++++++++++++++++++++++---- tools/extract_mt7612u_tables.py | 10 ++++- 6 files changed, 163 insertions(+), 13 deletions(-) diff --git a/src/mt7612u/caps.c b/src/mt7612u/caps.c index d895376..a7a7576 100644 --- a/src/mt7612u/caps.c +++ b/src/mt7612u/caps.c @@ -105,7 +105,14 @@ void mt7612u_clear_ack_responder(struct mt7612u_dev *d) /* 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. */ + * 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; diff --git a/src/mt7612u/eeprom.c b/src/mt7612u/eeprom.c index c3d61b1..44a03e8 100644 --- a/src/mt7612u/eeprom.c +++ b/src/mt7612u/eeprom.c @@ -113,6 +113,14 @@ void mt_get_rate_power(struct mt7612u_dev *d, struct mt_rate_power *t, int band) 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; diff --git a/src/mt7612u/radiotap.c b/src/mt7612u/radiotap.c index 7d31f90..770619b 100644 --- a/src/mt7612u/radiotap.c +++ b/src/mt7612u/radiotap.c @@ -142,8 +142,20 @@ int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r) if (!size) return (int)rlen; off = (off + align - 1) & ~((size_t)align - 1); - if (off + size > rlen) - return (int)rlen; + /* 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; @@ -176,7 +188,15 @@ int mt_radiotap_parse(const uint8_t *buf, size_t len, struct mt7612u_tx_rate *r) r->phy = MT7612U_PHY_HT; r->mcs = p[2]; r->nss = (uint8_t)(1 + (p[2] >> 3)); - if ((known & 0x02) && ((flags & 0x03) == 1)) + /* 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; diff --git a/src/mt7612u/tests/frame_shape.c b/src/mt7612u/tests/frame_shape.c index a23061c..10e8420 100644 --- a/src/mt7612u/tests/frame_shape.c +++ b/src/mt7612u/tests/frame_shape.c @@ -264,12 +264,79 @@ static void test_chan40_centre(void) } } +/* + * 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++; + } + } +} + int main(void) { test_hdrlen(); test_rx_l2pad(); test_vht_bandwidth(); test_chan40_centre(); + 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 index 5f307a9..ed0a357 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -266,7 +266,10 @@ static int gate_tx(uint8_t chan, int count, int phy, int mcs) .phy = (enum mt7612u_phy)phy, .mcs = (uint8_t)mcs, .nss = 1, .bw = MT7612U_BW_20, .no_ack = 1, .power_adj = 0, }; - const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + /* 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)) @@ -320,7 +323,10 @@ static int gate_tx(uint8_t chan, int count, int phy, int mcs) /* Gate F: monitor RX. Decode rate/BW and per-chain RSSI from the RXWI. */ static int gate_rx(uint8_t chan, int want) { - static const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + 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; @@ -425,6 +431,7 @@ static int gate_g(uint8_t chan, int count) 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; @@ -446,10 +453,16 @@ static int gate_g(uint8_t chan, int count) frame[36] = 'T'; frame[37] = (uint8_t)i; frame[38] = (uint8_t)(i >> 8); - mt7612u_tx(&dev, frame, 40, (i & 1) ? &mcs7 : &mcs0); + if (mt7612u_tx(&dev, frame, 40, (i & 1) ? &mcs7 : &mcs0) == 0) + sent_alt++; mt_usleep(2000); } - printf(" sent %d frames, even index = MCS0, odd = MCS7\n", count); + /* 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. */ @@ -463,18 +476,31 @@ static int gate_g(uint8_t chan, int count) 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; - mt_tx_raw(&dev, frame, 40, &mcs7, 1, arm); + 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 -> 150 frames\n", - arm ? 'B' : 'A', arm ? "SET" : "clear"); + 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; } @@ -596,7 +622,10 @@ static void arx_cb(void *user, const void *frame, size_t len, /* Async RX ring: the callback path StartRxLoop needs. */ static int gate_arx(uint8_t chan, int secs) { - static const char *phy_name[] = { "CCK", "OFDM", "HT", "HT-GF", "VHT" }; + 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; @@ -1041,6 +1070,17 @@ static int gate_ack(uint8_t chan, int secs, int arm) 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); mt_usleep((unsigned)secs * 1000000u); mt7612u_rx_stop(&dev); diff --git a/tools/extract_mt7612u_tables.py b/tools/extract_mt7612u_tables.py index aa1ecc2..bac92b4 100755 --- a/tools/extract_mt7612u_tables.py +++ b/tools/extract_mt7612u_tables.py @@ -288,7 +288,15 @@ def main() -> int: # artifact, so --check must compare identically on every platform. if args.check: path = root / OUTPUT_H - if not path.exists() or path.read_text(encoding="utf-8", newline="") != output: + # 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: From 1b032b506027bc5d8ca5ace31bfa5f5e1223ec7d Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 07:31:51 +0200 Subject: [PATCH 11/12] mt7612u: complete the subtree, and close the five deferred findings Per review: land this unwired but complete, and address the five items rather than waiting on a shape. Nothing here touches CMakeLists, WiFiDriver or the workflows -- the subtree is still unreachable from the shipped library. **The subtree is now complete on its own.** The invalid-PHY rate-word drop is back, as asked, and so is the rest of the subtree-level work that had drifted onto the integration branch, because the same argument applies to all of it: the per-frame noise floor and derived SNR, 80 MHz, the frame-size limits, and the adapter lock shared with UsbDeviceLock. What stayed behind is only the wiring -- the IRtlDevice implementation, the VID:PID table and the mapping header. **Write failures now propagate.** mt_wr() discarded mt_vendor_req()'s result, so a write that exhausted its retries left the hardware partly configured while the call returned success. Threading a status through every writer would touch several hundred call sites in a sequence that is deliberately a verbatim port of mt76's, and the churn would bury what it protects. So this follows the shape the reads already had -- mt_rr_chk() has always bumped d->io_err: writes stay best-effort at the call site, and a SEQUENCE checks the accumulator at its boundary. mt_eeprom_init(), mt_init_hardware() and mt_set_channel_ex() are bracketed and now fail as a whole; optional writes stay best-effort by not being bracketed. mt_wr_chk() exists for a caller that wants to fail at the write instead. **EEPROM read errors no longer reach calibration as data.** The slurp must not treat 0xffffffff as an error by value -- unprogrammed cells legitimately read all-ones -- but the accumulator distinguishes a blank cell from a failed transfer. A failure outside the two cells that were already validated used to leave all-ones power and calibration bytes in memory while the device opened successfully and transmitted from them. **Teardown ownership is explicit.** Leaking the ring on the two-second deadline was the safe half; the other half was that mt_close() then released the interface, closed the handle and exited the context while libusb still owned those transfers and the event thread had just been joined, so nothing could ever complete them. The device is marked stranded: mt_close() 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. **The harness counter race is closed.** arx_cb() and ack_cb() run on the libusb event thread while gate_arx()/gate_duplex() read the same fields before stopping it, so a displayed rate or a duplex verdict could be built from torn counts. Relaxed atomics rather than per-gate locks: they are counters, nothing orders off them, and this is the RX hot path in a throughput gate. **LOG/ERR follow docs/logging.md.** They used a private `[mt7612u]` prefix instead of `devourer [X] `, emitted text and newline as two stdio calls so a line from the event thread could split one from a caller, and never flushed. mt_diag() formats the line first and emits it with one fwrite + fflush, as src/Event.h does for the machine plane. Warning-level diagnostics moved to [W]. Verified on hardware after the rework, 19 gates, 0 failures: regs, fw, init, chan, caps, hop, pwr, linkstat, ampdu and soak self-check; rtap 781 frames, sweep 1617 at 20 MHz / 1639 at bw=1 / 828 at bw=2, coding 1919, gateg 450, mtu 400 and vht 748 all witnessed by an RTL8812AU; and MT7612U-to-MT7612U 2999 frames with rx_err, rx_invalid and rx_dropped all zero. Offline: make check green and the extractor --check still reproduces initvals.h against the pinned reference/mt76. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/Makefile | 4 +- src/mt7612u/README.md | 32 +- src/mt7612u/async.c | 64 +- src/mt7612u/caps.c | 12 +- src/mt7612u/eeprom.c | 20 +- src/mt7612u/include/mt7612u/mt7612u.h | 159 ++++- src/mt7612u/init.c | 170 ++++- src/mt7612u/internal.h | 62 +- src/mt7612u/phy.c | 214 ++++-- src/mt7612u/radiotap.c | 11 + src/mt7612u/regs.h | 32 + src/mt7612u/rx.c | 33 +- src/mt7612u/tests/api_link.c | 5 + src/mt7612u/tests/frame_shape.c | 204 ++++-- src/mt7612u/tools/bringup.c | 962 +++++++++++++++++++++++++- src/mt7612u/tx.c | 24 +- src/mt7612u/usb.c | 323 ++++++++- 17 files changed, 2151 insertions(+), 180 deletions(-) diff --git a/src/mt7612u/Makefile b/src/mt7612u/Makefile index e205d01..e0b9355 100644 --- a/src/mt7612u/Makefile +++ b/src/mt7612u/Makefile @@ -1,4 +1,6 @@ -# Standalone. Not reachable from the project's CMakeLists.txt. +# 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 diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md index 74b2210..4194a3c 100644 --- a/src/mt7612u/README.md +++ b/src/mt7612u/README.md @@ -1,8 +1,12 @@ -# src/mt7612u — MediaTek MT7612U, standalone +# src/mt7612u — MediaTek MT7612U -**Not built by `CMakeLists.txt`.** This subtree does not implement -`IRtlDevice`, is not reachable from `WiFiDriver`, and adds nothing to the -library target. It builds on its own: +**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 @@ -86,6 +90,26 @@ gateg per-frame rate control ack ACK responder (needs a stimul 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` diff --git a/src/mt7612u/async.c b/src/mt7612u/async.c index 5d75eae..aa8b84f 100644 --- a/src/mt7612u/async.c +++ b/src/mt7612u/async.c @@ -55,7 +55,21 @@ static void LIBUSB_CALL rx_done(struct libusb_transfer *t) struct mt7612u_rx_info info; int len = mt_rx_parse(d, t->buffer, t->actual_length, &frame, &info); - if (len > 0) { + 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); @@ -103,6 +117,11 @@ static void LIBUSB_CALL tx_done(struct libusb_transfer *t) 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; @@ -210,9 +229,18 @@ void mt_async_stop(struct mt7612u_dev *d) 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 rather than freeing memory libusb owns", - stuck_tx, stuck_rx); + "- leaking the ring, and the USB handle with it, rather than " + "freeing memory libusb still owns", stuck_tx, stuck_rx); return; } @@ -285,6 +313,8 @@ void mt_async_stats(struct mt7612u_dev *d, struct mt_async_stats *out) 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); } @@ -299,3 +329,31 @@ 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 index a7a7576..e387560 100644 --- a/src/mt7612u/caps.c +++ b/src/mt7612u/caps.c @@ -41,9 +41,15 @@ void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) c->chip_name = "MT7612U"; c->rev = d->rev; c->nss_rx = c->nss_tx = (uint8_t)((d->chainmask & 0xf) > 1 ? 2 : 1); - /* 80 MHz is silicon-capable but the width maths is not ported yet; - * advertise what this driver actually delivers, not what the part could. */ - c->bw_mask = 0x3; /* 20 and 40 MHz */ + /* 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; diff --git a/src/mt7612u/eeprom.c b/src/mt7612u/eeprom.c index 44a03e8..b286275 100644 --- a/src/mt7612u/eeprom.c +++ b/src/mt7612u/eeprom.c @@ -16,9 +16,19 @@ uint16_t mt_ee(const struct mt7612u_dev *d, unsigned off) int mt_eeprom_init(struct mt7612u_dev *d) { - /* Do NOT treat 0xffffffff as a read error here: unprogrammed EEPROM + /* 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). Validate the image below instead. */ + * 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)); @@ -27,6 +37,12 @@ int mt_eeprom_init(struct mt7612u_dev *d) 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", diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index cfa130f..871aa10 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -66,9 +66,38 @@ struct mt7612u_rx_info { 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; @@ -79,16 +108,42 @@ struct mt7612u_dev; * 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 an 802.11 channel number. Issues + * 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. Only 20 and 40 MHz are - * implemented; 80 MHz is silicon-capable but the width maths is not ported. + * 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); @@ -126,6 +181,18 @@ typedef void (*mt7612u_rx_cb)(void *user, const void *frame, size_t len, 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 @@ -153,6 +220,72 @@ size_t mt7612u_send_packets(struct mt7612u_dev *dev, 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); @@ -163,6 +296,26 @@ struct mt7612u_caps { 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 */ diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index d24344c..c30d56e 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -2,9 +2,10 @@ /* * 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 PLAN.md: trimming this sequence is a post-Gate-E activity, because a + * 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" @@ -86,7 +87,7 @@ static void power_on(struct mt7612u_dev *d) 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)) - LOG("warning: MTCMOS power-up did not ack"); + WARN("MTCMOS power-up did not ack"); mt_clear(d, CFG_ADDR(MT_CFG_MTC_CTRL), 0x7fu << 16); mt_usleep(20); @@ -171,7 +172,12 @@ static void mac_reset(struct mt7612u_dev *d) 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); @@ -304,7 +310,7 @@ int mt_mac_stop(struct mt7612u_dev *d) mt_usleep(15); } if (!mt_poll(d, MT_MAC_STATUS, MT_MAC_STATUS_RX, 0, 200000)) - LOG("warning: MAC RX failed to stop"); + WARN("MAC RX failed to stop"); mt_wr(d, MT_TX_RTS_CFG, rts_cfg); return 0; @@ -342,6 +348,13 @@ void mt_power_cycle(struct mt7612u_dev *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; } @@ -366,7 +379,7 @@ int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir) mac_setaddr(d); if (!mt_poll(d, MT_MAC_STATUS, MT_MAC_STATUS_TX | MT_MAC_STATUS_RX, 0, 100000)) - LOG("warning: TX/RX not idle before table clear"); + WARN("TX/RX not idle before table clear"); wcid_and_key_clear(d); @@ -387,20 +400,23 @@ int mt_init_hardware(struct mt7612u_dev *d, const char *fw_dir) /* Leave no half-full RX ring behind for the next run to inherit. */ mt_rx_flush(d); - return mt_mac_stop(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 --- */ -struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err) +/* 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) { - 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; } if (mt_eeprom_init(d)) { if (err) *err = "EEPROM image did not validate"; goto fail; @@ -417,6 +433,34 @@ struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err) 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; @@ -443,3 +487,103 @@ 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); +} + +static uint64_t g_stats_last_us; + +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 = g_stats_last_us ? (uint32_t)(now - g_stats_last_us) : 0; + g_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/internal.h b/src/mt7612u/internal.h index b344027..17e4a37 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -55,6 +55,21 @@ struct mt7612u_cal { #define MT_RX_BUFSZ 4096 #define MT_TX_BUFSZ 2048 #define MT_USB_AGG_BUF 16384 /* one aggregated bulk-OUT transfer */ + +/* + * 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 #define MT_USB_AGG_MAX 32 /* frames chained per transfer */ struct mt7612u_dev; @@ -80,7 +95,8 @@ struct mt_async { 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; + 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 { @@ -88,6 +104,10 @@ struct mt7612u_dev { 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]; @@ -96,6 +116,7 @@ struct mt7612u_dev { 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]; @@ -104,6 +125,8 @@ struct mt7612u_dev { 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 */ /* Oracle-diff log: every EP0 write we emit, in order. */ uint8_t ack_saved_mac[6]; @@ -115,6 +138,9 @@ struct mt7612u_dev { /* --- 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 @@ -124,6 +150,10 @@ 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. */ @@ -187,9 +217,11 @@ 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; + 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); @@ -211,9 +243,29 @@ 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_chan40_centre(uint8_t chan, uint8_t *bw_index, uint8_t *ch_group); +int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, + uint8_t *bw_index, uint8_t *ch_group); -#define LOG(...) do { fprintf(stderr, "[mt7612u] " __VA_ARGS__); fputc('\n', stderr); } while (0) -#define ERR(...) do { fprintf(stderr, "[mt7612u] ERROR " __VA_ARGS__); fputc('\n', stderr); } while (0) +/* + * 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/phy.c b/src/mt7612u/phy.c index 83cf758..5a06265 100644 --- a/src/mt7612u/phy.c +++ b/src/mt7612u/phy.c @@ -312,56 +312,121 @@ int8_t mt_tx_get_txpwr_adj(struct mt7612u_dev *d, int8_t txpwr, int8_t max_adj) } /* - * The 40 MHz centre channel, or -1 if this control channel cannot carry - * 40 MHz. mt76x2u_phy_set_channel()'s 40 MHz case, plus the validation it does - * not need: mt76 gets the segment centre from cfg80211's chandef, so an - * off-grid channel never reaches it. This API takes a bare channel number and - * derives the centre from the standard pairing - 36/44/149/157 take the upper - * half, 40/48/153/161 the lower - which means a channel that is not on the - * grid still produces *a* number, and the caller transmits 40 MHz wide - * somewhere it did not ask for. Every register write succeeds, so nothing - * says so. + * 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. * - * Two cases made that reachable through the public API: + * 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. * - * - the arithmetic ran in uint8_t, so control channel 254 computed 256 and - * truncated to 0, handing the MCU channel index 0 with the 5 GHz register - * set loaded. 255 gave 1. - * - in 2.4 GHz the pairing only ever reaches centres 6-9. Channel 1 computed - * -1, i.e. 255 after truncation; 12 and 13 would need a secondary above - * channel 13. - * - * Validating the centre against the ones that exist catches all of it at once, - * including both wraps - no wrapped value aliases onto a legal centre. The - * 5 GHz ceiling is channel 159 rather than 175 because centres 167 and 175 - * span past the band mt7612u_caps declares (5825 MHz); widen the declared band - * first if they are wanted. + * Returns 0, or -1 when the control channel cannot carry the width. */ -int mt_chan40_centre(uint8_t chan, uint8_t *bw_index, uint8_t *ch_group) +int mt_chan_group(uint8_t chan, uint8_t bw, uint8_t *hw_chan, + uint8_t *bw_index, uint8_t *ch_group) { - static const uint8_t centre_5g[] = { + /* + * 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, }; - static const uint8_t centre_2g[] = { 6, 7, 8, 9 }; - const uint8_t *ok = chan > 14 ? centre_5g : centre_2g; - size_t n_ok = chan > 14 ? sizeof centre_5g / sizeof centre_5g[0] - : sizeof centre_2g / sizeof centre_2g[0]; - int group = ((chan / 4) & 1) ? 0 : 1; - int idx = group ? 3 : 1; - /* int, not uint8_t: this is the expression that wrapped. */ - int centre = chan + 2 - group * 4; - size_t i; - - for (i = 0; i < n_ok; i++) { - if (centre != ok[i]) - continue; - if (bw_index) *bw_index = (uint8_t)idx; - if (ch_group) *ch_group = (uint8_t)group; - return centre; + /* 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; } - ERR("channel %u cannot carry 40 MHz: it would centre on %d, which is " - "not a usable 40 MHz centre channel in this band", chan, centre); + + 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 @@ -385,20 +450,22 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) int band = chan > 14 ? BAND_5GHZ : BAND_2GHZ; uint8_t bw_index = 0, ch_group_index = 0, hw_chan = chan; - if (bw == MT7612U_BW_40) { - int centre = mt_chan40_centre(chan, &bw_index, &ch_group_index); - - if (centre < 0) - return -1; - hw_chan = (uint8_t)centre; - } else if (bw != MT7612U_BW_20) { - ERR("only 20 and 40 MHz are implemented"); + 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); @@ -412,14 +479,41 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) MT_EXT_CCA_CFG_CCA3 | MT_EXT_CCA_CFG_CCA_MASK, ext_cca_chan[ch_group_index]); - if (mt_mcu_set_channel(d, hw_chan, bw, bw_index, 0)) + /* 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)) + } + 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]; @@ -465,6 +559,12 @@ int mt_set_channel_ex(struct mt7612u_dev *d, uint8_t chan, uint8_t bw, int fast) 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; } @@ -487,12 +587,16 @@ int mt7612u_set_txpower(struct mt7612u_dev *d, int dbm) /* * 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) { - ERR("set_channel: only 20 and 40 MHz are implemented (bw=%d)", (int)bw); + 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); diff --git a/src/mt7612u/radiotap.c b/src/mt7612u/radiotap.c index 770619b..b946b08 100644 --- a/src/mt7612u/radiotap.c +++ b/src/mt7612u/radiotap.c @@ -282,6 +282,17 @@ size_t mt7612u_send_packets(struct mt7612u_dev *d, /* 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; diff --git a/src/mt7612u/regs.h b/src/mt7612u/regs.h index 339d0cc..1c6607b 100644 --- a/src/mt7612u/regs.h +++ b/src/mt7612u/regs.h @@ -303,8 +303,40 @@ enum mt_mcu_cr_mode { MT_RF_CR, MT_BBP_CR, MT_RF_BBP_CR, MT_HL_TEMP_CR_UPDATE }; #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) diff --git a/src/mt7612u/rx.c b/src/mt7612u/rx.c index 631a97a..83afcb3 100644 --- a/src/mt7612u/rx.c +++ b/src/mt7612u/rx.c @@ -18,12 +18,23 @@ static uint32_t get_le32(const uint8_t *p) } 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. */ -static void decode_rate(uint16_t rate, struct mt7612u_rx_info *out) +/* + * 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); @@ -44,6 +55,7 @@ static void decode_rate(uint16_t rate, struct mt7612u_rx_info *out) out->nss = 1; break; } + return 0; } /* @@ -70,7 +82,10 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, info->seq = (uint16_t)(get_le16(rxwi + 8) >> 4); info->crc_err = !!(rxinfo & MT_RXINFO_CRCERR); info->ampdu = !!(rxinfo & MT_RXINFO_AMPDU); - decode_rate(get_le16(rxwi + 10), info); + 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 @@ -81,6 +96,18 @@ int mt_rx_parse(struct mt7612u_dev *d, uint8_t *buf, int n, (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; diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c index 8180835..5d2a955 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -15,6 +15,7 @@ static void *const api[] = { (void *)mt7612u_open, + (void *)mt7612u_open_handle, (void *)mt7612u_close, (void *)mt7612u_keep_detached, (void *)mt7612u_set_channel, @@ -25,10 +26,14 @@ static void *const api[] = { (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, diff --git a/src/mt7612u/tests/frame_shape.c b/src/mt7612u/tests/frame_shape.c index 10e8420..b2bf67f 100644 --- a/src/mt7612u/tests/frame_shape.c +++ b/src/mt7612u/tests/frame_shape.c @@ -194,73 +194,179 @@ static void test_vht_bandwidth(void) } /* - * The 40 MHz centre channel. The hardware tunes the centre, not the control - * channel, and an off-grid control channel still produces *a* number: every - * register write succeeds and the part transmits 40 MHz wide somewhere the - * caller did not ask for. Two cases were reachable through the public API - - * control channel 254 computed 256 and truncated to 0, and 2.4 GHz channel 1 - * computed -1, i.e. 255. + * 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_chan40_centre(void) +static void test_invalid_phy(void) { - static const struct { uint8_t chan; int centre; uint8_t idx, group; } ok[] = { - { 36, 38, 1, 0 }, { 40, 38, 3, 1 }, - { 44, 46, 1, 0 }, { 48, 46, 3, 1 }, - { 149, 151, 1, 0 }, { 153, 151, 3, 1 }, - { 157, 159, 1, 0 }, { 161, 159, 3, 1 }, - { 100, 102, 1, 0 }, { 128, 126, 3, 1 }, - /* 2.4 GHz reaches centres 6-9, i.e. control channels 4-11. */ - { 4, 6, 1, 0 }, { 6, 8, 1, 0 }, - { 8, 6, 3, 1 }, { 11, 9, 3, 1 }, + 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; const char *why; } refused[] = { - { 254, "computes 256, which truncated to channel 0" }, - { 255, "computes 257, which truncated to channel 1" }, - { 1, "computes -1, which truncated to channel 255" }, - { 2, "computes 0" }, - { 13, "would need a secondary above channel 13" }, - { 165, "centre 167 is outside the declared band" }, - { 177, "centre 175 is outside the declared band" }, - { 15, "not on the 5 GHz 40 MHz grid" }, - { 35, "not on the 5 GHz 40 MHz grid" }, + 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_chan40_centre:\n"); + printf("mt_chan_group:\n"); for (i = 0; i < sizeof ok / sizeof ok[0]; i++) { - uint8_t idx = 0xff, group = 0xff; - int got = mt_chan40_centre(ok[i].chan, &idx, &group); + uint8_t hw = 0xff, idx = 0xff, group = 0xff; - if (got != ok[i].centre || idx != ok[i].idx || group != ok[i].group) { - printf(" FAIL ch %3u: want centre %d idx %u group %u, " - "got %d idx %u group %u\n", ok[i].chan, ok[i].centre, - ok[i].idx, ok[i].group, got, idx, group); + 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 idx = 0x5a, group = 0x5a; + uint8_t hw = 0x5a, idx = 0x5a, group = 0x5a; - if (mt_chan40_centre(refused[i].chan, &idx, &group) >= 0) { - printf(" FAIL ch %3u accepted (%s)\n", - refused[i].chan, refused[i].why); + 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 tune what was left behind. */ - if (idx != 0x5a || group != 0x5a) { - printf(" FAIL ch %3u refused but wrote its outputs\n", - refused[i].chan); + * 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 centre must differ from the control channel. If - * this passes, the maths degenerated to a pass-through and every case - * above would still look right. */ - if (mt_chan40_centre(36, NULL, NULL) == 36) { - printf(" FAIL negative control: 40 MHz returned the control channel\n"); - 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++; + } } } @@ -330,12 +436,14 @@ static void test_ht_bandwidth(void) } } + int main(void) { test_hdrlen(); + test_invalid_phy(); test_rx_l2pad(); + test_chan_group(); test_vht_bandwidth(); - test_chan40_centre(); 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 index ed0a357..39c06d8 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -1,18 +1,22 @@ /* SPDX-License-Identifier: BSD-3-Clause-Clear */ /* - * MT7612U bringup harness. One subcommand per gate from PLAN.md, so each + * 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) @@ -21,6 +25,40 @@ static double now_ms(void) 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; @@ -412,7 +450,7 @@ static int gate_hop(void) } /* - * Gate G, as PLAN.md actually specified it: + * 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 @@ -514,6 +552,75 @@ static double cpu_ms(void) } /* 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. + */ +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; + + 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.\n" + "'accepted' is what this driver submitted; the witness reports\n" + "which lengths actually decoded.\n\n", chan, count); + printf(" %-6s %-10s %s\n", "bytes", "accepted", "note"); + + for (k = 0; k < sizeof sizes / sizeof sizes[0]; k++) { + int len = sizes[k]; + long ok = 0; + int i; + + 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 (i = 0; i < count; i++) { + frame[38] = (uint8_t)i; + if (mt7612u_tx(&dev, frame, (size_t)len, &rate) == 0) ok++; + mt_usleep(1500); + } + printf(" %-6d %ld/%-8d %s\n", len, ok, count, + ok == 0 ? "refused by this driver" : + (len > 2304 ? "above the 802.11 MPDU ceiling" : "")); + mt_usleep(120000); + } + + mt_mac_stop(&dev); + 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 }; @@ -609,14 +716,23 @@ static int gate_soak(uint8_t chan, int secs, int framelen) return 0; } -struct arx_ctx { unsigned long n; unsigned long by_phy[8]; }; +/* + * 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; - c->n++; - c->by_phy[info->phy & 7]++; + 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. */ @@ -640,17 +756,23 @@ static int gate_arx(uint8_t chan, int secs) printf("GATE arx: FAIL - rx_start failed\n"); return 1; } t0 = now_ms(); - while (now_ms() - t0 < secs * 1000.0) - mt_usleep(100000); + 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 %d s: %lu frames (%.0f/s), rx_err=%llu\n", - chan, secs, ctx.n, ctx.n / (double)secs, - (unsigned long long)st.rx_err); + 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 < 5; i++) + 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); @@ -883,7 +1005,8 @@ static void drain_cb(void *user, const void *frame, size_t len, const struct mt7612u_rx_info *info) { (void)frame; (void)len; (void)info; - (*(unsigned long *)user)++; + atomic_fetch_add_explicit((_Atomic unsigned long *)user, 1, + memory_order_relaxed); } /* Capability descriptor, TSF and 40 MHz. */ @@ -914,6 +1037,10 @@ static int gate_caps(uint8_t chan) 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", @@ -1017,7 +1144,8 @@ static int gate_caps(uint8_t chan) * The RX filter must keep MT_RX_FILTR_CFG_DUP clear or the hardware drops the * duplicates this test is counting. */ -struct ack_ctx { unsigned long to_us, retry_to_us, other; }; +/* 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 }; @@ -1029,9 +1157,13 @@ static void ack_cb(void *user, const void *frame, size_t len, (void)info; if (len < 16) return; - if (memcmp(f + 4, g_ack_mac, 6) != 0) { c->other++; return; } - c->to_us++; - if (f[1] & 0x08) c->retry_to_us++; /* FC Retry bit */ + 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) @@ -1082,7 +1214,15 @@ static int gate_ack(uint8_t chan, int secs, int arm) return 1; } printf("listening %d s ...\n", secs); - mt_usleep((unsigned)secs * 1000000u); + /* 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); @@ -1103,6 +1243,748 @@ static int gate_ack(uint8_t chan, int secs, int arm) 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; + 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); + 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); + 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; + 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); + 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); + 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]; + 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); + 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"); + return 0; +} + /* * The two radiotap entry points: send_packet (one framed MPDU) and * send_packets (several, chained into one bulk-OUT transfer via @@ -1198,7 +2080,23 @@ 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")) && + (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; + } + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); if (mt_open(&dev, &err)) { fprintf(stderr, "open failed: %s\n", err ? err : "?"); return 1; @@ -1215,6 +2113,34 @@ int main(int argc, char **argv) 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); @@ -1253,6 +2179,8 @@ int main(int argc, char **argv) } else { fprintf(stderr, "unknown subcommand '%s'\n", cmd); fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); + fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); + fprintf(stderr, " the witness must listen at the same width (DEVOURER_BW=40|80)\n"); rc = 2; } diff --git a/src/mt7612u/tx.c b/src/mt7612u/tx.c index 0624259..58e1050 100644 --- a/src/mt7612u/tx.c +++ b/src/mt7612u/tx.c @@ -10,7 +10,6 @@ #include #include "internal.h" -#define MT_TX_BUF_MAX 2048 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) @@ -138,7 +137,28 @@ int mt_tx_build(struct mt7612u_dev *d, uint8_t *buf, size_t bufsz, FIELD_PREP(MT_TXWI_FLAGS_MPDU_DENSITY, 4); put_le16(txwi + 0, fl); } - put_le16(txwi + 2, mt_tx_rate_word(rate)); /* rate */ + /* "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; diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index 288177b..710dd74 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -4,10 +4,47 @@ * 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 @@ -22,6 +59,9 @@ void mt_usleep(unsigned us) 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; @@ -99,7 +139,9 @@ void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val) b[0] = val & 0xff; b[1] = (val >> 8) & 0xff; b[2] = (val >> 16) & 0xff; b[3] = (val >> 24) & 0xff; - mt_vendor_req(d, req, REQ_OUT, (uint16_t)(a >> 16), (uint16_t)a, b, sizeof b); + 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) @@ -113,6 +155,36 @@ void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val) * (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; @@ -153,8 +225,11 @@ int mt_poll(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, void mt_single_wr(struct mt7612u_dev *d, uint8_t req, uint16_t off, uint32_t val) { - mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val & 0xffff), off, NULL, 0); - mt_vendor_req(d, req, REQ_OUT, (uint16_t)(val >> 16), (uint16_t)(off + 2), NULL, 0); + 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, @@ -182,15 +257,210 @@ int mt_wait_for_mac(struct mt7612u_dev *d) 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)) + h = NULL; + } + 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 = libusb_open_device_with_vid_pid(d->ctx, MT7612U_VID, MT7612U_PID); + d->h = open_selected(d->ctx, err); if (!d->h) { - if (err) *err = "MT7612U not found or permission denied (try sudo)"; libusb_exit(d->ctx); d->ctx = NULL; return -1; } @@ -214,7 +484,7 @@ int mt_open(struct mt7612u_dev *d, const char **err) /* Re-enumerated under a new address: reopen and re-detach. */ libusb_close(d->h); mt_usleep(200000); - d->h = libusb_open_device_with_vid_pid(d->ctx, MT7612U_VID, MT7612U_PID); + 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; @@ -223,7 +493,7 @@ int mt_open(struct mt7612u_dev *d, const char **err) if (libusb_kernel_driver_active(d->h, 0) == 1) libusb_detach_kernel_driver(d->h, 0); } else if (rc) { - LOG("warning: USB reset returned %s", libusb_error_name(rc)); + WARN("USB reset returned %s", libusb_error_name(rc)); } rc = libusb_claim_interface(d->h, 0); @@ -232,14 +502,7 @@ int mt_open(struct mt7612u_dev *d, const char **err) goto fail; } - /* 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)"; + if (mt_identify(d, err)) { libusb_release_interface(d->h, 0); goto fail; } @@ -257,15 +520,33 @@ 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) { - 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"); + 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); } - libusb_close(d->h); d->h = NULL; + d->h = NULL; } - if (d->ctx) { libusb_exit(d->ctx); d->ctx = 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. From 737b4c6e40a1ccdeaf93ac3ce21523e243715d92 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 7 Sep 2026 08:28:37 +0200 Subject: [PATCH 12/12] mt7612u: one TX ceiling, and four more from the re-review **The ring rejected what the 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 mt_tx_raw() takes the ring whenever one 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. The bench never saw it because the sync path has no ring, which is exactly the blind spot: the mtu sweep only ever exercised mt_bulk. Fixed by making the ring slot the single ceiling (MT_TX_BUFSZ = MT_USB_AGG_BUF) with a compile-time check that neither builder can outgrow it -- mutation-tested, putting the ring back to 2048 now fails the build rather than the link. Ring depth drops 32 -> 16 so slots carrying a full aggregate cost 256 KB rather than 512 KB; depth is not what buys throughput here, and soak/ampdu were re-measured after the change. The `mtu` gate now sweeps **both** TX paths and fails if they disagree, so this cannot hide again. On air, sync and async are now identical at every size: 40/40 each up to 4064, both refusing 4065, and the witness saw 80 per size -- proof both paths aired. Also from the re-review: - gate_coding / gate_sweep / gate_vht printed a shortfall and returned 0, the same structurally-guaranteed pass fixed in gate_g. They now count short arms and fail; non-positive frame counts are rejected outright. - gate_init and gate_chan passed NULL to mt_init_hardware(), so the documented `init ` form silently used the default directory. - open_selected() kept the adapter lock when libusb_open() failed. Only mt_close() releases it and a failed mt_open() never reaches there, so the process collided with its own stale lock on the next retry. - g_stats_last_us was file-static, so two adapters in one process overwrote each other's mark -- and every MIB counter is read-and-clear, so both got wrong rates. Now per device. Two more found while verifying, neither from the review: - bringup.c named a private downstream consumer in a comment. Described by role now. - gate_duplex's verdict was (n && ctx.n), which its own setup cannot satisfy: the radio is half duplex and the gate saturates TX, so RX during the flood is ~0. It has therefore never passed. Confirmed pre-existing by building the previous commit and reproducing it. Rewritten to assert what it can establish -- TX sustained, no errors, and the receiver still delivering after the flood stops -- and to say plainly that it needs a peer. Verified: 18 of 19 gates. The one gap is the MT7612U-to-MT7612U arm, blocked on the second adapter sitting in the RX-undrained wedge that only a physical replug clears (EP0 reads answer, bulk-OUT times out). The new ring is still covered on air by the mtu sweep's async pass against the RTL8812AU witness; what is NOT re-verified since the ring change is MT-to-MT, and the reworked duplex assertion has never had a working peer to run against. Both are stated rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- src/mt7612u/init.c | 9 +- src/mt7612u/internal.h | 29 +++++- src/mt7612u/tests/frame_shape.c | 48 +++++++++ src/mt7612u/tools/bringup.c | 175 +++++++++++++++++++++++++++----- src/mt7612u/usb.c | 9 +- 5 files changed, 238 insertions(+), 32 deletions(-) diff --git a/src/mt7612u/init.c b/src/mt7612u/init.c index c30d56e..6880eaf 100644 --- a/src/mt7612u/init.c +++ b/src/mt7612u/init.c @@ -525,7 +525,10 @@ static uint64_t stats_now_us(void) return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)(ts.tv_nsec / 1000); } -static uint64_t g_stats_last_us; +/* 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) { @@ -552,8 +555,8 @@ int mt7612u_link_stats(struct mt7612u_dev *d, struct mt7612u_link_stats *out) if (!d || !out) return -1; memset(out, 0, sizeof *out); - out->interval_us = g_stats_last_us ? (uint32_t)(now - g_stats_last_us) : 0; - g_stats_last_us = now; + 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); diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 17e4a37..d10e60e 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -51,11 +51,29 @@ struct mt7612u_cal { }; #define MT_RX_RING 16 -#define MT_TX_RING 32 +/* 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_TX_BUFSZ 2048 #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 @@ -70,6 +88,12 @@ struct mt7612u_cal { * 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; @@ -127,6 +151,7 @@ struct mt7612u_dev { 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]; diff --git a/src/mt7612u/tests/frame_shape.c b/src/mt7612u/tests/frame_shape.c index b2bf67f..3fd0826 100644 --- a/src/mt7612u/tests/frame_shape.c +++ b/src/mt7612u/tests/frame_shape.c @@ -437,9 +437,57 @@ static void test_ht_bandwidth(void) } +/* + * 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(); diff --git a/src/mt7612u/tools/bringup.c b/src/mt7612u/tools/bringup.c index 39c06d8..bfc0916 100644 --- a/src/mt7612u/tools/bringup.c +++ b/src/mt7612u/tools/bringup.c @@ -214,7 +214,7 @@ static int gate_init(const char *fw_dir) if (!dev.wrlog) printf("warning: could not open wrlog.txt for the oracle diff\n"); - if (mt_init_hardware(&dev, NULL)) { + if (mt_init_hardware(&dev, fw_dir)) { printf("GATE C: FAIL - init_hardware failed\n"); return 1; } @@ -258,7 +258,7 @@ static int gate_chan(uint8_t chan, const char *fw_dir) dev.wrlog = fopen("wrlog.txt", "w"); dev.mculog = fopen("mculog.txt", "w"); - if (mt_init_hardware(&dev, NULL)) { + if (mt_init_hardware(&dev, fw_dir)) { printf("GATE D: FAIL - init_hardware failed\n"); return 1; } @@ -564,6 +564,11 @@ static double cpu_ms(void) * 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 }; @@ -573,7 +578,8 @@ static int gate_mtu(uint8_t chan, int count) 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; + 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; @@ -588,33 +594,65 @@ static int gate_mtu(uint8_t chan, int count) memcpy(frame + 16, src, 6); memcpy(frame + 24, "MT7612U-HAL ", 12); - printf("ch%u, HT MCS7 20 MHz, %d frames per size.\n" + 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); - printf(" %-6s %-10s %s\n", "bytes", "accepted", "note"); + /* 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 = 0; - int i; + 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 (i = 0; i < count; i++) { - frame[38] = (uint8_t)i; - if (mt7612u_tx(&dev, frame, (size_t)len, &rate) == 0) ok++; - mt_usleep(1500); + + 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 %s\n", len, ok, count, - ok == 0 ? "refused by this driver" : + + 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"); @@ -779,8 +817,17 @@ static int gate_arx(uint8_t chan, int secs) return ctx.n ? 0 : 1; } -/* Concurrent TX and RX on one claimed handle - the InitWrite + StartRxLoop + - * send_packet shape waybeam-link uses. */ +/* + * 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 }; @@ -824,9 +871,52 @@ static int gate_duplex(uint8_t chan, int secs) (unsigned long long)st.tx_err, (unsigned long long)st.rx_err); } - mt7612u_rx_stop(&dev); - mt_mac_stop(&dev); - return (n && ctx.n) ? 0 : 1; + /* + * 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 @@ -1710,6 +1800,7 @@ static int gate_coding(uint8_t chan, int count, int bw) }; 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; @@ -1777,8 +1868,10 @@ static int gate_coding(uint8_t chan, int count, int bw) if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; mt_usleep(1200); } - if (sent != count) + if (sent != count) { printf(" submitted only %ld/%d\n", sent, count); + short_arms++; + } arms++; mt_usleep(80000); } @@ -1787,6 +1880,12 @@ static int gate_coding(uint8_t chan, int count, int bw) 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; } @@ -1808,6 +1907,7 @@ 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; @@ -1879,8 +1979,10 @@ static int gate_sweep(uint8_t chan, int count, int bw) if (mt7612u_tx(&dev, f, 44, &r) == 0) sent++; mt_usleep(1200); } - if (sent != count) + if (sent != count) { printf(" submitted only %ld/%d\n", sent, count); + short_arms++; + } arms++; mt_usleep(100000); } @@ -1892,6 +1994,11 @@ static int gate_sweep(uint8_t chan, int count, int bw) "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; } @@ -1929,6 +2036,7 @@ static int gate_vht(uint8_t chan, int count, int bw) { '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; @@ -1975,6 +2083,7 @@ static int gate_vht(uint8_t chan, int count, int bw) mt_usleep(1500); } printf(" submitted %ld/%d\n", sent, count); + if (sent != count) short_arms++; mt_usleep(150000); } @@ -1982,6 +2091,11 @@ static int gate_vht(uint8_t chan, int count, int bw) 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; } @@ -2087,12 +2201,21 @@ int main(int argc, char **argv) * 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")) && - (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; + 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); diff --git a/src/mt7612u/usb.c b/src/mt7612u/usb.c index 710dd74..88625df 100644 --- a/src/mt7612u/usb.c +++ b/src/mt7612u/usb.c @@ -433,8 +433,15 @@ static libusb_device_handle *open_selected(libusb_context *ctx, const char **err return NULL; } g_lock_fd = lk; - if (libusb_open(list[i], &h)) + 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)