Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,17 @@ jobs:
brew install libusb

- name: Configure CMake
# DEVOURER_MT7612U=ON: the option defaults OFF, so without it here the
# MediaTek subtree would be compiled by nothing but its own Makefile on
# Linux. Turning it on in the matrix is what makes MSVC and macOS
# first-class for it — the POSIX-only pieces it still has (the flock
# adapter lock) are _WIN32-guarded, and this is what proves it.
run: >
cmake -B ${{ steps.strings.outputs.build-output-dir }}
-DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }}
-DCMAKE_C_COMPILER=${{ matrix.c_compiler }}
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DDEVOURER_MT7612U=ON
-S ${{ github.workspace }}

- name: Build
Expand Down Expand Up @@ -136,6 +142,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER=gcc
-DCMAKE_CXX_COMPILER=g++
-DDEVOURER_MT7612U=ON

- name: Build (library + stream demos + self-tests)
# rxdemo / txdemo / precoder use POSIX-only APIs
Expand Down Expand Up @@ -184,6 +191,12 @@ jobs:
flags: "-DDEVOURER_JAGUAR1=OFF -DDEVOURER_8814=OFF -DDEVOURER_JAGUAR2_8822B=OFF -DDEVOURER_JAGUAR2_8821C=OFF -DDEVOURER_JAGUAR3_8822C=OFF -DDEVOURER_JAGUAR3_8822E=OFF -DDEVOURER_8733B=OFF -DDEVOURER_KESTREL_8852C=OFF"
- name: kestrel-8852c-only
flags: "-DDEVOURER_JAGUAR1=OFF -DDEVOURER_8814=OFF -DDEVOURER_JAGUAR2_8822B=OFF -DDEVOURER_JAGUAR2_8821C=OFF -DDEVOURER_JAGUAR3_8822C=OFF -DDEVOURER_JAGUAR3_8822E=OFF -DDEVOURER_8733B=OFF -DDEVOURER_KESTREL_8852B=OFF"
# Jaguar1 stays ON because it has to: MT7612U is deliberately not
# counted as chip support, so an MT7612U-only configure is a
# FATAL_ERROR. This proves the subtree is independent of the other
# seven Realtek chips' sources, not of Jaguar1's.
- name: mt7612u+jaguar1
flags: "-DDEVOURER_MT7612U=ON -DDEVOURER_8814=OFF -DDEVOURER_JAGUAR2_8822B=OFF -DDEVOURER_JAGUAR2_8821C=OFF -DDEVOURER_JAGUAR3_8822C=OFF -DDEVOURER_JAGUAR3_8822E=OFF -DDEVOURER_8733B=OFF -DDEVOURER_KESTREL_8852B=OFF -DDEVOURER_KESTREL_8852C=OFF"
- name: rtl8733b-only
flags: "-DDEVOURER_JAGUAR1=OFF -DDEVOURER_8814=OFF -DDEVOURER_JAGUAR2_8822B=OFF -DDEVOURER_JAGUAR2_8821C=OFF -DDEVOURER_JAGUAR3_8822C=OFF -DDEVOURER_JAGUAR3_8822E=OFF -DDEVOURER_KESTREL_8852B=OFF -DDEVOURER_KESTREL_8852C=OFF"
steps:
Expand Down Expand Up @@ -216,9 +229,13 @@ jobs:
run: sudo apt install libusb-1.0-0-dev

- name: Configure (ASan + UBSan)
# DEVOURER_MT7612U=ON here too: that subtree moved from calloc/free to
# new/delete and its sync primitives are constructed members now, so the
# sanitizers are the cheapest guard against a lifetime regression in it.
run: >
cmake -B build-asan -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DDEVOURER_SANITIZE=address+undefined -S ${{ github.workspace }}
-DDEVOURER_SANITIZE=address+undefined -DDEVOURER_MT7612U=ON
-S ${{ github.workspace }}

- name: Build (ASan + UBSan)
run: cmake --build build-asan -j
Expand Down
56 changes: 56 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ option(DEVOURER_KESTREL_8852C "RTL8852CU / 8832CU (Kestrel 11ax, G6)"
# with the older rtlwifi-style own-bit descriptor DMA, a different engine.
# OFF by default — libusb-only builds are bit-identical.
option(DEVOURER_PCIE "PCIe transport via vfio-pci (Linux; RTL8821CE)" OFF)
# MediaTek MT7612U / MT7662U — the first non-Realtek part. Compiles the
# src/mt7612u subtree (its own C++ library with a C ABI, landed unwired in #412)
# into libdevourer. OFF by default and deliberately NOT counted as chip support
# by the "No chip support selected" check below: there is no IRadio backend yet,
# so an MT7612U-only build would produce a library that can open nothing. It
# joins that list when the backend lands (#419). Turning it ON today buys one
# thing, and it is the point of the option: CI compiles the subtree across the
# whole platform matrix — gcc, clang, MSVC, mingw, macOS — where it was
# previously only ever built by its own Makefile, on Linux. It does NOT gate the
# factory's MediaTek refusal, which is unconditional.
option(DEVOURER_MT7612U "Compile the MediaTek MT7612U subtree (the id gate is always on)" OFF)

# Compile-time diagnostics floor (src/logger.h). Calls below the floor —
# including their argument expressions at DVR_TRACE/DVR_DEBUG sites — compile
Expand Down Expand Up @@ -346,6 +357,39 @@ if(DEVOURER_JAGUAR2_8821C)
target_compile_definitions(devourer PUBLIC DEVOURER_HAVE_JAGUAR2_8821C=1)
endif()

# --- MediaTek MT7612U (MT7662 MAC) ---
# Note the option gates only the COMPILE. Mt7612uUsbIds.h is header-only and is
# included unconditionally by WiFiDriver.cpp, so a default OFF build still
# refuses MediaTek adapters and still runs the mt7612u_usb_ids test; it just
# links none of the subtree.
# The subtree is self-contained: it talks to libusb directly rather than through
# RtlAdapter/ITransport, because 32-bit registers and an in-band MCU over EP8/EP5
# do not fit the Realtek shape. Its public surface is the C ABI in
# include/mt7612u/mt7612u.h; nothing here exports its internal headers, so the
# include directory is PRIVATE until the backend needs it.
if(DEVOURER_MT7612U)
target_sources(devourer PRIVATE
src/mt7612u/async.cpp
src/mt7612u/caps.cpp
src/mt7612u/eeprom.cpp
src/mt7612u/fw.cpp
src/mt7612u/init.cpp
src/mt7612u/mcu.cpp
src/mt7612u/phy.cpp
src/mt7612u/radiotap.cpp
src/mt7612u/rx.cpp
src/mt7612u/tx.cpp
src/mt7612u/usb.cpp
Comment thread
snokvist marked this conversation as resolved.
Comment thread
snokvist marked this conversation as resolved.
src/mt7612u/internal.h
src/mt7612u/regs.h
src/mt7612u/initvals.h
src/mt7612u/Mt7612uUsbIds.h) # listed for IDEs; compiled unconditionally
target_include_directories(devourer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u
${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u/include)
target_compile_definitions(devourer PUBLIC DEVOURER_HAVE_MT7612U=1)
endif()

# --- PCIe transport (vfio-pci; see src/PcieTransport.h) ---
if(DEVOURER_PCIE)
target_sources(devourer PRIVATE
Expand Down Expand Up @@ -787,6 +831,18 @@ target_link_libraries(RadioIfaceSelftest PRIVATE devourer)

add_test(NAME radio_iface COMMAND RadioIfaceSelftest)

# Headless guard for the MediaTek USB-id gate. CreateRadio consults it BEFORE
# the Realtek SYS_CFG2 read, so an id it wrongly claims is refused outright with
# no second chance. This pins the one property that makes that safe: the
# MediaTek pair set never claims a device a Realtek table owns. Header-only, so
# it costs a compile and no hardware.
add_executable(Mt7612uUsbIdsSelftest
tests/mt7612u_usb_ids_selftest.cpp
)
target_link_libraries(Mt7612uUsbIdsSelftest PRIVATE devourer)

add_test(NAME mt7612u_usb_ids COMMAND Mt7612uUsbIdsSelftest)

# Headless guard for the TX quiesce seam (ITransport::quiesce_tx via
# RtlAdapter): the explicit "stop TX and wait it out" call every device makes
# before anything is released. UsbTransport's cancel/drain is validated on
Expand Down
24 changes: 14 additions & 10 deletions docs/mt7612u.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Everything below was measured on one MT7612U (`0e8d:7612`, `MT_ASIC_VERSION`
running this project's own `rxdemo`/`txdemo`. Read `## Offline tests` and
`## Counterparts` before quoting any number here.

**This code is not wired into the build.** `CMakeLists.txt` is untouched, there
is no `IRadio` 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.
**There is still no radio backend.** `DEVOURER_MT7612U` (default OFF) compiles
the subtree into `libdevourer`, and `WiFiDriver::CreateRadio` now recognises the
MediaTek USB ids — but only to *refuse* them, because no `IRadio` implementation
exists yet. What the option buys today is that the whole CI matrix compiles the
subtree; what it does not buy is a devourer binary that can open the part.

## Why a MediaTek port is small

Expand Down Expand Up @@ -300,13 +301,14 @@ and nothing was ever queued on the healthy path.

## Offline tests

`make -C src/mt7612u check` runs three binaries. No hardware, no privileges.
`make -C src/mt7612u check` runs four binaries. No hardware, no privileges.

| test | what it holds |
|---|---|
| `api_link` | takes the address of all 25 public entry points while including only the public header, so a declaration that loses its definition is a link error |
| `api_link` | takes the address of all 27 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ |
| `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 |
| `log_sink` | that `mt7612u_set_log_sink()` **diverts** rather than copies — stderr must stay silent while a sink is installed — that the sink gets the bare message with no prefix to double up, that every level letter arrives, and that NULL restores the built-in sink instead of silencing the library |

Each was mutation-tested: removing one public definition, reverting either
frame-shape fix, and reverting the header-length fix each make the suite fail,
Expand Down Expand Up @@ -341,10 +343,12 @@ 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.** 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.
- **The library compiles in CI; its own tests still do not run there.** With
`DEVOURER_MT7612U=ON` the whole platform matrix (gcc, clang, MSVC, mingw,
macOS) builds the subtree, so a portability regression is caught. The offline
tests above and the table generator's `--check` are still driven only by
`src/mt7612u/Makefile`, which no workflow invokes — nor is there a sanitizer
build or a 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 delivers no FCS** (see above). That is a measured hardware
Expand Down
16 changes: 15 additions & 1 deletion src/AdapterCaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ enum class ChipGeneration : uint8_t {
Jaguar2,
Jaguar3,
Rtl8733b, /* HALMAC 87xx 802.11n: RTL8731BU / RTL8733BU */
Kestrel /* Wi-Fi 6 / 802.11ax (RTL8852BU/8852CU) */
Kestrel, /* Wi-Fi 6 / 802.11ax (RTL8852BU/8852CU) */
/* MediaTek MT7662 MAC (MT7612U / MT7662U, 2T2R 11ac USB) — the first
* non-Realtek generation. Register width, the vendor-request opcodes and the
* in-band MCU plane all differ; nothing that switches on this value may
* assume a Realtek register map. */
Mt7612u
};

inline const char *generation_name(ChipGeneration g) {
Expand All @@ -54,6 +59,8 @@ inline const char *generation_name(ChipGeneration g) {
return "rtl8733b";
case ChipGeneration::Kestrel:
return "kestrel";
case ChipGeneration::Mt7612u:
return "mt7612u";
default:
return "unknown";
}
Expand Down Expand Up @@ -83,8 +90,15 @@ inline uint8_t bw_mask_for_generation(ChipGeneration g) {
/* RTL8733B: 10 MHz qualified (SDR OBW + two-way cross-decode with a
* Jaguar3 peer, both bands); 5 MHz is refused — its BB small-BW mode airs
* no packets on this die (docs/rtl8733b.md "Narrowband status"). */
/* MT7612U: 20/40/80 and nothing narrower. MT_RATE_BW encodes only
* 20/40/80/160, so there is no 5 or 10 MHz to select — unlike the Realtek
* BB small-BW modes the trailing arm below is describing. Named explicitly
* because that trailing arm is the permissive one: without this case a
* MediaTek adapter would inherit kBw5|kBw10 and advertise two bandwidths the
* part cannot represent. 160 MHz is likewise absent (docs/mt7612u.md). */
return g == ChipGeneration::Rtl8733b ? (kBw10 | kBw20 | kBw40)
: g == ChipGeneration::Jaguar1 ? ac
: g == ChipGeneration::Mt7612u ? ac
: g == ChipGeneration::Unknown ? 0
: (ac | kBw5 | kBw10);
}
Expand Down
75 changes: 67 additions & 8 deletions src/WiFiDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>

Expand Down Expand Up @@ -30,6 +31,7 @@
#include "rtl8733b/Rtl8733bDevice.h"
#endif
#include "rtl8733b/Rtl8733bUsbIds.h"
#include "mt7612u/Mt7612uUsbIds.h" /* header-only VID:PID table, always compiled */

namespace {

Expand All @@ -43,14 +45,29 @@ namespace {
* 0x13 = RTL8822C, 0x17 = RTL8822E (RTL8812EU / RTL8822EU) -> Jaguar3
* 0x16 = RTL8733B (RTL8731BU / RTL8733BU, 1T1R) -> RTL8733B
* The chip-id (not the USB PID) is authoritative because the rtl8822e RTL8812EU
* shares PID 0x8812 with the Jaguar1 RTL8812AU. Returns 0 on a failed read,
* which falls through to the Jaguar1 path. (8821C = 0x09 hardware-verified on a
* CF-811AC; it is a HalMAC/phydm Jaguar2 chip, NOT the page-write Jaguar1 the
* "8821C" name might suggest — routing it to Jaguar1 would fail at DLFW.) */
uint8_t read_chip_id(libusb_device_handle *dev_handle) {
* shares PID 0x8812 with the Jaguar1 RTL8812AU. (8821C = 0x09 hardware-verified
* on a CF-811AC; it is a HalMAC/phydm Jaguar2 chip, NOT the page-write Jaguar1
* the "8821C" name might suggest — routing it to Jaguar1 would fail at DLFW.)
*
* Returns nullopt when the control transfer itself failed, which is NOT the same
* as reading 0x00 and matters: this request is Realtek's vendor protocol, so a
* device that does not answer it is not Realtek silicon. Measured on the bench,
* same transfer, both arms:
* RTL8812AU 0bda:8812 rc=1 chip_id=0x04
* MT7612U 0e8d:7612 rc=-7 LIBUSB_ERROR_TIMEOUT, destination byte UNTOUCHED
* (the MediaTek arm was run with a 0xAA poison byte, which survived — so the
* value the caller sees is its own initialiser, never a reading). Discarding the
* return code turned that into a plain 0, and 0 matches no id below, so the
* device reached the unconditional Jaguar1 fallback and came up as an RTL8812AU.
* Distinguishing the two lets CreateRadio refuse instead. A successful read of
* 0x00 is deliberately still returned as a value, so any cold-boot transient
* keeps its existing fall-through behaviour. */
std::optional<uint8_t> read_chip_id(libusb_device_handle *dev_handle) {
uint8_t id = 0;
libusb_control_transfer(dev_handle, REALTEK_USB_VENQT_READ, 5, 0x00FC, 0, &id,
sizeof(id), USB_TIMEOUT);
int rc = libusb_control_transfer(dev_handle, REALTEK_USB_VENQT_READ, 5, 0x00FC,
0, &id, sizeof(id), USB_TIMEOUT);
if (rc != static_cast<int>(sizeof(id)))
return std::nullopt;
return id;
}

Expand Down Expand Up @@ -170,7 +187,49 @@ WiFiDriver::CreateRadio(libusb_device_handle *dev_handle,
#endif
}

uint8_t chip_id = read_chip_id(dev_handle);
/* MediaTek MT7612U gates on the USB VID:PID BEFORE the SYS_CFG2 read, for the
* same reason Kestrel does above — but with a sharper failure mode. On this
* silicon the Realtek vendor request read_chip_id() issues (bRequest 5) is not
* implemented at all, so the control transfer stalls. Before the nullopt
* return below existed, that failure was discarded, `id` stayed 0, 0 matched
* no Realtek chip-id, and the adapter reached the unconditional Jaguar1
* fallback at the end of this function — coming up as an RTL8812AU with every
* subsequent register access addressed at the wrong MAC.
*
* The pair set is disjoint from all 91 Realtek ids devourer can serve, but the
* VENDOR ids are NOT: six of them ship both silicon families, and Netgear,
* ELECOM and ASUS interleave the two within one vendor's product-id range. So
* this must stay a vid:pid test and must never be widened to "not a Realtek
* vendor id". Mt7612uUsbIds.h carries the table and the evidence;
* Mt7612uUsbIdsSelftest.cpp fails if a later id addition breaks it. */
if (mt7612u::is_usb_id(vid, pid)) {
_logger->error("MediaTek MT7612U ({:04x}:{:04x}) detected; devourer has no "
"MediaTek radio backend yet — refusing rather than "
"misdetecting it as Realtek",
vid, pid);
return nullptr;
}

/* A vendor read that did not complete means this device is not speaking the
* Realtek protocol at all — refuse instead of dropping through to the Jaguar1
* fallback at the end of this function, which would bring an unknown part up
* as an RTL8812AU. This is the generation-independent half of the same guard
* the MediaTek gate above applies by id: the gate names the parts we know,
* this catches the ones we do not. It deliberately keys on TRANSFER FAILURE
* and not on "vid is not 0x0bda": devourer already serves Realtek silicon
* behind ASUS, Edimax, D-Link, ZyXEL, MSI and Mercury vendor ids, and every
* one of those answers this read normally (measured: an RTL8812AU returns
* 0x04), so a vendor-id rule would refuse working adapters. */
std::optional<uint8_t> chip_id_read = read_chip_id(dev_handle);
if (!chip_id_read) {
_logger->error("SYS_CFG2 chip-id read failed on {:04x}:{:04x} — not a "
"Realtek vendor-protocol device; refusing rather than "
"falling through to Jaguar1",
vid, pid);
return nullptr;
}
uint8_t chip_id = *chip_id_read;

if (rtl8733b::is_chip_id(chip_id)) {
#if defined(DEVOURER_HAVE_8733B)
_logger->info("Creating Rtl8733bDevice ({:04x}:{:04x}, chip-id 0x{:02x})",
Expand Down
1 change: 1 addition & 0 deletions src/mt7612u/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bringup
tests/api_link
tests/frame_shape
tests/field_macros
tests/log_sink
*.o
*.d
firmware/
Expand Down
Loading
Loading