From 23f797f4b8c93b73fc1f4fbdc66541102b8e5e9f Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:48:16 +0300 Subject: [PATCH] Kestrel FastRetune: drop the per-hop synth relock + DDV writes (25 -> 9 ms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more levers on top of the compose-cache, both on-air + soak validated (and both contradicting my earlier "that would deafen the radio" guess — tested, not assumed): - The synth PLL relock (halrf_set_s0_arfc18's ~13 ms LCK poll) was ~60% of the hop. For a SAME-sub-band hop the VCO moves little and settles during the caller's admission window, so a plain RF18 write holds channel accuracy; the full relock + LCK verify (the only path with MMD-reset lock recovery) is kept for a sub-band crossing (a bigger jump). Gated on the gain-bucket change that already drives the gain re-apply. - The DDV (d-die 0x10018) writes are unpopulated on the single-die 8852B — dropping them costs no channel accuracy. Same-sub-band hops land at ~9 ms, cross-sub-band ~13 ms, both zero wrong-channel (adjacent 36/40/44 delivery 98.5%; cross 36/44/149 wrong=0; aggressive-config soak 2000 hops zero wrong-channel, ~97% delivery). Net from the ~44 ms unoptimized hop: ~5x. A SetMonitorChannel still always relocks, so the fast hop's dropped guarantee is bounded to the intra-sub-band case. Co-Authored-By: Claude Opus 4.8 --- docs/frequency-hopping.md | 40 ++++++++++-------- src/kestrel/CLAUDE.md | 45 ++++++++++++-------- src/kestrel/HalKestrel.cpp | 84 ++++++++++++++++++++------------------ src/kestrel/HalKestrel.h | 6 +-- 4 files changed, 98 insertions(+), 77 deletions(-) diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index 5367082..1c47692 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -336,24 +336,30 @@ 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 +| RTL8852BU (Kestrel/AX) | ~90 ms | **~9 ms** | — | ~1–2 ms | ~4 | + +The Kestrel (rtw89/AX) hop needed the most work to reach 11ac-ish territory, +but the same techniques applied. 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. Three cuts, each validated on air: +- **Compose-cache** (`fast_rf_channel_8852b`, a write-only port of + `halrf_ctrl_ch_8852b`) primes the RF18/0xcf dwords once per epoch and writes + whole dwords thereafter — ~12 per-hop reads gone (~44 → ~25 ms). +- **Drop the DDV writes** — the d-die `0x10018` window is not populated on the + single-die 8852B, so its two SI writes + two 0xcf toggles cost no channel + accuracy (soak-confirmed). +- **Relock only on a sub-band crossing** — for a same-sub-band hop the synth + moves little and settles during the caller's admission window, so the ~13 ms + LCK poll (the profiler's ~60 % of the hop) is pure blocking; a plain RF18 + write holds channel accuracy (soak: 2000 hops, zero wrong-channel, ~97 % + delivery). The full relock + LCK verify (the only path with the MMD-reset + lock recovery) is kept for a sub-band crossing — a bigger VCO jump. + +Same-sub-band hops land at **~9 ms**, cross-sub-band ~13 ms — both zero +wrong-channel. 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. +Kestrel dwell-1 slot is ~30 ms (approaching the 8822B's ~20 ms) — the +N-channel data plane runs on AX at a 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/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index f43ef3b..96c0672 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -81,25 +81,36 @@ C2H (gated on a full BSS association, not just the registered NO_LINK role). `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 +bandwidth config and the ~1.2 ms RX-DCK. **~9 ms/hop same-sub-band, ~13 ms +cross-sub-band** (vs ~2 ms on the 8822B, ~44 ms unoptimized). The channel set +is `fast_rf_channel_8852b`, a **compose-cache write-only** port of the vendored +`halrf_ctrl_ch_8852b`. Three cuts (all on-air + soak validated): + +- **Compose-cache**: 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 + writes whole dwords — ~12 reads gone. +- **No DDV writes**: the d-die 0x10018 window is unpopulated on the single-die + 8852B; skipping it costs no channel accuracy (soak-confirmed). +- **Relock only on a sub-band bucket change** (`fast_rf_channel_8852b(..., + relock)`, `bucket_changed = gain_bucket != _last_gain_bucket`). A + same-sub-band hop moves the synth little and it settles during the caller's + admission window, so the ~13 ms path-A LCK poll is pure blocking — a plain + RF18 write holds channel accuracy (soak: 2000 hops, zero wrong-channel, + ~97 % delivery). The full `halrf_set_s0_arfc18` relock (RF 0xd3[8] hold + + RF18 write + RF 0xb7[8] LCK poll) + `fast_lck_check_8852b` (RF 0xc5[15] + verify + MMD-reset lock recovery) is kept for a sub-band crossing — a bigger + VCO jump, and the only path with the recovery. NOTE: the relock guarantees a + verified lock, so a `SetMonitorChannel` (full path) always runs it; the fast + hop trades that guarantee for latency within a sub-band, empirically safe. + +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 +bucket move. 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). +dwell-1 / N-channel data plane (`examples/dwelltx`) runs on Kestrel at ~30 ms +slots (soak: 2000 hops, zero wrong-channel, ~97 % delivery). ## TX power diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index ab5896f..0378f18 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -1701,56 +1701,58 @@ void HalKestrel::fast_lck_check_8852b() { } } -bool HalKestrel::fast_rf_channel_8852b(uint8_t channel) { +bool HalKestrel::fast_rf_channel_8852b(uint8_t channel, bool relock) { 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_rf18_dav = rf_rrf(0, 0x18, r::MASKRF); /* a-die (DAV) */ _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); + uint32_t rf18_dav = (_kfr_rf18_dav & ~0x3e3ffu) | channel; + if (channel > 14) + rf18_dav |= (1u << 16) | (1u << 8); + rf18_dav = (rf18_dav & 0xf0fffu) | (1u << 12); _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); + /* path-A channel write. The vendored path always runs the synth relock + * (halrf_set_ch_8852b: RF 0xb1[8:6] hold + set_s0_arfc18's RF 0xd3[8] hold + + * RF18 write + RF 0xb7[8] LCK poll, then fast_lck_check). For a SAME-sub-band + * hop the synth moves only a little and settles on its own during the + * caller's admission window — the ~13 ms LCK poll is pure blocking, and a + * plain RF18 write holds channel accuracy (soak: 2000 hops, zero + * wrong-channel, 97 % delivery). The relock is kept for a sub-band crossing + * (a bigger VCO jump, and the only path with the MMD-reset lock recovery). */ + if (relock) { + 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(); + } else { + rf_wrf(0, 0x18, r::MASKRF, rf18_dav); } - 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. */ + /* path-B DAV. The DDV (d-die, 0x10018) window is not populated on the + * single-die 8852B — skipping it costs no channel accuracy (soak-confirmed) + * and saves two SI writes + two 0xcf toggles per hop. */ 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; } @@ -1963,18 +1965,20 @@ 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; + /* The halbb gain-error table AND the synth relock are keyed by band + 5 GHz + * sub-band, not by the exact channel: a hop within one bucket (e.g. 36->44) + * needs neither a gain rewrite nor a synth relock (small VCO move, settles + * on its own), while a sub-band crossing needs both. A full set_channel + * invalidates the bucket. */ + const int gb = gain_bucket(channel, band_type); + const bool bucket_changed = (gb != _last_gain_bucket); /* 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)) + * ctl_ch's per-hop RF18/0xcf reads and, within a sub-band, the ~13 ms synth + * LCK poll. Falls back to the vendored tune on the 8852C or a cold cache; + * the synth-lock diagnostic reads are skipped either way (diag=false). */ + if (!fast_rf_channel_8852b(channel, /*relock=*/bucket_changed)) 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) { + if (bucket_changed) { vnd_bb_set_gain(channel, band_type); _last_gain_bucket = gb; } diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index 4031099..39e9e2f 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -563,11 +563,11 @@ class HalKestrel { * 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); + bool fast_rf_channel_8852b(uint8_t channel, bool relock); 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 */ + uint32_t _kfr_rf18_dav = 0; /* a-die RF18 compose 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) */