Skip to content

kestrel: 80 MHz on the 8852C and 8852B — RX width, TX sub-carrier, fast same-block hop - #494

Merged
josephnef merged 5 commits into
OpenIPC:masterfrom
agustinglez:kestrel-8852c-80mhz
Oct 18, 2026
Merged

josephnef merged 5 commits into
OpenIPC:masterfrom
agustinglez:kestrel-8852c-80mhz

Conversation

@agustinglez

Copy link
Copy Markdown
Contributor

In short

On the Kestrel chips, an 80 MHz channel did not really work for talking to another device. Three separate problems, one per commit (the first is 8852C-only, the other two apply to the 8852C and the 8852B alike):

  1. Received frames reported the wrong width. Every 80 MHz frame was reported as 40 MHz, and every 40 MHz frame as 20, because the 8852C stores the width in a slightly wider field than the 8852B and devourer read it the 8852B way.
  2. Our own 20 MHz frames went out in the wrong place. With the radio tuned to an 80 MHz channel, a normal 20 MHz frame was transmitted in the middle of the 80 MHz block instead of on the main (primary) channel, so a device listening on the primary channel never heard it. devourer never told the MAC about the channel width; it now does, the same way the Linux driver (rtw89) does.
  3. Hopping between two channels of the same 80 MHz block took ~213 ms on the 8852C (~145 ms on the 8852B). Both channels sit in the same block, so the radio does not have to move at all, only "which 20 MHz is the main one" changes. A new fast path does just that, in ~2.2 ms on the 8852C and ~18 ms on the 8852B.

Found and tested with an iPhone sending files over AWDL (AirDrop) to a TP-Link TX50UH. Before, the iPhone could not see the adapter at 80 MHz at all. With fix 2 it could, but transfers stalled at every 149/161 hop (49.5 MB in 49 s); with fix 3, 50.8 MB took 7.5 s with nothing else changed. (With further work on the AirDrop side, outside devourer, a 437 MB file now arrives in about 8 s on these three commits.)

Fix 2 was also checked on air on both chips with two adapters, one sending and one listening on the main channel: on master, none of the frames sent at 40, 80 or 160 MHz arrived (0 of 500 in every case, on the 8852C and the 8852B); with the fix, 400+ of 500 in every case, the same as the 20 MHz control.

Fix 3 was checked the same way on both chips: comparing every register against the slow path, and on air (frames sent after a fast hop arrive on the new main channel just like after a slow one). It applies at 80 MHz on 5 GHz; at 40 MHz there is no same-block hop to shortcut (a hop there moves the block), and every other hop keeps the existing path.

Technical detail

1. kestrel: read the 8852C RX bandwidth from dword1 [31:29]

The 8852C RX descriptor widens the bandwidth field to dword1 [31:29] (rtw89 AX_RXD_BW_v1_MASK, chosen 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 80 MHz read as 1 and 40 MHz as 0. The die is told apart by the 16-byte drv_info unit the parser already receives.

Only RxAtrib.bw changes. The library 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.

Measured: an iPhone sending a 437 MB file on ch149 at 80 MHz. 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 airtime than a 40 MHz channel has; after, all 309k data frames read 80 MHz.

2. 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 left the MAC at 20 MHz: R_AX_WMAC_RFMOD (0xC010) unwritten and R_AX_TX_SUB_CARRIER_VALUE (0xC088) at the zeros cmac_com_init writes. A PPDU narrower than the channel then airs at sub-carrier index 0, the block center. mac_set_bw_txsc() now writes both after every tune, as rtw8852c_set_channel_mac does: RFMOD 0/1/2/3 for 20/40/80/160, and the whole sub-carrier register from a line-for-line port of rtw89_phy_get_txsc. Primary 149 / center 155 gives TXSC20 4 and TXSC40 10 (0x0a4), primary 161 gives 0x093, as rtw89 computes.

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 TX50UH tuned to 149/80: without the writes the iPhone, listening on 149, never discovered the adapter (its own frames reached us fine); with them it did, and transfers ran.

On-air A/B on both dies, no iPhone: txdemo (500 canonical 6M beacons, DEVOURER_CCA_GATES=0,1) from a DUT tuned wide with DEVOURER_HOP_BW, rxdemo on the other adapter at 20 MHz on the primary, counting rx.txhit. Same txdemo source at upstream master (before) and at this branch (after); TX50UH (8852C) and TX20U Nano (8852B), swapping roles.

DUT channel / width (center) primary TXSC written before after
8852C 149 / 40 (151) lower 0x002 0 400+
8852C 153 / 40 (151) upper 0x001 0 400+
8852C 149 / 80 (155) index 4 0x0a4 0 400+
8852C 161 / 80 (155) index 3 0x093 0 400+
8852C 36 / 160 (50) index 8 0xec8 0 500
8852B 149/40, 153/40, 149/80, 161/80 as above as above 0 400+ each
both 149 / 20 (control) — 0x000 400+ 400+

rxdemo reports rx.txhit for the first 10 hits and then every 100th, so 400+ means 400-499 of 500. The TXSC values are what rtw89_phy_get_txsc gives for each primary and center. Band 0 only, like the rest of the HAL; 20 MHz writes RFMOD 0 and a zero register, the values it had before.

3. kestrel: fast retune within one 80 MHz block

A FastRetune at 80 MHz fell back to the full SetMonitorChannel (BB bandwidth config, RF tune, and on the 8852C the IQK). For a target in the same 80 MHz block on 5 GHz, FastRetune now calls fast_set_primary_80(), which rewrites only what halbb_ctrl_bw_ch_8852{b,c} derives from the primary at 80 MHz plus the MAC sub-carrier, then resets the BB. The registers are each die's own: the [11:8] sub-band index at 0x4978 (C) / 0x49c4 (B), and the halbb_5m_mask_8852{b,c} edge mask at 0x4c4c/0x4d10/0x4450[24] (C) / 0x46f8/0x47b8/0x4440[31] (B). Everything else keeps its existing path.

full path fast path
hop time, 8852C ~213 ms median 2.2 ms, max 2.5 ms (115 hops)
hop time, 8852B ~145 ms median 18 ms (its USB register round-trips are the slower ones, as for its 20 MHz hop)
iPhone transfer on the 8852C, schedule alternating 149/161 49.5 MB in 49 s, ~44 s of it deaf 50.8 MB in 7.5 s, nothing else changed
txdemo hopping 149/161 at 80 MHz, 250 frames on 161, rxdemo on 161 200+ (both dies) 200+ (both dies)

Register parity against the full path, on each 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 branches of the 5 MHz mask). Every register the full path sets per primary matched on all 36 hops of each die. On the 8852C:

register 149 153 157 161
BB 0x4978 0x403 0x203 0x103 0x303
BB 0x4c4c / 0x4d10 0x1044 0x0044 bit 12 clear, rest as before 0x1104
BB 0x4450 0x656e371b 0x646e371b 0x646e371b 0x656e371b
MAC 0xC088 0x0a4 0x0a2 0x091 0x093

On the 8852B the same pattern at its own addresses (0x49c4 = 0x4403/0x4203/0x4103/0x4303, the mask at 0x46f8/0x47b8/0x4440, plus their 0x69c4/0x6440 mirrors and 0x1b44, which the full path also moves per primary), and MAC 0xC088 as above.

The other differences either also differ between two full tunes of the same channel (SW-SI counters and records at 0x0370/0x0378/0x174c-0x1764, RX counters 0x1a14-0x1a80/0x1b10-0x1b34/0x3c94, the AFE status outputs 0x1cb0/0x3cb0, and NCTL/IQK engine status at 0xb1fc/0xbff4/0xbffc that the skipped IQK leaves from the previous tune), or showed on one hop of six (CMAC 0xC5B0/0xC5B4 on the C, 0xCCF8/0xCEF0 on the B, undocumented in rtw89). A write the fast path missed would show on every hop.

Limits: 80 MHz and 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; making it flip the offset instead would change what FastRetune does for existing 40 MHz hoppers. The 5 MHz mask uses the 0x4 value of both dies; the 8852D variant's 0x5 is not handled. The fast hop skips the IQK the full path re-runs; MAC retries stayed at 2-4% over the iPhone sessions (one adapter, one peer, near field), and the shortcut relies on the RF center not moving.

Docs

src/kestrel/CLAUDE.md: the MAC side of the width in the validated surface, the RX bandwidth field in the 8852C/8852B divergences, and the 80 MHz same-block hop in the FastRetune section. docs/frequency-hopping.md: a paragraph on the 80 MHz shortcut.

Build

Each commit builds on its own (MSVC, target devourer).

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.
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.
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.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix Kestrel wide-channel RX, primary-channel TX, and 80 MHz hops

🐞 Bug fix ✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Correct 8852C receive-width reporting and place narrow transmissions on the primary channel of
 wide channels.
• Speed up same-block 80 MHz hops on both Kestrel dies without moving the RF center.
• Document the shortcut’s limits and on-air validation.
Diagram

graph TD
  D["FastRetune"] --> G{"Eligible 80 MHz hop?"} -->|Yes| F["BB primary update"] --> M["MAC width and TXSC"]
  G -->|No| T["Full RF tune"] --> M
  R["RX descriptor"] --> P["Die-aware width parser"] --> A["RxAtrib bandwidth"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep full tunes for every 80 MHz hop
  • ➕ Preserves the existing calibration path and minimizes specialized register logic.
  • ➖ Retains approximately 145–213 ms hops even when the RF center does not change.
2. Firmware-offload the same-block register writes
  • ➕ Could reduce host-side USB round trips, particularly on the 8852B.
  • ➖ Adds firmware command and completion behavior to validate for a small, die-specific write sequence.

Recommendation: Use the bounded direct-register shortcut in this PR: it preserves full tuning for other hops and has documented register-parity and on-air checks on both dies. Firmware offload is worth considering only if further latency work justifies its additional validation; the shortcut’s skipped IQK warrants continued testing beyond the reported near-field sessions.

Files changed (7) +192 / -3

Enhancement (3) +133 / -0
HalKestrel.cppProgram MAC sub-carriers and add the fast primary update +116/-0

Program MAC sub-carriers and add the fast primary update

• Adds MAC width and TX sub-carrier programming to every full tune so narrower frames air on the primary channel. Adds a die-specific 80 MHz primary update that changes BB primary and edge-mask settings, updates the MAC, and resets the BB without an RF tune or IQK.

src/kestrel/HalKestrel.cpp

HalKestrel.hDeclare MAC width and fast-hop helpers +6/-0

Declare MAC width and fast-hop helpers

• Declares the channel-width programming helper and the 80 MHz primary-only retune helper, including the latter’s 5 GHz scope.

src/kestrel/HalKestrel.h

RtlKestrelDevice.cppRoute eligible 80 MHz hops through the fast path +11/-0

Route eligible 80 MHz hops through the fast path

• Detects same-block 5 GHz hops at 80 MHz and invokes the primary-only HAL update. Other hops continue through the existing retune behavior.

src/kestrel/RtlKestrelDevice.cpp

Bug fix (2) +8 / -2
FrameParserKestrel.hDecode RX bandwidth using each die’s field layout +6/-2

Decode RX bandwidth using each die’s field layout

• Reads dword1 bits [31:29] for the 8852C and retains [31:30] for the 8852B, using the existing driver-info unit to distinguish them. Corrects reported receive widths without changing frame handling.

src/kestrel/FrameParserKestrel.h

MacRegAx.hDefine the MAC radio-width register +2/-0

Define the MAC radio-width register

• Adds the WMAC RFMOD address and width mask used to select 20, 40, 80, or 160 MHz operation.

src/kestrel/MacRegAx.h

Documentation (2) +51 / -1
frequency-hopping.mdDocument same-block 80 MHz hop behavior +9/-0

Document same-block 80 MHz hop behavior

• Explains why a primary-channel change within one 80 MHz block avoids an RF tune. Records hop-time comparisons, the BB reset, and the skipped IQK.

docs/frequency-hopping.md

CLAUDE.mdRecord wide-channel fixes and validation +42/-1

Record wide-channel fixes and validation

• Documents MAC width and TX sub-carrier programming, the 8852C RX descriptor difference, and the fast-hop scope. Includes on-air results, register-parity observations, timing, and calibration limits.

src/kestrel/CLAUDE.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Narrow switches keep wide transmit mode ✓ Resolved
Description
set_channel() now programs the MAC width and transmit sub-carrier, but FastSetBandwidth() can
switch from 40 or 80 MHz to 20, 10, or 5 MHz without updating either register. After that switch,
the device records the narrower width while the MAC retains the previous wide setting, and later
fast retunes do not repair it.
Code

src/kestrel/HalKestrel.cpp[2780]

+  mac_set_bw_txsc(channel, center, bw);
Evidence
The new write establishes a width-dependent MAC state on full tunes. The existing fast bandwidth
dispatch accepts a narrow target regardless of the current width, updates the recorded width, and
calls a helper that writes BB registers but not the MAC registers; ordinary fast retunes also leave
those registers untouched.

src/kestrel/HalKestrel.cpp[2651-2695]
src/kestrel/RtlKestrelDevice.cpp[780-791]
src/kestrel/HalKestrel.cpp[2549-2564]
src/kestrel/HalKestrel.cpp[2501-2547]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A full channel tune now sets the MAC width, but a wide-to-narrow `FastSetBandwidth()` switch bypasses that write and leaves the MAC in its previous wide mode.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[780-791]
- src/kestrel/HalKestrel.cpp[2780-2780]
## Recommended Fix
Restrict the narrowband fast path to transitions whose current width is already 20, 10, or 5 MHz. Route transitions from 40, 80, or 160 MHz through `SetMonitorChannel()` so RF, BB, and the newly programmed MAC width change together.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can move or hide resolved findings to focus on what still needs action

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/kestrel/HalKestrel.cpp
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.
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.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed hunk by hunk against the vendored halbb and the vendor mac_ax, and reproduced the on-air A/B on an independent pair of TX50UH (8832CU) units. No correctness bugs found. Approving, with three notes below.

Source cross-check (all match):

  • phy_get_txsc is line-for-line halbb_get_txsc (every cbw/dbw branch incl. the 0xff case, which field() masks to 4 bits).
  • fast_set_primary_80's index formula and the 5 MHz mask writes (0x3f=0x4, [12], [8], [6], 0x4450[24] / 0x4440[31]) match halbb_ctrl_bw_ch_8852{b,c} + halbb_5m_mask_8852{b,c} for the 80 MHz case; block() in FastRetune is exactly set_channel's 5 GHz 80 MHz center formula.
  • fast_set_bw's 8852C addresses (0x4974 RF_BW, 0x4978 small-BW) match halbb_ctrl_bw_8852c. That commit fixes a real pre-existing bug (the C was being written at the B's 0x49C0/0x49C4) and is not in the PR description — worth a line there, since the body still says three commits and the branch has five.
  • AX_RXD_BW_v1_{SH,MSK} (29 / 0x7) is in both vendor rxdesc.h files.

On-air, two 8832CU on this bench (both USB 2.0, bus 3 TX / bus 9 RX), 500 canonical 6M/20 beacons per cell, DEVOURER_CCA_GATES=0,1, witness at 20 MHz on the primary:

cell txsc logged master this branch
149/20 control (start / end) — 408 / 404 324 / 500
149/40 (center 151) 0x002 0 329
153/40 upper (center 151) 0x001 0 316
149/80 (center 155) 0x0a4 0 308
161/80 (center 155) 0x093 0 297
36/160 (center 50) 0xec8 0 345
hop 149/161 at 80, 50/dwell, witness on 161 both 0 202
149/80, VHT1SS_MCS3/80, witness at 80 — decoded bw 0x0a4 66 hits, bw=1 58 hits, bw=2

The wide cells sit inside the control's own 324..500 band, so they are at parity with 20 MHz here as in your table. Hop switch_us over 10 dwells: master (full path) median 588 ms on this host, this branch median 3.2 ms (max 3.4). 8852B was not on the bench, so the B-side numbers are yours alone.

Notes (none blocking):

  1. mac_set_bw_txsc logs at info on every call, and fast_set_primary_80 calls it per hop — the hop cell above wrote 10 lines for 10 hops. At the slot cadence this hop is for (DEVOURER_HOP_SLOT_MS 10 ms) that is ~100 flushed stderr lines/s at the demos' default level, where the existing fast_retune logs at debug. Suggest debug, or log only from the set_channel call site.
  2. The vendor mac_set_bw writes R_AX_TXRATE_CHK (0xC628, mask 0x13: BAND_MODE on 2.4 GHz; CHECK_CCK_EN | RTS_LIMIT_IN_OFDM6 on 5/6 GHz) in the same step as RFMOD + TXSC, and nothing in src/kestrel/ writes it. Pre-existing, no measured failure; the plausible consumer is the MAC-chosen response rate of the hardware ACK responder. Either port it alongside or note it as out of scope in the CLAUDE.md paragraph.
  3. Same-block detection relies on _channel.Channel tracking; a caller that calls SetMonitorChannel with a channel outside the 80 MHz plan (e.g. 165/80) gets block() = 165, center 171, consistent with set_channel's own extrapolation — fine, just noting it was checked.

@josephnef
josephnef enabled auto-merge (squash) October 18, 2026 05:24
@josephnef
josephnef merged commit 357f045 into OpenIPC:master Oct 18, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants