Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions docs/frequency-hopping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 28 additions & 17 deletions src/kestrel/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
84 changes: 44 additions & 40 deletions src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/kestrel/HalKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) */

Expand Down
Loading