diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index e4e0f1e..2c21342 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -146,7 +146,7 @@ jobs: run: > cmake --build build --target WiFiDriver StreamTxDemo StreamDuplexDemo StreamStdinSelftest - ToneMaskSelftest BfReportDecodeSelftest + ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest - name: Test working-directory: build diff --git a/CLAUDE.md b/CLAUDE.md index bf3c07f..cdbb71c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,22 +302,35 @@ that exercise the salvage path. ## Frequency hopping -`RtlJaguarDevice::FastRetune(channel)` is a lean intra-band, same-bandwidth -channel retune (~1.5 ms vs ~275 ms for a full `SetMonitorChannel`) — it does the -RF channel switch only, skipping the per-rate TX-power loop, bandwidth post-set, -and thermal tick a hop doesn't need, and writes `RF_CHNLBW` from a cached value -so it never pays the C-cut RF-read sleep. It falls back to the full path on a -band change. `send_packet` also honours a radiotap `CHANNEL` field, so hopping is +`IRtlDevice::FastRetune(channel)` is a lean intra-band, same-bandwidth channel +retune every generation implements (default = the full `SetMonitorChannel`): it +does the RF channel switch only, skipping the steady-state work a hop doesn't +need, writes the channel RF register from a cached value, and re-writes the +channel-keyed constants only when their bucket changes. Measured medians: +8812AU ~251→1.6 ms, 8822BU ~62→18 ms, 8821CU ~29→5.6 ms, 8822CU/8812EU +~11→3-4 ms. It falls back to the full path on a band change; on Jaguar3 it +serializes on the coex thread's register mutex (the sanctioned in-session hop) +and preserves the 5/10 MHz narrowband dividers across hops. `send_packet` on +all three generations also honours a radiotap `CHANNEL` field, so hopping is per-packet and radiotap-driven like rate. Both demos hop via -`DEVOURER_HOP_CHANNELS="1,6,11"` (+ `DEVOURER_HOP_DWELL_FRAMES`, +`DEVOURER_HOP_CHANNELS="1,6,11"` (SweepSpec grammar — also `36-48/4` channel +ranges and `5170-5250/5` MHz ranges; + `DEVOURER_HOP_DWELL_FRAMES`, `DEVOURER_HOP_ROUNDS`, `DEVOURER_HOP_FAST=0|1|2`, `DEVOURER_HOP_RADIOTAP`, `DEVOURER_HOP_BW`/`DEVOURER_HOP_OFFSET`). Per-packet hopping doubles as a frequency-diversity interleaver for the outer FEC. Validated by `tests/run_hop_validation.sh` (B210 wideband), `tests/hop_parity_check.sh` -(register parity for the 40/80 path), and `tools/precoder/hop_diversity_sim.py`. -The implementation and the in-code techniques are documented in -**`docs/frequency-hopping.md`** (also a porting guideline for other chip -generations). +(family-aware register parity — all three generations dump the same canary +from both paths), and `tools/precoder/hop_diversity_sim.py`. The +implementation, the in-code techniques, and the per-generation ports are +documented in **`docs/frequency-hopping.md`**. + +The hop-driven RX counterpart is the sweep/sounding pair: `DEVOURER_RX_SWEEP` +dwells FastRetune-cheap bins emitting per-bin energy + frame stats +(`DEVOURER_RX_SWEEP_FULL=1` forces full retunes; `DEVOURER_RX_AGG_SA=canon` +filters the frame stats to the canonical probe SA), and +`tests/sounding_sweep.sh` + `tests/sounding_map.py` run the two-ended active +sounding that recovers a coarse per-bin H(f) of the link — down to 5 MHz bins +on Jaguar3 (`docs/rx-spectrum-sensing.md`). `WiFiDriverTxDemo` also honours a TX-gain ramp + duty knob for thermal / TX-power characterisation (drives `RtlJaguarDevice::SetTxPowerOverride` + diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a464b7..eaf63c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,6 +304,17 @@ target_link_libraries(ToneMaskSelftest PRIVATE WiFiDriver) add_test(NAME tone_mask_math COMMAND ToneMaskSelftest) +# Headless guard for the sweep/hop bin-list grammar (src/SweepSpec.h) — the +# channel-list / channel-range / MHz-range parsing behind DEVOURER_RX_SWEEP and +# DEVOURER_HOP_CHANNELS, so a grammar regression fails `ctest` instead of only +# surfacing on a radio. +add_executable(SweepSpecSelftest + tests/sweep_spec_selftest.cpp +) +target_link_libraries(SweepSpecSelftest PRIVATE WiFiDriver) + +add_test(NAME sweep_spec_math COMMAND SweepSpecSelftest) + # Headless guard for the beamforming-report decoder (src/BfReportDecode.h) — the # LSB-first Givens-angle unpacking + per-tone variance behind WiFiSenseDemo. It # decodes a real captured VHT MU report and checks the angles against an offline diff --git a/README.md b/README.md index 56a1118..95f796e 100644 --- a/README.md +++ b/README.md @@ -210,14 +210,25 @@ Common to both demos: mass into higher buckets (measured: peak bucket 5→8 on the 8822CU under a co-located CW tone). All three generations. - `DEVOURER_RX_SWEEP="1,6,11"` — coarse **live spectrum map**. Cycle the listed - channels, dwelling `DEVOURER_RX_SWEEP_DWELL_MS` (default 300) on each, and emit - one `ch=N` line per bin. The RX loop runs on a worker thread - while the main thread retunes (`SetMonitorChannel`) between reads — one process, - uniform across all three generations. Park a `DEVOURER_CW_TONE` on one channel - (another adapter) and the map peaks (or, on the saturating 1T1R parts, dips) at - that channel. Resolution = the channel grid (20 MHz), down to ~5 MHz on Jaguar3 - with `DEVOURER_NB_BW=5`. Render with `tests/rx_spectrum_sweep.sh` + + bins (also accepts channel ranges `36-48/4` and MHz ranges `5170-5250/5`), + dwelling `DEVOURER_RX_SWEEP_DWELL_MS` (default 300) on each, and emit one + `ch=N` line per bin carrying the frame-free counters, the + measured `retune_us`, and the dwell's per-frame + `frames/rssi_mean/rssi_max/snr_mean/snr_min/evm_mean` aggregate. The RX loop + runs on a worker thread while the main thread retunes between reads via the + lean `FastRetune` (`DEVOURER_RX_SWEEP_FULL=1` forces the full + `SetMonitorChannel` per dwell) — one process, uniform across all three + generations. Park a `DEVOURER_CW_TONE` on one channel (another adapter) and + the map peaks (or, on the saturating 1T1R parts, dips) at that channel. + Resolution = the channel grid (20 MHz), down to ~5 MHz on Jaguar3 with + `DEVOURER_NB_BW=5`. Render with `tests/rx_spectrum_sweep.sh` + `tests/rx_spectrum_sweep.py`. +- `DEVOURER_RX_AGG_SA=canon|` — restrict the per-frame aggregate (energy + telemetry + sweep) to frames whose SA matches (`canon` = the canonical txdemo + beacon SA). The active-sounding filter: a prober hopping fixed-rate beacons + across the sweep's bins (`tests/sounding_sweep.sh`) turns the sweep into a + coarse per-bin H(f) map of the actual link, rendered by + `tests/sounding_map.py` (see `docs/rx-spectrum-sensing.md`). `WiFiDriverTxDemo`-only knobs: diff --git a/demo/main.cpp b/demo/main.cpp index 8dc6839..1a01879 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -13,6 +13,7 @@ #include "BfReportDetect.h" #include "RxPacket.h" +#include "SweepSpec.h" #if defined(DEVOURER_HAVE_JAGUAR1) #include "jaguar1/RtlJaguarDevice.h" #endif @@ -142,50 +143,83 @@ static const uint32_t g_rx_energy_ms = []() -> uint32_t { return (e && *e) ? static_cast(std::strtoul(e, nullptr, 0)) : 0; }(); -/* DEVOURER_RX_SWEEP="1,6,11": live coarse spectrum sweep. Cycle the listed - * channels, dwelling DEVOURER_RX_SWEEP_DWELL_MS (default 300) on each, and emit - * one ch=N line per bin. The RX loop runs on a worker thread so - * the main thread is free to retune (SetMonitorChannel) between reads — a live +/* DEVOURER_RX_SWEEP="1,6,11" | "36-48/4" | "5170-5250/5": live coarse spectrum + * sweep. Cycle the listed bins (SweepSpec grammar: channels, channel ranges, + * or MHz ranges — the latter for issue-#149-style narrowband maps), dwelling + * DEVOURER_RX_SWEEP_DWELL_MS (default 300) on each, and emit one + * ch=N line per bin. The RX loop runs on a worker thread so + * the main thread is free to retune (FastRetune) between reads — a live * energy-vs-frequency map that localizes an interferer (peaks, or dips on the * 1T1R parts that saturate, at the tone's channel). Empty = disabled. */ -static const std::vector g_rx_sweep = []() -> std::vector { - std::vector out; - const char *e = std::getenv("DEVOURER_RX_SWEEP"); - if (!e || !*e) return out; - std::string s = e; - size_t pos = 0; - while (pos < s.size()) { - size_t c = s.find(',', pos); - std::string tok = - s.substr(pos, c == std::string::npos ? std::string::npos : c - pos); - if (!tok.empty()) out.push_back(std::atoi(tok.c_str())); - if (c == std::string::npos) break; - pos = c + 1; - } - return out; -}(); +static const std::vector g_rx_sweep = + devourer::parse_sweep_spec(std::getenv("DEVOURER_RX_SWEEP")); static const uint32_t g_rx_sweep_dwell_ms = []() -> uint32_t { const char *e = std::getenv("DEVOURER_RX_SWEEP_DWELL_MS"); return (e && *e) ? static_cast(std::strtoul(e, nullptr, 0)) : 300; }(); -/* Rolling per-frame RSSI/SNR aggregate (the frame-driven signal, all gens): - * updated per received frame in packetProcessor, drained each interval by the - * energy emitter. */ +/* Rolling per-frame RSSI/SNR/EVM aggregate (the frame-driven signal, all + * gens): updated per received frame in packetProcessor, drained each interval + * by the energy emitter and per dwell by the sweep loop. EVM counts only + * frames that report one (CCK / non-type1 phy-status leaves it 0), so a mixed + * stream doesn't bias the mean toward 0. */ static std::mutex g_rxagg_mu; struct RxAgg { uint32_t n = 0; int32_t rssi_sum = 0, rssi_max = -128, snr_sum = 0, snr_min = 127; - void add(int rssi, int snr) { + int32_t evm_sum = 0; + uint32_t evm_n = 0; + void add(int rssi, int snr, int evm) { ++n; rssi_sum += rssi; if (rssi > rssi_max) rssi_max = rssi; snr_sum += snr; if (snr < snr_min) snr_min = snr; + if (evm != 0) { evm_sum += evm; ++evm_n; } } }; static RxAgg g_rxagg; +/* The canonical txdemo beacon SA (same constant as txdemo/main.cpp and + * tests/regress.py CANONICAL_SA — change all three together): the + * matcher and the "canon" aggregate filter below. */ +static const uint8_t kTxSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; + +/* DEVOURER_RX_AGG_SA: restrict the per-frame aggregate to frames whose SA + * matches — the active-sounding filter, so ambient traffic doesn't pollute the + * per-bin link stats (a sweep dwell hears everything on the bin; H(f) wants + * only the probe TX). "canon" = kTxSa; "aa:bb:cc:dd:ee:ff" = that address; + * unset/"any" = every frame (existing behaviour). */ +static bool g_agg_sa_filter = false; +static uint8_t g_agg_sa[6]; +static const bool g_agg_sa_parsed = []() { + const char *e = std::getenv("DEVOURER_RX_AGG_SA"); + if (!e || !*e || std::strcmp(e, "any") == 0) + return false; + if (std::strcmp(e, "canon") == 0) { + std::memcpy(g_agg_sa, kTxSa, 6); + g_agg_sa_filter = true; + return true; + } + unsigned b[6]; + if (std::sscanf(e, "%x:%x:%x:%x:%x:%x", &b[0], &b[1], &b[2], &b[3], &b[4], + &b[5]) == 6) { + for (int i = 0; i < 6; i++) + g_agg_sa[i] = static_cast(b[i]); + g_agg_sa_filter = true; + return true; + } + return false; +}(); +static bool agg_sa_match(const Packet &packet) { + if (!g_agg_sa_filter) + return true; + /* SA at offset 10 (FC + duration + addr1) — same layout the tx-hit matcher + * keys on; frames too short to carry it don't count. */ + return packet.Data.size() >= 16 && + std::memcmp(packet.Data.data() + 10, g_agg_sa, 6) == 0; +} + /* Emit the frame-free NHM power histogram (IRtlDevice::GetRxEnergy fills it) as * a distinct line so it never disturbs the * format its regex consumers key on. `peak` = the fullest bucket (0 = quiet @@ -254,11 +288,14 @@ static void packetProcessor(const Packet &packet) { ++g_rx_count; - /* Feed the rolling per-frame RSSI/SNR aggregate for DEVOURER_RX_ENERGY_MS - * (the frame-driven half of the energy telemetry). path-A chain. */ - if (g_rx_energy_ms > 0) { + /* Feed the rolling per-frame RSSI/SNR/EVM aggregate for DEVOURER_RX_ENERGY_MS + * and the sweep's per-dwell frame stats (the frame-driven half of the energy + * telemetry). path-A chain; DEVOURER_RX_AGG_SA optionally restricts it to the + * sounding probe's SA. */ + if ((g_rx_energy_ms > 0 || !g_rx_sweep.empty()) && agg_sa_match(packet)) { std::lock_guard lk(g_rxagg_mu); - g_rxagg.add(packet.RxAtrib.rssi[0], packet.RxAtrib.snr[0]); + g_rxagg.add(packet.RxAtrib.rssi[0], packet.RxAtrib.snr[0], + packet.RxAtrib.evm[0]); } if (g_rx_count == 1) { @@ -302,7 +339,6 @@ static void packetProcessor(const Packet &packet) { * one adapter while WiFiDriverTxDemo runs against another on the same * channel, each hit confirms an injected frame made it over the air. */ if (packet.Data.size() >= 16) { - static const uint8_t kTxSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; if (std::memcmp(packet.Data.data() + 10, kTxSa, 6) == 0) { static int hits = 0; ++hits; @@ -681,11 +717,14 @@ int main() { else std::snprintf(igi, 16, "-"); int rssi_mean = agg.n ? agg.rssi_sum / static_cast(agg.n) : 0; int snr_mean = agg.n ? agg.snr_sum / static_cast(agg.n) : 0; + int evm_mean = + agg.evm_n ? agg.evm_sum / static_cast(agg.evm_n) : 0; printf("cca_ofdm=%s cca_cck=%s fa_ofdm=%s fa_cck=%s " "igi=%s frames=%u rssi_mean=%d rssi_max=%d snr_mean=%d " - "snr_min=%d\n", + "snr_min=%d evm_mean=%d\n", cco, ccc, fao, fac, igi, agg.n, rssi_mean, - agg.n ? agg.rssi_max : 0, snr_mean, agg.n ? agg.snr_min : 0); + agg.n ? agg.rssi_max : 0, snr_mean, agg.n ? agg.snr_min : 0, + evm_mean); fflush(stdout); emit_nhm(e, -1); nap(g_rx_energy_ms); @@ -742,19 +781,48 @@ int main() { fflush(stdout); } }); - /* Let bring-up complete before the first retune. */ - for (int s = 0; s < 2500 && !g_devourer_should_stop; s += 50) + /* Let bring-up complete before the first retune: a retune racing the + * worker thread's init (FW download, DACK/IQK on Jaguar3) interleaves + * register writes with calibration. The first RX frame proves bring-up + * finished; a silent channel falls through after the 10 s cap. */ + for (int s = 0; s < 10000 && !g_devourer_should_stop && g_rx_count == 0; + s += 50) std::this_thread::sleep_for(std::chrono::milliseconds(50)); size_t bi = 0; + /* Dwell retunes ride FastRetune (lean intra-band fast path on every + * generation, internal full-path fallback on a band change); + * DEVOURER_RX_SWEEP_FULL=1 forces the full SetMonitorChannel per dwell + * (A/B escape hatch). */ + const bool sweep_full = std::getenv("DEVOURER_RX_SWEEP_FULL") != nullptr; while (!g_devourer_should_stop) { int ch = g_rx_sweep[bi % g_rx_sweep.size()]; ++bi; - dev->SetMonitorChannel( - SelectedChannel{static_cast(ch), ch_offset, width}); + const auto rt0 = std::chrono::steady_clock::now(); + if (sweep_full) + dev->SetMonitorChannel( + SelectedChannel{static_cast(ch), ch_offset, width}); + else + dev->FastRetune(static_cast(ch), /*cache_rf=*/true); + const long long retune_us = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - rt0) + .count(); + { + /* Drop frames captured before/during the retune — the dwell's frame + * stats must only cover this bin. */ + std::lock_guard lk(g_rxagg_mu); + g_rxagg = RxAgg{}; + } for (uint32_t s = 0; s < g_rx_sweep_dwell_ms && !g_devourer_should_stop; s += 50) std::this_thread::sleep_for(std::chrono::milliseconds(50)); RxEnergy e = dev->GetRxEnergy(); + RxAgg agg; + { + std::lock_guard lk(g_rxagg_mu); + agg = g_rxagg; + g_rxagg = RxAgg{}; + } char cco[16], ccc[16], fao[16], fac[16], igi[16]; auto u = [](char *b, bool v, uint32_t x) { if (v) std::snprintf(b, 16, "%u", x); else std::snprintf(b, 16, "-"); @@ -764,9 +832,15 @@ int main() { u(fao, e.valid_fa, e.fa_ofdm); u(fac, e.valid_fa, e.fa_cck); u(igi, e.valid_igi, e.igi); + int rssi_mean = agg.n ? agg.rssi_sum / static_cast(agg.n) : 0; + int snr_mean = agg.n ? agg.snr_sum / static_cast(agg.n) : 0; + int evm_mean = agg.evm_n ? agg.evm_sum / static_cast(agg.evm_n) : 0; printf("ch=%d cca_ofdm=%s cca_cck=%s fa_ofdm=%s " - "fa_cck=%s igi=%s\n", - ch, cco, ccc, fao, fac, igi); + "fa_cck=%s igi=%s retune_us=%lld frames=%u rssi_mean=%d " + "rssi_max=%d snr_mean=%d snr_min=%d evm_mean=%d\n", + ch, cco, ccc, fao, fac, igi, retune_us, agg.n, rssi_mean, + agg.n ? agg.rssi_max : 0, snr_mean, agg.n ? agg.snr_min : 0, + evm_mean); fflush(stdout); emit_nhm(e, ch); } diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index 3fb02a7..18b753d 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -292,10 +292,51 @@ constants once); every subsequent same-band hop is ~1.5 ms. - The receiver still needs N adapters, a wideband SDR, or lockstep hopping to hear all channels — one adapter has one LO on RX too. -## Porting to other generations - -The same recipe ports to any Realtek generation (Jaguar2 / Jaguar3 / Kestrel). -Work through these in order: +## The ports: all three generations + +`IRtlDevice::FastRetune(channel, cache_rf)` is the generation-agnostic entry +point (default = the full `SetMonitorChannel` at the current width/offset), and +every generation overrides it with a lean path built from the tricks above: + +| DUT | full path | fast (cached) | fast (no cache) | +|-----|-----------|---------------|-----------------| +| RTL8812AU (Jaguar1) | ~251 ms | **~1.6 ms** | ~99 ms | +| RTL8822BU (Jaguar2) | ~62 ms | **~18 ms** | ~20 ms | +| RTL8821CU (Jaguar2) | ~29 ms | **~5.6 ms** | ~5.6 ms | +| RTL8822CU (Jaguar3) | ~11 ms | **~3.6 ms** | ~3 ms | +| RTL8812EU (Jaguar3) | ~11 ms | **~4.3 ms** | ~3 ms | + +(Median `switch_us` over a 1/6/11 hop set. The newer generations' +full paths are already far cheaper than Jaguar1's — no 20 ms C-cut read sleeps — +so their win comes from transfer-count reduction, and `cache_rf=false` costs +only the one extra RF18 read.) + +**Jaguar2** (`HalJaguar2::fast_retune`, both variants): one cached full-register +RF18 write collapses the full path's read-modify-write rounds (three of them on +the 8821C's band/channel/BW split); AGC index, CFO fc, the 8822B RF 0xBE VCO +band / ch144 RF 0xDF flag / 2G spur registers and the 8821C 2G CCK filter are +written on bucket change; the per-hop RX kick (8822B RF 0xb8 + RX-path toggles, +IGI toggle on both variants) runs every hop. RFE pins and the 8821C +switch-band/RF-set block are band-keyed and stay untouched. + +**Jaguar3** (`RadioManagementJaguar3::fast_retune`, both variants): the RF18 +write inside its 3-wire bracket + force-anapar + BB reset every hop; SCO fc, +TX DFIR and the AGC table on bucket change; the 8822c RF18 read-modify-write +becomes a lazily-primed cached write (primed on the first fast hop, after +init-time IQK has settled RF18 — priming at full-set time would cache a value +calibration then rewrites). In 5/10 MHz narrowband mode fast hops preserve the +divider re-clock (no per-dwell `set_bandwidth_dividers` + DAC-FIFO reset), and +the TX-DFIR write uses the NB-owned `0x808[6:4]` value on the 8822e. + +**End-state parity, not step parity** — the subtlety the Jaguar3 port +hardware-taught: init-time calibration (halrf IQK) rewrites BW-keyed state +*after* the channel set (e.g. it clears the 40 MHz TX_CCK_IND bit in the +8822e's RF 0x1a), and the full path re-imposes the channel-set values on every +retune. "Skip BW-keyed work" must therefore mean "re-assert it once per +fast-hop epoch", not "never touch it" — the parity oracle defines correct as +"the fast path ends in the same register state the full path ends in". + +## Porting to another generation (the method that produced the above) 1. **Find the channel write and the per-stage cost.** Instrument the channel set with a per-stage timer. Identify which stages are channel-dependent and @@ -315,11 +356,16 @@ Work through these in order: one-time secondary-channel write (Trick 4). Otherwise, reuse the full BW post-set with a skip-heavy flag. 5. **Gate band changes out of the fast path** — they need the front-end - reconfiguration and recalibration a hop skips. -6. **Validate two ways:** wideband SDR on-air for end-to-end behaviour, and - register-parity against the full path for cases where a clean on-air reading - isn't available. Always run a full-vs-full control to separate inherent register - variance (timers, calibration jitter) from real parity breaks. + reconfiguration and recalibration a hop skips. And check what the + generation's *calibration* rewrites after a channel set: anything it touches + that the full path re-imposes must be re-asserted once per fast-hop epoch + (the end-state parity rule above). +6. **Validate two ways:** wideband SDR on-air for end-to-end behaviour + (`tests/run_hop_validation.sh`), and register-parity against the full path + (`tests/hop_parity_check.sh` — family-aware; all three generations emit the + same `DumpCanary` format from both paths). Always run a full-vs-full control + to separate inherent register variance (timers, calibration jitter, live + AGC/thermal state) from real parity breaks. The throughline: a channel switch is mostly work a *hop* does not need. Strip it to the one register that actually changes, and remove the read that change diff --git a/docs/rx-spectrum-sensing.md b/docs/rx-spectrum-sensing.md index 087899d..c2b9cf4 100644 --- a/docs/rx-spectrum-sensing.md +++ b/docs/rx-spectrum-sensing.md @@ -105,20 +105,69 @@ stays in the moderate regime where `cca_ofdm` rises without saturating. The energy sensor reads one channel at a time; to localise an interferer in frequency, sweep. With `DEVOURER_RX_SWEEP="1,6,11"` the sensor cycles the listed -channels — the RX loop runs on a worker thread while the main thread retunes -between reads (`SetMonitorChannel`, uniform across all three generations) — and -emits one `ch=N` line per bin. Aggregating those into an -energy-vs-frequency bar chart peaks (or, on the saturating 1T1R parts, dips) at -the tone's channel. +bins — the RX loop runs on a worker thread while the main thread retunes between +reads via `IRtlDevice::FastRetune` (the lean intra-band hop path every +generation implements; `DEVOURER_RX_SWEEP_FULL=1` forces the full +`SetMonitorChannel` per dwell for A/B) — and emits one `ch=N` +line per bin. Aggregating those into an energy-vs-frequency bar chart peaks (or, +on the saturating 1T1R parts, dips) at the tone's channel. + +The bin spec uses the SweepSpec grammar (`src/SweepSpec.h`, shared with +`DEVOURER_HOP_CHANNELS`): channel lists (`1,6,11`), channel ranges (`36-48/4`), +or centre-frequency MHz ranges (`5170-5250/5`). + +Each `ch=N` line carries the frame-free counters plus `retune_us=` (the measured +dwell-switch cost) and the per-dwell frame aggregate — +`frames/rssi_mean/rssi_max/snr_mean/snr_min/evm_mean` over the frames decoded +during that dwell. `DEVOURER_RX_AGG_SA=canon|` restricts the aggregate to +one transmitter's SA (the active-sounding filter; default counts every frame). +The aggregate is drained at each dwell start, so retune-transient frames never +leak into a bin. The resolution is the channel grid: 20 MHz on the 2.4/5 GHz plan, and down to ~5 MHz on Jaguar3 (`DEVOURER_NB_BW=5` — the 2.4 GHz channels are 5 MHz apart, so -stepping them at 5 MHz bandwidth gives 5 MHz bins). This is a scalar-energy -spectrum, not an FFT — there is no sub-channel structure within a bin. +stepping them at 5 MHz bandwidth gives 5 MHz bins; fast dwells preserve the +narrowband dividers, so an NB sweep never re-runs the re-clock recipe). This is +a scalar-energy spectrum, not an FFT — there is no sub-channel structure within +a bin. + +Passive-map caveat (measured, both retune paths): a bare CW tone sitting exactly +on an NB bin's centre lands at DC after downconversion and the receiver's DC +null hides it from CCA — the tone registers on the *adjacent* 5 MHz bins +instead. A modulated interferer doesn't have this blind spot. `tests/rx_spectrum_sweep.sh` runs a single live sweep and `tests/rx_spectrum_sweep.py` renders the map + flags the peak/dip bin. +## Active two-ended sounding — a coarse H(f) of the link + +The passive map sees interferers but is blind to fading of *our own* path (a +faded-but-quiet bin looks clean). Active sounding probes it: the TX end hops +fixed-rate probe beacons (the canonical SA) across the bin list via FastRetune +while the RX end sweeps the same bins with `DEVOURER_RX_AGG_SA=canon`, so each +`ch=N` line reports the probe's per-bin RSSI/EVM — a genuine coarse sounding of +H(f) over the actual link. + +Synchronisation is asymmetric-duty with no control channel: the TX cycles all +bins fast and the RX dwells ≥ ~2 full TX cycles per bin, so every dwell overlaps +at least one probe visit. `tests/sounding_sweep.sh` orchestrates the pair +(computing the duty maths from the bin count), and `tests/sounding_map.py` +renders the recovered map — headline metric is the per-bin median of `rssi_max` +(off-channel bleed decodes weaker, never stronger, so the dwell max is +bleed-robust), with NOTCH (≥ 6 dB below the across-bin median) and DEAD (no +probe frames while other bins hit) flags, and an optional Spearman +rank-correlation against a B210 wideband capture +(`tests/hop_rx_probe.py --bin-power-csv`). + +Measured (8822CU → 8812EU, 5 GHz, `--bins 5170-5250/5 --nb-bw 5`): all 17 +five-MHz bins sounded, recovering a smooth ~15 dB notch centred at 5230 MHz with +monotone roll-in/out across neighbouring bins — frequency-selective structure a +20 MHz scalar cannot resolve. NB sounding needs both ends narrowband (the probe +must decode); a retune can wedge one in-flight probe frame into a bulk-OUT +timeout (seen on the 8812EU at NB on both retune paths), which the orchestrator +rides out with `DEVOURER_TX_MAXFAIL=0` — scattered single-frame loss is noise to +the map. + ## Per-tone localisation Finer than the channel grid needs a different mechanism: the self-sounding diff --git a/src/ChannelFreq.h b/src/ChannelFreq.h new file mode 100644 index 0000000..7431cf5 --- /dev/null +++ b/src/ChannelFreq.h @@ -0,0 +1,34 @@ +#ifndef CHANNEL_FREQ_H +#define CHANNEL_FREQ_H + +#include + +/* Wi-Fi channel <-> center-frequency mapping shared by the per-packet radiotap + * CHANNEL hop path (library side, all generations) and the demos' hop/sweep + * spec parsing. Generation-neutral, header-only. */ + +namespace devourer { + +/* Map a radiotap CHANNEL frequency (MHz) to a Wi-Fi channel number. Returns 0 + * for a frequency outside the bands devourer drives (caller ignores it). */ +static inline int freq_to_chan(uint16_t freq_mhz) { + if (freq_mhz == 2484) + return 14; + if (freq_mhz >= 2412 && freq_mhz <= 2472) + return (freq_mhz - 2407) / 5; /* 2.4 GHz ch1..13 */ + if (freq_mhz >= 5000 && freq_mhz <= 5895) + return (freq_mhz - 5000) / 5; /* 5 GHz UNII ch */ + return 0; +} + +/* Wi-Fi channel number -> center frequency (MHz), for the radiotap CHANNEL + * field that drives the library's per-packet retune (DEVOURER_HOP_RADIOTAP). */ +static inline uint16_t chan_to_freq(int ch) { + if (ch == 14) return 2484; + if (ch <= 14) return static_cast(2407 + 5 * ch); + return static_cast(5000 + 5 * ch); +} + +} /* namespace devourer */ + +#endif /* CHANNEL_FREQ_H */ diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index e71f509..ae1f458 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -46,6 +46,23 @@ class IRtlDevice { * whatever thread runs StartRxLoop). Default no-op. */ virtual void StopRxLoop() {} virtual void SetMonitorChannel(SelectedChannel channel) = 0; + + /* Lean intra-band, same-bandwidth channel retune for hop/sweep dwells: the RF + * channel switch only, skipping the per-rate TX-power loop and bandwidth + * post-set a hop doesn't need (see docs/frequency-hopping.md). Generations + * with a fast path override this (Jaguar1, Jaguar3 — both fall back to the + * full path internally on a band change); the default is the full + * SetMonitorChannel at the current width/offset, so callers may hop through + * this unconditionally on any chip. `cache_rf` selects the cached-RF-write + * variant where one exists (false = re-read per hop, for A/B measurement); + * the default argument binds here, at the interface. */ + virtual void FastRetune(uint8_t channel, bool cache_rf = true) { + (void)cache_rf; + SelectedChannel c = GetSelectedChannel(); + c.Channel = channel; + SetMonitorChannel(c); + } + virtual void SetTxPower(uint8_t power) = 0; virtual bool send_packet(const uint8_t *packet, size_t length) = 0; virtual SelectedChannel GetSelectedChannel() = 0; diff --git a/src/SweepSpec.h b/src/SweepSpec.h new file mode 100644 index 0000000..7e051ab --- /dev/null +++ b/src/SweepSpec.h @@ -0,0 +1,78 @@ +#ifndef SWEEP_SPEC_H +#define SWEEP_SPEC_H + +#include +#include +#include + +#include "ChannelFreq.h" + +/* Sweep/hop bin-list grammar shared by DEVOURER_RX_SWEEP (demo) and + * DEVOURER_HOP_CHANNELS (txdemo). Comma-separated tokens, each either + * + * a channel number "1,6,11" + * -[/] an inclusive range with step s (def. 1) "36-48/4" + * -[/] a centre-frequency range in MHz "5170-5250/5" + * (values >= 1000 are MHz; step in MHz — 5 MHz per channel + * number, so "2412-2462/25" = every 5th 2.4 GHz channel) + * + * A frequency range maps through devourer::freq_to_chan (5 GHz channels sit on + * the 5 MHz grid, so 5 MHz-wide narrowband bins get one bin per channel + * number). Malformed / out-of-band tokens are dropped. */ + +namespace devourer { + +inline std::vector parse_sweep_spec(const char *spec) { + std::vector out; + if (spec == nullptr || *spec == '\0') + return out; + const std::string s = spec; + size_t pos = 0; + while (pos < s.size()) { + const size_t comma = s.find(',', pos); + std::string tok = s.substr( + pos, comma == std::string::npos ? std::string::npos : comma - pos); + pos = (comma == std::string::npos) ? s.size() : comma + 1; + if (tok.empty()) + continue; + + long step = 1; + const size_t slash = tok.find('/'); + if (slash != std::string::npos) { + step = std::strtol(tok.c_str() + slash + 1, nullptr, 0); + tok = tok.substr(0, slash); + } + if (step < 1) + step = 1; + + /* A '-' past position 0 splits a range (a leading '-' would just make the + * token malformed; channels are positive). */ + const size_t dash = tok.find('-', 1); + if (dash == std::string::npos) { + const int ch = static_cast(std::strtol(tok.c_str(), nullptr, 0)); + if (ch > 0) + out.push_back(ch); + continue; + } + const long a = std::strtol(tok.substr(0, dash).c_str(), nullptr, 0); + const long b = std::strtol(tok.substr(dash + 1).c_str(), nullptr, 0); + if (a <= 0 || b < a) + continue; + /* MHz ranges live on the 5 MHz channel grid — a finer step would only + * emit duplicate bins. */ + if (a >= 1000 && step < 5) + step = 5; + for (long v = a; v <= b; v += step) { + const int ch = (a >= 1000) + ? freq_to_chan(static_cast(v)) + : static_cast(v); + if (ch > 0) + out.push_back(ch); + } + } + return out; +} + +} /* namespace devourer */ + +#endif /* SWEEP_SPEC_H */ diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 66e1361..c42d4c7 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -1,5 +1,6 @@ #include "RtlJaguarDevice.h" #include "BeamformingSounder.h" +#include "ChannelFreq.h" #include "EepromManager.h" #include "Hal8812PhyReg.h" #include "NhmReader.h" @@ -271,18 +272,6 @@ RxEnergy RtlJaguarDevice::GetRxEnergy() { return e; } -/* Map a radiotap CHANNEL frequency (MHz) to a Wi-Fi channel number. Returns 0 - * for a frequency outside the bands devourer drives (caller ignores it). */ -static int freq_to_chan(uint16_t freq_mhz) { - if (freq_mhz == 2484) - return 14; - if (freq_mhz >= 2412 && freq_mhz <= 2472) - return (freq_mhz - 2407) / 5; /* 2.4 GHz ch1..13 */ - if (freq_mhz >= 5000 && freq_mhz <= 5895) - return (freq_mhz - 5000) / 5; /* 5 GHz UNII ch */ - return 0; -} - SelectedChannel RtlJaguarDevice::GetSelectedChannel() { return _channel; } bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { @@ -347,7 +336,8 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { /* 2 x __le16: frequency (MHz), then flags. Frequency is authoritative * for the per-packet hop target; flags are ignored (rate/BW come from * the RATE/MCS/VHT fields). */ - radiotap_channel = freq_to_chan(get_unaligned_le16(iterator.this_arg)); + radiotap_channel = + devourer::freq_to_chan(get_unaligned_le16(iterator.this_arg)); break; case IEEE80211_RADIOTAP_MCS: { @@ -447,7 +437,7 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { * — at the honest cost of stalling this send by the retune latency. Done * before the 5 GHz CCK clamp below so the clamp keys off the new channel. */ if (radiotap_channel > 0 && radiotap_channel != _channel.Channel) { - FastRetune(static_cast(radiotap_channel)); + FastRetune(static_cast(radiotap_channel), /*cache_rf=*/true); } /* The radiotap carried no rate → apply the runtime TX-mode default set via diff --git a/src/jaguar1/RtlJaguarDevice.h b/src/jaguar1/RtlJaguarDevice.h index 00b8079..d25dd53 100644 --- a/src/jaguar1/RtlJaguarDevice.h +++ b/src/jaguar1/RtlJaguarDevice.h @@ -78,8 +78,10 @@ class RtlJaguarDevice : public IRtlDevice { * crosses the 2.4/5 GHz boundary or the current bandwidth isn't 20 MHz. * cache_rf=true additionally avoids the per-write 20 ms C-cut RF-read sleep * by writing RF_CHNLBW from a cached value. Intended for channel hopping; - * keeps the device channel state in sync for the 5 GHz CCK clamp. */ - void FastRetune(uint8_t channel, bool cache_rf = true); + * keeps the device channel state in sync for the 5 GHz CCK clamp. + * (The cache_rf default binds at IRtlDevice — virtual default arguments + * resolve statically, so overrides must not re-declare it.) */ + void FastRetune(uint8_t channel, bool cache_rf) override; void InitWrite(SelectedChannel channel) override; void SetTxPower(uint8_t power) override; /* Force the per-rate TXAGC index (0..63), bypassing the EFUSE per-rate diff --git a/src/jaguar2/HalJaguar2.cpp b/src/jaguar2/HalJaguar2.cpp index d15aa06..ce13b63 100644 --- a/src/jaguar2/HalJaguar2.cpp +++ b/src/jaguar2/HalJaguar2.cpp @@ -442,25 +442,44 @@ void HalJaguar2::igi_toggle() { _device.phy_set_bb_reg(0x0e50, 0x7f, igi); } +/* Central channel of the wide channel; `channel` is the primary 20 MHz channel + * and primary_ch_idx its position. 20 MHz: central = primary. 40 MHz: + * primary_ch_idx 1(lower)/2(upper) -> ±2. 80 MHz: 1..4 -> +6/+2/-2/-6. (The + * vendor's higher layer passes the central channel to + * config_phydm_switch_channel_* and primary_ch_idx to switch_bandwidth.) */ +uint8_t HalJaguar2::central_ch(uint8_t channel, uint8_t bw, + uint8_t primary_ch_idx) { + if (bw == 1) + return static_cast(primary_ch_idx == 2 ? channel - 2 + : channel + 2); + if (bw == 2) { + static const int off80[5] = {0, 6, 2, -2, -6}; + return static_cast( + channel + off80[primary_ch_idx <= 4 ? primary_ch_idx : 0]); + } + return channel; +} + void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, uint8_t primary_ch_idx) { + /* Any full channel set rewrites what fast_retune caches (RF18 band/BW bits, + * AGC/fc buckets, RF 0xBE, spur/CCK-filter regs) — invalidate; the caches + * only ever mirror fast-path writes. */ + _rf18_cached = false; + _last_agc_bucket = -1; + _last_fc = 0xffffffff; + _last_rf_be = -1; + _last_df18 = -1; + _last_cck_key = -1; + if (_variant == ChipVariant::C8821C) { set_channel_bw_8821c(channel, bw, rfe_type, primary_ch_idx); + _last_tuned_ch = channel; + if (std::getenv("DEVOURER_DUMP_CANARY")) + DumpCanary(); return; } - /* The RF/AGC tune to the CENTRAL channel of the wide channel; `channel` is the - * primary 20 MHz channel and primary_ch_idx its position. 20 MHz: central = - * primary. 40 MHz: primary_ch_idx 1(lower)/2(upper) -> ±2. 80 MHz: 1..4 -> - * +6/+2/-2/-6. (The vendor's higher layer passes the central channel to - * config_phydm_switch_channel_8822b and primary_ch_idx to switch_bandwidth.) */ - uint8_t cch = channel; - if (bw == 1) - cch = static_cast(primary_ch_idx == 2 ? channel - 2 : channel + 2); - else if (bw == 2) { - static const int off80[5] = {0, 6, 2, -2, -6}; - cch = static_cast(channel + - off80[primary_ch_idx <= 4 ? primary_ch_idx : 0]); - } + const uint8_t cch = central_ch(channel, bw, primary_ch_idx); const bool g2 = cch <= 14; const bool r2t2r = _ver.rf_2t2r != 0; @@ -500,27 +519,11 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x412); } - /* RF 0xBE[17:15] per-channel phase-noise / VCO-band setting - * (config_phydm_switch_channel_8822b): 0 for 2.4G, else a per-5G-channel band - * value from the low/middle/high-band tables. Omitting it (writing 0 on 5G) - * leaves the 5G VCO mis-tuned so 5G RX/TX fail entirely — this was the sole - * gap blocking 5G. Path A only (matches the vendor). */ - static const uint8_t low_band[15] = {0x7, 0x6, 0x6, 0x5, 0x0, 0x0, 0x7, 0xff, - 0x6, 0x5, 0x0, 0x0, 0x7, 0x6, 0x6}; - static const uint8_t middle_band[23] = { - 0x6, 0x5, 0x0, 0x0, 0x7, 0x6, 0x6, 0xff, 0x0, 0x0, 0x7, 0x6, - 0x6, 0x5, 0x0, 0xff, 0x7, 0x6, 0x6, 0x5, 0x0, 0x0, 0x7}; - static const uint8_t high_band[15] = {0x5, 0x5, 0x0, 0x7, 0x7, 0x6, 0x5, 0xff, - 0x0, 0x7, 0x7, 0x6, 0x5, 0x5, 0x0}; - uint8_t rf_be = 0xff; - if (cch <= 14) - rf_be = 0x0; - else if (cch >= 36 && cch <= 64) - rf_be = low_band[(cch - 36) >> 1]; - else if (cch >= 100 && cch <= 144) - rf_be = middle_band[(cch - 100) >> 1]; - else if (cch >= 149 && cch <= 177) - rf_be = high_band[(cch - 149) >> 1]; + /* RF 0xBE[17:15] per-channel phase-noise / VCO-band setting (shared with + * fast_retune). Omitting it (writing 0 on 5G) leaves the 5G VCO mis-tuned so + * 5G RX/TX fail entirely — this was the sole gap blocking 5G. Path A only + * (matches the vendor). */ + const uint8_t rf_be = rf_be_for_8822b(cch); if (rf_be != 0xff) rf_set(0, 0xbe, (1u << 17) | (1u << 16) | (1u << 15), rf_be); @@ -614,8 +617,218 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _device.phy_set_bb_reg(0x0808, 0xff, rx_ant | (rx_ant << 4)); igi_toggle(); + _last_tuned_ch = channel; _logger->info("Jaguar2: channel set ch={} bw={} (rf18=0x{:05x})", channel, (int)bw, rf18); + if (std::getenv("DEVOURER_DUMP_CANARY")) + DumpCanary(); +} + +void HalJaguar2::DumpCanary() { + /* Channel/BW-relevant set (both variants; the control run in the parity + * script classifies natural run-variance): RX path (0x808), CCA thresholds + * (0x82c/0x830/0x838), fc (0x860), BW block (0x8ac/0x8c4/0x8f0), RX DFIR + * (0x948/0x94c/0xc20/0xe20), AGC index (0x958 8822B / 0xc1c 8821C), CCK pri + * (0xa00), spur / CCK filter (0xa24/0xa28/0xaac), 8821C band block + * (0xa80/0xa84/0x814), RFE pins (0xcb0/0xcb4/0xcb8/0xca0/0xeb0/0xeb4/0xea0). + * MAC: CCK check (0x454). RF via the direct read window: 0x18 channel, + * 0xbe VCO band, 0xdf, 0xb8. IGI (0xc50/0xe50) excluded — live. */ + static const uint16_t bb_canary[] = { + 0x808, 0x814, 0x82c, 0x830, 0x838, 0x860, 0x8ac, 0x8c4, 0x8f0, + 0x948, 0x94c, 0x958, 0xa00, 0xa24, 0xa28, 0xaac, 0xa80, 0xa84, + 0xc1c, 0xc20, 0xe20, 0xca0, 0xcb0, 0xcb4, 0xcb8, 0xea0, 0xeb0, 0xeb4}; + static const uint16_t mac_canary[] = {0x454}; + static const uint8_t rf_canary[] = {0x18, 0xb8, 0xbe, 0xdf}; + const bool r2t2r = _ver.rf_2t2r != 0; + + _logger->info("=== DEVOURER_DUMP_CANARY (post channel-set ch={}) ===", + unsigned(_last_tuned_ch)); + for (uint16_t a : bb_canary) + _logger->info("BB 0x{:03x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint16_t a : mac_canary) + _logger->info("MAC 0x{:03x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint8_t a : rf_canary) + _logger->info("RF[A] 0x{:02x} = 0x{:05X}", a, rf_read(0, a)); + if (r2t2r) + for (uint8_t a : rf_canary) + _logger->info("RF[B] 0x{:02x} = 0x{:05X}", a, rf_read(1, a)); + _logger->info("=== END DEVOURER_DUMP_CANARY ==="); +} + +/* RF 0xBE[17:15] per-5G-channel phase-noise / VCO-band value + * (config_phydm_switch_channel_8822b low/middle/high-band tables). */ +uint8_t HalJaguar2::rf_be_for_8822b(uint8_t cch) { + static const uint8_t low_band[15] = {0x7, 0x6, 0x6, 0x5, 0x0, 0x0, 0x7, 0xff, + 0x6, 0x5, 0x0, 0x0, 0x7, 0x6, 0x6}; + static const uint8_t middle_band[23] = { + 0x6, 0x5, 0x0, 0x0, 0x7, 0x6, 0x6, 0xff, 0x0, 0x0, 0x7, 0x6, + 0x6, 0x5, 0x0, 0xff, 0x7, 0x6, 0x6, 0x5, 0x0, 0x0, 0x7}; + static const uint8_t high_band[15] = {0x5, 0x5, 0x0, 0x7, 0x7, 0x6, 0x5, 0xff, + 0x0, 0x7, 0x7, 0x6, 0x5, 0x5, 0x0}; + if (cch <= 14) + return 0x0; + if (cch >= 36 && cch <= 64) + return low_band[(cch - 36) >> 1]; + if (cch >= 100 && cch <= 144) + return middle_band[(cch - 100) >> 1]; + if (cch >= 149 && cch <= 177) + return high_band[(cch - 149) >> 1]; + return 0xff; +} + +bool HalJaguar2::fast_retune(uint8_t channel, uint8_t bw, + uint8_t primary_ch_idx, bool cache_rf) { + if (_last_tuned_ch == 0) + return false; /* never tuned — unknown band, cold BW/band state */ + const bool cur_2g = _last_tuned_ch <= 14; + if (cur_2g != (channel <= 14)) + return false; /* band change needs RFE pins / band block — full path only */ + if (channel == _last_tuned_ch) + return true; /* no-op hop */ + + const uint8_t cch = central_ch(channel, bw, primary_ch_idx); + const bool g2 = cch <= 14; + const bool c8821 = (_variant == ChipVariant::C8821C); + const bool r2t2r = _ver.rf_2t2r != 0; + + /* RF18: one cached full-register write replaces the full path's + * read-modify-write round(s) (three of them on the 8821C). One direct-window + * read primes the cache; the band bits ([16]/[8], band-keyed) and the BW bits + * ([11:10], bandwidth-keyed) ride along in the cached value. */ + if (!cache_rf || !_rf18_cached) { + _rf18_cache = rf_read(0, 0x18); + _rf18_cached = true; + } + uint32_t rf18 = _rf18_cache & ~((1u << 18) | (1u << 17) | 0xffu); + rf18 |= cch; + + /* Channel-keyed constants, written only when their bucket moves (the caches + * are invalidated by every full set, so the first fast hop writes them once). + * AGC table index + CFO-tracking fc share buckets across the variants; only + * the AGC register differs (0x958[4:0] on 8822B, 0xc1c[11:8] on 8821C). The + * bucket gaps (e.g. cch 65..99) match the full path, which leaves the + * registers untouched there. */ + int agc_bucket = -1; + if (g2) + agc_bucket = 0; + else if (cch >= 36 && cch <= 64) + agc_bucket = 1; + else if (cch >= 100 && cch <= 144) + agc_bucket = 2; + else if (cch >= 149) + agc_bucket = 3; + if (agc_bucket >= 0 && agc_bucket != _last_agc_bucket) { + if (c8821) + _device.phy_set_bb_reg(0x0c1c, 0x00000F00, + static_cast(agc_bucket)); + else + _device.phy_set_bb_reg(0x0958, 0x1f, static_cast(agc_bucket)); + _last_agc_bucket = agc_bucket; + } + + uint32_t fc = 0xffffffff; + if (g2) + fc = 0x96a; + else if (cch >= 36 && cch <= 48) + fc = 0x494; + else if (cch >= 52 && cch <= 64) + fc = 0x453; + else if (cch >= 100 && cch <= 116) + fc = 0x452; + else if (cch >= 118 && cch <= 177) + fc = 0x412; + if (fc != 0xffffffff && fc != _last_fc) { + _device.phy_set_bb_reg(0x0860, 0x1ffe0000, fc); + _last_fc = fc; + } + + if (!c8821) { + /* 8822B: RF 0xBE VCO band (per-5G-channel — omitting it detunes the 5G + * VCO), the ch144 RF 0xDF[18] flag, and the 2G spur registers. */ + const uint8_t rf_be = rf_be_for_8822b(cch); + if (rf_be != 0xff && rf_be != _last_rf_be) { + rf_set(0, 0xbe, (1u << 17) | (1u << 16) | (1u << 15), rf_be); + _last_rf_be = rf_be; + } + const int df18 = (cch == 144) ? 1 : 0; + if (df18 != _last_df18) { + rf_set(0, 0xdf, (1u << 18), static_cast(df18)); + _last_df18 = df18; + } + if (cch == 144) + rf18 |= (1u << 17); + else if (cch > 144) + rf18 |= (1u << 18); + else if (cch >= 80) + rf18 |= (1u << 17); + if (g2) { + const int key = (cch == 14) ? 1 : 0; + if (key != _last_cck_key) { + if (cch == 14) { + _device.phy_set_bb_reg(0x0a24, 0xffffffff, 0x00006577); + _device.phy_set_bb_reg(0x0a28, 0x0000ffff, 0x0000); + } else { + _device.phy_set_bb_reg(0x0a24, 0xffffffff, 0x384f6577); + _device.phy_set_bb_reg(0x0a28, 0x0000ffff, 0x1525); + } + _last_cck_key = key; + } + /* Reproduce the vendor's clean 2G RF18 (the primed read can carry the + * spurious bits 8/16 — see set_channel_bw). */ + rf18 &= 0x00060cffu; + } + } else { + /* 8821C: 5G sub-band bits use its own buckets; 2G CCK filter is ch14 vs + * the BB-table snapshot. The band bits [16]/[8] and the BTG/WLG/WLA RF-set + * are band-keyed (switch_band) and stay untouched. */ + if (!g2) { + if (cch >= 100 && cch <= 140) + rf18 |= (1u << 17); + else if (cch > 140) + rf18 |= (1u << 18); + } else { + const int key = (cch == 14) ? 1 : 0; + if (key != _last_cck_key) { + if (cch == 14) { + _device.phy_set_bb_reg(0x0a24, 0xffffffff, 0x0000b81c); + _device.phy_set_bb_reg(0x0a28, 0x0000ffff, 0x0000); + _device.phy_set_bb_reg(0x0aac, 0xffffffff, 0x00003667); + } else if (_cck_saved_8821c) { + _device.phy_set_bb_reg(0x0a24, 0xffffffff, _cck_a24_8821c); + _device.phy_set_bb_reg(0x0a28, 0x0000ffff, _cck_a28_8821c & 0xffff); + _device.phy_set_bb_reg(0x0aac, 0xffffffff, _cck_aac_8821c); + } + _last_cck_key = key; + } + } + } + + rf_write(0, 0x18, rf18); + if (!c8821 && r2t2r) + rf_write(1, 0x18, rf18); + _rf18_cache = rf18; /* refresh so the next hop merges into what we wrote */ + + /* Per-hop RX kick — the vendor switch_channel tail: 8822B RF read-error + * toggle + RX-path toggle (leave the RX dead-zone), then the IGI toggle on + * both variants. */ + if (!c8821) { + rf_set(0, 0xb8, (1u << 19), 0); + rf_set(0, 0xb8, (1u << 19), 1); + const uint8_t rx_ant = r2t2r ? 0x3 : 0x1; + _device.phy_set_bb_reg(0x0808, 0xff, 0x0); + _device.phy_set_bb_reg(0x0808, 0xff, + static_cast(rx_ant | (rx_ant << 4))); + } + igi_toggle(); + + _last_tuned_ch = channel; + _logger->debug("Jaguar2: fast retune -> ch {} (central {}, RF18=0x{:05x})", + channel, cch, rf18); + /* The fast path must emit the canary itself (it does not pass through the + * full path) or the parity diff compares a stale full-set dump. */ + if (std::getenv("DEVOURER_DUMP_CANARY")) + DumpCanary(); + return true; } void HalJaguar2::config_trx_mode() { @@ -787,14 +1000,7 @@ void HalJaguar2::switch_rf_set_8821c(uint8_t rf_set) { * routed by switch_rf_set (BTG/WLG). Covers 2.4G and 5G at 20/40/80 MHz. */ void HalJaguar2::set_channel_bw_8821c(uint8_t channel, uint8_t bw, uint8_t rfe_raw, uint8_t primary_ch_idx) { - uint8_t cch = channel; - if (bw == 1) - cch = static_cast(primary_ch_idx == 2 ? channel - 2 : channel + 2); - else if (bw == 2) { - static const int off80[5] = {0, 6, 2, -2, -6}; - cch = static_cast(channel + - off80[primary_ch_idx <= 4 ? primary_ch_idx : 0]); - } + const uint8_t cch = central_ch(channel, bw, primary_ch_idx); const bool g2 = cch <= 14; /* rfe_type_expand -> BTG (else WLG) per config_phydm_parameter_init_8821c. */ const bool btg = rfe_raw == 2 || rfe_raw == 4 || rfe_raw == 7 || diff --git a/src/jaguar2/HalJaguar2.h b/src/jaguar2/HalJaguar2.h index c3bcbcd..aaea5b5 100644 --- a/src/jaguar2/HalJaguar2.h +++ b/src/jaguar2/HalJaguar2.h @@ -74,6 +74,28 @@ class HalJaguar2 { void set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, uint8_t primary_ch_idx = 0); + /* Lean intra-band, same-bandwidth hop retune — the Jaguar2 FastRetune core + * (see docs/frequency-hopping.md), variant-dispatched like set_channel_bw. + * Only the per-hop essentials run: one cached full-register RF18 write (a + * single direct-window read primes the cache, collapsing the full path's + * read-modify-write rounds), the channel-keyed constants (AGC table index, + * CFO-tracking fc, 8822B RF 0xBE VCO band / ch144 RF 0xDF flag / 2G spur + * regs, 8821C 2G CCK filter) written only when their bucket moves, and the + * per-hop RX kick (8822B RF 0xb8 toggle + RX-path toggle; both variants the + * IGI toggle). Everything bandwidth-keyed (0x8ac/0x8c4 block, RX DFIR, CCA + * thresholds) and band-keyed (RFE pins, 8821C switch-band/RF-set block) + * stays untouched — set by the last full set at this BW/band. Returns false + * (chip untouched) on a band change or when the radio was never tuned; the + * caller falls back to the full set_channel_bw. */ + bool fast_retune(uint8_t channel, uint8_t bw, uint8_t primary_ch_idx, + bool cache_rf); + + /* Channel/BW register-canary dump for tests/hop_parity_check.sh — the same + * grep format as the Jaguar1/Jaguar3 DumpCanary. Emitted by both the full + * and fast channel paths when DEVOURER_DUMP_CANARY is set. Live registers + * (IGI 0xc50/0xe50, FA counters) deliberately excluded. */ + void DumpCanary(); + /* Enable the MAC RX engine (CR MACRXEN + promiscuous RCR for monitor). */ void enable_rx(); @@ -162,6 +184,25 @@ class HalJaguar2 { void rfe_ifem(uint8_t channel); /* phydm_igi_toggle_8822b: toggle 0xc50/0xe50 IGI to enter RX mode. */ void igi_toggle(); + /* Central channel of the wide channel (shared full/fast paths): 20 MHz -> + * primary; 40 MHz -> ±2 by primary_ch_idx; 80 MHz -> +6/+2/-2/-6. */ + static uint8_t central_ch(uint8_t channel, uint8_t bw, uint8_t primary_ch_idx); + /* RF 0xBE[17:15] per-5G-channel phase-noise / VCO-band value (8822B); 0 for + * 2.4 GHz, 0xff = no write (shared full/fast paths). */ + static uint8_t rf_be_for_8822b(uint8_t cch); + + /* fast_retune write-on-change caches. Invalidated by every full + * set_channel_bw (either variant); they only ever mirror fast-path writes — + * the Jaguar1 RadioManagementModule pattern. _last_tuned_ch also gates the + * fast path (0 = never tuned / unknown band). */ + uint8_t _last_tuned_ch = 0; + uint32_t _rf18_cache = 0; + bool _rf18_cached = false; + int _last_agc_bucket = -1; + uint32_t _last_fc = 0xffffffff; + int _last_rf_be = -1; + int _last_df18 = -1; /* 8822B ch144 RF 0xdf[18] state */ + int _last_cck_key = -1; /* 2G spur (8822B) / CCK-filter (8821C) key: ch14? */ /* --- 8821C-specific (C8821C variant) channel/RF-set/LCK, transcribed from * reference/8821cu phydm_hal_api8821c.c + halrf_8821c.c. The 8822B channel-set diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 726bac4..4c00de1 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -12,6 +12,7 @@ #include #include "BeamformingSounder.h" +#include "ChannelFreq.h" /* radiotap CHANNEL freq -> channel (per-packet hop) */ #include "FrameParserJaguar2.h" #include "Jaguar2Calibration.h" #include "NhmReader.h" @@ -508,6 +509,21 @@ void RtlJaguar2Device::SetMonitorChannel(SelectedChannel channel) { channel.ChannelOffset); } +void RtlJaguar2Device::FastRetune(uint8_t channel, bool cache_rf) { + if (channel == _channel.Channel) + return; + if (_hal.fast_retune(channel, static_cast(_channel.ChannelWidth), + _channel.ChannelOffset, cache_rf)) { + _channel.Channel = channel; + return; + } + /* Fast path declined (band change / never tuned) — full channel set at the + * current bandwidth + offset. */ + _channel.Channel = channel; + _hal.set_channel_bw(channel, static_cast(_channel.ChannelWidth), + _rfe, _channel.ChannelOffset); +} + RxEnergy RtlJaguar2Device::GetRxEnergy() { /* Scalar FA/CCA/IGI come from the DIG thread's cached snapshot (no USB); * append a fresh NHM power histogram (11AC register map). NHM's registers @@ -544,6 +560,8 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { ChannelWidth_t bwidth = CHANNEL_WIDTH_20; bool vht = (radiotap_length != 0x0d); bool rate_from_radiotap = false; + /* Per-packet hop target from a radiotap CHANNEL field (0 = none present). */ + int radiotap_channel = 0; auto *rtap_hdr = reinterpret_cast( const_cast(packet)); @@ -559,6 +577,13 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { fixed_rate = *it.this_arg; rate_from_radiotap = true; break; + case IEEE80211_RADIOTAP_CHANNEL: + /* 2 x __le16: frequency (MHz), then flags. Frequency is authoritative + * for the per-packet hop target; flags are ignored (rate/BW come from + * the RATE/MCS/VHT fields). Same contract as the Jaguar1/Jaguar3 paths. */ + radiotap_channel = + devourer::freq_to_chan(get_unaligned_le16(it.this_arg)); + break; case IEEE80211_RADIOTAP_MCS: { uint8_t mcs_flags = it.this_arg[1]; if ((mcs_flags & IEEE80211_RADIOTAP_MCS_BW_MASK) == @@ -605,6 +630,13 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { } } + /* Radiotap CHANNEL is authoritative for per-packet frequency, exactly as + * RATE/MCS are for rate: a frame asking for a different channel triggers a + * lean FastRetune before the descriptor is built (same contract as the + * Jaguar1/Jaguar3 paths). */ + if (radiotap_channel > 0 && radiotap_channel != _channel.Channel) + FastRetune(static_cast(radiotap_channel), /*cache_rf=*/true); + /* Rate-less frame -> apply the SetTxMode default; per-packet radiotap wins. */ if (!rate_from_radiotap && _tx_mode_default.has_value()) { const devourer::TxParams tp = devourer::tx_mode_to_params(*_tx_mode_default); diff --git a/src/jaguar2/RtlJaguar2Device.h b/src/jaguar2/RtlJaguar2Device.h index 2ec593e..ea359c1 100644 --- a/src/jaguar2/RtlJaguar2Device.h +++ b/src/jaguar2/RtlJaguar2Device.h @@ -47,6 +47,16 @@ class RtlJaguar2Device : public IRtlDevice { void StartRxLoop(Action_ParsedRadioPacket packetProcessor) override; void StopRxLoop() override { _rx_stop = true; } void SetMonitorChannel(SelectedChannel channel) override; + /* Lean frequency-hop retune (the Jaguar2 port of the Jaguar1 FastRetune — + * see docs/frequency-hopping.md): cached RF18 write + on-change channel + * constants + the RX kick, via HalJaguar2::fast_retune; falls back to the + * full set_channel_bw on a band change. Same concurrency model as + * SetMonitorChannel: no lock — the DIG thread (RX sessions only) may + * interleave IGI writes with the retune's IGI toggle, which is benign + * (single-register writes; DIG re-converges), and TX-only sessions have no + * concurrent register writer. cache_rf=false re-reads RF18 per hop (A/B + * measurement). */ + void FastRetune(uint8_t channel, bool cache_rf) override; void InitWrite(SelectedChannel channel) override; void SetTxPower(uint8_t power) override; bool send_packet(const uint8_t *packet, size_t length) override; diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index fa5a31f..6f60113 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -37,26 +37,17 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, * 40 MHz frame lands on the lower 40 of the block. */ const bool is40 = (bwmode == CHANNEL_WIDTH_40); const bool is80 = (bwmode == CHANNEL_WIDTH_80); - uint8_t central = channel; - uint8_t pri = 0; /* 0 = 20 MHz / no sub-channel */ - if (is40) { - if (channel_offset == 2) { central = channel - 2; pri = 1; } /* HT40- */ - else { central = channel + 2; pri = 2; } /* HT40+ */ - } else if (is80) { - central = static_cast(channel + 6); /* 80 MHz centre */ - /* pri-ch index for 80 MHz is a VHT_DATA_SC_* code (get_pri_ch_id), NOT a - * plain 0-3 slot: primary = lowest 20 => VHT_DATA_SC_20_LOWEST_OF_80MHZ = 4. - * This lands in 0x9b0[15:8]=0x44 and REG_DATA_SC 0x483=0xa4 (kernel-parity; - * verified against a register dump of `iw 149 80MHz`). */ - pri = 4; - } - - /* RF register write via the 8822C direct-write window: - * BB[base + (rf_addr<<2)], 20-bit mask. base 0x3c00 (A) / 0x4c00 (B). */ - auto rf_write = [this](uint16_t base, uint8_t rfaddr, uint32_t v) { - _device.phy_set_bb_reg(static_cast(base + (rfaddr << 2)), - 0x000fffff, v); - }; + uint8_t central, pri; + central_and_pri(channel, channel_offset, bwmode, central, pri); + + /* Any full channel set rewrites what fast_retune caches (RF18 BW/band bits, + * SCO, TX DFIR, AGC tables, RXBB) — invalidate; the caches only ever mirror + * fast-path writes (the J1 RadioManagementModule pattern). */ + _rf18_cached = false; + _last_sco = 0xffffffff; + _last_dfir = 0xffffffff; + _last_agc_key = -1; + _rxbb_asserted = false; _last_channel = channel; @@ -127,31 +118,12 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, /* RF 0x18 channel/band tune. 8822C is a read-modify-write preserving [15:10] * and setting the BW20 bits (config_phydm_switch_channel/bandwidth_8822c); * 8822E configures its RF band via its own phydm path (config_channel_8822e), - * so it takes the plain write below. */ - /* RF bandwidth bits in RF18: BW20 = BIT13|BIT12, BW40 = BIT13, BW80 = BIT12 - * (config_phydm_switch_bandwidth). */ - const uint32_t rf_bw_bits = is80 ? (1u << 12) - : is40 ? (1u << 13) - : ((1u << 13) | (1u << 12)); - uint32_t rf18; - if (is_c) { - /* Clear the channel/band bits AND the BW bits [13:12]: the vendor does - * rf_reg18 &= ~(BIT13|BIT12) before setting them, and the RMW mask must - * too — otherwise a prior 20 MHz tune's BIT12 survives and a 40 MHz tune - * ends up BIT13|BIT12 = 20 MHz RF while BB/descriptor say 40 MHz, which - * halts the TX engine (frames fill TXPKTBUF but never drain). */ - rf18 = (_device.rtw_read32(static_cast(0x3c00 + (0x18 << 2))) & - 0xfffffu & ~0x733ffu); - rf18 |= (central & 0xffu); - rf18 |= rf_bw_bits; - } else { - rf18 = (central & 0xffu) | rf_bw_bits; - } - if (central > 14) { /* 5 GHz */ - rf18 |= (1u << 16) | (1u << 8); - if (central > 144) rf18 |= (1u << 18); - else if (central >= 80) rf18 |= (1u << 17); - } + * so it composes the value from scratch (base 0). */ + const uint32_t rf18 = + is_c ? rf18_for(central, bwmode, + _device.rtw_read32( + static_cast(0x3c00 + (0x18 << 2)))) + : rf18_for(central, bwmode, 0); _logger->info("Jaguar3: central ch {} (pri idx {}) RF18=0x{:05x}", central, pri, rf18); @@ -160,20 +132,11 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, * (false) 0x1c90[8]=0 before, (true) 0x1c90[8]=1 + 0x1830[29]/0x4130[29]=1 * "force update anapar" after — the latter pushes the RF/analog shadow to * hardware, which is required for the per-channel RF (RXBB/RF18) to reach the - * analog front-end. RXBB (RF 0x3f) is a gated write: RF 0xee[2]=1, - * RF 0x33[4:0]=0x12, RF 0x3f, RF 0xee[2]=0 (config_phydm_switch_bandwidth_8822c). */ - /* RXBB (RF 0x3f): BW20 = BIT4|BIT3 (0x18), BW40 = BIT4 (0x10), - * BW80 = BIT3 (0x08) (config_phydm_switch_bandwidth_8822c). */ - const uint32_t rxbb = is80 ? 0x08u : is40 ? 0x10u : 0x18u; + * analog front-end. */ _device.phy_set_bb_reg(0x1c90, 1u << 8, 0x0); /* rstb_3wire(false) */ - for (uint16_t base : {uint16_t(0x3c00), uint16_t(0x4c00)}) { - _device.phy_set_bb_reg(static_cast(base + (0xee << 2)), 0x4, 0x1); - _device.phy_set_bb_reg(static_cast(base + (0x33 << 2)), 0x1f, 0x12); - rf_write(base, 0x3f, rxbb); - _device.phy_set_bb_reg(static_cast(base + (0xee << 2)), 0x4, 0x0); - } - rf_write(0x3c00, 0x18, rf18); - rf_write(0x4c00, 0x18, rf18); + apply_rxbb(bwmode); + rf_window_write(0x3c00, 0x18, rf18); + rf_window_write(0x4c00, 0x18, rf18); _device.phy_set_bb_reg(static_cast(0x3c00 + (0xdf << 2)), 1u << 18, is_2g ? 1 : 0); _device.phy_set_bb_reg(0x1c90, 1u << 8, 0x1); /* rstb_3wire(true) */ @@ -183,29 +146,9 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, /* rstb_3wire(false) before the RF writes — see the force-update-anapar note * below (phydm_rstb_3wire_8822e). */ _device.phy_set_bb_reg(0x1c90, 1u << 8, 0x0); - rf_write(0x3c00, 0x18, rf18); - rf_write(0x4c00, 0x18, rf18); - /* 8822e switch_bandwidth: RXBB/TXBB bandwidth lives in RF 0x1a[14:10] - * (config_phydm_switch_bandwidth_8822e RMW: rf_reg1a &= ~0x7c00 then set - * the BW bits) — 20M = BIT11|BIT10, 40M = BIT12|BIT11, 80M = BIT13|BIT10; - * the 40M path also does the TX_CCK_IND workaround (RF 0x1a BIT0=1, - * BIT16=0). The 8822e has NO RF 0x3f RXBB register (that is an 8822c-only - * write): the 20 MHz path previously wrote RF 0x3f=0x18 and left RF 0x1a - * unset, so the 8822e RX baseband filter stayed at the wrong bandwidth and - * the receiver was deaf on 2.4 GHz (5 GHz limped on whatever the init RF - * table left in RF 0x1a). Set RF 0x1a for every bandwidth, matching the - * vendor (kernel 2G RF 0x1a = 0xc00, RF 0x3f = 0x2 default). */ - const uint32_t bb = is80 ? ((1u << 13) | (1u << 10)) - : is40 ? ((1u << 12) | (1u << 11)) - : ((1u << 11) | (1u << 10)); /* 20M */ - for (uint16_t base : {uint16_t(0x3c00), uint16_t(0x4c00)}) { - uint32_t r1a = _device.rtw_read32( - static_cast(base + (0x1a << 2))) & 0xfffffu; - r1a &= ~0x7c00u; - r1a |= bb; - if (is40) { r1a |= (1u << 0); r1a &= ~(1u << 16); } - rf_write(base, 0x1a, r1a); - } + rf_window_write(0x3c00, 0x18, rf18); + rf_window_write(0x4c00, 0x18, rf18); + apply_rxbb(bwmode); /* 8822e RF 0x1a RMW — see apply_rxbb */ _device.phy_set_bb_reg(static_cast(0x3c00 + (0xdf << 2)), 1u << 18, is_2g ? 1 : 0); /* Force-update-anapar (phydm_rstb_3wire_8822e, enable=true): 0x1c90[8]=1 + @@ -222,10 +165,165 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, _device.phy_set_bb_reg(0x4130, 1u << 29, 0x1); } - /* Per-band RX AGC-table selection (phydm_cck/ofdm_agc_tab_sel_8822c) — 8822C - * only; the EU (8822e) selects its AGC table via its own phydm path. 0x18ac/ - * 0x41ac [15:12]=CCK table, [8:4]=OFDM table; 0x828[7:3]=AGC lower bound - * (L_BND_DEFAULT_8822C 0xd; the measured ofdm_rxagc_l_bnd is not tracked). */ + /* Per-band RX AGC-table selection (shared with fast_retune). */ + select_agc_tables(central, bwmode); + + /* --- 2G/5G band BB (phydm switch_channel "Other BB Settings") --- */ + if (is_2g) { + /* Enable CCK Rx IQ (phydm_cck_rxiq_8822{c,e} SET): CCK source 5 + weighting + * [1,1]. The 8822c and 8822e writes are register-identical (0x1a9c[20]=1, + * 0x1a14[9:8]=0), so both variants take this path — the EU needs it too. */ + _device.phy_set_bb_reg(0x1a9c, 1u << 20, 0x1); + _device.phy_set_bb_reg(0x1a14, 0x300, 0x0); + _device.rtw_write8(0x454, + static_cast(_device.rtw_read8(0x454) & ~0x80)); + _device.phy_set_bb_reg(0x1a80, 1u << 18, 0x0); + _device.phy_set_bb_reg(0x1c80, 0x3F000000, 0xF); + } else { + /* Disable CCK Rx IQ (phydm_cck_rxiq_8822{c,e} REVERT). Register-identical + * across variants (0x1a9c[20]=0, 0x1a14[9:8]=3). */ + _device.phy_set_bb_reg(0x1a9c, 1u << 20, 0x0); + _device.phy_set_bb_reg(0x1a14, 0x300, 0x3); + _device.rtw_write8(0x454, + static_cast(_device.rtw_read8(0x454) | 0x80)); + _device.phy_set_bb_reg(0x1a80, 1u << 18, 0x1); + _device.phy_set_bb_reg(0x1c80, 0x3F000000, 0x22); + } + + /* SCO tracking f_c (phydm_sco_trk_fc_setting_8822c): BB 0xc30[11:0]. Keyed + * on the central channel (shared with fast_retune). */ + _device.phy_set_bb_reg(0xc30, 0xfff, sco_for(central)); + + /* TX DFIR (phydm_tx_dfir_setting_8822c, shared with fast_retune). */ + apply_tx_dfir(central); + + /* MAC-side bandwidth + TX sub-channel (halmac cfg_bw / cfg_pri_ch_idx). */ + set_mac_bw_txsc(bwmode, pri); + + /* phydm_bb_reset_8822c: toggle the BB reset (MAC reg 0x0 BIT16, 1->0->1) to + * (re)start the receiver after channel/BW config — the kernel does this after + * every switch_channel; without it the RX engine never runs. */ + bb_reset_toggle(); + + /* halrf_ex_dac_fifo_rst — the vendor runs this after EVERY switch_bandwidth + * ("fix dac fifo error after TXCK setting"): 40/80 MHz change the TX clock, + * so reset the DAC FIFO for vendor parity. 8822e only (its AFE DACK-bank soft + * reset); the 8822c has its own path. */ + if ((is40 || is80) && _variant == ChipVariant::C8822E) + dack_soft_rst_8822e(); + + _logger->info("Jaguar3: central ch {} / {} MHz set (RF18=0x{:05x}) + BB reset", + central, is80 ? 80 : is40 ? 40 : 20, rf18); + + /* For 5/10 MHz: channel is now tuned at 20 MHz; re-clock the baseband. */ + if (bwmode == CHANNEL_WIDTH_5 || bwmode == CHANNEL_WIDTH_10) + set_bandwidth_dividers(bwmode); + + if (std::getenv("DEVOURER_DUMP_CANARY")) + DumpCanary(); +} + +void RadioManagementJaguar3::DumpCanary() { + /* Channel/BW-relevant set: TX DFIR + block enables (0x808), RX DFIR (0x810), + * AGC bound/tables (0x828/0x18ac/0x41ac), subtune (0x88c), small-BW/RF-BW/ + * pri-ch (0x9b0), clock dividers (0x9b4), SCO (0xc30), pilot smoothing + * (0xcbc), CCK block regs (0x1a00/0x1a14/0x1a80/0x1a9c/0x1abc/0x1ae8/0x1aec), + * AGC 5G bound (0x1c80), 80 MHz decimation (0x1944/0x4044). MAC: BB-reset + * word (0x0), AFE clk (0x24, 8822e NB), CCK check (0x454), DATA_SC (0x483), + * us-ticks (0x55c/0x638), TRXPTCL (0x668). RF (via the BB direct window, + * both paths): 0x18 channel, 0x1a RXBB (8822e), 0x3f RXBB (8822c), 0xdf. + * Live counters (IGI 0x1d70, FA/CCA) deliberately excluded. */ + static const uint16_t bb_canary[] = { + 0x808, 0x810, 0x828, 0x88c, 0x9b0, 0x9b4, 0xc30, 0xcbc, + 0x1a00, 0x1a14, 0x1a80, 0x1a9c, 0x1abc, 0x1ae8, 0x1aec, 0x1c80, + 0x18ac, 0x41ac, 0x1944, 0x4044}; + static const uint16_t mac_canary[] = {0x0, 0x24, 0x454, 0x483, + 0x55c, 0x638, 0x668}; + static const uint8_t rf_canary[] = {0x18, 0x1a, 0x3f, 0xdf}; + + _logger->info("=== DEVOURER_DUMP_CANARY (post channel-set ch={}) ===", + unsigned(_last_channel)); + for (uint16_t a : bb_canary) + _logger->info("BB 0x{:04x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint16_t a : mac_canary) + _logger->info("MAC 0x{:03x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint8_t a : rf_canary) + _logger->info("RF[A] 0x{:02x} = 0x{:05X}", a, + _device.rtw_read32(static_cast(0x3c00 + (a << 2))) & + 0xfffffu); + for (uint8_t a : rf_canary) + _logger->info("RF[B] 0x{:02x} = 0x{:05X}", a, + _device.rtw_read32(static_cast(0x4c00 + (a << 2))) & + 0xfffffu); + _logger->info("=== END DEVOURER_DUMP_CANARY ==="); +} + +void RadioManagementJaguar3::central_and_pri(uint8_t channel, + uint8_t channel_offset, + ChannelWidth_t bwmode, + uint8_t ¢ral, uint8_t &pri) { + central = channel; + pri = 0; /* 0 = 20 MHz / no sub-channel */ + if (bwmode == CHANNEL_WIDTH_40) { + if (channel_offset == 2) { central = channel - 2; pri = 1; } /* HT40- */ + else { central = channel + 2; pri = 2; } /* HT40+ */ + } else if (bwmode == CHANNEL_WIDTH_80) { + central = static_cast(channel + 6); /* 80 MHz centre */ + /* pri-ch index for 80 MHz is a VHT_DATA_SC_* code (get_pri_ch_id), NOT a + * plain 0-3 slot: primary = lowest 20 => VHT_DATA_SC_20_LOWEST_OF_80MHZ = 4. + * This lands in 0x9b0[15:8]=0x44 and REG_DATA_SC 0x483=0xa4 (kernel-parity; + * verified against a register dump of `iw 149 80MHz`). */ + pri = 4; + } +} + +/* RF bandwidth bits in RF18: BW20 = BIT13|BIT12, BW40 = BIT13, BW80 = BIT12 + * (config_phydm_switch_bandwidth; 5/10 MHz tune the RF at 20 MHz). The base's + * channel/band bits AND the BW bits [13:12] are cleared before merging: the + * vendor does rf_reg18 &= ~(BIT13|BIT12) before setting them, and the RMW mask + * must too — otherwise a prior 20 MHz tune's BIT12 survives and a 40 MHz tune + * ends up BIT13|BIT12 = 20 MHz RF while BB/descriptor say 40 MHz, which halts + * the TX engine (frames fill TXPKTBUF but never drain). base 0 (8822e) composes + * the value from scratch. */ +uint32_t RadioManagementJaguar3::rf18_for(uint8_t central, + ChannelWidth_t bwmode, + uint32_t base) { + const uint32_t rf_bw_bits = (bwmode == CHANNEL_WIDTH_80) ? (1u << 12) + : (bwmode == CHANNEL_WIDTH_40) ? (1u << 13) + : ((1u << 13) | (1u << 12)); + uint32_t rf18 = (base & 0xfffffu & ~0x733ffu) | (central & 0xffu) | rf_bw_bits; + if (central > 14) { /* 5 GHz */ + rf18 |= (1u << 16) | (1u << 8); + if (central > 144) rf18 |= (1u << 18); + else if (central >= 80) rf18 |= (1u << 17); + } + return rf18; +} + +/* SCO tracking f_c value (phydm_sco_trk_fc_setting_8822c), keyed on central. */ +uint32_t RadioManagementJaguar3::sco_for(uint8_t central) { + if (central >= 36 && central <= 51) return 0x494; + if (central >= 52 && central <= 55) return 0x493; + if (central >= 56 && central <= 111) return 0x453; + if (central >= 112 && central <= 119) return 0x452; + if (central >= 120 && central <= 172) return 0x412; + if (central >= 173) return 0x411; + if (central >= 1 && central <= 10) return 0x9aa; + if (central == 11 || central == 12) return 0x96a; + return 0x969; /* 13/14 */ +} + +/* Per-band RX AGC-table selection. 8822C: phydm_cck/ofdm_agc_tab_sel_8822c — + * 0x18ac/0x41ac [15:12]=CCK table, [8:4]=OFDM table; 0x828[7:3]=AGC lower bound + * (L_BND_DEFAULT_8822C 0xd; the measured ofdm_rxagc_l_bnd is not tracked). + * 8822E: phydm_set_agc_table_8822e — same registers, 8822e-specific indices + * (OFDM 2G=0 / 5G_LOW=1 / MID=2 / HIGH=3, CCK 2G=8; per-band, no BW20/BW40 + * split). Without this the EU keeps the init-table AGC index when it tunes to + * 2.4 GHz, so the front-end runs at the wrong gain and the receiver is stone + * deaf on 2G (5 GHz survives on the init-table defaults). WiFi-only coex here, + * so the no-BT table (not the *_BTC variants). */ +void RadioManagementJaguar3::select_agc_tables(uint8_t central, + ChannelWidth_t bwmode) { auto ofdm_agc_sel = [this](uint8_t table) { _device.phy_set_bb_reg(0x18ac, 0x1f0, table); _device.phy_set_bb_reg(0x41ac, 0x1f0, table); @@ -237,15 +335,7 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, }; const bool bw20 = (bwmode == CHANNEL_WIDTH_20 || bwmode == CHANNEL_WIDTH_10 || bwmode == CHANNEL_WIDTH_5); - if (!is_c) { - /* 8822E AGC-table selection (phydm_set_agc_table_8822e / ofdm_agc_tab_sel_ - * 8822e / cck_agc_tab_sel_8822e). Same registers as the 8822c lambdas, but - * 8822e-specific table indices (phydm_hal_api8822e.h): OFDM 2G=0 / 5G_LOW=1 - * / MID=2 / HIGH=3, CCK 2G=8. The 8822e index is per-band (no BW20/BW40 - * split). Without this the EU keeps the init-table AGC index when it tunes - * to 2.4 GHz, so the front-end runs at the wrong gain and the receiver is - * stone deaf on 2G (5 GHz survives on the init-table defaults). WiFi-only - * coex here, so the no-BT table (not the *_BTC variants). */ + if (_variant != ChipVariant::C8822C) { if (central <= 14) { cck_agc_sel(8); /* CCK_8822E */ ofdm_agc_sel(0); /* OFDM_2G_8822E */ @@ -266,76 +356,181 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, } else { ofdm_agc_sel(3); /* OFDM_5G_HIGH */ } +} - /* --- 2G/5G band BB (phydm switch_channel "Other BB Settings") --- */ - if (is_2g) { - /* Enable CCK Rx IQ (phydm_cck_rxiq_8822{c,e} SET): CCK source 5 + weighting - * [1,1]. The 8822c and 8822e writes are register-identical (0x1a9c[20]=1, - * 0x1a14[9:8]=0), so both variants take this path — the EU needs it too. */ - _device.phy_set_bb_reg(0x1a9c, 1u << 20, 0x1); - _device.phy_set_bb_reg(0x1a14, 0x300, 0x0); - _device.rtw_write8(0x454, - static_cast(_device.rtw_read8(0x454) & ~0x80)); - _device.phy_set_bb_reg(0x1a80, 1u << 18, 0x0); - _device.phy_set_bb_reg(0x1c80, 0x3F000000, 0xF); +void RadioManagementJaguar3::rf_window_write(uint16_t base, uint8_t rfaddr, + uint32_t v) { + _device.phy_set_bb_reg(static_cast(base + (rfaddr << 2)), + 0x000fffff, v); +} + +/* BW-keyed RXBB baseband-filter write, inside the caller's 3-wire bracket. + * 8822c: RF 0x3f gated sequence — RF 0xee[2]=1, RF 0x33[4:0]=0x12, RF 0x3f, + * RF 0xee[2]=0 (config_phydm_switch_bandwidth_8822c); BW20 = BIT4|BIT3 (0x18), + * BW40 = BIT4 (0x10), BW80 = BIT3 (0x08). + * 8822e: RXBB/TXBB bandwidth lives in RF 0x1a[14:10] + * (config_phydm_switch_bandwidth_8822e RMW: rf_reg1a &= ~0x7c00 then set the + * BW bits) — 20M = BIT11|BIT10, 40M = BIT12|BIT11, 80M = BIT13|BIT10; the 40M + * path also does the TX_CCK_IND workaround (RF 0x1a BIT0=1, BIT16=0). The + * 8822e has NO RF 0x3f RXBB register (that is an 8822c-only write): the 20 MHz + * path previously wrote RF 0x3f=0x18 and left RF 0x1a unset, so the 8822e RX + * baseband filter stayed at the wrong bandwidth and the receiver was deaf on + * 2.4 GHz (5 GHz limped on whatever the init RF table left in RF 0x1a). Set + * RF 0x1a for every bandwidth, matching the vendor (kernel 2G RF 0x1a = 0xc00, + * RF 0x3f = 0x2 default). */ +void RadioManagementJaguar3::apply_rxbb(ChannelWidth_t bwmode) { + const bool is40 = (bwmode == CHANNEL_WIDTH_40); + const bool is80 = (bwmode == CHANNEL_WIDTH_80); + if (_variant == ChipVariant::C8822C) { + const uint32_t rxbb = is80 ? 0x08u : is40 ? 0x10u : 0x18u; + for (uint16_t base : {uint16_t(0x3c00), uint16_t(0x4c00)}) { + _device.phy_set_bb_reg(static_cast(base + (0xee << 2)), 0x4, 0x1); + _device.phy_set_bb_reg(static_cast(base + (0x33 << 2)), 0x1f, 0x12); + rf_window_write(base, 0x3f, rxbb); + _device.phy_set_bb_reg(static_cast(base + (0xee << 2)), 0x4, 0x0); + } } else { - /* Disable CCK Rx IQ (phydm_cck_rxiq_8822{c,e} REVERT). Register-identical - * across variants (0x1a9c[20]=0, 0x1a14[9:8]=3). */ - _device.phy_set_bb_reg(0x1a9c, 1u << 20, 0x0); - _device.phy_set_bb_reg(0x1a14, 0x300, 0x3); - _device.rtw_write8(0x454, - static_cast(_device.rtw_read8(0x454) | 0x80)); - _device.phy_set_bb_reg(0x1a80, 1u << 18, 0x1); - _device.phy_set_bb_reg(0x1c80, 0x3F000000, 0x22); + const uint32_t bb = is80 ? ((1u << 13) | (1u << 10)) + : is40 ? ((1u << 12) | (1u << 11)) + : ((1u << 11) | (1u << 10)); /* 20M */ + for (uint16_t base : {uint16_t(0x3c00), uint16_t(0x4c00)}) { + uint32_t r1a = _device.rtw_read32( + static_cast(base + (0x1a << 2))) & 0xfffffu; + r1a &= ~0x7c00u; + r1a |= bb; + if (is40) { r1a |= (1u << 0); r1a &= ~(1u << 16); } + rf_window_write(base, 0x1a, r1a); + } } +} - /* SCO tracking f_c (phydm_sco_trk_fc_setting_8822c): BB 0xc30[11:0]. Keyed - * on the central channel. */ - uint32_t sco; - if (central >= 36 && central <= 51) sco = 0x494; - else if (central >= 52 && central <= 55) sco = 0x493; - else if (central >= 56 && central <= 111) sco = 0x453; - else if (central >= 112 && central <= 119) sco = 0x452; - else if (central >= 120 && central <= 172) sco = 0x412; - else if (central >= 173) sco = 0x411; - else if (central >= 1 && central <= 10) sco = 0x9aa; - else if (central == 11 || central == 12) sco = 0x96a; - else sco = 0x969; /* 13/14 */ - _device.phy_set_bb_reg(0xc30, 0xfff, sco); - - /* TX DFIR (phydm_tx_dfir_setting_8822c): BB 0x808[22:20] + [6:4]. */ - if (is_2g) { +/* TX DFIR (phydm_tx_dfir_setting_8822c): BB 0x808[22:20] + [6:4]. */ +void RadioManagementJaguar3::apply_tx_dfir(uint8_t central) { + if (central <= 14) { _device.phy_set_bb_reg(0x808, 0x700000, central == 11 ? 0x3 : 0x1); _device.phy_set_bb_reg(0x808, 0x70, central == 13 ? 0x3 : 0x1); } else { _device.phy_set_bb_reg(0x808, 0x700000, 0x1); _device.phy_set_bb_reg(0x808, 0x70, 0x3); } +} - /* MAC-side bandwidth + TX sub-channel (halmac cfg_bw / cfg_pri_ch_idx). */ - set_mac_bw_txsc(bwmode, pri); - - /* phydm_bb_reset_8822c: toggle the BB reset (MAC reg 0x0 BIT16, 1->0->1) to - * (re)start the receiver after channel/BW config — the kernel does this after - * every switch_channel; without it the RX engine never runs. */ +/* phydm_bb_reset_8822c: toggle the BB reset (MAC reg 0x0 BIT16, 1->0->1) so + * the receiver (re)starts after channel/BW config. */ +void RadioManagementJaguar3::bb_reset_toggle() { uint32_t r0 = _device.rtw_read32(0x0); _device.rtw_write32(0x0, r0 | (1u << 16)); _device.rtw_write32(0x0, r0 & ~(1u << 16)); _device.rtw_write32(0x0, r0 | (1u << 16)); +} - /* halrf_ex_dac_fifo_rst — the vendor runs this after EVERY switch_bandwidth - * ("fix dac fifo error after TXCK setting"): 40/80 MHz change the TX clock, - * so reset the DAC FIFO for vendor parity. 8822e only (its AFE DACK-bank soft - * reset); the 8822c has its own path. */ - if ((is40 || is80) && _variant == ChipVariant::C8822E) - dack_soft_rst_8822e(); +bool RadioManagementJaguar3::fast_retune(uint8_t channel, + uint8_t channel_offset, + ChannelWidth_t bwmode, bool cache_rf) { + if (_last_channel == 0) + return false; /* never tuned — unknown band, cold BW/band state */ + const bool cur_2g = _last_channel <= 14; + if (cur_2g != (channel <= 14)) + return false; /* band change needs RFE/AGC/CCK-RxIQ — full path only */ + if (channel == _last_channel) + return true; /* no-op hop */ + + uint8_t central, pri; + central_and_pri(channel, channel_offset, bwmode, central, pri); + + /* RF18: merge the new central into the cached full value (8822c — one + * BB-window read primes the cache; cache_rf=false re-reads per hop for A/B + * measurement) or compose from scratch (8822e — plain write, no read ever). */ + const bool is_c = (_variant == ChipVariant::C8822C); + uint32_t rf18; + if (is_c) { + if (!cache_rf || !_rf18_cached) { + _rf18_cache = _device.rtw_read32( + static_cast(0x3c00 + (0x18 << 2))) & + 0xfffffu; + _rf18_cached = true; + } + rf18 = rf18_for(central, bwmode, _rf18_cache); + _rf18_cache = rf18; /* refresh so the next hop merges into what we wrote */ + } else { + rf18 = rf18_for(central, bwmode, 0); + } - _logger->info("Jaguar3: central ch {} / {} MHz set (RF18=0x{:05x}) + BB reset", - central, is80 ? 80 : is40 ? 40 : 20, rf18); + /* The per-hop core of config_phydm_switch_channel: 3-wire bracket, RF18 on + * both paths, force-update-anapar (pushes the RF/analog shadow to the + * front-end — required on both variants, see set_channel_bwmode). The + * band-keyed RF 0xdf write stays untouched (set by the last full set at + * this band). The BW-keyed RXBB is re-asserted ONCE per epoch — the + * init-time halrf calibration rewrites it after the channel set + * (hardware-observed on the 8812EU: IQK clears the 40 MHz TX_CCK_IND bit + * in RF 0x1a) and a fast hop must end in the full path's state; per-variant + * order matches the full path (8822c: RXBB then RF18; 8822e: RF18 then + * RF 0x1a). */ + _device.phy_set_bb_reg(0x1c90, 1u << 8, 0x0); /* rstb_3wire(false) */ + if (is_c && !_rxbb_asserted) + apply_rxbb(bwmode); + rf_window_write(0x3c00, 0x18, rf18); + rf_window_write(0x4c00, 0x18, rf18); + if (!is_c && !_rxbb_asserted) + apply_rxbb(bwmode); + _rxbb_asserted = true; + _device.phy_set_bb_reg(0x1c90, 1u << 8, 0x1); /* rstb_3wire(true) */ + _device.phy_set_bb_reg(0x1830, 1u << 29, 0x1); /* force update anapar (A) */ + _device.phy_set_bb_reg(0x4130, 1u << 29, 0x1); /* force update anapar (B) */ + + /* Channel-keyed constants, written only when their bucket moves (invalidated + * by every full set, so the first fast hop writes them once). */ + const bool bw20 = (bwmode == CHANNEL_WIDTH_20 || bwmode == CHANNEL_WIDTH_10 || + bwmode == CHANNEL_WIDTH_5); + const int agc_key = (central <= 14 ? 0 + : central < 80 ? 1 + : central <= 144 ? 2 + : 3) | + (bw20 ? 0x10 : 0); + if (agc_key != _last_agc_key) { + select_agc_tables(central, bwmode); + _last_agc_key = agc_key; + } - /* For 5/10 MHz: channel is now tuned at 20 MHz; re-clock the baseband. */ - if (bwmode == CHANNEL_WIDTH_5 || bwmode == CHANNEL_WIDTH_10) - set_bandwidth_dividers(bwmode); + const uint32_t sco = sco_for(central); + if (sco != _last_sco) { + _device.phy_set_bb_reg(0xc30, 0xfff, sco); + _last_sco = sco; + } + + /* TX DFIR: write the full path's END state. [22:20] is the per-channel rule + * (2G ch11) everywhere; [6:4] is the per-channel rule (2G ch13 / 5G 0x3) + * EXCEPT in 5/10 MHz mode on the 8822e, where the narrowband re-clock owns + * it (set_bandwidth_dividers writes [6:4]=0x1 after apply_tx_dfir, so the + * full path always ends at 0x1 there — the ch13 rule must not undo it). */ + const bool nb = + (bwmode == CHANNEL_WIDTH_5 || bwmode == CHANNEL_WIDTH_10); + const uint32_t dfir_msb = + (central <= 14) ? (central == 11 ? 0x3u : 0x1u) : 0x1u; + const uint32_t dfir_lsb = + (nb && !is_c) ? 0x1u + : (central <= 14) ? (central == 13 ? 0x3u : 0x1u) + : 0x3u; + const uint32_t dfir = (dfir_msb << 8) | dfir_lsb; + if (dfir != _last_dfir) { + _device.phy_set_bb_reg(0x808, 0x700000, dfir_msb); + _device.phy_set_bb_reg(0x808, 0x70, dfir_lsb); + _last_dfir = dfir; + } + + /* BB reset every hop (the kernel runs it after every switch_channel; the RX + * engine must relatch on the new channel). */ + bb_reset_toggle(); + + _last_channel = channel; + _logger->debug("Jaguar3: fast retune -> ch {} (central {}, RF18=0x{:05x})", + channel, central, rf18); + /* The J1 parity lesson: the fast path must emit the canary itself — it does + * not pass through the full path, so without this the parity diff would + * compare the fast run's stale full-set dump. */ + if (std::getenv("DEVOURER_DUMP_CANARY")) + DumpCanary(); + return true; } void RadioManagementJaguar3::set_mac_bw_txsc(ChannelWidth_t bw, uint8_t pri) { diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index c566d70..c88047d 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -29,6 +29,33 @@ class RadioManagementJaguar3 { void set_channel_bwmode(uint8_t channel, uint8_t channel_offset, ChannelWidth_t bwmode); + /* Lean intra-band, same-bandwidth hop retune — the FastRetune core (see + * docs/frequency-hopping.md). The subset of set_channel_bwmode a hop needs: + * the RF18 channel write inside its 3-wire bracket + force-anapar, the BB + * reset, and the channel-keyed constants (SCO fc, TX DFIR, AGC table) only + * when their bucket changes. Everything bandwidth-keyed (clock/DFIR block, + * RXBB, MAC BW/TXSC, narrowband dividers, DAC-FIFO reset) and band-keyed + * (CCK-RxIQ, RF 0xdf) is untouched — set by the last full set at this + * BW/band. In 5/10 MHz mode the TX-DFIR write is skipped entirely (the + * narrowband re-clock owns 0x808 on the 8822e), so fast hops preserve the + * narrowband clocking without re-running the divider recipe. + * + * cache_rf=true reuses the cached full RF18 value (primed by one BB-window + * read on the first fast hop; the 8822e composes RF18 from scratch and + * needs no cache/read at all). Returns false — chip untouched — when the + * hop crosses the 2.4/5 GHz boundary or the radio was never tuned; the + * caller falls back to the full set_channel_bwmode. */ + bool fast_retune(uint8_t channel, uint8_t channel_offset, + ChannelWidth_t bwmode, bool cache_rf); + + /* Channel/BW register-canary dump for tests/hop_parity_check.sh — the same + * grep format as the Jaguar1 DumpCanary (BB/MAC/RF[A|B] ADDR = VALUE inside + * === DEVOURER_DUMP_CANARY === markers). Emitted by both the full and fast + * channel paths when DEVOURER_DUMP_CANARY is set, so a full-vs-fast diff + * compares the same post-set snapshot. Live counters (IGI/FA) are excluded + * — they'd be pure run-variant noise. */ + void DumpCanary(); + /* Narrowband re-clock applied on top of an already-tuned channel: switches * the baseband DAC/ADC clock + small-BW field for 5/10/20 MHz. */ void set_bandwidth_dividers(ChannelWidth_t bwmode); @@ -92,10 +119,44 @@ class RadioManagementJaguar3 { void write_check_afe_8822e(uint16_t add, uint32_t data); void dack_soft_rst_8822e(); + /* Shared by the full and fast channel paths so they cannot diverge. */ + static void central_and_pri(uint8_t channel, uint8_t channel_offset, + ChannelWidth_t bwmode, uint8_t ¢ral, + uint8_t &pri); + static uint32_t sco_for(uint8_t central); /* BB 0xc30[11:0] value */ + static uint32_t rf18_for(uint8_t central, ChannelWidth_t bwmode, + uint32_t base); /* merge into base (0 = none) */ + void select_agc_tables(uint8_t central, ChannelWidth_t bwmode); + void apply_tx_dfir(uint8_t central); + void bb_reset_toggle(); /* MAC 0x0 BIT16 1->0->1 */ + /* RF register write via the direct-write window: BB[base + (rf_addr<<2)], + * 20-bit mask. base 0x3c00 (A) / 0x4c00 (B). */ + void rf_window_write(uint16_t base, uint8_t rfaddr, uint32_t v); + /* BW-keyed RXBB filter (8822c RF 0x3f gated sequence / 8822e RF 0x1a RMW). + * Must run inside a rstb_3wire(false)..(true) bracket — the caller owns the + * bracket. */ + void apply_rxbb(ChannelWidth_t bwmode); + RtlUsbAdapter _device; Logger_t _logger; ChipVariant _variant; uint8_t _last_channel = 0; /* set by set_channel_bwmode; 0 = not yet tuned */ + + /* fast_retune write-on-change caches. Invalidated whenever the full + * set_channel_bwmode runs (it rewrites all of them); refreshed by the fast + * path's own writes, so they only ever mirror fast-path state — the J1 + * RadioManagementModule pattern. */ + uint32_t _rf18_cache = 0; /* 8822c full RF18 (BB-window read, 20-bit) */ + bool _rf18_cached = false; + uint32_t _last_sco = 0xffffffff; + uint32_t _last_dfir = 0xffffffff; /* packed (0x808[22:20]<<8)|[6:4] */ + int _last_agc_key = -1; /* band/sub-band bucket + bw20 flag */ + /* BW-keyed RXBB state, re-asserted on the first fast hop of each epoch: + * the init-time halrf calibration rewrites it after the channel set + * (hardware-observed on the 8812EU — IQK clears the 40 MHz TX_CCK_IND bit + * in RF 0x1a), and the fast path must end in the same state as the full + * path, which re-imposes it on every retune. */ + bool _rxbb_asserted = false; }; } /* namespace jaguar3 */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index aa68447..3cf3b59 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -8,6 +8,7 @@ #include "BeamformingSounder.h" /* generation-neutral BF self-sounding recipe */ +#include "ChannelFreq.h" /* radiotap CHANNEL freq -> channel (per-packet hop) */ #include "FrameParserJaguar3.h" #include "NhmReader.h" /* frame-free NHM power histogram (shared) */ #include "RateDefinitions.h" /* MGN_* rate enum (shared across the family) */ @@ -649,11 +650,32 @@ RxEnergy RtlJaguar3Device::GetRxEnergy() { } void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { + /* Serialize against the coex thread's housekeeping tick (and any concurrent + * FastRetune) — channel config is register RMW. Init/InitWrite call the + * radio-management core directly (no lock needed: the coex thread isn't + * running yet), so locking here cannot self-deadlock. */ + std::lock_guard lk(_reg_mu); _channel = channel; _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); } +void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { + std::lock_guard lk(_reg_mu); + if (channel == _channel.Channel) + return; + if (_radioManagement.fast_retune(channel, _channel.ChannelOffset, + _channel.ChannelWidth, cache_rf)) { + _channel.Channel = channel; + return; + } + /* Fast path declined (band change / never tuned) — full channel set at the + * current bandwidth + offset, under the same lock (the core is unlocked). */ + _channel.Channel = channel; + _radioManagement.set_channel_bwmode(channel, _channel.ChannelOffset, + _channel.ChannelWidth); +} + void RtlJaguar3Device::SetTxPower(uint8_t power) { /* Record a flat TXAGC reference override; applied during InitWrite (may be * called before bring-up). Without it the chip uses its efuse-calibrated @@ -682,6 +704,8 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) { ChannelWidth_t bwidth = CHANNEL_WIDTH_20; bool vht = (radiotap_length != 0x0d); bool rate_from_radiotap = false; /* did the frame's radiotap carry a rate? */ + /* Per-packet hop target from a radiotap CHANNEL field (0 = none present). */ + int radiotap_channel = 0; auto *rtap_hdr = reinterpret_cast( const_cast(packet)); @@ -697,6 +721,13 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) { fixed_rate = *it.this_arg; rate_from_radiotap = true; break; + case IEEE80211_RADIOTAP_CHANNEL: + /* 2 x __le16: frequency (MHz), then flags. Frequency is authoritative + * for the per-packet hop target; flags are ignored (rate/BW come from + * the RATE/MCS/VHT fields). Same contract as the Jaguar1 path. */ + radiotap_channel = + devourer::freq_to_chan(get_unaligned_le16(it.this_arg)); + break; case IEEE80211_RADIOTAP_MCS: { uint8_t mcs_known = it.this_arg[0]; uint8_t mcs_flags = it.this_arg[1]; @@ -744,6 +775,14 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) { } } + /* Radiotap CHANNEL is authoritative for per-packet frequency, exactly as + * RATE/MCS are for rate: a frame asking for a different channel triggers a + * lean FastRetune before the descriptor is built (so the 40-in-80 DATA_SC + * below keys off up-to-date channel state). FastRetune takes _reg_mu for the + * retune only — the TX hot path stays lock-free when not hopping. */ + if (radiotap_channel > 0 && radiotap_channel != _channel.Channel) + FastRetune(static_cast(radiotap_channel), /*cache_rf=*/true); + /* Radiotap carried no rate -> apply the runtime TX-mode default (SetTxMode). * Without this a rate-less frame (e.g. the demo's beacon) falls back to * MGN_1M, so an "MCS7" flood silently went on-air at 1 Mbps. Per-packet diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 291aa7c..5f32fde 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -46,6 +46,18 @@ class RtlJaguar3Device : public IRtlDevice { void StartRxLoop(Action_ParsedRadioPacket packetProcessor) override; void StopRxLoop() override { _rx_stop = true; } void SetMonitorChannel(SelectedChannel channel) override; + /* Lean frequency-hop retune (Jaguar3 port of the Jaguar1 FastRetune — see + * docs/frequency-hopping.md): the RF18 channel write inside its 3-wire + * bracket + force-anapar + BB reset, with the channel-keyed constants (SCO, + * TX DFIR, AGC table) written only when their bucket changes. Skips the + * bandwidth block, RXBB, MAC BW/TXSC, and — in 5/10 MHz mode — preserves the + * narrowband dividers, so fast hops keep the NB re-clock without re-running + * the divider recipe. Falls back to the full set_channel_bwmode on a band + * change. cache_rf selects the cached-RF18 variant (meaningful on the 8822c, + * whose RF18 is a read-modify-write; the 8822e composes it from scratch). + * Serializes on _reg_mu, so it is safe during an active TX session — the + * sanctioned in-session hop primitive (unlike a bare SetMonitorChannel). */ + void FastRetune(uint8_t channel, bool cache_rf) override; void InitWrite(SelectedChannel channel) override; void SetTxPower(uint8_t power) override; bool send_packet(const uint8_t *packet, size_t length) override; @@ -121,12 +133,13 @@ class RtlJaguar3Device : public IRtlDevice { * C2H reports (BT-info etc.) and runs the periodic coex decision so the FW's * PTA keeps the antenna with WLAN during sustained TX. * THREADING CONTRACT: while a TX session is active (between InitWrite and Stop) - * this thread owns all chip register access; the ~2 s housekeeping tick and - * StartRxLoop's one-shot RX-filter restore serialize on _reg_mu. The TX hot - * path (send_packet) does no register I/O, so it runs lock-free alongside it; - * callers must NOT invoke SetMonitorChannel/SetTxPower (which do register RMW) - * during an active TX session, or those reads-modify-writes will race the coex - * thread. Bulk-IN has exactly one reader at a time: while StartRxLoop is + * the ~2 s housekeeping tick, StartRxLoop's one-shot RX-filter restore, and + * every channel retune (SetMonitorChannel / FastRetune — register RMW) + * serialize on _reg_mu. The TX hot path (send_packet) does no register I/O of + * its own, so it runs lock-free alongside them; a radiotap-CHANNEL hop inside + * send_packet takes the lock only for the retune itself (so a hop can be + * delayed by up to one coex tick). Callers must still not invoke SetTxPower + * mid-session. Bulk-IN has exactly one reader at a time: while StartRxLoop is * active (_rx_loop_active) the coex thread skips its C2H drain — the RX async * loop sees the C2H reports as part of its stream. */ std::thread _coex_thread; diff --git a/tests/bf_ndpa_onair.sh b/tests/bf_ndpa_onair.sh index e901db3..dccf740 100755 --- a/tests/bf_ndpa_onair.sh +++ b/tests/bf_ndpa_onair.sh @@ -30,7 +30,7 @@ DUR=8 mkdir -p "$OUT" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -f "bf_ndpa_probe.py" 2>/dev/null wait 2>/dev/null } diff --git a/tests/bf_report_sniff.sh b/tests/bf_report_sniff.sh index 0ca383f..f5e861a 100755 --- a/tests/bf_report_sniff.sh +++ b/tests/bf_report_sniff.sh @@ -24,7 +24,7 @@ BFER_MAC="57:42:75:05:d6:00" # canonical SA = NDPA TA (8812AU sounder) mkdir -p "$OUT" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -x WiFiDriverDemo 2>/dev/null wait 2>/dev/null } diff --git a/tests/bf_selfsound_jaguar2.sh b/tests/bf_selfsound_jaguar2.sh index 5576357..3479705 100755 --- a/tests/bf_selfsound_jaguar2.sh +++ b/tests/bf_selfsound_jaguar2.sh @@ -45,7 +45,7 @@ J1AUX_PID="${J1AUX_PID:-0x0120}" # (8821AU, Archer T2U Plus) mkdir -p "$OUT" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -x WiFiDriverDemo 2>/dev/null wait 2>/dev/null } diff --git a/tests/bf_selfsound_jaguar3.sh b/tests/bf_selfsound_jaguar3.sh index 203e8a3..de2d6a1 100755 --- a/tests/bf_selfsound_jaguar3.sh +++ b/tests/bf_selfsound_jaguar3.sh @@ -30,7 +30,7 @@ mkdir -p "$OUT" tag="$(echo "$BFEE_PID" | tr -d 'x')" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -x WiFiDriverDemo 2>/dev/null wait 2>/dev/null } diff --git a/tests/bf_selfsound_onair.sh b/tests/bf_selfsound_onair.sh index 0cb6d0d..caa38bb 100755 --- a/tests/bf_selfsound_onair.sh +++ b/tests/bf_selfsound_onair.sh @@ -29,7 +29,7 @@ BFER_MAC="57:42:75:05:d6:00" # canonical SA = NDPA TA mkdir -p "$OUT" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -x WiFiDriverDemo 2>/dev/null pkill -f "bf_ndpa_probe.py" 2>/dev/null wait 2>/dev/null diff --git a/tests/bf_waterfall.sh b/tests/bf_waterfall.sh index 4a15df1..6847c9b 100755 --- a/tests/bf_waterfall.sh +++ b/tests/bf_waterfall.sh @@ -25,7 +25,7 @@ OP_SNR="${1:-}" [ -x "$TXDEMO" ] && [ -x "$RXDEMO" ] || { echo "build demos first"; exit 1; } cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null + pkill -x WiFiDriverTxDem 2>/dev/null pkill -x WiFiDriverDemo 2>/dev/null wait 2>/dev/null } diff --git a/tests/cu_kernel_rx_check.sh b/tests/cu_kernel_rx_check.sh index 892f68c..23ad497 100755 --- a/tests/cu_kernel_rx_check.sh +++ b/tests/cu_kernel_rx_check.sh @@ -16,7 +16,7 @@ TXS=$(find_sys 8812) [ -z "$TXS" ] && { echo "no 8812AU"; exit 1; } cleanup() { - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null sudo pkill -9 -f "tcpdump -i ${CUIF}mon" 2>/dev/null sudo iw dev ${CUIF}mon del 2>/dev/null sudo ip link set "$CUIF" up 2>/dev/null diff --git a/tests/cw_tone_sdr.sh b/tests/cw_tone_sdr.sh index c1bd7ce..1788781 100755 --- a/tests/cw_tone_sdr.sh +++ b/tests/cw_tone_sdr.sh @@ -70,7 +70,7 @@ done DEMO_PID="" cleanup() { [ -n "$DEMO_PID" ] && kill "$DEMO_PID" 2>/dev/null || true - pkill -x WiFiDriverTxDemo 2>/dev/null || true + pkill -x WiFiDriverTxDem 2>/dev/null || true pkill -f "tests/cw_tone_probe.py" 2>/dev/null || true } trap cleanup EXIT INT TERM diff --git a/tests/eu_5g_cold_repeat.sh b/tests/eu_5g_cold_repeat.sh index f21d716..19adf02 100755 --- a/tests/eu_5g_cold_repeat.sh +++ b/tests/eu_5g_cold_repeat.sh @@ -9,11 +9,11 @@ FREQ=5180e6 N=${1:-4} KMOD=rtl88x2eu_ohd -cleanup(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null; } +cleanup(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null; } trap cleanup EXIT cold_cycle(){ # full VBUS off/on of the EU's hub port - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null sudo rmmod $KMOD 2>/dev/null; sleep 1 sudo uhubctl -l $HUB -a off >/dev/null 2>&1; sleep 4 sudo uhubctl -l $HUB -a on >/dev/null 2>&1; sleep 6 @@ -38,7 +38,7 @@ for i in $(seq 1 $N); do stdbuf -oL timeout -k 5 14 build/WiFiDriverTxDemo >/tmp/cold_dev_$i.log 2>&1 & sleep 6 echo " devourer cold run $i:$(sweep)" - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sleep 1 + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sleep 1 done echo "=== $N COLD kernel EU 5G runs (power-cycled each) ===" diff --git a/tests/eu_5g_txpwr_probe.sh b/tests/eu_5g_txpwr_probe.sh index 470a10c..25f20d3 100755 --- a/tests/eu_5g_txpwr_probe.sh +++ b/tests/eu_5g_txpwr_probe.sh @@ -15,7 +15,7 @@ PY=${PY:-python3} SECS=5 EU_VID=0x0bda; EU_PID=0xa81a -cleanup(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; } +cleanup(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT cell() { # $1=label $2=channel $3=freq $4=extra-env diff --git a/tests/eu_kernel_baseline.sh b/tests/eu_kernel_baseline.sh index 069a723..4f58fc0 100755 --- a/tests/eu_kernel_baseline.sh +++ b/tests/eu_kernel_baseline.sh @@ -12,7 +12,7 @@ cleanup(){ trap cleanup EXIT # Ensure no devourer userspace holds the device. -sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null +sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null sudo pkill -9 -x WiFiDriverDe 2>/dev/null sleep 1 diff --git a/tests/eu_rx_validate.sh b/tests/eu_rx_validate.sh index c0013b6..52990d4 100755 --- a/tests/eu_rx_validate.sh +++ b/tests/eu_rx_validate.sh @@ -28,7 +28,7 @@ echo "[harness] TX $TXPID=$TXS RX $RXPID=$RXS ch=$CH ${SECS}s" TXHUB=${TXS%.*}; TXPORT=${TXS##*.} cleanup() { - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null sudo pkill -9 -x WiFiDriverDemo 2>/dev/null echo "$TXS:1.0" | sudo tee /sys/bus/usb/drivers/$TXDRV/bind >/dev/null 2>&1 [ -n "$RXDRV" ] && echo "$RXS:1.0" | sudo tee /sys/bus/usb/drivers/$RXDRV/bind >/dev/null 2>&1 diff --git a/tests/eu_tx_sdr_check.sh b/tests/eu_tx_sdr_check.sh index 60606a3..097d104 100755 --- a/tests/eu_tx_sdr_check.sh +++ b/tests/eu_tx_sdr_check.sh @@ -20,13 +20,13 @@ run_tx() { # $1=pid $2=sysfs $3=driver $4=label sleep 4 echo "--- $label TX duty @ ${FREQ}Hz ---" sudo $PY tests/sdr_duty.py --freq $FREQ --secs $SECS --mcs 7 --bw 20 2>&1 | grep -iE "duty|mbps|noise|error" | head - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; wait $txpid 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; wait $txpid 2>/dev/null [ -n "$drv" ] && echo "$sys:1.0" | sudo tee /sys/bus/usb/drivers/$drv/bind >/dev/null 2>&1 sleep 2 echo " ($label sends: $(grep -c 'rc=1\|bulk_send.*OK' /tmp/eutx_$label.log), fails: $(grep -c 'Failed to send' /tmp/eutx_$label.log))" } -trap 'sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null' EXIT +trap 'sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null' EXIT [ -n "$CUS" ] && run_tx c812 "$CUS" rtw88_8822cu CU-control [ -n "$EUS" ] && run_tx a81a "$EUS" "" EU echo "Done. duty >> noise = radiates; duty ~ noise = dark front-end." diff --git a/tests/eu_txagc_regdiff.sh b/tests/eu_txagc_regdiff.sh index a7d85e2..2d95d71 100644 --- a/tests/eu_txagc_regdiff.sh +++ b/tests/eu_txagc_regdiff.sh @@ -17,7 +17,7 @@ OUT=/tmp/eu_regdiff mkdir -p $OUT cleanup(){ - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null sudo pkill -9 -f kernel_tx_inject 2>/dev/null sudo pkill -9 -f "cat /sys/kernel/debug/usb/usbmon" 2>/dev/null } diff --git a/tests/eu_txpwr_ab.sh b/tests/eu_txpwr_ab.sh index 6ec3b35..515c582 100755 --- a/tests/eu_txpwr_ab.sh +++ b/tests/eu_txpwr_ab.sh @@ -7,10 +7,10 @@ set -u KMOD=rtl88x2eu_ohd HUB=3-2.3 FREQ=5180e6 -cleanup(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null; } +cleanup(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null; } trap cleanup EXIT -cycle(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null +cycle(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sudo pkill -9 -f kernel_tx_inject 2>/dev/null sudo rmmod $KMOD 2>/dev/null; sleep 1; sudo uhubctl -l $HUB -a cycle >/dev/null 2>&1; sleep 4; } sweep(){ # prints "8:xx 10:xx 12:xx 14:xx" @@ -34,7 +34,7 @@ meas_devourer(){ stdbuf -oL timeout -k 5 30 build/WiFiDriverTxDemo >/dev/null 2>&1 & sleep 5 echo " devourer:$(sweep)" - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; sleep 1 + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; sleep 1 } meas_kernel(){ diff --git a/tests/eu_txref_test.sh b/tests/eu_txref_test.sh index add85a3..87e87b2 100755 --- a/tests/eu_txref_test.sh +++ b/tests/eu_txref_test.sh @@ -5,7 +5,7 @@ # sudo tests/eu_txref_test.sh set -u PY=${PY:-python3}; EU_VID=0x0bda; EU_PID=0xa81a; CH=6; FREQ=2437e6; SECS=5 -cleanup(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; } +cleanup(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT sudo rmmod rtl88x2eu_ohd 2>/dev/null diff --git a/tests/eu_usbmon_capture.sh b/tests/eu_usbmon_capture.sh index 98b0600..86455e4 100755 --- a/tests/eu_usbmon_capture.sh +++ b/tests/eu_usbmon_capture.sh @@ -12,7 +12,7 @@ cleanup(){ sudo pkill -9 -f "cat $MON" 2>/dev/null; } trap cleanup EXIT # Start fresh: unload driver so we capture a clean cold init. -sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null +sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null for i in $(ls /sys/class/net | grep -E "^wl"); do sudo ip link set "$i" down 2>/dev/null; done sudo rmmod rtl88x2eu_ohd 2>/dev/null; sleep 2 diff --git a/tests/eu_usbmon_devourer.sh b/tests/eu_usbmon_devourer.sh index 59319bf..aea1cbe 100755 --- a/tests/eu_usbmon_devourer.sh +++ b/tests/eu_usbmon_devourer.sh @@ -4,7 +4,7 @@ # sudo tests/eu_usbmon_devourer.sh set -u BUS=3; MON=/sys/kernel/debug/usb/usbmon/${BUS}u; RAW=/tmp/eu_dev_usbmon.txt -cleanup(){ sudo pkill -9 -f "cat $MON" 2>/dev/null; sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; } +cleanup(){ sudo pkill -9 -f "cat $MON" 2>/dev/null; sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT for i in $(ls /sys/class/net | grep -E "^wl"); do sudo ip link set "$i" down 2>/dev/null; done sudo rmmod rtl88x2eu_ohd 2>/dev/null; sleep 2 diff --git a/tests/hop_parity_check.sh b/tests/hop_parity_check.sh index bc83e7d..acb6b3f 100755 --- a/tests/hop_parity_check.sh +++ b/tests/hop_parity_check.sh @@ -1,20 +1,24 @@ #!/usr/bin/env bash -# Register-parity validation for the 40/80 MHz FastRetune path. +# Register-parity validation for the FastRetune path, any generation. # -# The 5 GHz bench power sags, so a clean on-air 40 MHz hop test isn't possible -# here. Instead we prove the fast 40/80 retune leaves the chip in the SAME -# channel/BW register state as the known-good full SetMonitorChannel: drive the -# chip to the same target channel two ways (full path, then fast path) with -# DEVOURER_DUMP_CANARY=1, and diff the post-channel-set register dumps. +# Prove the fast retune leaves the chip in the SAME channel/BW register state +# as the known-good full SetMonitorChannel: drive the chip to the same target +# channel two ways (full path, then fast path) with DEVOURER_DUMP_CANARY=1, +# and diff the post-channel-set register dumps. All three generations emit the +# same canary format (Jaguar1 RadioManagementModule::DumpCanary, Jaguar2 +# HalJaguar2::DumpCanary, Jaguar3 RadioManagementJaguar3::DumpCanary); the +# family — selected from TX_PID, override with FAMILY= — only picks the +# expected-to-differ exclusion lists: +# jaguar1: TX-power / thermal registers (the fast path deliberately skips +# the per-rate TX-power loop + pwrtrk) and IQK measurement outputs. +# jaguar2/jaguar3: none — their channel set has no TX-power stage and the +# canary excludes live counters, so every dumped register must +# match up to the full-vs-full control. # -# Channel/BW registers (RF 0x18 = center channel + BW + MOD_AG, BB 0x860 fc_area, -# 0x8ac rRFMOD/SubChnl, 0x8c4 ADC clk, ...) MUST match. TX-power / thermal -# registers (0xc1c/0xe1c, 0xc20..0xc40, 0xe20..0xe40) are EXPECTED to differ — -# the fast path deliberately skips the per-rate TX-power loop and the pwrtrk -# tick (same as the 20 MHz fast path), so those keep the prior channel's values. -# -# ./tests/hop_parity_check.sh # 5 GHz 40 MHz, ch36->ch44 +# ./tests/hop_parity_check.sh # 8812AU, 5 GHz 40 MHz, ch36->44 # INIT=36 TARGET=44 BW=40 OFFSET=1 ./tests/hop_parity_check.sh +# TX_PID=0xc812 INIT=1 TARGET=6 BW=20 ./tests/hop_parity_check.sh # 8822CU +# TX_PID=0xa81a NB_BW=5 INIT=1 TARGET=6 BW=20 ./tests/hop_parity_check.sh set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -25,10 +29,22 @@ INIT="${INIT:-36}" TARGET="${TARGET:-44}" BW="${BW:-40}" OFFSET="${OFFSET:-1}" # 1 = HT40+/LOWER primary +NB_BW="${NB_BW:-}" # 5|10 = Jaguar3 narrowband re-clock OUT="${OUT:-/tmp/devourer-hop-parity}" mkdir -p "$OUT" -cleanup() { pkill -x WiFiDriverTxDemo 2>/dev/null || true; } +# Chip family from the PID (override with FAMILY=jaguar1|jaguar2|jaguar3). +if [ -z "${FAMILY:-}" ]; then + case "$TX_PID" in + 0xc812|0xc82c|0xc82e|0xa81a|0x881a|0x881b|0x881c|0xe822|0xa82a) + FAMILY=jaguar3 ;; + 0xb82c|0x012d|0x012e|0xc811) FAMILY=jaguar2 ;; + *) FAMILY=jaguar1 ;; + esac +fi +echo "family: $FAMILY (TX_PID=$TX_PID)" + +cleanup() { pkill -x WiFiDriverTxDem 2>/dev/null || true; } trap cleanup EXIT INT TERM echo "== building WiFiDriverTxDemo ==" @@ -39,12 +55,14 @@ cmake --build "$ROOT/build" -j --target WiFiDriverTxDemo >/dev/null # canary block is TARGET's. DUMP_CANARY logs to the demo's stdout/err. run_path() { # $1 = HOP_FAST value, $2 = output file sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_CHANNEL="$INIT" \ + ${TX_VID:+DEVOURER_VID="$TX_VID"} \ DEVOURER_DUMP_CANARY=1 \ + ${NB_BW:+DEVOURER_NB_BW="$NB_BW"} \ DEVOURER_HOP_CHANNELS="$INIT,$TARGET" DEVOURER_HOP_BW="$BW" \ DEVOURER_HOP_OFFSET="$OFFSET" \ DEVOURER_HOP_DWELL_FRAMES=3 DEVOURER_HOP_ROUNDS=1 \ DEVOURER_HOP_FAST="$1" \ - timeout 40 "$ROOT/build/WiFiDriverTxDemo" >"$2" 2>&1 || true + timeout 60 "$ROOT/build/WiFiDriverTxDemo" >"$2" 2>&1 || true } # Extract the LAST canary block (the TARGET channel) as "REG ADDR = VALUE" lines. @@ -73,13 +91,26 @@ done # Addresses that differ between the two full runs = inherently run-variant. addrs() { grep -E '^[<>]' | grep -oE "0x[0-9a-fA-F]+ =" | grep -oE "0x[0-9a-fA-F]+" | sort -u; } VARIANT="$(diff "$OUT/full.regs" "$OUT/full2.regs" | addrs || true)" -# Registers the fast path intentionally does NOT reprogram (per-rate TX-power -# loop + tx-power training + thermal swing — same stages the 20 MHz path skips). -TXPWR='0x8b0 0xc1c 0xe1c 0xc20 0xc24 0xc28 0xc2c 0xc30 0xc34 0xc38 0xc3c 0xc40 0xe20 0xe24 0xe28 0xe2c 0xe30 0xe34 0xe38 0xe3c 0xe40' -# IQK / RxIQC measurement-output registers (set by the init IQK, not by the -# channel set). They carry run-to-run measurement jitter — the full-vs-full -# control catches them probabilistically, so list them too for robustness. -IQK='0xc10 0xc14 0xe10 0xe14 0xc90 0xc94 0xe90 0xe94' +# Family exclusion lists. Jaguar1: registers the fast path intentionally does +# NOT reprogram (per-rate TX-power loop + tx-power training + thermal swing) +# plus the IQK / RxIQC measurement outputs (init-IQK jitter the full-vs-full +# control only catches probabilistically). Jaguar2/Jaguar3 channel sets have +# no TX-power stage and their canaries exclude live counters — nothing is +# expected to differ beyond the control. +if [ "$FAMILY" = jaguar1 ]; then + TXPWR='0x8b0 0xc1c 0xe1c 0xc20 0xc24 0xc28 0xc2c 0xc30 0xc34 0xc38 0xc3c 0xc40 0xe20 0xe24 0xe28 0xe2c 0xe30 0xe34 0xe38 0xe3c 0xe40' + IQK='0xc10 0xc14 0xe10 0xe14 0xc90 0xc94 0xe90 0xe94' + # Live chip state in the J1 canary, not channel/BW config: RF 0x00's low + # bits carry the AGC-driven mode/gain word (samples differently depending + # on the instant of the read) and RF 0x42 is the thermal meter. Like the + # IQK outputs, the full-vs-full control catches them only probabilistically + # — observed flipping between runs — so exclude them statically. + LIVE='0x00 0x42' +else + TXPWR='' + IQK='' + LIVE='' +fi echo; echo "== fast-vs-full diff (< full > fast) ==" diff "$OUT/full.regs" "$OUT/fast.regs" || true @@ -89,9 +120,9 @@ echo; echo "run-variant (control): ${VARIANT:-none}" echo "tx-power (fast skips): $TXPWR" # A real channel/BW break = a fast-vs-full diff address that's neither -# run-variant nor an intentionally-skipped tx-power register. -EXCLUDE="$(printf '%s\n%s\n%s\n' "$VARIANT" "$(echo $TXPWR | tr ' ' '\n')" \ - "$(echo $IQK | tr ' ' '\n')" | sort -u)" +# run-variant, an intentionally-skipped tx-power register, nor live chip state. +EXCLUDE="$(printf '%s\n%s\n%s\n%s\n' "$VARIANT" "$(echo $TXPWR | tr ' ' '\n')" \ + "$(echo $IQK | tr ' ' '\n')" "$(echo $LIVE | tr ' ' '\n')" | sort -u)" BREAKS="" for a in $FAST_DIFF; do echo "$EXCLUDE" | grep -qx "$a" || BREAKS="$BREAKS $a" diff --git a/tests/hop_rx_probe.py b/tests/hop_rx_probe.py index 6774415..09b5a12 100755 --- a/tests/hop_rx_probe.py +++ b/tests/hop_rx_probe.py @@ -187,6 +187,21 @@ def analyse(args, channels, rate, center, total, overflows) -> int: peak_snr.append(float(np.max(pdb[ci]) - floor_db[ci])) strong_frac.append(float(np.mean(strong[ci]))) + # Per-bin integrated power export (--bin-power-csv): the near-field TX + # level per channel = median power over that channel's STRONG slices (its + # dwells), falling back to the peak when a bin never crosses the strong + # threshold. This is the wideband ground truth tests/sounding_map.py + # rank-correlates against the sounding sweep's per-bin RSSI. + if args.bin_power_csv: + with open(args.bin_power_csv, "w") as f: + f.write("ch,power_db,peak_snr_db,strong_frac,bursts\n") + for ci, ch in enumerate(channels): + sel = pdb[ci][strong[ci]] + p = float(np.median(sel)) if sel.size else float(np.max(pdb[ci])) + f.write(f"{ch},{p:.2f},{peak_snr[ci]:.2f}," + f"{strong_frac[ci]:.4f},{counts[ci]}\n") + sys.stderr.write(f"hop_rx_probe: per-bin power -> {args.bin_power_csv}\n") + # Dominant strong channel over coarse time windows -> hop sequence. win_slices = max(1, int((args.seq_window_ms * 1e-3) / slice_dt)) nwin = nslices // win_slices @@ -299,6 +314,9 @@ def main() -> int: help="time granularity for the dominant-channel sequence") ap.add_argument("--expect-rounds", type=int, default=0, help="expected full hop cycles (0 = just require order+presence)") + ap.add_argument("--bin-power-csv", default="", + help="write per-channel integrated power (CSV) — the SDR " + "ground truth for tests/sounding_map.py --sdr-csv") ap.add_argument("--analyse-only", action="store_true", help="skip capture, analyse an existing --raw file") args = ap.parse_args() diff --git a/tests/jaguar2_8821c_bw_sdr.sh b/tests/jaguar2_8821c_bw_sdr.sh index 9f6e9cf..223d232 100755 --- a/tests/jaguar2_8821c_bw_sdr.sh +++ b/tests/jaguar2_8821c_bw_sdr.sh @@ -18,7 +18,7 @@ PWR=0x2d OUT=/tmp/j2_8821c_bw_sdr rm -rf "$OUT"; mkdir -p "$OUT" -cleanup() { sudo pkill -x WiFiDriverTxDemo 2>/dev/null; sudo modprobe rtw88_8821cu 2>/dev/null; } +cleanup() { sudo pkill -x WiFiDriverTxDem 2>/dev/null; sudo modprobe rtw88_8821cu 2>/dev/null; } trap cleanup EXIT probe() { # label freq @@ -39,10 +39,10 @@ run_bw() { # bw offset ch freq label ./build/WiFiDriverTxDemo >"$OUT/dev_${LBL}.log" 2>&1 & sleep 10 # power-on -> DLFW -> init -> TX flooding if grep -q 'ready for TX' "$OUT/dev_${LBL}.log"; then break; fi - echo " (try $try: bring-up failed, retrying)"; sudo pkill -x WiFiDriverTxDemo 2>/dev/null; wait 2>/dev/null + echo " (try $try: bring-up failed, retrying)"; sudo pkill -x WiFiDriverTxDem 2>/dev/null; wait 2>/dev/null done probe "$LBL" "$FREQ" - sudo pkill -x WiFiDriverTxDemo 2>/dev/null; wait 2>/dev/null; sleep 1 + sudo pkill -x WiFiDriverTxDem 2>/dev/null; wait 2>/dev/null; sleep 1 } echo "[bw-sdr] baseline (no TX) @ 5755MHz" diff --git a/tests/jaguar2_bw_sdr.sh b/tests/jaguar2_bw_sdr.sh index 2e29ba7..c26b1ad 100644 --- a/tests/jaguar2_bw_sdr.sh +++ b/tests/jaguar2_bw_sdr.sh @@ -14,7 +14,7 @@ NS=4e6 OUT=/tmp/j2_bw_sdr rm -rf "$OUT"; mkdir -p "$OUT" -cleanup() { sudo pkill -x WiFiDriverTxDemo 2>/dev/null; } +cleanup() { sudo pkill -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT recover() { @@ -42,7 +42,7 @@ run_bw() { # bw offset ch freq label ./build/WiFiDriverTxDemo >"$OUT/dev_${LBL}.log" 2>&1 & sleep 12 # power-on -> DLFW -> init -> TX flooding probe "$LBL" "$FREQ" - sudo pkill -x WiFiDriverTxDemo 2>/dev/null; wait 2>/dev/null; sleep 1 + sudo pkill -x WiFiDriverTxDem 2>/dev/null; wait 2>/dev/null; sleep 1 } run_bw "" "" 149 5745e6 bw20 # 20 MHz (no HOP_BW) diff --git a/tests/jaguar2_tx_bw40.sh b/tests/jaguar2_tx_bw40.sh index 7dc9765..9903652 100644 --- a/tests/jaguar2_tx_bw40.sh +++ b/tests/jaguar2_tx_bw40.sh @@ -12,7 +12,7 @@ SNIFF_IF=${SNIFF_IF:-wlp4s0u2u4} # 8822cu (supports 5G + HT40) CANON=57:42:75:05:d6:00 DUR=${DUR:-16} -cleanup() { sudo pkill -f "tcpdump -i $SNIFF_IF" 2>/dev/null; sudo pkill -x WiFiDriverTxDemo 2>/dev/null; } +cleanup() { sudo pkill -f "tcpdump -i $SNIFF_IF" 2>/dev/null; sudo pkill -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT echo "[bw40] recovering T3U first" diff --git a/tests/jaguar2_tx_onair.sh b/tests/jaguar2_tx_onair.sh index 895aeed..dd0ffe3 100755 --- a/tests/jaguar2_tx_onair.sh +++ b/tests/jaguar2_tx_onair.sh @@ -16,7 +16,7 @@ DUR=${DUR:-10} cleanup() { sudo pkill -f "tcpdump -i $SNIFF_IF" 2>/dev/null - sudo pkill -x WiFiDriverTxDemo 2>/dev/null + sudo pkill -x WiFiDriverTxDem 2>/dev/null } trap cleanup EXIT diff --git a/tests/jaguar3_rx_usbmon.sh b/tests/jaguar3_rx_usbmon.sh index 8406d3d..c33c07a 100755 --- a/tests/jaguar3_rx_usbmon.sh +++ b/tests/jaguar3_rx_usbmon.sh @@ -20,7 +20,7 @@ MON=/sys/kernel/debug/usb/usbmon/${RXBUS}u echo "[harness] TX 8812AU=$TXS RX c812=$RXS (bus $RXBUS dev $RXDEV)" cleanup() { - sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null + sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null sudo pkill -9 -x WiFiDriverDemo 2>/dev/null sudo pkill -9 -f "cat $MON" 2>/dev/null echo "$TXS:1.0" | sudo tee /sys/bus/usb/drivers/rtw88_8812au/bind >/dev/null 2>&1 diff --git a/tests/link_probe.sh b/tests/link_probe.sh index 6537c0c..63f1123 100755 --- a/tests/link_probe.sh +++ b/tests/link_probe.sh @@ -63,7 +63,7 @@ TS='import sys,time for l in sys.stdin: sys.stdout.write(f"{time.time():.3f} {l}"); sys.stdout.flush()' -cleanup() { for c in WiFiDriverTxDemo WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done; } +cleanup() { for c in WiFiDriverTxDem WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done; } trap cleanup EXIT INT TERM echo "== building ==" diff --git a/tests/pseudo_puncture_detect.sh b/tests/pseudo_puncture_detect.sh index 0a1b9b0..a073632 100755 --- a/tests/pseudo_puncture_detect.sh +++ b/tests/pseudo_puncture_detect.sh @@ -46,11 +46,11 @@ mkdir -p "$LOGDIR" JAM_PID="" cleanup() { [ -n "$JAM_PID" ] && kill -INT "$JAM_PID" 2>/dev/null - pkill -INT -x WiFiDriverTxDemo 2>/dev/null + pkill -INT -x WiFiDriverTxDem 2>/dev/null pkill -INT -x WiFiDriverDemo 2>/dev/null pkill -INT -f sdr_interferer 2>/dev/null sleep 1 - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null pkill -KILL -x WiFiDriverDemo 2>/dev/null pkill -KILL -f sdr_interferer 2>/dev/null } diff --git a/tests/pseudo_puncture_onair.sh b/tests/pseudo_puncture_onair.sh index e761d95..bf6d577 100755 --- a/tests/pseudo_puncture_onair.sh +++ b/tests/pseudo_puncture_onair.sh @@ -45,11 +45,11 @@ rm -rf "$LOGDIR"; mkdir -p "$LOGDIR" JAM_PID="" cleanup() { [ -n "$JAM_PID" ] && kill -INT "$JAM_PID" 2>/dev/null - pkill -INT -x WiFiDriverTxDemo 2>/dev/null + pkill -INT -x WiFiDriverTxDem 2>/dev/null pkill -INT -x WiFiDriverDemo 2>/dev/null pkill -INT -f sdr_interferer 2>/dev/null sleep 1 - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null pkill -KILL -x WiFiDriverDemo 2>/dev/null pkill -KILL -f sdr_interferer 2>/dev/null } diff --git a/tests/rail_check.sh b/tests/rail_check.sh index adc5597..c07a5ba 100755 --- a/tests/rail_check.sh +++ b/tests/rail_check.sh @@ -26,7 +26,7 @@ RAIL_MIN_DUTY=${RAIL_MIN_DUTY:-50} # absolute 5 GHz duty floor (%), tune per RAIL_MIN_RATIO=${RAIL_MIN_RATIO:-0.6} # min 5GHz/2.4GHz duty ratio on a healthy rail TXBIN=${TXBIN:-build/WiFiDriverTxDemo} -cleanup(){ sudo pkill -9 -x WiFiDriverTxDe 2>/dev/null; } +cleanup(){ sudo pkill -9 -x WiFiDriverTxDem 2>/dev/null; } trap cleanup EXIT # free the control from any bound kernel driver diff --git a/tests/rendezvous.sh b/tests/rendezvous.sh index 8c4b3da..8e6497f 100755 --- a/tests/rendezvous.sh +++ b/tests/rendezvous.sh @@ -42,7 +42,7 @@ done [ -n "$BEACON_PID" ] && [ -n "$SCANNER_PID" ] || { echo "need --beacon-pid and --scanner-pid" >&2; exit 2; } -cleanup() { for c in WiFiDriverTxDemo WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done; } +cleanup() { for c in WiFiDriverTxDem WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done; } trap cleanup EXIT INT TERM echo "== building ==" diff --git a/tests/run_hop_validation.sh b/tests/run_hop_validation.sh index 5e3f6f0..78ea39d 100755 --- a/tests/run_hop_validation.sh +++ b/tests/run_hop_validation.sh @@ -45,7 +45,7 @@ TX_LOG="$OUT/tx.log" RX_LOG="$OUT/rx.log" cleanup() { - pkill -x WiFiDriverTxDemo 2>/dev/null || true + pkill -x WiFiDriverTxDem 2>/dev/null || true pkill -f "tests/hop_rx_probe.py" 2>/dev/null || true } trap cleanup EXIT INT TERM diff --git a/tests/rx80_narrow_tx_probe.sh b/tests/rx80_narrow_tx_probe.sh index 0cc0bfb..962c3a7 100755 --- a/tests/rx80_narrow_tx_probe.sh +++ b/tests/rx80_narrow_tx_probe.sh @@ -30,10 +30,10 @@ LOGDIR=/tmp/devourer-rx80-narrow-tx rm -rf "$LOGDIR"; mkdir -p "$LOGDIR" cleanup() { - pkill -INT -x WiFiDriverTxDemo 2>/dev/null + pkill -INT -x WiFiDriverTxDem 2>/dev/null pkill -INT -x WiFiDriverDemo 2>/dev/null sleep 1 - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null pkill -KILL -x WiFiDriverDemo 2>/dev/null } trap cleanup EXIT INT TERM diff --git a/tests/rx_energy_probe.sh b/tests/rx_energy_probe.sh index c1dc089..9061078 100755 --- a/tests/rx_energy_probe.sh +++ b/tests/rx_energy_probe.sh @@ -45,7 +45,7 @@ done [ -n "$SENSOR_PID" ] && [ -n "$TONE_PID" ] || { echo "need --sensor-pid and --tone-pid" >&2; exit 2; } -cleanup() { for c in WiFiDriverDemo WiFiDriverTxDemo; do pkill -x "$c" 2>/dev/null || true; done; } +cleanup() { for c in WiFiDriverDemo WiFiDriverTxDem; do pkill -x "$c" 2>/dev/null || true; done; } trap cleanup EXIT INT TERM echo "== building ==" diff --git a/tests/rx_spectrum_sweep.sh b/tests/rx_spectrum_sweep.sh index c215ca2..0aaff0d 100755 --- a/tests/rx_spectrum_sweep.sh +++ b/tests/rx_spectrum_sweep.sh @@ -49,7 +49,7 @@ while [ $# -gt 0 ]; do done [ -n "$SENSOR_PID" ] || { echo "need --sensor-pid" >&2; exit 2; } -cleanup() { for c in WiFiDriverDemo WiFiDriverTxDemo; do pkill -x "$c" 2>/dev/null || true; done; } +cleanup() { for c in WiFiDriverDemo WiFiDriverTxDem; do pkill -x "$c" 2>/dev/null || true; done; } trap cleanup EXIT INT TERM echo "== building ==" diff --git a/tests/rx_tone_localize.sh b/tests/rx_tone_localize.sh index dd82f2b..2f4114c 100755 --- a/tests/rx_tone_localize.sh +++ b/tests/rx_tone_localize.sh @@ -53,7 +53,7 @@ done mkdir -p "$OUT" cleanup() { - for c in WiFiDriverTxDemo WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done + for c in WiFiDriverTxDem WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done wait 2>/dev/null || true } trap cleanup EXIT INT TERM diff --git a/tests/sounding_map.py b/tests/sounding_map.py new file mode 100755 index 0000000..2a5d8cb --- /dev/null +++ b/tests/sounding_map.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Coarse H(f) map from a two-ended sounding sweep (issue #149). + +Parses the RX side's per-dwell `ch=N ... frames=N +rssi_mean=.. rssi_max=.. snr_mean=.. evm_mean=..` lines (WiFiDriverDemo with +DEVOURER_RX_SWEEP + DEVOURER_RX_AGG_SA=canon, driven by tests/sounding_sweep.sh) +and renders the recovered per-bin link map. + +Headline metric: the per-bin **median of rssi_max** over live dwells. During a +dwell on bin k the prober is co-channel only ~1/nbins of the time; decoded +off-channel bleed drags rssi_mean down, but bleed decodes weaker — never +stronger — so the dwell's rssi_max is the bleed-robust probe level. rssi_mean / +snr / evm ride along as columns. + +Flags per bin: + NOTCH median rssi_max >= --notch-db below the across-bin median (frames>0) + DEAD zero probe frames in every dwell while other bins have hits + +--sdr-csv: rank-correlate the recovered map against a wideband SDR's per-bin +integrated power (tests/hop_rx_probe.py --bin-power-csv) — Spearman rho over +the common live bins. + +--expand "SPEC": print the channel list for a SweepSpec bin spec (the shell +orchestrator's helper; mirrors src/SweepSpec.h). +""" +from __future__ import annotations + +import argparse +import re +import statistics +import sys + +KV = re.compile(r"(\w+)=(-?\d+)") + + +def expand_spec(spec: str) -> list[int]: + """Mirror of devourer::parse_sweep_spec (src/SweepSpec.h; guarded there by + ctest sweep_spec_math): channels, channel ranges A-B/S, MHz ranges.""" + out: list[int] = [] + + def freq_to_chan(mhz: int) -> int: + if mhz == 2484: + return 14 + if 2412 <= mhz <= 2472: + return (mhz - 2407) // 5 + if 5000 <= mhz <= 5895: + return (mhz - 5000) // 5 + return 0 + + for tok in (spec or "").split(","): + tok = tok.strip() + if not tok: + continue + step = 1 + if "/" in tok: + tok, s = tok.split("/", 1) + try: + step = int(s, 0) + except ValueError: + step = 1 + step = max(1, step) + dash = tok.find("-", 1) + if dash < 0: + try: + ch = int(tok, 0) + except ValueError: + continue + if ch > 0: + out.append(ch) + continue + try: + a, b = int(tok[:dash], 0), int(tok[dash + 1:], 0) + except ValueError: + continue + if a <= 0 or b < a: + continue + if a >= 1000 and step < 5: + step = 5 + for v in range(a, b + 1, step): + ch = freq_to_chan(v) if a >= 1000 else v + if ch > 0: + out.append(ch) + return out + + +def chan_to_mhz(ch: int) -> int: + if ch == 14: + return 2484 + return 2407 + 5 * ch if ch <= 14 else 5000 + 5 * ch + + +def spearman(x: list[float], y: list[float]) -> float: + """Spearman rank correlation (no scipy dependency; average ranks on ties).""" + def ranks(v: list[float]) -> list[float]: + order = sorted(range(len(v)), key=lambda i: v[i]) + r = [0.0] * len(v) + i = 0 + while i < len(order): + j = i + while j + 1 < len(order) and v[order[j + 1]] == v[order[i]]: + j += 1 + avg = (i + j) / 2.0 + 1.0 + for k in range(i, j + 1): + r[order[k]] = avg + i = j + 1 + return r + + rx, ry = ranks(x), ranks(y) + mx, my = statistics.mean(rx), statistics.mean(ry) + num = sum((a - mx) * (b - my) for a, b in zip(rx, ry)) + dx = sum((a - mx) ** 2 for a in rx) ** 0.5 + dy = sum((b - my) ** 2 for b in ry) ** 0.5 + return num / (dx * dy) if dx > 0 and dy > 0 else 0.0 + + +def main() -> int: + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--log", help="RX sweep log (ch= lines)") + ap.add_argument("--expand", metavar="SPEC", + help="print the channel list for a SweepSpec and exit") + ap.add_argument("--notch-db", type=float, default=6.0, + help="dB below the across-bin median to flag NOTCH") + ap.add_argument("--min-dwells", type=int, default=2, + help="min live dwells for a bin to count as sounded") + ap.add_argument("--sdr-csv", + help="per-bin SDR power (hop_rx_probe.py --bin-power-csv) " + "to rank-correlate against") + args = ap.parse_args() + + if args.expand is not None: + print(" ".join(str(c) for c in expand_spec(args.expand))) + return 0 + if not args.log: + ap.error("--log (or --expand) required") + + # Per-bin dwell records; the first dwell per bin is dropped (partial dwell + # while the loops phase-align + AGC settle — same convention as + # rx_spectrum_sweep.py). + dwells: dict[int, list[dict[str, int]]] = {} + for line in open(args.log, errors="replace"): + if "" not in line or "ch=" not in line: + continue + kv = {k: int(v) for k, v in KV.findall(line)} + if "ch" not in kv or "frames" not in kv: + continue # no per-dwell frame stats (older binary) or steady-state emitter + dwells.setdefault(kv["ch"], []).append(kv) + dwells = {ch: d[1:] if len(d) > 1 else d for ch, d in dwells.items()} + if not dwells: + print("no sounding samples in log (need ch= lines with frames=)", + file=sys.stderr) + return 1 + + rows = [] + for ch in sorted(dwells): + d = dwells[ch] + live = [x for x in d if x.get("frames", 0) > 0] + rec = { + "ch": ch, + "dwells": len(d), + "live": len(live), + "hits": statistics.median([x["frames"] for x in live]) if live else 0, + "rssi_max": statistics.median([x["rssi_max"] for x in live]) if live else None, + "rssi_mean": statistics.median([x["rssi_mean"] for x in live]) if live else None, + "snr_mean": statistics.median([x["snr_mean"] for x in live]) if live else None, + "evm_mean": statistics.median([x["evm_mean"] for x in live + if x.get("evm_mean", 0) != 0]) if any( + x.get("evm_mean", 0) != 0 for x in live) else None, + } + rows.append(rec) + + sounded = [r for r in rows if r["live"] >= args.min_dwells] + levels = [r["rssi_max"] for r in sounded if r["rssi_max"] is not None] + med = statistics.median(levels) if levels else 0 + any_live = any(r["live"] for r in rows) + + print("=== sounding map (per-bin median over dwells; headline = rssi_max," + " bleed-robust) ===") + lo = min(levels) - 3 if levels else 0 + hi = max(levels) if levels else 1 + span = max(1, hi - lo) + width = 40 + for r in rows: + ch, mhz = r["ch"], chan_to_mhz(r["ch"]) + flag = "" + if r["live"] == 0: + flag = " DEAD" if any_live else "" + elif r["rssi_max"] is not None and levels and \ + r["rssi_max"] <= med - args.notch_db: + flag = " NOTCH" + def fmt(v): # medians come back int or float; render uniformly + return " -" if v is None else f"{v:>4g}" + if r["rssi_max"] is None: + bar = "" + else: + bar = "#" * max(1, round(width * (r["rssi_max"] - lo) / span)) + print(f" ch {ch:>3} ({mhz:>4} MHz) | {bar:<{width}} " + f"rssi_max={fmt(r['rssi_max'])} rssi_mean={fmt(r['rssi_mean'])} " + f"snr={fmt(r['snr_mean'])} evm={fmt(r['evm_mean'])} " + f"hits={fmt(r['hits'])} live={r['live']}/{r['dwells']}{flag}") + + n_dead = sum(1 for r in rows if r["live"] == 0 and any_live) + n_notch = sum(1 for r in rows if r["live"] > 0 and r["rssi_max"] is not None + and levels and r["rssi_max"] <= med - args.notch_db) + print(f"\nbins={len(rows)} sounded={len(sounded)} dead={n_dead} " + f"notched={n_notch} median_rssi_max={med}") + + if args.sdr_csv: + sdr = {} + with open(args.sdr_csv) as f: + next(f) # header + for ln in f: + parts = ln.strip().split(",") + if len(parts) >= 2: + sdr[int(parts[0])] = float(parts[1]) + common = [r for r in sounded if r["ch"] in sdr and r["rssi_max"] is not None] + vals = [float(r["rssi_max"]) for r in common] + spread = (max(vals) - min(vals)) if vals else 0.0 + if len(common) < 3: + print(f"sdr-crosscheck: only {len(common)} common live bins — " + "need >=3 for a rank correlation") + elif spread <= 2.0: + # A flat map (bench near-field, no frequency selectivity) has no + # rank structure to correlate — rho on ties+noise is meaningless. + print(f"sdr-crosscheck: map is flat (rssi_max spread {spread:g}) " + "— nothing to rank-correlate; both ends see a flat channel") + else: + rho = spearman(vals, [sdr[r["ch"]] for r in common]) + verdict = "MATCH" if rho >= 0.5 else "WEAK" if rho > 0 else "MISMATCH" + print(f"sdr-crosscheck bins={len(common)} spearman_rho={rho:.2f} " + f"-> {verdict}") + + if not any_live: + print("\nSOUNDING: FAIL — no probe frames on any bin (prober down, " + "wrong SA filter, or ends on different bands?)") + return 1 + print("\nSOUNDING: map recovered.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/sounding_sweep.sh b/tests/sounding_sweep.sh new file mode 100755 index 0000000..1d19cba --- /dev/null +++ b/tests/sounding_sweep.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Active two-ended sounding sweep (issue #149) — recover a coarse H(f) of +# the actual TX->RX path. The prober hops fixed-rate probe beacons (the +# canonical SA) across the bin list via FastRetune; the sounder sweeps the same +# bins with per-dwell probe-frame RSSI/EVM aggregation (DEVOURER_RX_AGG_SA). +# +# Synchronization is asymmetric-duty, no control channel: the TX cycles all +# bins fast, the RX dwells >= ~2 full TX cycles per bin, so every RX dwell +# overlaps at least one TX visit to its bin. A faded bin shows up as low +# rssi_max / few hits; a dead bin catches nothing. +# +# sudo tests/sounding_sweep.sh --tx-pid 0x8812 --rx-pid 0xc812 --bins 1,6,11 +# sudo tests/sounding_sweep.sh --tx-pid 0xc812 --rx-pid 0xa81a \ +# --bins 1-13/1 --nb-bw 5 # 5 MHz bins, Jaguar3 both ends +# sudo tests/sounding_sweep.sh ... --sdr # + B210 wideband cross-check +# +# Bins use the SweepSpec grammar ("1,6,11", "36-48/4", "5170-5250/5") — one +# spec drives both sides. All bins must be intra-band (FastRetune's fast path; +# a cross-band list still works but pays full retunes at the band edge). +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +DEMO_TX="$ROOT/build/WiFiDriverTxDemo" +DEMO_RX="$ROOT/build/WiFiDriverDemo" + +TX_VID=0x0bda TX_PID="" RX_VID=0x0bda RX_PID="" +BINS="1,6,11" ROUNDS=4 RATE=MCS0 DWELL_FRAMES=20 GAP_US=2000 NB_BW="" +SDR=0 SDR_GAIN=45 OUT=/tmp/devourer-sounding + +while [ $# -gt 0 ]; do + case "$1" in + --tx-vid) TX_VID="$2"; shift 2 ;; + --tx-pid) TX_PID="$2"; shift 2 ;; + --rx-vid) RX_VID="$2"; shift 2 ;; + --rx-pid) RX_PID="$2"; shift 2 ;; + --bins) BINS="$2"; shift 2 ;; + --rounds) ROUNDS="$2"; shift 2 ;; + --rate) RATE="$2"; shift 2 ;; + --dwell-frames) DWELL_FRAMES="$2"; shift 2 ;; + --gap-us) GAP_US="$2"; shift 2 ;; + --nb-bw) NB_BW="$2"; shift 2 ;; + --sdr) SDR=1; shift ;; + --sdr-gain) SDR_GAIN="$2"; shift 2 ;; + --outdir) OUT="$2"; shift 2 ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done +[ -n "$TX_PID" ] && [ -n "$RX_PID" ] || { echo "need --tx-pid and --rx-pid" >&2; exit 2; } + +cleanup() { for c in WiFiDriverTxDem WiFiDriverDemo; do pkill -x "$c" 2>/dev/null || true; done; } +trap cleanup EXIT INT TERM + +echo "== building ==" +cmake --build "$ROOT/build" -j >/dev/null +mkdir -p "$OUT"; rm -f "$OUT"/sweep.log "$OUT"/sdr.csv +cleanup; sleep 1 + +# Expand the bin spec (sounding_map.py mirrors src/SweepSpec.h) and size the +# duty cycle: RX dwell >= 2x the TX full-cycle time so every dwell overlaps a +# TX visit; ~12 ms/hop retune margin, ~0.6 ms airtime per probe frame. +CHLIST="$(python3 "$HERE/sounding_map.py" --expand "$BINS")" +NBINS="$(wc -w <<< "$CHLIST")" +[ "$NBINS" -ge 2 ] || { echo "bin spec '$BINS' expands to <2 bins" >&2; exit 2; } +FIRST_CH="$(awk '{print $1}' <<< "$CHLIST")" +CYCLE_MS="$(( NBINS * (DWELL_FRAMES * (GAP_US + 600) / 1000 + 12) ))" +RX_DWELL="$(( 2 * CYCLE_MS ))" +[ "$RX_DWELL" -lt 400 ] && RX_DWELL=400 +RX_SECS="$(( (NBINS * ROUNDS * RX_DWELL) / 1000 + 20 ))" +echo "bins: $CHLIST" +echo "duty: TX cycle ~${CYCLE_MS} ms -> RX dwell ${RX_DWELL} ms x $ROUNDS rounds (~${RX_SECS}s)" + +echo "== prober: $TX_PID hopping $NBINS bins at $RATE (dwell $DWELL_FRAMES frames) ==" +# DEVOURER_TX_MAXFAIL=0: a retune can wedge one in-flight frame into a +# bulk-OUT timeout (hardware-observed on the 8812EU at 5 MHz NB, on BOTH the +# full and fast paths — the BB reset lands while a frame is draining). A +# scattered lost probe frame is noise to the map; the prober must ride it out +# rather than take the consecutive-failure bail mid-sweep. +timeout -sINT "$((RX_SECS + 30))" env DEVOURER_VID="$TX_VID" DEVOURER_PID="$TX_PID" \ + DEVOURER_CHANNEL="$FIRST_CH" DEVOURER_TX_RATE="$RATE" \ + DEVOURER_TX_MAXFAIL=0 \ + ${NB_BW:+DEVOURER_NB_BW="$NB_BW"} \ + DEVOURER_HOP_CHANNELS="$BINS" DEVOURER_HOP_DWELL_FRAMES="$DWELL_FRAMES" \ + DEVOURER_HOP_FAST=1 DEVOURER_TX_GAP_US="$GAP_US" \ + "$DEMO_TX" >"$OUT/tx.log" 2>&1 & +for _ in $(seq 60); do grep -q "devourer-hop" "$OUT/tx.log" && break; sleep 0.5; done +grep -q "devourer-hop" "$OUT/tx.log" && echo " prober hopping" \ + || { echo " FAIL: prober never hopped (see $OUT/tx.log)"; exit 1; } + +if [ "$SDR" = 1 ]; then + # Wideband ground truth: capture while both ends run, then export per-bin + # integrated power for the rank-correlation cross-check. Center on the mean + # bin frequency (python does the math). + SDR_SECS="$(( RX_SECS < 20 ? RX_SECS : 20 ))" + CENTER="$(python3 - "$CHLIST" <<'PY' +import sys +chs = [int(c) for c in sys.argv[1].split()] +f = [2484 if c == 14 else (2407 + 5*c if c <= 14 else 5000 + 5*c) for c in chs] +print(f"{(min(f)+max(f))/2*1e6:.0f}") +PY +)" + echo "== SDR: B210 wideband capture (${SDR_SECS}s, center ${CENTER%??????}.x MHz) ==" + python3 "$HERE/hop_rx_probe.py" --channels "$(tr ' ' ',' <<< "$CHLIST")" \ + --center "$CENTER" --gain "$SDR_GAIN" --duration "$SDR_SECS" \ + --bin-power-csv "$OUT/sdr.csv" >"$OUT/sdr.log" 2>&1 & + SDR_PID=$! +fi + +echo "== sounder: $RX_PID sweeping (SA filter = canonical probe) ==" +timeout -sINT "$RX_SECS" env DEVOURER_VID="$RX_VID" DEVOURER_PID="$RX_PID" \ + DEVOURER_RX_SWEEP="$BINS" DEVOURER_RX_SWEEP_DWELL_MS="$RX_DWELL" \ + DEVOURER_RX_AGG_SA=canon \ + ${NB_BW:+DEVOURER_NB_BW="$NB_BW"} \ + "$DEMO_RX" 2>/dev/null | grep --line-buffered "devourer-energy" \ + | tee "$OUT/sweep.log" || true + +[ "$SDR" = 1 ] && wait "${SDR_PID:-}" 2>/dev/null +cleanup + +echo; echo "== recovered H(f) map ==" +python3 "$HERE/sounding_map.py" --log "$OUT/sweep.log" \ + ${SDR:+$( [ -s "$OUT/sdr.csv" ] && echo --sdr-csv "$OUT/sdr.csv" )} +rc=$? +echo "logs: $OUT/{tx,sweep}.log$( [ "$SDR" = 1 ] && echo " sdr.{log,csv}" )" +exit $rc diff --git a/tests/sweep_spec_selftest.cpp b/tests/sweep_spec_selftest.cpp new file mode 100644 index 0000000..2854634 --- /dev/null +++ b/tests/sweep_spec_selftest.cpp @@ -0,0 +1,61 @@ +/* Headless guard for the sweep/hop bin-list grammar (src/SweepSpec.h) — the + * channel-list / channel-range / MHz-range parsing behind DEVOURER_RX_SWEEP + * and DEVOURER_HOP_CHANNELS, so a grammar regression fails `ctest` instead of + * only surfacing on a radio. Prints the failing spec and exits nonzero. */ +#include +#include + +#include "SweepSpec.h" + +static int g_fail = 0; + +static void expect(const char *spec, const std::vector &want) { + const std::vector got = devourer::parse_sweep_spec(spec); + if (got == want) + return; + ++g_fail; + std::printf("FAIL: spec \"%s\" -> [", spec ? spec : "(null)"); + for (size_t i = 0; i < got.size(); i++) + std::printf("%s%d", i ? "," : "", got[i]); + std::printf("] want ["); + for (size_t i = 0; i < want.size(); i++) + std::printf("%s%d", i ? "," : "", want[i]); + std::printf("]\n"); +} + +int main() { + /* Plain channel lists (the pre-existing grammar). */ + expect("1,6,11", {1, 6, 11}); + expect("36", {36}); + expect("", {}); + expect(nullptr, {}); + expect(",,6,", {6}); + + /* Channel ranges. */ + expect("1-13", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}); + expect("36-48/4", {36, 40, 44, 48}); + expect("1-11/5", {1, 6, 11}); + expect("100-100", {100}); + + /* MHz ranges (>= 1000 = frequency; issue #149's "5170-5250/5" example: + * ch34..ch50, one bin per 5 MHz grid step). */ + expect("5170-5190/5", {34, 35, 36, 37, 38}); + expect("2412-2462/25", {1, 6, 11}); + expect("2484-2484", {14}); + expect("5180-5200/20", {36, 40}); + + /* Mixed lists + junk tolerance. */ + expect("1,36-40/4,5180-5180", {1, 36, 40, 36}); + expect("0,-3,abc", {}); + expect("2400-2410/5", {}); /* below the 2.4 GHz channel grid -> dropped */ + expect("5170-5250/0", {34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50}); /* MHz step clamps to the 5 MHz grid */ + expect("5170-5180/1", {34, 35, 36}); /* sub-grid MHz step -> 5 */ + + if (g_fail) { + std::printf("sweep_spec_selftest: %d failure(s)\n", g_fail); + return 1; + } + std::printf("sweep_spec_selftest: all checks passed\n"); + return 0; +} diff --git a/tests/thermal_hwcheck.sh b/tests/thermal_hwcheck.sh index 00697ba..452ed0d 100755 --- a/tests/thermal_hwcheck.sh +++ b/tests/thermal_hwcheck.sh @@ -23,7 +23,7 @@ cleanup() { kill -KILL "$CHILD_PID" 2>/dev/null fi # Backstop: reap any stray demo by exact comm name. - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null } trap cleanup EXIT INT TERM diff --git a/tests/tone_mask_rx_sanity.sh b/tests/tone_mask_rx_sanity.sh index dff4acb..bd87d79 100755 --- a/tests/tone_mask_rx_sanity.sh +++ b/tests/tone_mask_rx_sanity.sh @@ -26,10 +26,10 @@ LOGDIR=/tmp/devourer-tonemask-sanity rm -rf "$LOGDIR"; mkdir -p "$LOGDIR" cleanup() { - pkill -INT -x WiFiDriverTxDemo 2>/dev/null + pkill -INT -x WiFiDriverTxDem 2>/dev/null pkill -INT -x WiFiDriverDemo 2>/dev/null sleep 1 - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null pkill -KILL -x WiFiDriverDemo 2>/dev/null } trap cleanup EXIT INT TERM diff --git a/tests/validate_jaguar3_physts.sh b/tests/validate_jaguar3_physts.sh index d7cdb94..d45cceb 100755 --- a/tests/validate_jaguar3_physts.sh +++ b/tests/validate_jaguar3_physts.sh @@ -28,9 +28,9 @@ RX_PIDS=("${RX_PIDS[@]:-0xc812 0xa81a}") mkdir -p "$OUT" cleanup() { - # NB: the kernel truncates comm to 15 chars, so `pkill -x WiFiDriverTxDemo` + # NB: the kernel truncates comm to 15 chars, so `pkill -x WiFiDriverTxDem` # (16 chars) never matches — use the truncated form. - pkill -x WiFiDriverTxDe 2>/dev/null || true + pkill -x WiFiDriverTxDem 2>/dev/null || true pkill -x WiFiDriverDemo 2>/dev/null || true } trap cleanup EXIT INT TERM diff --git a/tests/wide_rx_penalty_sweep.sh b/tests/wide_rx_penalty_sweep.sh index 89d7867..3763d5f 100755 --- a/tests/wide_rx_penalty_sweep.sh +++ b/tests/wide_rx_penalty_sweep.sh @@ -43,11 +43,11 @@ rm -rf "$LOGDIR"; mkdir -p "$LOGDIR" JAM_PID="" cleanup() { [ -n "$JAM_PID" ] && kill -INT "$JAM_PID" 2>/dev/null - pkill -INT -x WiFiDriverTxDemo 2>/dev/null + pkill -INT -x WiFiDriverTxDem 2>/dev/null pkill -INT -x WiFiDriverDemo 2>/dev/null pkill -INT -f sdr_interferer 2>/dev/null sleep 1 - pkill -KILL -x WiFiDriverTxDemo 2>/dev/null + pkill -KILL -x WiFiDriverTxDem 2>/dev/null pkill -KILL -x WiFiDriverDemo 2>/dev/null pkill -KILL -f sdr_interferer 2>/dev/null } diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 60070d8..f260396 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -28,7 +28,9 @@ #endif #include "BfReportDetect.h" +#include "ChannelFreq.h" #include "RxPacket.h" +#include "SweepSpec.h" #if defined(DEVOURER_HAVE_JAGUAR1) #include "jaguar1/RtlJaguarDevice.h" #endif @@ -68,14 +70,6 @@ static long long ms_since_start() { .count(); } -/* Wi-Fi channel number -> center frequency (MHz), for the radiotap CHANNEL - * field that drives the library's per-packet retune (DEVOURER_HOP_RADIOTAP). */ -static uint16_t chan_to_freq(int ch) { - if (ch == 14) return 2484; - if (ch <= 14) return static_cast(2407 + 5 * ch); - return static_cast(5000 + 5 * ch); -} - /* Build a radiotap header carrying CHANNEL (freq) + TX_FLAGS, then append the * 802.11 body. The library reads the CHANNEL field and FastRetunes per packet. */ static std::vector build_frame_with_channel(uint16_t freq, @@ -682,22 +676,10 @@ int main(int argc, char **argv) { * exercises the radiotap-driven path. */ const char *hop_rt_env = std::getenv("DEVOURER_HOP_RADIOTAP"); const bool hop_radiotap = hop_rt_env != nullptr && hop_rt_env[0] != '\0'; - if (const char *e = std::getenv("DEVOURER_HOP_CHANNELS")) { - 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()) { - int ch = std::atoi(tok.c_str()); - if (ch > 0) hop_channels.push_back(ch); - } - if (comma == std::string::npos) break; - pos = comma + 1; - } - } + /* SweepSpec grammar: channels ("1,6,11"), channel ranges ("36-48/4"), or + * MHz ranges ("5170-5250/5") — the same bin-list language as + * DEVOURER_RX_SWEEP, so a sounding pair takes one spec for both sides. */ + hop_channels = devourer::parse_sweep_spec(std::getenv("DEVOURER_HOP_CHANNELS")); if (!hop_channels.empty()) { if (const char *e = std::getenv("DEVOURER_HOP_DWELL_FRAMES")) { hop_dwell = std::strtol(e, nullptr, 0); @@ -909,16 +891,19 @@ int main(int argc, char **argv) { if (hop_radiotap) { /* Library retunes from the CHANNEL field on the next send_packet; the * 802.11 body is beacon_frame past its 10-byte radiotap. */ - tx_buf = build_frame_with_channel(chan_to_freq(ch), beacon_frame + 10, + tx_buf = build_frame_with_channel(devourer::chan_to_freq(ch), + beacon_frame + 10, sizeof(beacon_frame) - 10); mode = " radiotap"; } -#if defined(DEVOURER_HAVE_JAGUAR1) - else if (hop_fast && jag) { - jag->FastRetune(static_cast(ch), /*cache_rf=*/hop_fast != 2); + else if (hop_fast) { + /* IRtlDevice virtual: every generation implements the lean fast path + * (cached RF18 write + on-change constants, with the internal + * band-change fallback to the full set). */ + rtlDevice->FastRetune(static_cast(ch), + /*cache_rf=*/hop_fast != 2); mode = " fast"; } -#endif else { rtlDevice->SetMonitorChannel(SelectedChannel{ .Channel = static_cast(ch), diff --git a/txdemo/stream_tx_demo/main.cpp b/txdemo/stream_tx_demo/main.cpp index c1a9e54..06132a3 100644 --- a/txdemo/stream_tx_demo/main.cpp +++ b/txdemo/stream_tx_demo/main.cpp @@ -321,11 +321,10 @@ int main(int argc, char **argv) { * it per packet is fine. */ if (!hop_channels.empty()) { int ch = hop_channels[(tx_count / hop_dwell) % hop_channels.size()]; -#if defined(DEVOURER_HAVE_JAGUAR1) - if (hop_fast && jag) - jag->FastRetune(static_cast(ch), /*cache_rf=*/hop_fast != 2); + if (hop_fast) + rtlDevice->FastRetune(static_cast(ch), + /*cache_rf=*/hop_fast != 2); else -#endif rtlDevice->SetMonitorChannel(SelectedChannel{ .Channel = static_cast(ch), .ChannelOffset = 0,