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
18 changes: 18 additions & 0 deletions docs/frequency-hopping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions examples/dwelltx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
24 changes: 24 additions & 0 deletions src/kestrel/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
129 changes: 122 additions & 7 deletions src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<int16_t>(_txpwr_dbm_q2 + _txpwr_offset_qdb));
_logger->debug("Kestrel FastRetune: ch{} ({})", channel,
Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/kestrel/HalKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) */

Expand Down
Loading