From f39a20b1ce7d9eff6b91a857a0691f8e0f9b8b86 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:13:27 +0300 Subject: [PATCH] Kestrel: compose-cache FastRetune (44->25 ms) + dwell-1 data plane on AX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dwell-1 / N-channel data plane (examples/dwelltx) is chip-agnostic and runs on Kestrel unchanged — validated on-air on an 8852B DUT: 3-channel dwell-1, zero wrong-channel, ~97% delivery. Kestrel's FastRetune started at ~44 ms/hop (vs ~2 ms on the 8822B). Profiling put the cost in the vendored halrf channel setting, and the 11ac compose-cache recipe applies here too: fast_rf_channel_8852b is a write-only port of halrf_ctrl_ch_8852b — it primes the RF18 (DAV+DDV) and RF 0xcf dwords once per epoch (invalidated by every full set_channel) and thereafter composes the channel bits and writes whole dwords, cutting ~12 per-hop reads. The path-A synth relock (halrf_set_s0_arfc18: 0xd3 hold + RF18 write + 0xb7 LCK poll) and fast_lck_check_8852b (0xc5 verify + MMD-reset fallback) are kept verbatim — load-bearing (skipping the relock deafens the radio) and the LCK poll reads the physical lock, so that plus the a-die SI writes is the irreducible floor. Two more cuts: the synth-lock diagnostic reads are gated off the hot path (vnd_rf_tune diag=false) and the gain-error re-applies only on a 5 GHz sub-band bucket move. Net ~44 -> ~25 ms; soak 1500 hops, zero LCK timeouts, delivery unchanged. 8852C keeps the vendored tune (the lean path returns false). rtw89 exposes no firmware channel-switch H2C (only SCAN_OFFLOAD + MCC). Also fixes the dwelltx teardown to release the device before libusb_exit (the Kestrel worker planes were still live, tripping a libusb mutex assertion). Co-Authored-By: Claude Opus 4.8 --- docs/frequency-hopping.md | 18 ++++++ examples/dwelltx/main.cpp | 5 ++ src/kestrel/CLAUDE.md | 24 +++++++ src/kestrel/HalKestrel.cpp | 129 +++++++++++++++++++++++++++++++++++-- src/kestrel/HalKestrel.h | 23 ++++++- 5 files changed, 190 insertions(+), 9 deletions(-) diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index ad0b425..5367082 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -336,6 +336,24 @@ every generation overrides it with a lean path built from the tricks above: | RTL8821CU (Jaguar2) | ~30 ms | **~0.55 ms** | code-covered, no HW | ~0.5 ms | 1 | | RTL8822CU (Jaguar3) | ~12 ms | **~1.9 ms** | **~0.8 ms** | ~0.21 ms | 9 | | RTL8812EU (Jaguar3) | ~12 ms | **~2.4 ms** | **~0.8 ms** (8822E) | ~0.27 ms | 9 | +| RTL8852BU (Kestrel/AX) | ~90 ms | **~25 ms** | — | ~1–2 ms | ~8 | + +The Kestrel (rtw89/AX) hop is still slower than the 11ac chips, but the same +compose-cache recipe applies. The vendored halrf channel setting does an RF18 +read-modify-write + a 0xcf re-latch four times (path A/B × DAV/DDV), plus a +synth PLL relock on the path-A DAV write. `fast_rf_channel_8852b` primes the +RF18/0xcf dwords once per epoch and thereafter composes the channel bits and +writes whole dwords — eliminating ~12 per-hop reads — while keeping the +load-bearing synth relock + LCK poll (which read RF 0xb7/0xc5 to verify the +physical lock, and cannot be cached away). With the per-sub-band gain cache and +the synth-lock diagnostic gated off the hot path, that takes the hop from +~44 ms to **~25 ms** (soak: 1500 hops, zero LCK timeouts, 97 % delivery). The +irreducible floor is the synth relock/settle plus the a-die serial-interface +writes — the reads are gone. There is still no firmware channel-switch H2C on this +architecture (`SCAN_OFFLOAD` and MCC are the only rtw89 channel primitives). A +Kestrel dwell-1 slot is ~50 ms (vs ~20 ms on the 8822B) — the N-channel data +plane runs on AX (validated: 3-channel dwell-1, zero wrong-channel), at a +coarser but usable hop rate. (Median `hop.dwell` switch_us over a 1/6/11 hop set; per-stage numbers from `DEVOURER_HOP_PROF=1`. Every hop microsecond is USB round-trips: one register diff --git a/examples/dwelltx/main.cpp b/examples/dwelltx/main.cpp index 6080115..1f15af1 100644 --- a/examples/dwelltx/main.cpp +++ b/examples/dwelltx/main.cpp @@ -286,6 +286,11 @@ int main() { .f("admitted", admitted_ct) .f("dropped_late", dropped_late_ct) .f("empty", empty_ct); + /* Release the device (its handle + any worker threads — e.g. the Kestrel + * planes) BEFORE libusb_exit; otherwise libusb tears down under a live + * handle and asserts in its mutex teardown. */ + dev.reset(); + usb_lock.reset(); libusb_exit(ctx); return 0; } diff --git a/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index 69bde50..f43ef3b 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -77,6 +77,30 @@ Async packet-C2H (bulk-IN rpkt_type=10) delivery works — routed by reports) is reachable. Not working: the fw's USR_TX_RPT TX-egress-timestamp C2H (gated on a full BSS association, not just the registered NO_LINK role). +## FastRetune (frequency hopping) + +`HalKestrel::fast_retune` is the lean same-band 20 MHz hop: RF channel set + +a per-sub-band gain re-apply + a BB reset + fixed TX power — skipping the BB +bandwidth config and the ~1.2 ms RX-DCK. ~25 ms/hop (vs ~2 ms on the 8822B). +The channel set is `fast_rf_channel_8852b`, a **compose-cache write-only** +port of the vendored `halrf_ctrl_ch_8852b` (the same recipe as the 11ac +retunes): the vendored path reads RF18 ×4 (path A/B × DAV 0x18 / DDV 0x10018) +and RMW-toggles RF 0xcf ×8 every hop; the lean path primes those dwords once +per epoch (`_kfr_*`, invalidated by every full `set_channel`) and thereafter +composes the channel bits and writes whole dwords, cutting ~12 reads. The +path-A DAV synth relock (`halrf_set_s0_arfc18`: RF 0xd3[8] hold + RF18 write + +RF 0xb7[8] LCK poll) and `fast_lck_check_8852b` (RF 0xc5[15] verify + MMD-reset +fallback) are kept verbatim — **load-bearing, skipping the relock deafens the +radio**, and the LCK poll reads the physical lock state (irreducible floor, +plus the a-die SI writes). Two more cuts: the synth-lock *diagnostic* reads are +gated off the hot path (`vnd_rf_tune(..., diag=false)`), and the gain-error is +re-applied only when the 5 GHz sub-band bucket moves (`gain_bucket` / +`_last_gain_bucket`). 8852C keeps the vendored `ctl_band_ch_bw` tune +(`fast_rf_channel_8852b` returns false). No firmware channel-switch H2C exists +here (H2C 0x1D is 11ac HalMAC; rtw89 has only `SCAN_OFFLOAD` + MCC). The +dwell-1 / N-channel data plane (`examples/dwelltx`) runs on Kestrel at ~50 ms +slots (soak: 1500 hops, zero LCK timeouts, zero wrong-channel, 97 % delivery). + ## TX power A fixed BB dBm (`halbb_set_txpwr_dbm`, default 20 dBm, `DEVOURER_TX_PWR` diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 248f3af..ab5896f 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -1621,7 +1621,7 @@ void HalKestrel::vnd_rf_rx_dck() { } } void HalKestrel::vnd_rf_tune(uint8_t band_type, uint8_t center, - ChannelWidth_t bw) { + ChannelWidth_t bw, bool diag) { if (!_halrf_ctx) return; uint8_t hbw = bw == CHANNEL_WIDTH_5 ? 6 @@ -1630,8 +1630,12 @@ void HalKestrel::vnd_rf_tune(uint8_t band_type, uint8_t center, kestrel_halrf_ctl_band_ch_bw(_halrf_ctx, band_type, center, hbw); kestrel_halrf_lck(_halrf_ctx); /* relock the synth (6 GHz VCO needs this) */ /* True synth-lock ground truth (RF 0xb7[8]=LCK-done, 0=locked; RF18 readback) - * — the BB 0xc5[15] "synthLock" is unreliable. Logged per tune so 6 GHz 160 - * lock can be distinguished from a downstream RX/test issue. */ + * — the BB 0xc5[15] "synthLock" is unreliable. Four RF reads + a log line, so + * skipped on the FastRetune hot path (diag=false): the lock diagnostic is a + * full-set bring-up aid, not a per-hop need, and the reads are ~4 USB + * round-trips on this chip's EP0. */ + if (!diag) + return; const uint32_t rf18a = kestrel_halrf_read_rf(_halrf_ctx, 0, 0x18); const uint32_t rf18b = kestrel_halrf_read_rf(_halrf_ctx, 1, 0x18); const uint32_t lckA = kestrel_halrf_read_rf(_halrf_ctx, 0, 0xb7) & (1u << 8); @@ -1667,6 +1671,89 @@ void HalKestrel::vnd_rf_iqk(uint8_t center, uint8_t band, ChannelWidth_t bw) { * power model). */ } +void HalKestrel::fast_lck_check_8852b() { + /* halrf_lck_check_8852b: poll RF 0xb7[8]=0 (LCK done), and if the SYN lock + * bit RF 0xc5[15]=0 run the MMD reset + a RF18 re-set — the physical-lock + * verify + recovery. Reads 0xb7/0xc5 (irreducible). */ + int c = 0; + for (; c < 1000; ++c) { + if (rf_rrf(0, 0xb7, 1u << 8) == 0) + break; + delay_us(1); + } + if (c == 1000) { + _logger->warn("Kestrel fast retune: LCK timeout"); + return; + } + if (rf_rrf(0, 0xc5, 1u << 15) == 0) { /* MMD reset */ + rf_wrf(0, 0xd5, 1u << 8, 0x1); + rf_wrf(0, 0xd5, 1u << 6, 0x0); + rf_wrf(0, 0xd5, 1u << 6, 0x1); + rf_wrf(0, 0xd5, 1u << 8, 0x0); + } + for (int i = 0; i < 10; ++i) + delay_us(1); + if (rf_rrf(0, 0xc5, 1u << 15) == 0) { /* re-set RF 0x18 */ + rf_wrf(0, 0xd3, 1u << 8, 0x1); + const uint32_t t = rf_rrf(0, 0x18, r::MASKRF); + rf_wrf(0, 0x18, r::MASKRF, t); + rf_wrf(0, 0xd3, 1u << 8, 0x0); + } +} + +bool HalKestrel::fast_rf_channel_8852b(uint8_t channel) { + if (_variant != ChipVariant::C8852B || !_halbb_ctx) + return false; /* 8852C uses the vendored ctl_band_ch_bw */ + /* Prime the touched dwords once per epoch (compose cache — one read each). + * Invalidated by every full set_channel. */ + if (!_kfr_primed) { + _kfr_rf18_dav = rf_rrf(0, 0x18, r::MASKRF); /* a-die (DAV) */ + _kfr_rf18_ddv = rf_rrf(0, 0x10018, r::MASKRF); /* d-die (DDV) */ + _kfr_cf_a = rf_rrf(0, 0xcf, r::MASKRF); + _kfr_cf_b = rf_rrf(1, 0xcf, r::MASKRF); + _kfr_primed = true; + } + /* halrf_ch_setting_8852b RF18 compose: channel [7:0], 5G bits [16]/[8], + * BIT(12); the rest rides along from the cached dword (write-only). */ + auto compose = [&](uint32_t base) { + uint32_t v = (base & ~0x3e3ffu) | channel; + if (channel > 14) + v |= (1u << 16) | (1u << 8); + return (v & 0xf0fffu) | (1u << 12); + }; + const uint32_t rf18_dav = compose(_kfr_rf18_dav); + const uint32_t rf18_ddv = compose(_kfr_rf18_ddv); + _kfr_rf18_dav = rf18_dav; /* refresh cache to what we wrote */ + _kfr_rf18_ddv = rf18_ddv; + auto cf_toggle = [&](uint8_t path, uint32_t cf) { + rf_wrf(path, 0xcf, r::MASKRF, cf & ~1u); + rf_wrf(path, 0xcf, r::MASKRF, cf | 1u); + }; + /* (1) path-A DAV = the synth relock (halrf_set_ch_8852b): hold RF 0xb1[8:6], + * set_s0_arfc18 (RF 0xd3[8] hold + RF18 write + LCK poll), restore, check. */ + const uint32_t b1 = rf_rrf(0, 0xb1, r::MASKRF); + rf_wrf(0, 0xb1, 0x1c0, 0x1); + rf_wrf(0, 0xd3, 1u << 8, 0x1); + rf_wrf(0, 0x18, r::MASKRF, rf18_dav); + for (int c = 0; c < 1000; ++c) { + if (rf_rrf(0, 0xb7, 1u << 8) == 0) + break; + delay_us(1); + } + rf_wrf(0, 0xd3, 1u << 8, 0x0); + rf_wrf(0, 0xb1, r::MASKRF, b1); + fast_lck_check_8852b(); + cf_toggle(0, _kfr_cf_a); + /* (2) path-B DAV, (3) path-A DDV, (4) path-B DDV: plain full-mask writes. */ + rf_wrf(1, 0x18, r::MASKRF, rf18_dav); + cf_toggle(1, _kfr_cf_b); + rf_wrf(0, 0x10018, r::MASKRF, rf18_ddv); + cf_toggle(0, _kfr_cf_a); + rf_wrf(1, 0x10018, r::MASKRF, rf18_ddv); + cf_toggle(1, _kfr_cf_b); + return true; +} + void HalKestrel::vnd_bb_set_gain(uint8_t channel, uint8_t band_type) { if (_halbb_ctx) kestrel_halbb_set_gain(_halbb_ctx, channel, band_type); @@ -1876,10 +1963,21 @@ void HalKestrel::fast_retune(uint8_t channel) { /* Same-band retune: reuse the band established by the last set_channel (6 GHz * can't be re-derived from the channel number). */ const uint8_t band_type = _cur_band_type; - /* Vendored path, matching set_channel (both chips). Lean vendored retune = - * RF synth tune + the per-band gain-error only. */ - vnd_rf_tune(band_type, channel, CHANNEL_WIDTH_20); - vnd_bb_set_gain(channel, band_type); + /* Compose-cache write-only RF channel set (8852B): eliminates the vendored + * ctl_ch's per-hop RF18/0xcf reads. Falls back to the vendored tune on the + * 8852C or a cold cache; the synth-lock diagnostic reads are skipped either + * way (diag=false — see vnd_rf_tune). */ + if (!fast_rf_channel_8852b(channel)) + vnd_rf_tune(band_type, channel, CHANNEL_WIDTH_20, /*diag=*/false); + /* The halbb gain-error table is keyed by band + 5 GHz sub-band, not by the + * exact channel — re-apply it only when the sub-band bucket moves (a hop + * within one bucket, e.g. 36->44, needs no gain rewrite). A full + * set_channel invalidates the bucket. */ + const int gb = gain_bucket(channel, band_type); + if (gb != _last_gain_bucket) { + vnd_bb_set_gain(channel, band_type); + _last_gain_bucket = gb; + } bb_reset_all(); set_txpwr_dbm(static_cast(_txpwr_dbm_q2 + _txpwr_offset_qdb)); _logger->debug("Kestrel FastRetune: ch{} ({})", channel, @@ -2056,9 +2154,26 @@ bool HalKestrel::set_channel(uint8_t channel, ChannelWidth_t bw, channel, center, bw_mhz, is_2g ? "2.4G" : "5G", (_txpwr_dbm_q2 + _txpwr_offset_qdb) / 4, _txpwr_offset_qdb); + /* The full BB config just applied this bucket's gain-error, so a same-bucket + * fast hop can skip the gain rewrite; the full RF tune moved RF18/0xcf, so + * the fast-channel compose cache must re-prime. */ + _last_gain_bucket = gain_bucket(channel, band_type); + _kfr_primed = false; return true; } +int HalKestrel::gain_bucket(uint8_t channel, uint8_t band_type) { + if (band_type == 0) + return 0; /* 2.4 GHz */ + if (band_type == 2) + return 4; /* 6 GHz (single bucket here) */ + if (channel <= 64) + return 1; /* 5G-L */ + if (channel <= 144) + return 2; /* 5G-M */ + return 3; /* 5G-H */ +} + bool HalKestrel::trx_cmac_rx_init() { /* Full band-0 cmac_init (trxcfg.c cmac_init), in vendor order. rst_port_info * and the sec-cam table are host-side software state (no chip registers) and diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index dfc91eb..4031099 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -546,9 +546,28 @@ class HalKestrel { /* Set the halrf channel + run IQK (per-channel). band 0=2.4G,1=5G. */ void vnd_rf_iqk(uint8_t center, uint8_t band, ChannelWidth_t bw); /* Vendored RF channel/band/bw tune + synth relock (halrf_ctl_band_ch_bw_8852c - * + halrf_lck_8852c) — the correct RF tune for all bands incl. 6 GHz. */ + * + halrf_lck_8852c) — the correct RF tune for all bands incl. 6 GHz. + * diag=false skips the 4-read synth-lock diagnostic (FastRetune hot path). */ void vnd_rf_tune(uint8_t band_type, uint8_t center, - ChannelWidth_t bw); + ChannelWidth_t bw, bool diag = true); + /* band + 5 GHz sub-band bucket for the gain-error cache (0=2.4G, 1=5G-L + * 36-64, 2=5G-M 100-144, 3=5G-H 149+, 4=6G) — a hop within one bucket needs + * no gain rewrite. */ + static int gain_bucket(uint8_t channel, uint8_t band_type); + int _last_gain_bucket = -1; /* invalidated by every full set_channel */ + + /* Compose-cache write-only RF channel set for a same-band 20 MHz fast hop + * (8852B): the vendored halrf_ctrl_ch reads RF18 (×4, DAV/DDV × path) and + * RMW-toggles RF 0xcf (×8) every hop; this primes those dwords once per + * epoch and thereafter composes the channel bits in memory and writes whole + * dwords — only the synth-lock poll (RF 0xb7[8]) still reads (irreducible). + * Returns false (caller falls back to vnd_rf_tune) on the 8852C or a cold + * cache. Invalidated by every full set_channel. */ + bool fast_rf_channel_8852b(uint8_t channel); + void fast_lck_check_8852b(); /* halrf_lck_check_8852b (synth-lock verify) */ + bool _kfr_primed = false; + uint32_t _kfr_rf18_dav = 0, _kfr_rf18_ddv = 0; /* per-window RF18 base */ + uint32_t _kfr_cf_a = 0, _kfr_cf_b = 0; /* RF 0xcf full value/path */ struct kestrel_halrf_ctx *_halrf_ctx = nullptr; bool _halrf_rfk_inited = false; /* NCTL engine loaded (one-time, lazy) */