diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index 3a0d2bc5..ca487517 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -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 diff --git a/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index 8625aa16..f9fa61aa 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -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`, @@ -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/ @@ -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 diff --git a/src/kestrel/FrameParserKestrel.h b/src/kestrel/FrameParserKestrel.h index b4b5c821..bbf251d2 100644 --- a/src/kestrel/FrameParserKestrel.h +++ b/src/kestrel/FrameParserKestrel.h @@ -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) */ @@ -87,7 +87,11 @@ inline bool parse_rx_8852b(const uint8_t *buf, size_t buflen, out.ppdu_cnt = static_cast((d1 >> 4) & 0x7); out.rx_rate = static_cast((d1 >> 16) & 0x1ff); out.gi_ltf = static_cast((d1 >> 25) & 0x7); - out.bw = static_cast((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((d1 >> 29) & 0x7) + : static_cast((d1 >> 30) & 0x3); out.freerun_cnt = detail::rd_le32(buf + 8); diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 62fc34f7..9439bb6f 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -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(); @@ -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( + 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((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(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 @@ -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); if (_variant == ChipVariant::C8852C) { /* DIG PD lower-bound disable (halbb_dig_init_io_en -> dyn_pd_th, set_en=0): diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index a5cd8fd2..1b917316 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -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 @@ -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 */ diff --git a/src/kestrel/MacRegAx.h b/src/kestrel/MacRegAx.h index 8d374be0..5cfcc688 100644 --- a/src/kestrel/MacRegAx.h +++ b/src/kestrel/MacRegAx.h @@ -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; diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index aa5655f8..83c8cf85 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -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(block(channel) + 6)); + _channel.Channel = channel; + return; + } + } if (!same_band || _channel.ChannelWidth != CHANNEL_WIDTH_20) { SelectedChannel c = _channel; c.Channel = channel; @@ -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);