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
12 changes: 10 additions & 2 deletions .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ jobs:
# 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.
# first-class for the LIBRARY — the POSIX-only pieces it still has (the
# flock adapter lock) are _WIN32-guarded, and this is what proves it.
# The mt7612uprobe bench tool is NOT portable and is gated on UNIX in
# CMakeLists.txt; it is deliberately outside that claim.
run: >
cmake -B ${{ steps.strings.outputs.build-output-dir }}
-DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }}
Expand Down Expand Up @@ -195,6 +197,12 @@ jobs:
# 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.
# MediaTek alone, valid now that the option carries a radio backend.
# This is the cell that can catch the subtree depending on a Realtek
# chip's sources - the one below cannot, because it must leave
# Jaguar1 on.
- name: mt7612u-only
flags: "-DDEVOURER_MT7612U=ON -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 -DDEVOURER_KESTREL_8852C=OFF"
- 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
Expand Down
72 changes: 61 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@ option(DEVOURER_KESTREL_8852C "RTL8852CU / 8832CU (Kestrel 11ax, G6)"
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)
# into libdevourer along with Mt7612uRadio, the IRadio backend WiFiDriver
# constructs for a MediaTek adapter. OFF by default, but it DOES count as chip
# support in the "No chip support selected" check below: with the backend behind
# it an MT7612U-only build opens, receives and transmits, which is exactly what
# that check exists to require.
#
# Turning it on also 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. The option does NOT gate the factory's MediaTek id
# gate, which is unconditional: with it OFF, such an adapter is refused rather
# than misdetected as Realtek.
option(DEVOURER_MT7612U "MediaTek MT7612U / MT7662U (2T2R 11ac USB)" 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 @@ -95,7 +98,8 @@ else()
endif()
if(NOT DEVOURER_JAGUAR1 AND NOT DEVOURER_JAGUAR2 AND NOT DEVOURER_JAGUAR3
AND NOT DEVOURER_8733B
AND NOT DEVOURER_KESTREL)
AND NOT DEVOURER_KESTREL
AND NOT DEVOURER_MT7612U)
message(FATAL_ERROR
"No chip support selected. Enable at least one of DEVOURER_JAGUAR1 / "
"DEVOURER_JAGUAR2_8822B / DEVOURER_JAGUAR2_8821C / DEVOURER_JAGUAR3_8822C "
Expand Down Expand Up @@ -380,12 +384,18 @@ if(DEVOURER_MT7612U)
src/mt7612u/rx.cpp
src/mt7612u/tx.cpp
src/mt7612u/usb.cpp
src/mt7612u/Mt7612uRadio.cpp src/mt7612u/Mt7612uRadio.h
src/mt7612u/Mt7612uMapping.h
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)
# PUBLIC: Mt7612uRadio.h is reached as "mt7612u/Mt7612uRadio.h" and pulls in
# <mt7612u/mt7612u.h>, so a consumer compiling against this target needs the
# subtree's public include root too.
target_include_directories(devourer PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u/include)
target_compile_definitions(devourer PUBLIC DEVOURER_HAVE_MT7612U=1)
endif()
Expand Down Expand Up @@ -622,6 +632,32 @@ if(DEVOURER_8733B)
target_include_directories(rtl8733bprobe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
endif()

# mt7612uprobe — the MediaTek bring-up harness, the rtl8733bprobe/kestrelprobe
# sibling: one subcommand per verified hardware gate (registers, firmware, MAC,
# channel, TX, RX, ACK responder, the wedge experiments). Same source the
# subtree's own Makefile builds as ./bringup, built here so the chip-specific
# tool is not the one part of this backend that only a second build system can
# produce — and so it picks up CMAKE_C_COMPILER and DEVOURER_SANITIZE, which
# shelling out to that Makefile would silently ignore.
#
# It talks to the C library directly, NOT through Mt7612uRadio: it predates the
# backend and its whole purpose is to exercise the layer underneath one. It
# never substitutes for the production IRadio path.
# UNIX-only, unlike its kestrelprobe/rtl8733bprobe siblings. Those are portable
# C++; this one is the hardware bench harness and uses <unistd.h>,
# <sys/resource.h> and getrusage() unguarded, so on the MSVC cell of the
# multi-platform matrix - which now passes DEVOURER_MT7612U=ON - an unguarded
# add_executable would put it in `all` and break the build. The LIBRARY is
# MSVC-clean; this tool is not, and does not need to be.
if(DEVOURER_MT7612U AND UNIX)
add_executable(mt7612uprobe
src/mt7612u/tools/bringup.cpp
)
target_link_libraries(mt7612uprobe PRIVATE devourer PkgConfig::libusb)
target_include_directories(mt7612uprobe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u)
endif()

# reglat — register round-trip latency microbench (USB ctrl-xfer vs PCIe MMIO).
# Times RtlAdapter::rtw_read32; the PCIe path compiles only with DEVOURER_PCIE
# (inherited via the devourer PUBLIC DEVOURER_HAVE_PCIE define). See its header.
Expand Down Expand Up @@ -843,6 +879,20 @@ target_link_libraries(Mt7612uUsbIdsSelftest PRIVATE devourer)

add_test(NAME mt7612u_usb_ids COMMAND Mt7612uUsbIdsSelftest)

# Headless guard for the MT7612U <-> devourer descriptor translations. Pure
# lookups, so no hardware and no subtree compile — this cell runs whether or
# not DEVOURER_MT7612U is on, which is the point: the header it covers is the
# one place a wrong RSSI base or a per-chain copy that publishes the noise
# floor would be introduced, and both have happened.
add_executable(Mt7612uMappingSelftest
tests/mt7612u_mapping_selftest.cpp
)
target_link_libraries(Mt7612uMappingSelftest PRIVATE devourer)
target_include_directories(Mt7612uMappingSelftest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u/include)

add_test(NAME mt7612u_mapping COMMAND Mt7612uMappingSelftest)

# 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
86 changes: 65 additions & 21 deletions docs/mt7612u.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ 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.

**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.
**It is wired in.** `DEVOURER_MT7612U` (default OFF) compiles the subtree into
`libdevourer` and `WiFiDriver::CreateRadio` constructs an `Mt7612uRadio`, so a
devourer binary opens, receives and transmits on this part. Measured through
`rxdemo`/`txdemo`: 12000 frames received on ch36, and 19 `rx.txhit` on an
independent RTL8812AU witnessing our transmit at −38 dBm.

The knobs this backend does NOT implement refuse or warn rather than going
quiet — `SetTxMode`, `SetAmpduMode`, `SetCcaMode(true)`, `SetTxPowerIndexOverride`,
and the `tx.ack_timeout_us` / `tuning.disable_cca` / `tx.usb_agg_max` config
values. See "Counterparts" for what that costs.

## Why a MediaTek port is small

Expand Down Expand Up @@ -168,8 +173,8 @@ control.
Measured, because it constrains integration rather than being a detail.
`Packet::Data` carries the trailing FCS whenever `rx_pkt_attrib::fcs_present`
is set, which every Realtek parser leaves at its default. MT7612U cannot: the
MAC strips it, so this backend will clear that flag when it is wired in
(nothing sets it today - see Open below).
MAC strips it, so `Mt7612uRadio::on_rx()` clears that flag on every frame it
delivers.

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,
Expand Down Expand Up @@ -323,6 +328,21 @@ 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.
- **Two adapters share one diagnostic route.** The C library's log sink is a
single process-global pair, so `Mt7612uRadio` keeps a registry and routes to
the first live radio's `Logger`. With two MediaTek adapters open, the second
one's library diagnostics are gated by the first one's log level and stream
and carry no adapter identity. That is better than the alternative — per-object
install meant the second constructor stole the first's routing and the first
destructor unhooked the survivor's, dropping it to raw stderr — but it is not
right. The fix is a per-device sink in the C library
(`mt7612u_set_log_sink(dev, ...)`), which is a public-header change.
- **`TxStats::failed` counts refusals, not wire deaths.** A frame the transport
accepted and the URB then failed to deliver does not move it. The library's
own `tx_err` cannot simply be added: it double-counts submit failures, counts
URBs where this counts frames, and restarts at zero when a ring is torn down,
so `failed` would go backwards across an RX restart. Needs a monotonic
wire-failure counter in the library.
- **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.
Expand All @@ -343,12 +363,18 @@ 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.
- **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.
- **The library compiles in CI, and two ctest cells cover the integration; the
subtree's own four tests still do not run there.** With `DEVOURER_MT7612U=ON`
the whole platform matrix (gcc, clang, MSVC, mingw, macOS) builds the subtree
and the sanitizer job links it, so a portability or lifetime regression is
caught. `mt7612u_usb_ids` and `mt7612u_mapping` run on every configuration —
but both cover pure LOOKUPS, not the backend's behaviour, which has no
automated coverage beyond compiling. Every hardware claim in this document is
hand-run.
The four offline tests under `src/mt7612u/tests/` and the table generator's
`--check` are still driven only by `src/mt7612u/Makefile`, which no workflow
invokes — nor is there 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 All @@ -360,14 +386,32 @@ Stated because the numbers above are uniformly favourable.

Ordered, and honest about which are unknowns rather than typing:

1. `IRadio` implementation, `WiFiDriver` dispatch, `DeviceConfig` plumbing,
`CMakeLists.txt`, `ctest` cells. None of this exists.
Part of that adapter, called out because nothing will fail loudly if it is
forgotten: it MUST set `attrib.fcs_present = false` on every frame it
delivers. The field defaults to `true` - correct for every Realtek parser,
wrong for this part - so omitting the assignment silently feeds four bytes
of real payload to consumers that trim an FCS. There is no compile error,
no assert and no test that catches it.
1. **Two data races the library has when a channel change runs while the RX
ring is up.** Found with ThreadSanitizer against real hardware, driving
`rxdemo` with `DEVOURER_RX_SWEEP` so the main thread retunes while the
libusb event thread delivers frames. Neither is in `Mt7612uRadio` — that
class's `_mu` discipline held, and TSan reported nothing inside it — and
neither is reachable from the bring-up harness, which sets the channel
before starting a ring and measured 0 warnings over 10667 frames.

- `mt_read_rx_gain()` (`eeprom.cpp`) rewrites the per-channel `lna_gain` and
`rssi_offset[]` from the EEPROM on every tune, while `mt_rx_parse()`
(`rx.cpp`) reads them on the event thread to correct each frame's RSSI. A
frame parsed mid-retune therefore gets a mixed correction, i.e. a wrong
RSSI for that frame. Wrong number, not a crash — which is the class this
port cares most about.
- A synchronous control transfer issued during the tune (`mt_rr_chk` ->
`libusb_control_transfer`) reaches `libusb_free_transfer()`, destroying a
transfer's mutex, while the async ring's event thread locks it. That is
inside libusb, and it is the documented hazard of mixing the synchronous
API with a dedicated event thread on one context.

For scale: the same stress on the shipping RTL8812AU path produces **8**
TSan reports, all in devourer's own Jaguar1 state (`RtlJaguarDevice.cpp`,
`RtlAdapter.h`). Retune-during-RX is not a race-free operation anywhere in
this project today, so this is a shared gap rather than a MediaTek
regression — but the two above are specific and fixable, and the second one
is a use-after-destroy rather than a torn read.
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.
Expand Down
6 changes: 6 additions & 0 deletions examples/common/env_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ devourer::DeviceConfig devourer_config_from_env() {
cfg.bf.ndpa_period = p > 0 ? p : 1;
}

/* ---- MediaTek MT7612U ---- */
/* Folded in here rather than read inside the backend, so neither the C
* library nor the device class consults ambient process state. */
if (const char *e = env_str("DEVOURER_MT7612U_FW_DIR"))
cfg.mt7612u.firmware_dir = std::string(e);

/* ---- tuning ---- */
/* Defaults ON, so this reads the negation: only an explicit 0 disables it. */
if (const char *e = env_str("DEVOURER_TEARDOWN_POWER_DOWN"))
Expand Down
23 changes: 23 additions & 0 deletions src/DeviceConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,29 @@ struct DeviceConfig {
* (DEVOURER_PCIE_BDF) is likewise demo-local, like USB device selection. */
std::optional<int> rx_poll_us;
} pcie;

/* ---- MediaTek MT7612U (DEVOURER_MT7612U builds) ---------------------- */
struct Mt7612u {
/* env: DEVOURER_MT7612U_FW_DIR — directory holding mt7662_rom_patch.bin
* and mt7662.bin. Unset = search /lib/firmware/mediatek then ./firmware.
*
* A path rather than an embedded blob, unlike every Realtek backend: this
* firmware ships in linux-firmware under its own licence rather than being
* generated into hal/, and it is zstd-compressed on most distributions, so
* it can be neither vendored here nor assumed ready at a fixed path.
* Decompress both and point this at the directory.
*
* Here rather than a getenv inside the backend so the library and the
* device class both stay free of ambient process state; the demos fold the
* variable in, the way they do for every other knob in this file. */
std::optional<std::string> firmware_dir;
/* No adapter selector here on purpose. devourer chooses the adapter before
* a backend exists (DEVOURER_USB_BUS / _PORT / _VID / _PID) and hands the
* backend an already-claimed handle, so a MediaTek-specific selector would
* be read by nothing. The C library's own mt7612u_open_selected() is for a
* consumer that opens the device itself; MT7612U_DEV drives the bring-up
* tool, not devourer. */
} mt7612u;
};

} // namespace devourer
15 changes: 12 additions & 3 deletions src/WiFiDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
#endif
#include "rtl8733b/Rtl8733bUsbIds.h"
#include "mt7612u/Mt7612uUsbIds.h" /* header-only VID:PID table, always compiled */
#if defined(DEVOURER_HAVE_MT7612U)
#include "mt7612u/Mt7612uRadio.h"
#endif

namespace {

Expand Down Expand Up @@ -203,11 +206,17 @@ WiFiDriver::CreateRadio(libusb_device_handle *dev_handle,
* 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",
#if defined(DEVOURER_HAVE_MT7612U)
_logger->info("Creating Mt7612uRadio ({:04x}:{:04x})", vid, pid);
return std::make_unique<Mt7612uRadio>(dev_handle, ctx, std::move(usb_lock),
_logger, cfg);
#else
_logger->error("MediaTek MT7612U ({:04x}:{:04x}) detected but MediaTek "
"support is not compiled in (DEVOURER_MT7612U=OFF) — "
"refusing rather than misdetecting it as Realtek",
vid, pid);
return nullptr;
#endif
}

/* A vendor read that did not complete means this device is not speaking the
Expand Down
7 changes: 6 additions & 1 deletion src/mt7612u/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ CFLAGS ?= -O2 -g -Wall -Wextra
DEPFLAGS = -MMD -MP
INCS = -I. -Iinclude
LDLIBS = -lusb-1.0 -lpthread
LIBSRCS = $(wildcard *.cpp)
# Everything here EXCEPT the devourer-side backend. Mt7612uRadio.cpp implements
# devourer's IRadio and includes its headers (IRadio.h, logger.h, DeviceConfig.h),
# which this standalone build has no include path for and should not - the point
# of this Makefile is that the library builds with nothing but libusb. CMake
# compiles that file; this does not.
LIBSRCS = $(filter-out Mt7612uRadio.cpp,$(wildcard *.cpp))
TESTS = tests/api_link tests/frame_shape tests/field_macros tests/log_sink
CXXSRCS = $(LIBSRCS) tools/bringup.cpp tests/frame_shape.cpp tests/field_macros.cpp \
tests/log_sink.cpp
Expand Down
Loading
Loading