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
9 changes: 9 additions & 0 deletions docs/frequency-hopping.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,15 @@ changes the `FastRetune` timing contract (it returns before the channel is
usable); the hop demos' admission window already honours the settle, and
`DEVOURER_KFR_OFLD=0` restores the self-pacing direct path.

At 80 MHz both Kestrel dies have a narrower shortcut of their own: a hop
between two channels of the same 80 MHz block (149 <-> 161, center 155) moves
no RF, only which 20 MHz is primary. `fast_set_primary_80` rewrites the BB
primary index, the 5 MHz edge mask and the MAC TX sub-carrier and resets the
BB — median ~2.2 ms on the 8852C against ~213 ms for the full path (which
also re-runs the IQK this one skips), ~18 ms on the 8852B against ~145 ms,
its register round-trips being the slower ones (`src/kestrel/CLAUDE.md` has
the measurement and its limits).

(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
read or write is one synchronous control transfer, whose latency is a property
Expand Down
43 changes: 42 additions & 1 deletion src/kestrel/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ rates), channel/BW **5/10/20/40/80 MHz on both dies + 160 MHz on the 8852C**
(the 8852B die has no 160 MHz — vendor bw_sup; caps report accordingly).
40 MHz tunes to the block center (primary ±2); 80/160 MHz derive the center
from the channel plan (160 = an 8-wide block, center = block_start+14).
Every tune also programs the MAC side of the width (`mac_set_bw_txsc`, as
rtw89 `rtw8852c_set_channel_mac`): `R_AX_WMAC_RFMOD` and the TX sub-carrier
indices in `R_AX_TX_SUB_CARRIER_VALUE` (`rtw89_phy_get_txsc`). Without them a
PPDU narrower than the channel — 20 MHz on an 80 MHz channel — airs at the
block center and nothing on the primary hears it (an iPhone on ch149 never
saw an 8852C tuned 149/80 until they were written). The wide-channel TX
validation above injected PPDUs as wide as the channel, which do not use the
index, so it could not catch this. On-air A/B, txdemo 6M beacons from a DUT
tuned wide, rxdemo at 20 MHz on the primary, 500 frames a cell: before, 0 on
every cell of both dies (8852C 149/40, 153/40, 149/80, 161/80, 36/160; 8852B
the same minus 160); after, 400+ on every cell; a 20 MHz control 400+ both.
`halbb_ctrl_bw_ch`'s `pri_ch` argument is the primary **channel number**, not a
sub-band index (vendor phl passes `rtw_chan_def::chan` through unchanged): the
vendor derives the 0x4978[11:8] sub-band index from `pri_ch` vs `central_ch`,
Expand Down Expand Up @@ -148,6 +159,33 @@ 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 ~30 ms
slots (soak: 2000 hops, zero wrong-channel, ~97 % delivery).

Both dies have one shortcut at 80 MHz on 5 GHz: a hop to another channel of
the **same 80 MHz block** (149 <-> 161 in 149-161) moves no RF, only the
primary. `fast_set_primary_80` rewrites what `halbb_ctrl_bw_ch_8852{b,c}`
derives from the primary at 80 MHz — the [11:8] sub-band index (0x4978 on the
C, 0x49c4 on the B) and the 5 MHz edge mask (`halbb_5m_mask_8852{b,c}`:
0x4c4c/0x4d10/0x4450[24] on the C, 0x46f8/0x47b8/0x4440[31] on the B, the
0x4 value on both; the 8852D's 0x5 is not handled) — plus the MAC TX
sub-carrier, then resets the BB. Median hop **~2.2 ms on the 8852C** (115
hops, max 2.5 ms) against ~213 ms for the full path, which re-runs the IQK
this one skips; **~18 ms on the 8852B** against ~145 ms (its register
round-trips are the slower ones, as above). Measured with an iPhone whose
AWDL schedule alternates 149/161, on the 8852C: on the full path its 49.5 MB
transfer spent ~44 s of 49 s deaf; on this one a 50.8 MB transfer took 7.5 s
with nothing else changed. txdemo alternating 149/161 at 80 MHz (50 frames a
dwell, 250 on 161) reached an rxdemo witness on 161 at 200+ on both dies,
fast or full. With the IQK skipped, MAC retries stayed at 2-4% over the
iPhone sessions — one adapter, one peer, near field — and the fast hop
relies on the RF center staying put. Register parity against the full path
(BB 0x0000-0xFFFF incl. the RF window, CMAC 0xC000-0xCFFF; pairs 149/161,
149/153 and 157/161, 3 full tunes per primary and 12 fast hops each, on each
die): every register the full path sets per primary matched on all 36 hops
of each die. The rest that differed either differs between two full tunes
of the same channel too (SW-SI counters and records, RX counters, AFE status
outputs, the NCTL/IQK engine's status) or showed on a single hop of six
(a few undocumented CMAC status words); a missing write would show on every
hop. Any other wide-channel hop still takes the full path.

### Firmware IO-offload hop (`DEVOURER_KFR_OFLD`, default on)

`fast_retune_ofld_8852b` ships the *entire* same-sub-band hop — the six RF18/
Expand Down Expand Up @@ -208,7 +246,10 @@ Almost every 8852C divergence is a **`_V1` register-bank** move
32-byte `wd_body_t_v1` (not 24), and the RX-descriptor drv_info unit is
16 bytes (not 8 — `FrameParserKestrel.h`; with the B's unit the C "receives"
frames with correct lengths but unreadable bodies, i.e. looks deaf to any
SA-matching harness). The CBV-cut die loads the `u2_nic` fw image (CAV→u1).
SA-matching harness); the same descriptor widens the RX bandwidth field to
dword1 [31:29] (rtw89 `AX_RXD_BW_v1_MASK`) — read with the B's [31:30],
every width came out halved (80 MHz as 40). The CBV-cut die loads the
`u2_nic` fw image (CAV→u1).
Two 8852C-specific TX pieces the 8852B path lacks: the BB IFFT→TX-chain
routing (`halbb_ctrl_tx_path_tmac_8852c`, the 0xD800..0xD82C "path-com" block
the 8852B replaces with its per-STA CMAC antenna model), and the V1
Expand Down
8 changes: 6 additions & 2 deletions src/kestrel/FrameParserKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ struct KestrelRxFrame {
bool a1_match = false;
uint16_t rx_rate = 0; /* 9-bit datarate code (legacy/HT/VHT/HE) */
uint8_t gi_ltf = 0;
uint8_t bw = 0; /* 0=20 1=40 2=80 */
uint8_t bw = 0; /* 0=20 1=40 2=80 3=160 (3 bits on the 8852C) */
uint8_t ppdu_type = 0xf; /* RX_DESC_PPDU_T_*: 7=HE_SU 8=HE_ERSU (15=unknown) */
uint8_t ppdu_cnt = 0;
uint32_t freerun_cnt = 0; /* free-running counter (TSF-ish) */
Expand Down Expand Up @@ -87,7 +87,11 @@ inline bool parse_rx_8852b(const uint8_t *buf, size_t buflen,
out.ppdu_cnt = static_cast<uint8_t>((d1 >> 4) & 0x7);
out.rx_rate = static_cast<uint16_t>((d1 >> 16) & 0x1ff);
out.gi_ltf = static_cast<uint8_t>((d1 >> 25) & 0x7);
out.bw = static_cast<uint8_t>((d1 >> 30) & 0x3);
/* The 8852C widens the field to [31:29] (rtw89 AX_RXD_BW_v1_MASK); reading
* [31:30] there halves every width (80 MHz reads as 40, 40 as 20). The
* 16-byte drv_info unit is what marks that die here. */
out.bw = drv_info_unit == 16 ? static_cast<uint8_t>((d1 >> 29) & 0x7)
: static_cast<uint8_t>((d1 >> 30) & 0x3);

out.freerun_cnt = detail::rd_le32(buf + 8);

Expand Down
130 changes: 125 additions & 5 deletions src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2548,15 +2548,19 @@ void HalKestrel::fast_retune(uint8_t channel) {

void HalKestrel::fast_set_bw(ChannelWidth_t bw) {
/* Lean 20 <-> 5/10 MHz toggle (same channel). Only the BB small-BW field
* (0x49C4[13:12]) + ACI-detect (0x4738/0x4AA4[16]) differ between 20 MHz and
* narrowband; RF_BW stays 0 and RF18[11:10] stays "both" for all of 20/5/10,
* so no RF re-tune or gain re-apply is needed. */
* ([13:12], per-die address below) + ACI-detect (0x4738/0x4AA4[16])
* differ between 20 MHz and narrowband; RF_BW stays 0 and RF18[11:10]
* stays "both" for all of 20/5/10, so no RF re-tune or gain re-apply is
* needed. */
const uint32_t smallbw = bw == CHANNEL_WIDTH_5 ? 0x1
: bw == CHANNEL_WIDTH_10 ? 0x2
: 0x0; /* 20 MHz */
const uint32_t aci = bw == CHANNEL_WIDTH_20 ? 0x1 : 0x0; /* off for NB */
bb_rmw(0x49C0, 0xC0000000u, 0x0); /* RF_BW = 0 */
bb_rmw(0x49C4, 0x3000u, smallbw);
/* RF_BW and small-BW sit at 0x4974/0x4978 on the 8852C and 0x49C0/0x49C4 on
* the 8852B (halbb_ctrl_bw_8852{c,b}); the ACI bits are shared. */
const bool c = _variant == ChipVariant::C8852C;
bb_rmw(c ? 0x4974 : 0x49C0, 0xC0000000u, 0x0); /* RF_BW = 0 */
bb_rmw(c ? 0x4978 : 0x49C4, 0x3000u, smallbw);
bb_rmw(0x4738, 0x10000u, aci);
bb_rmw(0x4AA4, 0x10000u, aci);
bb_reset_all();
Expand Down Expand Up @@ -2611,6 +2615,121 @@ void HalKestrel::bb_reset_all() {
bb_rmw(0xc3c, 1u << 9, 0);
}

namespace {
/* rtw89_phy_get_txsc: which sub-channel of a `cbw` channel a `dbw`-wide PPDU
* airs on, from the primary channel number vs the block center. 0 when the
* PPDU is as wide as the channel. */
uint8_t phy_get_txsc(uint8_t pri, uint8_t center, ChannelWidth_t cbw,
ChannelWidth_t dbw) {
if (cbw == dbw || cbw == CHANNEL_WIDTH_20)
return 0;
const bool above = pri > center;
const uint8_t idx20 = static_cast<uint8_t>(
above ? (pri - center) >> 1 : ((center - pri) >> 1) + 1);
switch (cbw) {
case CHANNEL_WIDTH_40:
return above ? 1 : 2;
case CHANNEL_WIDTH_80:
return dbw == CHANNEL_WIDTH_20 ? idx20 : (above ? 9 : 10);
case CHANNEL_WIDTH_160:
if (dbw == CHANNEL_WIDTH_20)
return idx20;
if (dbw == CHANNEL_WIDTH_40) {
if (idx20 == 1 || idx20 == 3)
return 9;
if (idx20 == 5 || idx20 == 7)
return 11;
if (idx20 == 2 || idx20 == 4)
return 10;
if (idx20 == 6 || idx20 == 8)
return 12;
return 0xff;
}
return above ? 13 : 14;
default:
return 0;
}
}
} /* namespace */

void HalKestrel::mac_set_bw_txsc(uint8_t channel, uint8_t center,
ChannelWidth_t bw) {
/* The MAC side of a tune, as rtw89 rtw8852c_set_channel_mac does it:
* R_AX_WMAC_RFMOD carries the channel width, and R_AX_TX_SUB_CARRIER_VALUE
* the sub-channel each narrower PPDU airs on. Left at its cmac_com_init
* zeros, a 20 MHz frame on an 80 MHz channel airs at the block center,
* where nothing listening on the primary hears it. */
uint8_t rfmod = 0;
uint32_t txsc = 0;
const auto field = [](uint8_t v, uint8_t sh) {
return (uint32_t{v} & r::B_AX_TXSC_MSK) << sh;
};
switch (bw) {
case CHANNEL_WIDTH_160:
rfmod = 3;
txsc = field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_20),
r::B_AX_TXSC_20M_SH) |
field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_40),
r::B_AX_TXSC_40M_SH) |
field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_80),
r::B_AX_TXSC_80M_SH);
break;
case CHANNEL_WIDTH_80:
rfmod = 2;
txsc = field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_20),
r::B_AX_TXSC_20M_SH) |
field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_40),
r::B_AX_TXSC_40M_SH);
break;
case CHANNEL_WIDTH_40:
rfmod = 1;
txsc = field(phy_get_txsc(channel, center, bw, CHANNEL_WIDTH_20),
r::B_AX_TXSC_20M_SH);
break;
default: /* 20 MHz and the 5/10 MHz small-BW modes: RF in 20 MHz mode */
break;
}
const uint8_t m = _device.rtw_read8(r::R_AX_WMAC_RFMOD);
_device.rtw_write8(r::R_AX_WMAC_RFMOD,
static_cast<uint8_t>((m & ~r::B_AX_WMAC_RFMOD_MSK) | rfmod));
_device.rtw_write32(r::R_AX_TX_SUB_CARRIER_VALUE, txsc);
if (rfmod)
_logger->info("Kestrel set_channel: pri {} center {} rfmod {} txsc 0x{:03x}",
channel, center, rfmod, txsc);
}

void HalKestrel::fast_set_primary_80(uint8_t channel, uint8_t center) {
/* Move the primary 20 inside the 80 MHz block the RF already sits on: the
* pieces of halbb_ctrl_bw_ch_8852{b,c} that read pri_ch at 80 MHz -- the
* [11:8] sub-band index and the 5 MHz edge mask (halbb_5m_mask_8852{b,c})
* -- and the MAC TXSC, then a BB reset. No RF tune, no IQK: the ~210 ms of
* the full path. Same steps on both dies, at the registers each one's
* halbb uses; the mask's 0x3f field is 0x4 on both (the 8852D's 0x5 is not
* handled). */
const bool c = _variant == ChipVariant::C8852C;
const uint32_t pri_reg = c ? 0x4978 : 0x49C4;
const uint32_t mask_regs[2] = {c ? 0x4c4cu : 0x46f8u, c ? 0x4d10u : 0x47b8u};
const uint32_t mask_en_reg = c ? 0x4450 : 0x4440;
const uint32_t mask_en_bit = c ? 1u << 24 : 1u << 31;
const int d = int(channel) - int(center);
const uint8_t idx = static_cast<uint8_t>(d > 0 ? d / 2 : -d / 2 + 1);
bb_rmw(pri_reg, 0xf00, idx);
const bool mask_en = idx == 3 || idx == 4, mask_low = idx == 4;
for (const uint32_t a : mask_regs) {
if (mask_en) {
bb_rmw(a, 0x3f, 0x4);
bb_rmw(a, 1u << 12, 1);
bb_rmw(a, 1u << 8, mask_low ? 0 : 1);
bb_rmw(a, 1u << 6, mask_low ? 1 : 0);
} else {
bb_rmw(a, 1u << 12, 0);
}
}
bb_rmw(mask_en_reg, mask_en_bit, mask_en ? 1 : 0);
mac_set_bw_txsc(channel, center, CHANNEL_WIDTH_80);
bb_reset_all();
}

bool HalKestrel::set_channel(uint8_t channel, ChannelWidth_t bw,
uint8_t offset, uint8_t band) {
/* band: 0 = auto (2.4 GHz if ch<=14 else 5 GHz), 6 = 6 GHz. band_type is the
Expand Down Expand Up @@ -2662,6 +2781,7 @@ bool HalKestrel::set_channel(uint8_t channel, ChannelWidth_t bw,
"MHz for 6 GHz TX.");
}

mac_set_bw_txsc(channel, center, bw);
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

if (_variant == ChipVariant::C8852C) {
/* DIG PD lower-bound disable (halbb_dig_init_io_en -> dyn_pd_th, set_en=0):
Expand Down
6 changes: 6 additions & 0 deletions src/kestrel/HalKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ class HalKestrel {
* must ensure same band + same width (20 MHz). */
void fast_retune(uint8_t channel);

/* 80 MHz, 5 GHz: move the primary 20 within the current 80 MHz block
* (BB primary index + 5 MHz mask + MAC TXSC + BB reset), no RF tune. */
void fast_set_primary_80(uint8_t channel, uint8_t center);

/* Lean same-channel 20 <-> 5/10 MHz narrowband toggle: only the BB small-BW
* field + ACI-detect change (the RF stays in 20 MHz mode for all of 20/5/10,
* so no RF re-tune, no gain/channel re-apply). ~a BB write + reset vs the full
Expand Down Expand Up @@ -609,6 +613,8 @@ class HalKestrel {
bool pcie_ltr_silence();
/* BB/RF channel helpers (all over the wIndex=1 window). */
void bb_rmw(uint32_t addr, uint32_t mask, uint32_t val); /* masked BB write */
/* R_AX_WMAC_RFMOD + R_AX_TX_SUB_CARRIER_VALUE for `bw` around `center`. */
void mac_set_bw_txsc(uint8_t channel, uint8_t center, ChannelWidth_t bw);
uint32_t bb_read(uint32_t addr, uint32_t mask); /* masked+shifted BB read */
uint32_t rf_read(uint8_t path, uint8_t rf_addr); /* DDV RF read */
void rf_write(uint8_t path, uint8_t rf_addr, uint32_t val); /* DDV RF write */
Expand Down
2 changes: 2 additions & 0 deletions src/kestrel/MacRegAx.h
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,8 @@ constexpr uint8_t B_AX_TXSC_20M_SH = 0;
constexpr uint8_t B_AX_TXSC_40M_SH = 4;
constexpr uint8_t B_AX_TXSC_80M_SH = 8;
constexpr uint32_t B_AX_TXSC_MSK = 0xf;
constexpr uint16_t R_AX_WMAC_RFMOD = 0xC010;
constexpr uint32_t B_AX_WMAC_RFMOD_MSK = 0x3; /* [1:0]: 0/1/2/3 = 20/40/80/160 */
constexpr uint16_t R_AX_PTCL_RRSR1 = 0xC090;
constexpr uint8_t B_AX_RRSR_RATE_EN_SH = 8;
constexpr uint32_t B_AX_RRSR_RATE_EN_MSK = 0xf;
Expand Down
23 changes: 20 additions & 3 deletions src/kestrel/RtlKestrelDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,17 @@ void RtlKestrelDevice::FastRetune(uint8_t channel, bool /*cache_rf*/) {
* (band change or wide/narrow BW, where the center + BB config differ) takes
* the full SetMonitorChannel. */
const bool same_band = (_channel.Channel <= 14) == (channel <= 14);
/* 80 MHz, 5 GHz, target inside the same block: only the primary moves. */
if (same_band && channel > 14 && _channel.ChannelWidth == CHANNEL_WIDTH_80 &&
_channel.Band != 6) {
const auto block = [](int ch) { return ch - (((ch - 36) / 4) % 4) * 4; };
if (block(channel) == block(_channel.Channel)) {
if (channel != _channel.Channel)
_hal.fast_set_primary_80(channel, static_cast<uint8_t>(block(channel) + 6));
_channel.Channel = channel;
return;
}
}
if (!same_band || _channel.ChannelWidth != CHANNEL_WIDTH_20) {
SelectedChannel c = _channel;
c.Channel = channel;
Expand All @@ -768,9 +779,15 @@ void RtlKestrelDevice::FastRetune(uint8_t channel, bool /*cache_rf*/) {

void RtlKestrelDevice::FastSetBandwidth(ChannelWidth_t bw) {
/* Lean path only for the 20 <-> 5/10 MHz narrowband toggle (BB small-BW
* field, same channel); 40/80 MHz change the center + RF bw so take the full
* SetMonitorChannel. */
if (bw != CHANNEL_WIDTH_20 && bw != CHANNEL_WIDTH_5 && bw != CHANNEL_WIDTH_10) {
* field, same channel), so both ends must be narrow. Leaving a 40/80/160 MHz
* channel moves the RF center and RF bw back to the primary and resets the
* MAC width (RFMOD + TX sub-carrier), none of which the lean path touches:
* from 149/80 it left RF18 on 155/80 and the MAC at 80 MHz. Any wide end
* takes the full SetMonitorChannel. */
const auto narrow = [](ChannelWidth_t w) {
return w == CHANNEL_WIDTH_20 || w == CHANNEL_WIDTH_5 || w == CHANNEL_WIDTH_10;
};
if (!narrow(bw) || !narrow(_channel.ChannelWidth)) {
SelectedChannel c = _channel;
c.ChannelWidth = bw;
SetMonitorChannel(c);
Expand Down
Loading