mt7612u: a MediaTek backend, measured — is it in scope? - #412
Conversation
## 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
josephnef
left a comment
There was a problem hiding this comment.
Scope: yes. A MediaTek backend belongs here. The project will need a refactor first, and that is the first milestone rather than wiring this subtree in as-is.
The seams the refactor has to open, from reading the current tree:
IRtlDevice/RtlAdapter/WiFiDriver::CreateRtlDeviceare Realtek-branded names on an interface that is already vendor-neutral (Init / InitWrite / StartRxLoop / send_packet / SetMonitorChannel / FastRetune / GetAdapterCaps). That is a rename, not a redesign.WiFiDriverdispatches from a SYS_CFG2 read issued as a Realtek vendor request. It needs a vendor-neutral VID:PID gate in front of that read, the way Kestrel already gates PID-first.IRtlTransportis shaped around 16-bit registers and Realtek bulk endpoints. MT7612U is 32-bit registers plus in-band MCU commands over bulk EP8/EP5, so the MediaTek backend gets its own transport behind the same factory rather than a shim over the Realtek one.- A
ChipGenerationentry, aDEVOURER_MT7612UCMake option following theDEVOURER_HAVE_*pattern, and aregress.pycell against the MediaTek vendor driver underreference/.
Licensing is fine. I checked the mt76 file headers upstream: they really are BSD-3-Clause-Clear, so the attribution here is accurate and the code is GPL-2 compatible. A subtree LICENSE notice would be good practice in this tree.
The code has bugs that sit under the measurements, so it is not mergeable even as an unwired subtree. Inline below, most severe first: the public API is declared but never defined; RX corrupts every QoS-data header; TX header length is wrong for BAR/BA/CF-End; async teardown frees in-flight TX transfers; a failed register read feeds all-ones into every RMW; and the harness's caps/ack gates enable MAC RX without draining it, which is the wedge pattern the doc says is fixed.
Two cross-cutting items:
initvals.hsays it is generated by a script inPLAN.md; neither the script norPLAN.md/INVESTIGATION.md/BRINGUP-RESULTS.mdis in the PR, and 13 comments point at them. This project's rule is that tables come fromtools/extract_*.pywith pinned source hashes and a--checkmode that reproduces the checked-in output. The hand-typed constants inphy.candinit.cshould come from the same generator.- Windows/MSVC is first-class here.
__builtin_ctzunder every FIELD_PREP/GET, pthreads throughoutasync.c,nanosleep,clock_gettime, the<libusb-1.0/libusb.h>include path and the gcc-only Makefile all need to land in portability shims during the integration, not per-platform gating.
| * 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); |
There was a problem hiding this comment.
mt7612u_open, _close, _keep_detached, _set_channel, _set_chainmask, _start, _stop, _asic_version and _mac_addr are declared here but no .c file defines them. The harness works only because it includes internal.h and calls mt_open / mt_init_hardware directly. Anything linking against this header gets undefined references, so the "standalone C library" cannot be used as one yet. (set_channel also takes unsigned here and uint8_t internally.)
There was a problem hiding this comment.
Fixed in 2142a5a. All nine are defined now — open/close/start/stop in init.c, set_channel/set_chainmask in phy.c, keep_detached/asic_version/mac_addr in usb.c. You spotted the signature disagreement too: set_channel took unsigned in the header against uint8_t internally, and the header is uint8_t now.
tests/api_link.c takes the address of all twenty public entry points while including only <mt7612u/mt7612u.h> and no internal header, so a declaration that loses its definition is a link error rather than something a caller discovers later. Mutation-tested: deleting one definition gives undefined reference to mt7612u_asic_version.
While defining them: mt7612u_start() enables RX only when an RX ring is already running, and the header says why.
|
|
||
| *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) { |
There was a problem hiding this comment.
This folds a fixed 24 bytes over the L2 pad. L2PAD is only set when the header is not 4-aligned, which is QoS data (26 bytes) or 4-address (30 bytes). With a 26-byte header the QoS Control field at offsets 24-25 is overwritten by the two pad zeros, so every A-MPDU / QoS frame reaching the callback has TID and ack-policy zeroed. Those are exactly the frames the aggregation and ACK numbers are about. tx.c already computes the real header length from FC; reuse it here.
There was a problem hiding this comment.
Correct, and it was every QoS frame that reached the callback, not an edge case. Fixed in 2142a5a: the fold moves the real header length now, exactly as mt76x02_remove_hdr_pad() does, and TX and RX share one mt_hdrlen_from_fc() so the two sides cannot drift apart again.
tests/frame_shape covers it on a synthetic 26-byte-header QoS frame, and carries a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed — without that, a passing test would say nothing about what it caught. Reverting the fix makes it report QoS Control zeroed by the pad fold: aa aa, which is the pad bytes.
| int len = 24; | ||
|
|
||
| if (type == 1) /* control */ | ||
| return ((stype == 0xb) || (stype == 0xa)) ? 16 : 10; |
There was a problem hiding this comment.
Only CTS and ACK are 10-byte control frames. BlockAckReq (0x8), BlockAck (0x9) and CF-End / CF-End+CF-Ack (0xe/0xf) are 16 bytes like RTS and PS-Poll. With this table the pad is inserted at offset 10 inside a BAR/BA, so the frame airs corrupted.
There was a problem hiding this comment.
Right. Rather than extend the list of 16-byte subtypes, I ported ieee80211_hdrlen(): control frames default to 16 and only CTS and ACK are 10. That is the shape that cannot be incomplete, and it is the same function mt76 reaches through ieee80211_get_hdrlen_from_skb() on both the TX and RX sides — so this now matches upstream rather than approximating it. It also picks up the HT Control cases (+4 on Order) that the old version missed.
Fixed in 2142a5a, covered in tests/frame_shape over all eight control subtypes and the five data shapes.
Worth being explicit: this one has no on-air observable in the gates here, because nothing in them injects a control frame. The unit test is the whole of the evidence for it.
| pthread_join(a->evt, NULL); | ||
|
|
||
| for (int i = 0; i < MT_TX_RING; i++) | ||
| if (a->tx[i]) libusb_free_transfer(a->tx[i]); |
There was a problem hiding this comment.
mt_async_stop cancels only the RX transfers. TX transfers are never cancelled; after the bounded wait above (200 x 10 ms) running is cleared, the event thread joined, and every transfer freed regardless of tx_inflight / rx_inflight. On a wedged chip, the case docs/mt7612u.md itself describes, TX URBs never complete, so this frees transfers libusb still owns. Cancel TX too and only free once inflight counts reach zero. Related: the fail: path at line 102 joins a zeroed pthread_t if libusb_alloc_transfer fails before pthread_create, and rx_inflight / running are plain volatile int shared across threads.
There was a problem hiding this comment.
All of it, and the wedged chip is the case that makes it real rather than theoretical. In 2142a5a 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 whole mt_async is deliberately leaked rather than freeing memory libusb owns.
Each slot now names its own ring instead of dev->a, so a leaked ring's late completions cannot land on a replacement ring. fail: no longer joins a zeroed pthread_t — an evt_started flag guards it.
running, rx_active and both inflight counters moved under the mutex; you are right that volatile orders nothing and makes no read-modify-write atomic. The statistics moved with them, behind mt_async_stats(), because the harness was reading them live off the struct — same bug, one level down. And a caller blocked waiting for a TX slot now gives up when the ring stops instead of parking forever.
|
|
||
| if (mt_vendor_req(d, req, REQ_IN, (uint16_t)(a >> 16), (uint16_t)a, | ||
| b, sizeof b) != (int)sizeof b) | ||
| return ~0u; |
There was a problem hiding this comment.
~0u on failure is consumed unchecked by mt_rmw, set_wlan_state, the fw handshake and every mt_set / mt_clear caller, so one exhausted EP0 read during bring-up writes 0xFFFFFFFF | val into MT_WLAN_FUN_CTRL / MT_MAC_SYS_CTRL / BBP AGC. Return a status and make the RMW helpers refuse to write after a failed read. Also: the retry loop retries every error except NO_DEVICE 10x with a 1 s timeout each, so a dead read stalls 10 s and mt_poll's microsecond timeout_us is meaningless.
There was a problem hiding this comment.
Fixed in 2142a5a. mt_rr_chk() reports failure separately from the value, mt_rmw() refuses to write after a failed read, and mt_wait_for_mac() distinguishes "not ready" from "no transport". 0xffffffff is a real MT_MAC_CSR0 value while the core comes up, so it could never have served as a sentinel.
The second half of the problem mattered as much: mt_poll() counted sleeps, so with one access costing up to VEND_RETRIES × timeout a caller asking for 200 ms could block for seconds. It polls against a real deadline now and aborts on a read failure. The control timeout also drops from 1000 ms to mt76's own MT_VEND_REQ_TOUT_MS of 300, which bounds the worst case at 3 s.
Re-verified on hardware afterwards — bring-up is unaffected.
| 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; |
There was a problem hiding this comment.
This enables MAC RX and then never reads EP4: the gate sleeps 200 ms and injects 300 frames before mt_mac_stop. Same at line 978 in the ack gate. docs/mt7612u.md says the wedge was fixed by "never enabling RX for a caller that will not drain it", and mt_rx_flush drains at most 64 x 4 KiB. Either start the async RX ring here or pass RX off.
There was a problem hiding this comment.
You are right, and the doc claiming the wedge was fixed a few lines away made it worse rather than better. Both gates start the RX ring before enabling the receiver now (2142a5a) — caps in particular then sat through two 200 ms sleeps and a 40 MHz channel switch with nothing reading EP 4.
Re-run on hardware: three consecutive caps cycles clean, each logging async: 16 RX transfers in flight.
The structural half is that mt7612u_start() enables RX only when a ring is already running, so the public API cannot reproduce the pattern even if a caller wants to.
| @@ -0,0 +1,70 @@ | |||
| /* SPDX-License-Identifier: BSD-3-Clause-Clear */ | |||
| /* GENERATED from openwrt/mt76 mt76x2/init.c mt76_write_mac_initvals() | |||
There was a problem hiding this comment.
The generator this comment points at (../../PLAN.md Stage C) is not in the PR, so as shipped this is a hand-transcribed copy of mt76x2's MAC initvals. Project rule: tables are produced by a tools/extract_*.py generator with pinned per-source SHA-256 hashes and a --check mode that reproduces the checked-in file byte-for-byte (see tools/extract_8733b_*.py for the shape). The constants in phy.c and init.c belong in the same generator.
There was a problem hiding this comment.
Done in c64975d. reference/mt76 is a pinned shallow submodule at be5ce79, and tools/extract_mt7612u_tables.py follows the extract_8733b_*.py shape: UPSTREAM string, per-source SHA-256, EXPECTED count and table hash, and --check byte-comparing the checked-in header.
The source 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. That is what lets the four DEFAULT_PROT_CFG_* macros — defined inside mt76_write_mac_initvals() itself, not in the register header — be computed rather than copied. Both the symbol table and the evaluator raise on anything they do not understand rather than guessing.
It reproduces the hand-typed table byte for byte, all sixty rows including the four computed protection-config words, so it confirms the original transcription instead of silently replacing it. --check is load-bearing: appending one newline to the header exits 1 with stale generated output.
reference/mt76 is the one entry in that directory that is not a Realtek vendor drop — the MediaTek parts have no out-of-tree vendor driver to mirror, and mt76 being BSD-3-Clause-Clear rather than GPL-2-only is what lets src/mt7612u/ carry ported sequences at all. reference/README.md says so. No workflow checks out submodules, so the twenty builds are unaffected.
The stale ../../PLAN.md pointer is gone with the regenerated header.
| #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)) |
There was a problem hiding this comment.
__builtin_ctz under every FIELD_PREP/GET is the first MSVC blocker; _SHIFT is also a reserved identifier. A constexpr-style ctz shim (or _BitScanForward behind #ifdef _MSC_VER) is the usual fix, and it should land with the pthreads / nanosleep / clock_gettime / <libusb-1.0/libusb.h> items during integration rather than per-platform gating.
There was a problem hiding this comment.
Both fixed in 2ac9b9a. _SHIFT is gone — leading underscore followed by a capital is reserved to the implementation in every scope.
I did not use _BitScanForward: it is a function taking an out-parameter, so it cannot appear in a constant expression, and these macros have to stay constant expressions because ext_cca_chan in phy.c is a static table built from FIELD_PREP. MT_CTZ isolates the low bit and binary-searches its position — a constant expression on every compiler, folded to one instruction under optimisation.
tests/field_macros checks it against __builtin_ctz over all 32 single-bit masks and all 528 contiguous GENMASK(h, l) ranges, with a FIELD_PREP/FIELD_GET round-trip on each. It also carries a static initialiser built from FIELD_PREP, so if the macro ever stops being constant-foldable the test fails to compile rather than passing at runtime.
Same commit: <libusb.h> — this project's spelling — is tried first via __has_include, with the distribution's libusb-1.0/ path as the fallback for the standalone build.
Deliberately not done, and I would rather say so than half-do it: async.c still uses pthreads and usb.c still 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 the moment the subtree joins the build. Those are the only two files involved and the README names them. Same reasoning for the Makefile staying a Makefile while nothing in CMakeLists.txt reaches the subtree — a CMake target now would be a target nothing builds.
|
|
||
| if (flags & 0x04) r->sgi = 1; | ||
| if (coding & 0x01) r->ldpc = 1; | ||
| r->bw = bwc == 0 ? MT7612U_BW_20 |
There was a problem hiding this comment.
Radiotap VHT bandwidth codes 2 and 3 are the 20 MHz sub-channels of a 40 MHz frame, and 5-10 are the 20/40 sub-channels of 80. Mapping 1-3 to 40 and >=4 to 80 airs a requested 20-in-40 frame at 40 MHz. Also, DBM_TX_POWER is silently ignored a few lines up; worth a diagnostic until per-packet power is wired.
There was a problem hiding this comment.
You are right, and I had this backwards on the first read. Fixed in a9cd156 with a table over the eleven codes the part can express: 2 and 3 are 20 MHz, 5 and 6 are 40, 7–10 are 20. Codes 11 and up are 160 MHz and its sub-channels, and the rate word has no 160 encoding, so those log and fall back to 20 rather than narrowing silently.
What settled it was internal to the repository rather than the spec text: the HT branch a few lines above already reads the equivalent codes correctly, and src/ieee80211_radiotap.h:112 names them IEEE80211_RADIOTAP_MCS_BW_20L and _20U. The VHT branch was disagreeing with the repository, not just with the standard.
tests/frame_shape now pins all eleven codes; restoring the old expression fails eight of them.
DBM_TX_POWER emits one diagnostic per process naming the two knobs that do work, rather than accepting the field and dropping it (2142a5a).
One thing to flag rather than fix from here: 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. I have not touched them — changing a shipping backend's on-air behaviour from inside a new-backend PR seemed like the wrong place. Happy to send it as its own PR if you want it.
| #include "internal.h" | ||
|
|
||
| /* | ||
| * DW0 is the LOW word. mt76's mt76x02u_restart_pre_tbtt_timer() assembles this |
There was a problem hiding this comment.
The claim that mt76's mt76x02u_restart_pre_tbtt_timer() assembles TSF backwards is asserted without a file:line. The measured behaviour here stands on its own; cite the upstream line (or drop the upstream-bug claim) before it goes into the doc.
There was a problem hiding this comment.
Cited in 2142a5a: mt76x02_usb_core.c:155-158, where
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 is consumed only by that dev_dbg(), which is why the order has survived upstream. The comment quotes those four lines now.
The caps gate also prints both orders against a known 200 ms sleep, so the claim is re-checkable on any sample rather than taken on trust — this run reads 200347 µs for (DW1 << 32) | DW0.
## Problem Review of OpenIPC#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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## Problem Review of OpenIPC#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.** `<libusb.h>` 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Thanks — that review was worth more than the measurements were. Every one of the eleven findings was real, including the VHT one I initially thought I could defend. All are fixed and answered in-thread; I have left the threads for you to resolve. Five commits on top of the original:
Two of the fixes change what the driver puts on air or hands to a caller, so nothing here is claimed from a rebuild alone. The gates were re-run on the same bench against the same RTL8812AU witness:
On sequencing. Your review reads two ways and it changes what I build next, so let me state how I read it and you can correct me.
I took that to mean an unwired subtree is the mergeable shape once the bugs are gone, and that
puts the refactor before wiring, not before landing. So my plan is: this PR lands unwired, and the refactor plus integration arrives as its own PR. Bundling a rename that touches all three shipping backends together with a brand-new backend makes one diff nobody can review. Say the word if you meant the other order and I will hold this one. On the refactor itself, one question, because I would rather ask than guess at the shape of your codebase. Everything substantive in your list I agree with: a vendor-neutral VID:PID gate ahead of the Realtek The part I would push back on gently is the rename. That also decides where the remaining portability work lands. |
`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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
One more, found while working out what the integration has to look like, and it is a limit rather than a fix.
Every RX buffer does carry 4–7 bytes past It is not the FCS. CRC-32 over the MPDU matched those four bytes on 0 of 4263 frames. I checked the probe's own CRC-32 against the standard So a consumer that trims four bytes at its protocol boundary — which the Documented in |
|
The integration exists and works, on a branch rather than a second PR — that ordering is yours to call, so I have not opened one: Shape, following your list: The one place I diverged from your list is the rename, and I have argued the case in the comment above rather than quietly skipping it. Everything else is as you described it. Two bugs surfaced that only integration could find — both invisible to the bring-up harness, because it never crosses this boundary:
The second is the one worth the paragraph: 244 beacons looks like a working receiver. Only the A/B against a second implementation of the same silicon made it visible, and that is now written down in
One thing to flag about the branch split: it also carries a subtree fix that arguably belongs in this PR instead — |
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Marked ready for review. It should have been from the start — asking you to weigh in on merge order while leaving it a draft was my mistake, not a signal. A defect in this branch, found since your review
Two cases reach the MCU straight through
The caveats matter more than the fix. No measurement in this PR moves: Also here: The second branch, and a question backThe stacked branch now carries the integration plus 80 MHz, and I've brought its build wiring to parity with 80 MHz airs and is witnessed as 80 MHz, with the negative control: a 20 MHz witness decodes 0 of it, and a witness on a sibling control channel of the same group decodes it, which is what shows both ends resolved the same centre. Still one witness generation; VHT 2SS MCS5–9 thin out at 80 MHz in a way I attribute to link budget without having measured the cause; and the periodic RX gain worker ( On sequencing: you asked for the refactor before wiring this in, and I read that as before wiring, not before landing. That is still a guess, and it's the one thing I don't want to decide for you. Either shape is one push from here — land this unwired and keep the integration as a separate PR, or I push the integration commits onto this branch so you review a single full-parity backend against the others. Say which and it's done. |
PR Summary by QodoAdd standalone, hardware-validated MT7612U MediaTek backend
AI Description
Diagram
High-Level Assessment
Files changed (26)
|
Code Review by Qodo
1. Stuck transfers outlive USB teardown
|
| 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; |
There was a problem hiding this comment.
1. Stuck transfers outlive usb teardown 📘 Rule violation ☼ Reliability
mt_async_stop stops the event thread and returns after two seconds even when tx_inflight or rx_inflight is still nonzero. When cancellation completion is delayed, mt7612u_close proceeds through mt_close, releasing the interface, handle, and libusb context while libusb still owns the leaked transfers.
Agent Prompt
## Issue description
Device teardown can close the libusb handle and context while cancelled asynchronous transfers remain in flight.
## Issue Context
`mt_async_stop` abandons outstanding transfers after a two-second deadline, but `mt7612u_close` then continues with normal USB teardown. Keep the event machinery, handle, and context alive until libusb has returned ownership of every submitted transfer.
## Fix Focus Areas
- src/mt7612u/async.c[175-225]
- src/mt7612u/init.c[420-426]
- src/mt7612u/usb.c[256-269]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| #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) |
There was a problem hiding this comment.
2. Concurrent diagnostics can interleave 📘 Rule violation ☼ Reliability
LOG and ERR construct each diagnostic with separate fprintf and fputc calls, omit an explicit flush, and use a format other than devourer [level] message. When the event thread and a caller log concurrently, text can be inserted before another message's newline, while buffered stderr can leave a piped consumer waiting.
Agent Prompt
## Issue description
MT7612U diagnostics do not follow the repository's atomic, flushed stderr logging contract.
## Issue Context
Format each complete human diagnostic as `devourer [level] message`, emit it to stderr with one write, and preserve thread safety and required flushing.
## Fix Focus Areas
- src/mt7612u/internal.h[216-217]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| 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, |
There was a problem hiding this comment.
3. Receiver guidance can drift 📘 Rule violation ⚙ Maintainability
src/mt7612u/README.md repeats the public header's warning that enabling receive without draining bulk-IN wedges the device and requires a physical replug. If the receiver startup contract or recovery behavior changes, maintainers must update two authoritative-looking explanations and readers can follow the stale copy.
Agent Prompt
## Issue description
The README duplicates the receiver startup and recovery contract already documented by the public API header.
## Issue Context
Keep the detailed contract in the authoritative header and replace the README copy with a concise reference to that declaration.
## Fix Focus Areas
- src/mt7612u/README.md[34-41]
- src/mt7612u/include/mt7612u/mt7612u.h[101-107]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (!is_5ghz) | ||
| v >>= 8; | ||
| t->vht[0] = t->vht[1] = rate_power_val(v >> 8); |
There was a problem hiding this comment.
4. 2.4 ghz frames use the wrong power 🐞 Bug ≡ Correctness
mt_get_rate_power() shifts the 2.4 GHz VHT EEPROM word twice, so both VHT power entries are always decoded from zero. Every 2.4 GHz VHT channel setup then programs those incorrect entries into the transmit-power registers.
Agent Prompt
## Issue description
Correct the 2.4 GHz VHT power-table decoding so the selected EEPROM byte is passed directly to `rate_power_val()` rather than shifted twice.
## Issue Context
The decoded entries are consumed by channel setup and written to the VHT transmit-power registers.
## Fix Focus Areas
- src/mt7612u/eeprom.c[116-120]
- src/mt7612u/phy.c[237-245]
- src/mt7612u/phy.c[273-274]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| 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]); |
There was a problem hiding this comment.
14. Bringup reports unreliable receive measurements 🐞 Bug ☼ Reliability
arx_cb() increments ctx.n and ctx.by_phy on the async libusb event thread while gate_arx() and gate_duplex() read those same non-atomic fields before stopping that thread. These unsynchronized accesses are data races, so the displayed receive rates and the duplex pass/fail decision can use corrupted or stale counts.
Agent Prompt
## Issue description
RX callbacks update harness counters concurrently with main-thread reporting and result checks.
## Issue Context
Protect the callback-owned counters with a mutex or use atomics, then snapshot them consistently for reporting and pass/fail checks. Apply the same mechanism to both asynchronous RX gates.
## Fix Focus Areas
- src/mt7612u/tools/bringup.c[586-628]
- src/mt7612u/tools/bringup.c[657-678]
- src/mt7612u/async.c[35-65]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Qodo posted fourteen findings when this left draft. I checked each against the code and against the pinned mt76 tree rather than taking them on trust, which turned out to matter: two were wrong, and "fixing" either would have made things worse. Fixed — six, one of which is on-air correctnessThe radiotap HT bandwidth was gated on the wrong bit. The MCS 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 simply cannot read the rest of it.
Plus three in the harness: On air, unchanged: Two that are not defectsThe 2.4 GHz VHT power "double shift" is
Five left open deliberatelyTransfer teardown racing libusb ownership after the two-second cancel deadline; These are real. I have not touched them because each is a design change rather than a fix — error propagation through |
Thanks for validating the proposed fixes against the pinned tree. I agree with leaving the two upstream-matching behaviors unchanged and would prioritize the remaining issues in this order:
For the next change, I’d take teardown ownership plus the checked-write/error-status design together, then handle EEPROM validation and the harness race separately. I would not alter either of the two behaviors you identified as matching mt76. |
|
Filed #413 for the cross-backend half of this: Keeping it out of this PR deliberately: the field is shared, the "what should the default be" question is yours rather than a backend's, and the Realtek numbers need a register hunt plus a bench sweep that has nothing to do with MediaTek. What did land here, because it was a defect rather than a design question: this backend had two disagreeing ceilings behind one public API — The numbers, measured rather than assumed: TX is not the constraint on this part — every size up to 7900 bytes aired 60/60 against an RTL8812AU witness with zero CRC errors, well past 802.11's 2304 non-A-MSDU ceiling, so the old 2048 was a buffer constant with nothing behind it. RX is the constraint and it is the MAC's: Both are now in One correction worth stating plainly, since it was mine: I added an Caveats that stand: one witness generation, and 7900 is "at least" — I stopped at the buffer I had, not at a refusal. |
Opening this to ask a scope question, not to merge: does a MediaTek backend belong in this project at all? Every generation here is Realtek, and MT7612U is not. I would rather ask against measurements than against a proposal, so the work is on the branch and
docs/mt7612u.mdcarries the numbers, the methods and the limits.If the answer is no, that is a fine answer and the branch can be closed — the measurements are still recorded for anyone who asks later.
What changed
src/mt7612u/— a standalone C library (~4600 lines including the harness) plus a per-gate bringup tool, anddocs/mt7612u.md.CMakeLists.txtis untouched. There is noIRtlDeviceimplementation, noWiFiDriverdispatch, noDeviceConfigplumbing and noctestcell. Nothing in the shipped library changes; CI sees a docs-and-new-directory diff. The subtree builds on its own withmake -C src/mt7612u. That integration work is deliberately not done, because doing it before the scope question is answered would be wasted either way.Why a MediaTek port is small
On the Realtek generations the host programs the synthesizer and carries the PHY tables. Here the RF plane lives behind the MCU — a channel change is one 8-byte
CMD_SWITCH_CHANNEL_OPplus a firmware calibration burst, and there are no RF register tables to reimplement. That is also exactly why it can never hop fast.What is measured
All on one MT7612U (
0e8d:7612,MT_ASIC_VERSION0x76120044, 2T2R, SuperSpeed) against an RTL8812AU witness running this project's ownrxdemo/txdemo.TX rate is authoritative, unconditionally. Three rates, three exact matches; per-frame alternation 300 sent → 229 received, 229 aired the rate their own index called for, 0 mismatches. The hardware rate LUT cannot override the descriptor:
MT_TXWI_FLAGS_TX_RATE_LUTLUT loaded with OFDM 6 Mbps and read back; setting the flag mt76 defines and never sets changed nothing.
TX power — all nine registers identical to what the kernel driver programs for ch149 (
MT_TX_PWR_CFG_0..4/7/8/9,MT_TX_ALC_CFG_0=2f2f171a), checked against ausbmoncapture.A-MPDU works on injected frames — the opposite of the Jaguar1 result in
docs/aggregation.md:paggr=1QSEL_MGMTHardware ACK responder — no separate responder-address register exists, so arming retargets the port identity with
MT_AUTO_RSP_ENgating; clear moves the identity back, following the finding in #410 that closing the gate alone is not enough.len14USB bulk aggregation —
MT_TXD_INFO_NEXT_VLDchains blocks inside one bulk-OUT transfer. mt76 never sets it, so this is not a port and needed on-air proof: 400 frames in 25 transfers, 352 aired.Also measured: 40 MHz (242/242 frames at
bw=1), TSF (200231 µs over a 200000 µs sleep), monitor RX decoding CCK/OFDM/HT with per-chain RSSI, radiotapsend_packet.A register-stream diff against the kernel driver's own probe: 522 kernel EP0 writes vs 521 ours, 376 common addresses, one final-value mismatch (beacon config, which we skip), six kernel-only addresses (all beacon config), and zero addresses we write that the kernel does not.
Two results stated against interest
The async rings bought no throughput. At saturation sync and async both sit at 3040 fps / 34 Mbit/s with an identical 0.329 ms mean submit, because that is the airtime of one frame. What they bought is the callback RX path, concurrent TX+RX on one handle, and 3× lower submit latency below saturation (21 µs vs 65 µs). A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s.
The first ACK-responder attempt returned INCONCLUSIVE, not a pass. It used this project's retry-collapse method, but
txdemoinjects without retries, so there was nothing to collapse. The gate says so rather than reporting success; the passing result above uses a different observable.What it cannot do
Our implementation has headroom, but the floor is four MCU round trips over bulk endpoints plus firmware time — 10–20 ms at best. FHSS and per-packet hopping are out of reach for this part; seconds-scale migration is not. Narrowband 5/10 MHz has no encoding:
MT_RATE_BWis two bits with three defined values.Also worth knowing: unicast injection is a 40× cliff (3037 → 75 fps). The MAC arms an ACK timeout for a peer that never answers, and neither clearing
txwi.ack_ctlREQ nor a QoS No-Ack policy prevents it. A one-way link must use broadcast.Counterparts
Stated because the numbers above are uniformly favourable.
rxdemo.paggr,bwandrateare that implementation's reading, not an independent instrument.reset_wlan+power_ondemonstrably does not clear the firmware-running bit.ctest, no sanitizer build, no lifecycle soak.One more, on the code rather than the measurements: enabling MAC RX without draining the bulk-IN endpoint wedges the chip below USB level, unrecoverable by
libusb_reset_device(), theauthorizedtoggle or the kernel driver — only a physical replug. Fixed by never enabling RX for a caller that will not drain it, plus an endpoint flush (20 consecutive 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.Verification
make -C src/mt7612uclean at-Wall -Wextra.src/mt7612u/README.mdrun on hardware; the numbers above are those runs.Open list
If this is in scope, the work I would continue with, in order:
IRtlDevice,WiFiDriverdispatch,DeviceConfig,CMakeLists.txt,ctestcells.mt76x2_phy_tssi_compensate()— without it output power drifts with die temperature.CMD_RANDOM_WRITE, drop the inter-command sleep) — worth doing only if 10–20 ms is useful to someone.🤖 Generated with Claude Code
https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj