From 5432b83cd00e6c3bba8ebf6b4f8ed9cb9c2d95b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Fri, 16 Oct 2026 15:52:56 -0300 Subject: [PATCH 1/5] kestrel: read the 8852C RX bandwidth from dword1 [31:29] The 8852C RX descriptor widens the bandwidth field to three bits, dword1 [31:29] (rtw89 AX_RXD_BW_v1_MASK, selected for RTL8852C in rtw89_core_query_rxdesc); the 8852B keeps [31:30]. parse_rx_8852b read [31:30] on both dies, so on the 8852C every width came out halved: an 80 MHz PPDU reported bw=1 (40 MHz) and a 40 MHz one bw=0 (20 MHz). The die is told apart by the 16-byte drv_info unit the parser already takes. - Measured on the 8852C (TX50UH) on ch149 at 80 MHz, an iPhone sending a 437 MB file: before, every frame read 40 MHz while the delivered rate (~45 MB/s at VHT 2SS MCS7-9 in ~46-MPDU A-MPDUs) needed more than the whole channel's airtime at 40 MHz; after, the same transfer reads 80 MHz on all 309k data frames. - Only RxAtrib.bw changes: the library copies it to the caller and branches on it nowhere; rxdemo's rx events report it, so on the 8852C their "bw" now reads 2 for 80 MHz where it read 1. The field can carry the 3-bit codes above 3 that the wider layout allows. --- src/kestrel/CLAUDE.md | 5 ++++- src/kestrel/FrameParserKestrel.h | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index 8625aa16..56177192 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -208,7 +208,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); From 2fee71e7affb114dd76cfa26f5627f355a0b95b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Sat, 17 Oct 2026 11:39:22 -0300 Subject: [PATCH 2/5] kestrel: program RFMOD and the TX sub-carrier for 40/80/160 MHz set_channel tuned the BB and RF to a wide channel but never told the MAC: R_AX_WMAC_RFMOD (0xC010) stayed at 20 MHz and R_AX_TX_SUB_CARRIER_VALUE (0xC088) at the zeros cmac_com_init writes. A PPDU narrower than the channel -- a 20 MHz frame on an 80 MHz channel -- then airs at TXSC 0, the block center, where nothing listening on the primary hears it. mac_set_bw_txsc() now writes both after every tune, as rtw89 does in rtw8852c_set_channel_mac: RFMOD 0/1/2/3 for 20/40/80/160, and the whole sub-carrier register from the 20/40/80 MHz indices of a line-for-line port of rtw89_phy_get_txsc (primary vs center), zero at 20 MHz. The existing 40/80/160 MHz TX validation injected PPDUs as wide as the channel, which do not use the sub-carrier index, so it could not see this. - Measured on the 8852C (TX50UH) tuned to ch149 at 80 MHz (center 155), sending 20 MHz frames to an iPhone listening on 149: without the writes the iPhone never discovered us (its frames reached us fine); with them it did and transfers ran. - On-air A/B on both dies: txdemo (500 6M canonical beacons, EDCCA gate off) from a DUT tuned wide, rxdemo at 20 MHz on the primary on the other adapter, counting rx.txhit. Before / after, hits of 500: DUT channel/width primary TXSC before after 8852C 149/40 (151) lower 0x002 0 400+ 8852C 153/40 (151) upper 0x001 0 400+ 8852C 149/80 (155) idx 4 0x0a4 0 400+ 8852C 161/80 (155) idx 3 0x093 0 400+ 8852C 36/160 (50) idx 8 0xec8 0 500 8852B 149/40, 153/40, 149/80, 161/80 0 400+ each both 149/20 control 0x000 400+ 400+ (rxdemo reports every 100th hit, so 400+ is 400-499.) The TXSC values are what rtw89_phy_get_txsc gives for each primary/center. - Band 0 only, like the rest of this HAL. 20 MHz writes RFMOD 0 and zero indices, the values it had before. --- src/kestrel/CLAUDE.md | 11 +++++ src/kestrel/HalKestrel.cpp | 84 ++++++++++++++++++++++++++++++++++++++ src/kestrel/HalKestrel.h | 2 + src/kestrel/MacRegAx.h | 2 + 4 files changed, 99 insertions(+) diff --git a/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index 56177192..3b87d68e 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`, diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 62fc34f7..cd51cb9d 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -2611,6 +2611,89 @@ 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); +} + 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 +2745,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..42808384 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -609,6 +609,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; From 20295a5e2038a4f562c43e5218e5d82ea8c3887a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Sat, 17 Oct 2026 11:39:22 -0300 Subject: [PATCH 3/5] kestrel: fast retune within one 80 MHz block A FastRetune at 80 MHz always fell back to the full SetMonitorChannel (BB bandwidth config, RF tune, and on the 8852C the IQK): ~213 ms a hop on the 8852C, ~145 ms on the 8852B. A hop between two channels of the same 80 MHz block (149 <-> 161, both in 149-161 / center 155) moves no RF at all, only which 20 MHz is primary. fast_set_primary_80() rewrites just 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}) -- plus the MAC TX sub-carrier, then resets the BB. FastRetune takes that path when the width is 80 MHz, the band is 5 GHz and the target is in the current block; everything else is unchanged. - Hop time: median 2.2 ms on the 8852C (max 2.5 ms over 115 hops) vs ~213 ms full; median 18 ms on the 8852B vs ~145 ms full (its USB register round-trips are the slower ones, as for its 20 MHz hop). - iPhone over AWDL, schedule alternating 149/161, on the 8852C: full path 206 hops, ~44 s deaf in a 49 s transfer (49.5 MB, ~1 MB/s); fast path, nothing else changed, 50.8 MB in 7.5 s (~6.8 MB/s). - txdemo alternating 149/161 at 80 MHz (50 frames a dwell, 5 rounds, 250 frames on 161), rxdemo at 20 MHz on 161: 200+ of 250 on both dies, with DEVOURER_HOP_FAST=1 and with the full path alike (rxdemo reports every 100th hit). - Register parity against the full path, per die: BB 0x0000-0xFFFF (RF direct window included) and CMAC 0xC000-0xCFFF dumped after 3 full tunes of each primary and after 12 fast hops, for the pairs 149/161, 149/153 and 157/161 (both 5 MHz mask branches). Every register the full path sets per primary matched on all 36 hops of each die. The other differences either also differ between two full tunes of the same channel (SW-SI counters and records, RX counters, AFE status outputs, the NCTL/IQK engine status the skipped IQK leaves from the previous tune) or showed on one hop of six (a few CMAC status words undocumented in rtw89); a write the fast path missed would show on every hop. - The fast hop skips the IQK the full path re-runs and relies on the RF center being unchanged; MAC retries stayed at 2-4% over the iPhone sessions (one adapter, one peer, near field). - 80 MHz only, 5 GHz only. At 40 MHz FastRetune keeps the tune's primary offset, so a hop moves the block (center = channel +/- 2) and there is no same-block case to shortcut. The 5 MHz mask uses the 0x4 value of both dies; the 8852D variant's 0x5 is not handled. --- docs/frequency-hopping.md | 9 +++++++++ src/kestrel/CLAUDE.md | 27 +++++++++++++++++++++++++++ src/kestrel/HalKestrel.cpp | 32 ++++++++++++++++++++++++++++++++ src/kestrel/HalKestrel.h | 4 ++++ src/kestrel/RtlKestrelDevice.cpp | 11 +++++++++++ 5 files changed, 83 insertions(+) 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 3b87d68e..f9fa61aa 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -159,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/ diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index cd51cb9d..89807913 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -2694,6 +2694,38 @@ void HalKestrel::mac_set_bw_txsc(uint8_t channel, uint8_t center, 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 diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index 42808384..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 diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index aa5655f8..f71cf871 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; From d0048dff959c9b5ff1ed05003488097079588521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Sun, 18 Oct 2026 00:08:01 -0300 Subject: [PATCH 4/5] kestrel: FastSetBandwidth leaves a wide channel through the full path FastSetBandwidth took the lean 20 <-> 5/10 MHz toggle whenever the target was narrow, whatever the current width. From a 40/80/160 MHz channel that path only rewrites the BB small-BW field: the RF stays on the block center at the wide RF bw, and with set_channel now programming the MAC width (RFMOD + TX sub-carrier) the MAC stays wide too, and no later FastRetune repairs either. The lean path now needs both ends narrow; leaving a wide channel takes the full SetMonitorChannel, as entering one already did. - Measured with FastSetBandwidth(20) from 149/80, compared with a full SetMonitorChannel(149/20) over BB 0x0000-0xFFFF and CMAC 0xC000-0xCFFF: before, RFMOD 2 vs 0, TXSC 0x0a4 vs 0, RF18 0x1059b (ch 155, 80 MHz) vs 0x10d95 (ch 149, 20 MHz), 43 registers differing on the 8852C and 36 on the 8852B; after, RFMOD, TXSC and RF18 match on both dies. The 3 (C) / 7 (B) registers still differing are the ones that differ between any two full tunes of a channel, which is what the comparison now is. - The RF part predates the MAC writes: the lean path never re-tuned. --- src/kestrel/RtlKestrelDevice.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index f71cf871..83c8cf85 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -779,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); From 293236d9a1eb0d7eb0f8969b97d06cf89fed5206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Sun, 18 Oct 2026 00:13:37 -0300 Subject: [PATCH 5/5] kestrel: 8852C fast 20 <-> 5/10 MHz toggle writes its own BB registers fast_set_bw wrote RF_BW and the small-BW field at 0x49C0/0x49C4, the 8852B's addresses (halbb_ctrl_bw_8852b), on both dies. halbb_ctrl_bw_8852c keeps them at 0x4974[31:30] and 0x4978[13:12], so on the 8852C the lean narrowband toggle never changed the bandwidth: the BB stayed at the previous width while the device recorded the new one. The ACI-detect bits (0x4738/0x4AA4[16]) are shared and unchanged. - Measured with FastSetBandwidth(10), (5), then (20) from a full 149/20 tune, each compared with full SetMonitorChannel(149/10|5|20) tunes: before, 8852C 0x4978 read 0x0003 after the 10 MHz toggle where a full tune leaves 0x2003, and 0x2003 after the 5 MHz one where it leaves 0x1003; after, 0x4978 matches on every step. The 8852B, whose addresses were right, is unchanged. --- src/kestrel/HalKestrel.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 89807913..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();