diff --git a/CMakeLists.txt b/CMakeLists.txt index 50291f8..7e715d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,8 @@ add_library(WiFiDriver hal/phydm/rtl8814a/Hal8814_PhyTables.h src/ieee80211_radiotap.h + src/BbDbgportReader.cpp + src/BbDbgportReader.h src/EepromManager.cpp src/EepromManager.h src/Firmware.h diff --git a/demo/main.cpp b/demo/main.cpp index 718eb5d..efe9d24 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include @@ -46,6 +48,43 @@ static const uint32_t g_qd_poll_ms = []() -> uint32_t { return e ? static_cast(std::strtoul(e, nullptr, 0)) : 0u; }(); +/* DEVOURER_RX_DUMP_CSI=hex,hex,... (or "0x1a,0x20,0x40"): F2 research + * spike. On each canonical-SA RX frame (first N frames), read BB + * dbgport 0x8FC at each selector and emit + * selector=0xNN value=0xNNNNNNNN + * + * This is a SELECTOR-SWEEP framework — the actual per-subcarrier IQ + * selector is missing from in-tree sources (see BbDbgportReader.h for + * details), so this knob exists so a researcher can try selectors at + * runtime, capture the resulting words, and look for plausible + * IQ-like patterns without recompiling. Throttled to the first 8 + * canonical-SA frames to bound brick-risk. + * + * BRICK RISK: enabling this writes to 0x8FC while RX is live. If the + * chip stops responding after a sweep, the reader self-wedges (see + * ) and refuses further writes; recover with + * libusb_reset_device / usbreset / power-cycle. */ +static const std::vector g_csi_selectors = []() -> std::vector { + const char *e = std::getenv("DEVOURER_RX_DUMP_CSI"); + if (!e || !*e) return {}; + std::vector out; + std::string s = e; + size_t pos = 0; + while (pos < s.size()) { + size_t comma = s.find(',', pos); + std::string tok = s.substr(pos, comma == std::string::npos + ? std::string::npos + : comma - pos); + if (!tok.empty()) { + out.push_back(static_cast(std::strtoul(tok.c_str(), nullptr, 0))); + } + if (comma == std::string::npos) break; + pos = comma + 1; + } + return out; +}(); +static constexpr int kCsiMaxFrames = 8; + static void packetProcessor(const Packet &packet) { /* C2H packets carry chip-side status updates, not 802.11 frames. Handle * them up front so the rest of this function (which assumes a normal @@ -121,6 +160,23 @@ static void packetProcessor(const Packet &packet) { hits, g_rx_count, packet.Data.size()); fflush(stdout); } + /* F2: BB-dbgport sweep on the first kCsiMaxFrames canonical-SA frames. */ + if (!g_csi_selectors.empty() && g_rtl_device != nullptr && + hits <= kCsiMaxFrames && !g_rtl_device->bb_dbgport_wedged()) { + for (uint32_t sel : g_csi_selectors) { + uint32_t v = g_rtl_device->read_bb_dbgport(sel); + if (g_rtl_device->bb_dbgport_wedged()) { + printf("after selector=0x%08x — reader " + "refusing further writes. Recover with " + "libusb_reset_device / usbreset.\n", sel); + fflush(stdout); + break; + } + printf("hit=%d selector=0x%08x value=0x%08x\n", + hits, sel, v); + } + fflush(stdout); + } /* DEVOURER_DUMP_SCRAMBLER=1: print the descrambler seed the chip * recovered from this frame's SERVICE field. Consumed by * tools/precoder/seed_probe.py --mode rx to learn the seed a precoder TX diff --git a/src/BbDbgportReader.cpp b/src/BbDbgportReader.cpp new file mode 100644 index 0000000..5448877 --- /dev/null +++ b/src/BbDbgportReader.cpp @@ -0,0 +1,46 @@ +#include "BbDbgportReader.h" + +namespace devourer { + +namespace { +constexpr uint16_t kRegSysCfg = 0x00F0; // hal/hal_com_reg.h:101 +} // namespace + +BbDbgportReader::BbDbgportReader(RtlUsbAdapter& device, Logger_t logger) + : _device{device}, _logger{logger} {} + +bool BbDbgportReader::is_chip_alive() { + uint32_t v = _device.rtw_read32(kRegSysCfg); + /* All-zeros or all-ones reads typically indicate libusb returned an + * error and zeroed/poisoned the buffer; a healthy SYS_CFG read always + * has *some* bit set in the upper half (chip-id / cut-version). */ + if (v == 0 || v == 0xFFFFFFFFu) { + _logger->warn("BbDbgportReader: SYS_CFG read returned 0x{:08x} — chip " + "appears wedged", v); + return false; + } + return true; +} + +uint32_t BbDbgportReader::read_dbgport(uint32_t selector) { + if (_wedged) { + /* Refuse further writes once we've seen a dead-chip — fail loud, not + * silently corrupt subsequent samples. */ + return 0; + } + uint32_t saved = _device.rtw_read32(kDbgPortSelectorReg); + _device.rtw_write32(kDbgPortSelectorReg, selector); + uint32_t value = _device.rtw_read32(kDbgPortReadbackReg); + _device.rtw_write32(kDbgPortSelectorReg, saved); + + if (!is_chip_alive()) { + _wedged = true; + _logger->error("BbDbgportReader: chip wedged after selector=0x{:08x} " + "(value=0x{:08x}). Recovery: libusb_reset_device, " + "usbreset, power-cycle. Reader will refuse further " + "writes until reconstructed.", selector, value); + } + return value; +} + +} // namespace devourer diff --git a/src/BbDbgportReader.h b/src/BbDbgportReader.h new file mode 100644 index 0000000..18f7c2c --- /dev/null +++ b/src/BbDbgportReader.h @@ -0,0 +1,88 @@ +/* BB-dbgport reader — exploration framework for the Realtek "Jaguar" PHY + * debug port. + * + * STATUS: research dead-end as shipped. The transport (write u32 selector + * to 0x8FC, read u32 result from 0xFA0 via libusb vendor control) works + * and is wrapped here with the canonical save/restore pattern from + * upstream's only in-tree user (hal/rtl8814a/rtl8814a_phycfg.c:460-545, + * `phy_ADC_CLK_8814A`). What's MISSING is the selector that routes the + * post-FFT per-subcarrier IQ bus to 0xFA0 — that selector lives in + * Realtek's phydm sources, which are not vendored in this tree. Without + * it, sweeping selectors gives raw BB internals (clock-domain status, + * MAC_Active bits, BB busy flags) but not the IQ samples the precoder + * README ("Tier 4") wants for shape verification. + * + * What this file provides is the FRAMEWORK so a researcher with access to + * a phydm selector catalogue can plug in the right value at runtime and + * read it back — without recompiling and without rediscovering the + * save/restore + chip-alive plumbing. + * + * BRICK RISK + * Poking 0x8FC while RX is live can leave demod state machines spinning + * (cf. the MAC_Active busy-wait at rtl8814a_phycfg.c:475-479). The + * canonical save/restore wrapper used here is the only safe pattern. + * Recovery ladder if the chip stops responding to RX after a sweep: + * 1. libusb_reset_device (set DEVOURER_SKIP_RESET=0 on next launch) + * 2. USB port-level usbreset (tests/regress.py style) + * 3. power-cycle / replug + * Symptoms are typically RX-stalls with control transfers still alive; + * no permanent damage observed in-tree, but treat first runs as + * destructive until proven otherwise. + * + * Gating: every helper is no-op unless an explicit env var is set in + * demo/main.cpp. There is no automatic invocation — read+restore happens + * only when a researcher asks for it. + */ + +#ifndef BB_DBGPORT_READER_H +#define BB_DBGPORT_READER_H + +#include + +#include "RtlUsbAdapter.h" +#include "logger.h" + +namespace devourer { + +class BbDbgportReader { + public: + /* Per Hal8814PhyReg.h:120,178 — bDPort_Sel / rDPdt is the upstream + * naming. We hard-code the addresses both for cross-chip portability + * (the pair sits in the same BB window on 8812 / 8821 / 8814) and + * because the Hal8814PhyReg.h aliases aren't in scope here. */ + static constexpr uint16_t kDbgPortSelectorReg = 0x08FC; + static constexpr uint16_t kDbgPortReadbackReg = 0x0FA0; + + BbDbgportReader(RtlUsbAdapter& device, Logger_t logger); + + /* Save 0x8FC, write `selector`, read 0xFA0, restore 0x8FC. Returns the + * 32-bit value read from 0xFA0. Pre-condition: caller has already + * paused TX or accepted that an in-flight TX may glitch (the upstream + * A-cut user pauses TX, zeros RXIQC, and waits for MAC_Active to clear; + * this v1 helper does NONE of that — it is the bare transport only). + * + * If is_chip_alive() returns false after the restore, the next call + * will refuse to write to 0x8FC and return 0 instead. The dead state + * persists for the lifetime of the BbDbgportReader instance to make + * the failure mode loud rather than silently corrupting samples. */ + uint32_t read_dbgport(uint32_t selector); + + /* Cheap chip-liveness check: read REG_SYS_CFG and verify the bits we + * already know to be stable across init. Returns false if the chip's + * stopped servicing reads (all-zeros or all-ones). Called automatically + * after every read_dbgport(); can also be called externally. */ + bool is_chip_alive(); + + /* Has any prior read_dbgport() detected a dead-chip post-write? Once + * true, stays true for the lifetime of the instance. */ + bool is_wedged() const { return _wedged; } + + private: + RtlUsbAdapter& _device; + Logger_t _logger; + bool _wedged = false; +}; + +} // namespace devourer + +#endif // BB_DBGPORT_READER_H diff --git a/src/RtlJaguarDevice.cpp b/src/RtlJaguarDevice.cpp index 3467717..f10afca 100644 --- a/src/RtlJaguarDevice.cpp +++ b/src/RtlJaguarDevice.cpp @@ -385,3 +385,14 @@ std::array RtlJaguarDevice::get_queue_depth() const { } return out; } + +uint32_t RtlJaguarDevice::read_bb_dbgport(uint32_t selector) { + if (!_bb_dbgport) { + _bb_dbgport = std::make_unique(_device, _logger); + } + return _bb_dbgport->read_dbgport(selector); +} + +bool RtlJaguarDevice::bb_dbgport_wedged() const { + return _bb_dbgport && _bb_dbgport->is_wedged(); +} diff --git a/src/RtlJaguarDevice.h b/src/RtlJaguarDevice.h index 3cf654e..09fb92a 100644 --- a/src/RtlJaguarDevice.h +++ b/src/RtlJaguarDevice.h @@ -6,9 +6,11 @@ #include #include #include +#include #include #include "logger.h" +#include "BbDbgportReader.h" #include "HalModule.h" #include "SelectedChannel.h" #include "EepromManager.h" @@ -62,6 +64,13 @@ class RtlJaguarDevice { void start_queue_depth_poller(uint32_t interval_ms); std::array get_queue_depth() const; + /* F2 research helper: read a u32 from the BB debug port at `selector`, + * with save/restore around register 0x8FC. Lazy-constructs the reader + * on first call. Returns 0 if the chip wedged on a prior call. See + * BbDbgportReader.h for the brick-risk caveats. */ + uint32_t read_bb_dbgport(uint32_t selector); + bool bb_dbgport_wedged() const; + private: void StartWithMonitorMode(SelectedChannel selectedChannel); bool NetDevOpen(SelectedChannel selectedChannel); @@ -69,6 +78,8 @@ class RtlJaguarDevice { std::array, 5> _qd_snap{}; std::thread _qd_thread; std::atomic _qd_stop{false}; + + std::unique_ptr _bb_dbgport; }; /* Backwards-compatibility alias. External callers using the old name still diff --git a/tools/precoder/README.md b/tools/precoder/README.md index 6a1aca4..2931ff0 100644 --- a/tools/precoder/README.md +++ b/tools/precoder/README.md @@ -122,6 +122,84 @@ tree: `0x8FC` dbgport is already poked at `HalModule.cpp:2167`, and `0x95C` (`rDMA_trigger_Jaguar2`, "ADC sample mode") is in `Hal8814PhyReg.h:190`. **Research spike** — undocumented register dance, BB-state brick risk. +### Tier 4 — current status (dead-end as shipped, framework in tree) + +The PR that wired this up shipped the *framework* and verified the +transport works, but not per-subcarrier IQ. Specifically: + +* **Transport confirmed.** `src/BbDbgportReader.{h,cpp}` wraps the + canonical "write u32 selector to `0x8FC` → read u32 result from + `0xFA0`" pattern with save/restore on `0x8FC`. The only in-tree user + of the same pair is `hal/rtl8814a/rtl8814a_phycfg.c:460-545` + (`phy_ADC_CLK_8814A`, A-cut 8814A only), which reads a "MAC active" + busy bit via this path — proving the transport is real. +* **Per-subcarrier selector missing from the tree.** Realtek's phydm + source — which contains the `phydm_set_bb_dbg_port` / `phydm_get_bb_dbg_port_value` + helpers and, crucially, the **selector catalogue** that maps a u32 + selector to a specific BB internal bus (CSI, AGC state, RF status, ...) + — is **not vendored** in this repo. `hal/phydm/` carries init tables + only. Without that catalogue, we can sweep selectors and read + whatever lands at `0xFA0`, but we can't claim "this selector value + routes the post-FFT IQ". +* **`0x95C` ADC-DMA path stays on the to-do list.** That register is + the per-symbol ADC capture trigger; reading the resulting buffer is + via DMA, not BB-register poll. Needs a FW-mailbox dance that's also + not in-tree. + +#### Using the framework to look for a selector + +`DEVOURER_RX_DUMP_CSI=0x1a,0x20,0x40 ./build/WiFiDriverDemo` performs +the save→write→read→restore dance for each selector on the first 8 +canonical-SA frames and emits + +``` +hit=1 selector=0x0000001a value=0x???????? +hit=1 selector=0x00000020 value=0x???????? +hit=1 selector=0x00000040 value=0x???????? +... +``` + +A "promising" selector is one where (a) `value` changes between +canonical-SA hits (i.e. tracks RX content, not chip clock), and +(b) the high half doesn't look like a status flag (no busy bit, no +sticky 0xF...). Per-subcarrier IQ would be a packed `int16 i, int16 q` +in a single u32, with selector encoding the subcarrier index in +its low bits — that's the upstream phydm convention, but unverified +here. + +`tools/precoder/csi_dump.py` parses `` lines and reports +which selectors changed across frames, which stayed constant, and +the per-bit toggle ratio (high = bus, low = status flag). + +#### Brick recovery if the chip stalls after a sweep + +Symptoms: RX stops producing `RX pkt` lines, control transfers +still succeed. The `BbDbgportReader` auto-detects this via a SYS_CFG +sanity read after every selector and self-wedges — `` +on stdout means subsequent selectors were skipped. + +Recovery ladder (each step is a fresh process invocation): +1. Re-run without `DEVOURER_SKIP_RESET` — `libusb_reset_device` clears + most stalls. +2. USB port-level reset (`tests/regress.py` reuses `usbreset` for the + 8814 USB-IO mitigation; same mechanism here). +3. Unplug-replug. No permanent damage has been observed in-tree from + `0x8FC` writes alone, but treat first runs as destructive until proven + otherwise. + +#### What would unblock real IQ capture + +1. Vendoring (or independently rediscovering) Realtek's phydm dbgport + selector catalogue. The variable name in the upstream FW is typically + `phydm_dbgport_sel` or similar; rtwpriv MP-tool dumps from a Realtek + reference platform would expose it. +2. Or: implementing the `0x95C` ADC-DMA path. That gives raw ADC samples + (pre-FFT) at full bandwidth; an off-chip 64-point FFT would recover + per-subcarrier IQ. Larger scope, separate PR. + +Until one of those lands, this Tier stays "framework in tree, capture +out of reach." `src/BbDbgportReader.h` carries the same warning. + **Tier 5 — IQK TX→RX loopback** (`*`if it carries a full PPDU). The path-enable dance already lives in `Iqk8812a.cpp`/`Iqk8814a.cpp` (the `0x77…` writes). The risk isn't the regs — it's that IQK loopback carries the calibration *tone* via diff --git a/tools/precoder/csi_dump.py b/tools/precoder/csi_dump.py new file mode 100644 index 0000000..0afa799 --- /dev/null +++ b/tools/precoder/csi_dump.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Parse lines emitted by WiFiDriverDemo with +DEVOURER_RX_DUMP_CSI=hex,hex,... set and report which selectors look +like a real BB bus vs static status flags. + +The C++ side runs the dbgport sweep on the first 8 canonical-SA frames. +This script groups the values per selector and reports: + + * which selectors changed across frames (= dynamic, possibly a bus) + * which stayed constant (= static, usually a status flag or clock-domain bit) + * per-bit toggle ratio (a high ratio across MANY bits suggests data; + a high ratio on a few bits suggests counters or single-bit flags) + +A real per-subcarrier IQ selector would show: + * value changing on every frame (different RX content -> different IQ) + * high toggle ratio on the low 16 bits (Q) AND the high 16 bits (I), + since both should swing across all 16 bits worth of range + * NO sticky bits in the upper byte (no busy / clock-domain marker) + +Run: + ./build/WiFiDriverDemo > /tmp/csi.log 2>&1 # with DEVOURER_RX_DUMP_CSI set + uv run python tools/precoder/csi_dump.py /tmp/csi.log +""" + +from __future__ import annotations + +import argparse +import re +import sys +from collections import defaultdict +from pathlib import Path + +CSI_RE = re.compile( + r"hit=(?P\d+)\s+selector=0x(?P[0-9a-fA-F]+)" + r"\s+value=0x(?P[0-9a-fA-F]+)" +) +WEDGE_RE = re.compile(r"after selector=0x([0-9a-fA-F]+)") + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("log", type=Path, help="WiFiDriverDemo stdout/stderr capture") + args = p.parse_args() + + if not args.log.exists(): + print(f"no such file: {args.log}", file=sys.stderr) + return 1 + + per_selector: dict[int, list[int]] = defaultdict(list) + wedged_after: int | None = None + + for line in args.log.read_text().splitlines(): + m = CSI_RE.search(line) + if m: + sel = int(m.group("sel"), 16) + val = int(m.group("val"), 16) + per_selector[sel].append(val) + continue + w = WEDGE_RE.search(line) + if w: + wedged_after = int(w.group(1), 16) + + if not per_selector: + print("no lines found", file=sys.stderr) + return 2 + + print(f"# parsed {sum(len(v) for v in per_selector.values())} samples " + f"across {len(per_selector)} selectors") + if wedged_after is not None: + print(f"# WEDGED after selector=0x{wedged_after:08x}") + print() + print(f"{'selector':>12} {'n':>3} {'unique':>6} {'mean':>10} " + f"{'high32':>10} {'low32':>10} {'toggle/32':>9} classification") + + for sel in sorted(per_selector): + vs = per_selector[sel] + n = len(vs) + unique = len(set(vs)) + mean = sum(vs) / n + # Stickiness: how many bits stayed constant across samples? + and_mask = 0xFFFFFFFF + or_mask = 0 + for v in vs: + and_mask &= v + or_mask |= v + sticky_bits = bin(~(or_mask ^ and_mask) & 0xFFFFFFFF).count("1") + toggle_bits = 32 - sticky_bits + # Classify + if unique == 1: + cls = "STATIC (status flag / const)" + elif toggle_bits >= 16: + cls = "DYNAMIC (wide-bit bus — candidate)" + elif toggle_bits >= 4: + cls = "PARTIAL (counter or narrow field)" + else: + cls = "SINGLE-BIT (flag/clock)" + print(f" 0x{sel:08x} {n:>3} {unique:>6} {int(mean):>10} " + f"0x{(vs[0] >> 16) & 0xFFFF:>8x} 0x{vs[0] & 0xFFFF:>8x} " + f"{toggle_bits:>9} {cls}") + + print() + print("# Notes:") + print("# - 'DYNAMIC (wide-bit bus)' selectors are the IQ-candidate set;") + print("# cross-check against frame-by-frame variance by raising") + print("# DEVOURER_RX_DUMP_CSI sample count (edit kCsiMaxFrames in demo/main.cpp).") + print("# - Per-subcarrier IQ would pack int16 i + int16 q in one u32;") + print("# high16 AND low16 both swing wide. A 'DYNAMIC' selector with") + print("# only the low 16 bits toggling is more likely a counter.") + print("# - Selector encoding for per-subcarrier capture (upstream phydm)") + print("# typically packs subcarrier index in the low bits of the selector.") + print("# Sweep adjacent values (0x40,0x41,0x42,...) to look for a") + print("# monotonic-index relationship.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())