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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <thread>
#include <vector>

#include <libusb.h>

Expand Down Expand Up @@ -46,6 +48,43 @@ static const uint32_t g_qd_poll_ms = []() -> uint32_t {
return e ? static_cast<uint32_t>(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
* <devourer-csi>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
* <devourer-csi-wedged>) and refuses further writes; recover with
* libusb_reset_device / usbreset / power-cycle. */
static const std::vector<uint32_t> g_csi_selectors = []() -> std::vector<uint32_t> {
const char *e = std::getenv("DEVOURER_RX_DUMP_CSI");
if (!e || !*e) return {};
std::vector<uint32_t> 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<uint32_t>(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
Expand Down Expand Up @@ -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("<devourer-csi-wedged>after selector=0x%08x — reader "
"refusing further writes. Recover with "
"libusb_reset_device / usbreset.\n", sel);
fflush(stdout);
break;
}
printf("<devourer-csi>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
Expand Down
46 changes: 46 additions & 0 deletions src/BbDbgportReader.cpp
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions src/BbDbgportReader.h
Original file line number Diff line number Diff line change
@@ -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 <cstdint>

#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
11 changes: 11 additions & 0 deletions src/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,14 @@ std::array<uint32_t, 5> RtlJaguarDevice::get_queue_depth() const {
}
return out;
}

uint32_t RtlJaguarDevice::read_bb_dbgport(uint32_t selector) {
if (!_bb_dbgport) {
_bb_dbgport = std::make_unique<devourer::BbDbgportReader>(_device, _logger);
}
return _bb_dbgport->read_dbgport(selector);
}

bool RtlJaguarDevice::bb_dbgport_wedged() const {
return _bb_dbgport && _bb_dbgport->is_wedged();
}
11 changes: 11 additions & 0 deletions src/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
#include <functional>
#include <iostream>
#include <iomanip>
#include <memory>
#include <thread>

#include "logger.h"
#include "BbDbgportReader.h"
#include "HalModule.h"
#include "SelectedChannel.h"
#include "EepromManager.h"
Expand Down Expand Up @@ -62,13 +64,22 @@ class RtlJaguarDevice {
void start_queue_depth_poller(uint32_t interval_ms);
std::array<uint32_t, 5> 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);

std::array<std::atomic<uint32_t>, 5> _qd_snap{};
std::thread _qd_thread;
std::atomic<bool> _qd_stop{false};

std::unique_ptr<devourer::BbDbgportReader> _bb_dbgport;
};

/* Backwards-compatibility alias. External callers using the old name still
Expand Down
78 changes: 78 additions & 0 deletions tools/precoder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
<devourer-csi>hit=1 selector=0x0000001a value=0x????????
<devourer-csi>hit=1 selector=0x00000020 value=0x????????
<devourer-csi>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 `<devourer-csi>` 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 `<devourer>RX pkt` lines, control transfers
still succeed. The `BbDbgportReader` auto-detects this via a SYS_CFG
sanity read after every selector and self-wedges — `<devourer-csi-wedged>`
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
Expand Down
Loading
Loading