Skip to content

Kestrel (RTL8852B/RTL8852C, Wi-Fi 6): one HAL for both dies on the verbatim-vendored halbb/halrf plane#293

Merged
josephnef merged 175 commits into
masterfrom
feat/kestrel-8852
Jul 16, 2026
Merged

Kestrel (RTL8852B/RTL8852C, Wi-Fi 6): one HAL for both dies on the verbatim-vendored halbb/halrf plane#293
josephnef merged 175 commits into
masterfrom
feat/kestrel-8852

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Kestrel (RTL8852BU / RTL8832CU) — one design for both dies, vendored BB/RF plane

Refactors the Kestrel HAL so both chips share one architecture, with Realtek's C reused verbatim wherever the vendor ships it:

  • mac_ax plane (power-on / FWDL / efuse / MAC TRX / H2C-C2H / USB / descriptors): hand-ported C++, both dies through the same code with per-variant register banks (_V1) and descriptors.
  • BB/RF plane: Realtek halbb/halrf compiled VERBATIM for both dies — one chip-agnostic core + per-chip backends under hal/halbb/g6/ + hal/halrf/g6/ (vendor/ untouched vendor files, shim/ + hal_headers_le.h the minimal PHL surface, kestrel_hal{bb,rf}_glue.c the C entry points, tools/vendor_hal{bb,rf}_*.sh re-vendor). The vendored loaders own the BB/RF/gain tables, per-channel BB config, RF tune, DACK/RX-DCK (+ IQK on the 8852C); radio pages reach the fw through the bridge send_h2c into the mac_ax H2C encoder.
  • All hand-rolled BB/RF fallbacks and the extract-generated table copies are deleted (−54K lines): the vendored hwimg arrays are the only tables.
  • Builds and CI-passes on all platforms — Linux GCC/Clang, macOS, MinGW, MSVC — plus new kestrel-8852b-only / kestrel-8852c-only CI cells.
  • No milestones/WIP language; no usbmon-capture debris (the port is source-matched to reference/rtl8852bu @ v1.19.21 and reference/rtl8852cu).

Real bugs found and fixed on the way

  • Cold-boot power-on (both dies): a USB chip from real cold reads WLMAC_PWR_STE=MAC_ON (its AFSM auto-powers to enumerate); the vendor pwr.c prologue forces the MAC off first — without it the WCPU bootrom never raises H2C_PATH_RDY. The auto-probing in-tree rtw89 had pre-initialized the chip in every earlier session, masking this.
  • HALBB_*_SUPPORT=0 defines: the vendor tests them with #ifdef, so =0 compiled fw-offload IN and the 8852B BBCR writer applied zero registers. Off = undefined.
  • Coex GNT from cold: B_AX_ENBT must be set before any LTE-space access; the GNT arbitration is now SW-forced to WiFi (mac_cfg_gnt_8852b stance).
  • RFK prologue position is load-bearing: NCTL/SI-reset/LCK/RCK/efuse-trim now run at phy_bb_rf_init (the vendor's halrf_dm_init position). Run lazily after the tune, the a-die SI reset deafens the radio both ways (bisected on-air).
  • Shutdown SIGABRT: Stop() now joins the WP-release drain thread before the demos call libusb_exit (libusb usbi_mutex_destroy assertion, 5/5 reproducible → 0/10).

Evidence-gated calibration stance

TSSI/DPK (both dies) and per-channel IQK (8852B) are gated off: under the fixed-dBm power model they measurably degrade TX (8852B IQK: paired cold A/B on a 5 GHz MCS0 flood, 8812AU monitor — 10/1 decodes with IQK vs 100/37 without). The 8852C validated on-air WITH IQK and keeps it. Gates fall with a TSSI-referenced power model.

Hardware validation (final ladder, VBUS-cold, rtw89 temp-blacklisted)

Check Result Baseline
identity (kestrelprobe) PASS both dies
8852BU rx_smoke ch36 17 events 17
8852BU tx_sdr duty ch6 / ch36 79.5% / 66.9% 80.6% / 66.0%
narrowband occ-BW 20/10/5 15.6 / 8.2 / 4.1 MHz 16.4 / 8.2 / 4.2
bw_rx 20/40/80 + clean DACK PASS PASS
beacon_onair (HW TBTT) PASS (11 txhits) PASS
8832CU rx_smoke ch36+ch6 PASS PASS
8832CU TX matrix (6 cells incl. HE) 6/6, 58–88% duty 6/6, 79–88%
5G-160 HE-MCS7 49.2% duty ≈ 66.4 Mbps ~66.6 Mbps
6G-80 HE-MCS7 45.3% duty 45%
6G-160 (documented limitation) 2.1% duty (witness) 0.8%
EVM MCS0 (8812AU monitor) 8832CU −14 dB n=1606; 8852BU −6 dB n=769 — (absolute)
inject_sanity (rtw89 witness) PASS PASS
streamtx_sdr (video path) 32.1% duty, no stall PASS
init→first-RX-frame 1.3 s (B) / 3.1 s (C) no regression gate hit
regress.py ch6 + ch36 4/4 both 4/4

Known limitation kept as documented: 6 GHz TX tops out at 80 MHz on the 8832CU (the 6G+160 TX-enable path is un-ported; RF locks and RX-160 works — B210-confirmed). MCS7 TX→monitor decode is bench-limited at the current geometry (link EVM ≈ −14 dB at MCS0; both dies, pre- and post-refactor identically).

🤖 Generated with Claude Code

josephnef and others added 30 commits July 12, 2026 11:51
… submodules, PID-first dispatch, kestrelprobe, fw extractor

Start of the Kestrel HAL (issue #236 prerequisite): the 802.11ax G6-phl
generation, first target RTL8852BU (variant C8852B), second RTL8832CU
(C8852C). The 8852A family stays out (frozen 2021 v1.15 vendor drop only).

- reference/rtl8852bu + reference/rtl8852cu submodules (josephnef forks of
  the morrownr v1.19.21/v1.19.22 Realtek drops — the line whose fwcmd
  surface carries the TWT-OFDMA + F2P trigger H2Cs #236 needs).
- PID-first factory dispatch: on AX silicon 0x00FC is R_AX_SYS_CHIPINFO,
  and the 8852A die-id (0x50) collides with the 8822B cold-boot transient —
  so Kestrel gates on the vendor usb_intf.c PID table
  (src/kestrel/KestrelUsbIds.h) before the SYS_CFG2 read; the die-id
  (0x51/0x52) + cut (R_AX_SYS_CFG1[15:12]) are read back as confirmation.
- src/kestrel/ skeleton: RtlKestrelDevice (IRtlDevice; identity implemented,
  unported entry points throw naming their milestone), ChipVariant.
- kestrelprobe: staged bring-up prober (id | power | fw), pcieprobe sibling;
  tests/kestrelprobe_id.sh wraps it with rtw89 unbind/rebind.
- tools/extract_8852b_fw.py -> hal/hal8852b_fw.{c,h}: both NICCE images
  (cut-selected at runtime, CBV->u2 / CCV+->u3), the category the Linux CE
  driver itself loads.
- CMake DEVOURER_KESTREL_8852B/_8852C options (+ DEVOURER_HAVE_* defines),
  kestrel-only CI config row, Kestrel-OFF flags on the existing rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ware confirmation

tests/kestrel_vendor_ko_smoke.sh — insmod each Kestrel vendor module
(8852bu.ko / 8852cu.ko), watch it bind the adapter and register a netdev,
bring the link up (udev-rename tolerant), then rmmod and rebind rtw89.
Both pinned tips pass unpatched on 6.18.33-1-lts — no devourer-kbuild
branch needed for this generation (noted in reference/README.md).

Hardware M0 exit criteria met on both DUTs meanwhile:
  35bc:0108 -> die-id 0x51 (8852B), cut 2 (CCV -> u3 NICCE image for M1)
  35bc:0101 -> die-id 0x52 (8852C), cut 1 (CBV) — hardware proof of the
  TX50UH = RTL8832CU identification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from reference/rtl8852bu mac_ax:
- HalKestrel: register-op helpers (poll, XTAL_SI indirect write, efuse
  power-cut), the USB power-on sequence (usb_pre_init_8852b +
  mac_pwr_on_usb_8852b verbatim — SYS_PW_CTRL handshake, AFE LDO, SPS rails,
  platform-enable toggle, the ANAPAR_WL XTAL_SI recipe, DMAC/CMAC func-en),
  and the physical efuse read loop (read_hw_efuse, DDV bank, 8852B
  EFUSE_WAIT_CNT_PLUS) + the eeprom_parser physical->logical map walk.
- MacRegAx.h: the AX register/bit subset the bring-up touches (verbatim from
  mac_reg_ax.h; 0x00FC is R_AX_SYS_CHIPINFO here, a different map from 11ac).
- kestrelprobe "power" stage + tests/kestrelprobe_id.sh stage arg.

HARDWARE VALIDATED on the TX20U Nano (35bc:0108): power_ok, pwr-K=0xAA
(power-cal done), and the parsed efuse MAC = 3c:6a:d2:79:52:02 — byte-for-byte
identical to the kernel rtw89_8852bu-reported MAC. That match validates the
whole chain (power-on -> efuse block enable -> physical read -> logical
parse) at once. xtal=0x4d rfe=0x01 also read cleanly.

Next (M1b): firmware download over the H2C/CH12 path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…complete

Ported the full FWDL path from reference/rtl8852bu mac_ax into
src/kestrel/KestrelFw:
- dmac_pre_init: dmac_func_pre_en + dle_init(DLFW quota: wde_size9/ple_size8,
  wcpu=6, c2h=16, h2c=48) + hfc_init(rst,en=0,h2c_en=1) — stands up the H2C
  (CH12) transport.
- FWDL state machine: mac_disable_cpu -> mac_enable_cpu(dlfw) -> mac_fwdl
  (fwhdr parse -> phase0 H2C_PATH_RDY -> phase1 header H2C -> phase2 section
  chunks -> check_fw_rdy WCPU_FWDL_STS==INIT_RDY), 3-try retry (AX MIPS).
- H2C/FWDL framing: WD body (CH_DMA=12, FWDL_EN) + fwcmd_hdr (cat MAC / class
  FWDL / func FWHDR_DL) + payload, bulk-out to the CH12 endpoint = BULKOUTID2 =
  the 3rd discovered bulk-OUT (0x07 on the 8852BU; RtOutPipe[2] order verified
  in the vendor USB layer).
- Secure-boot MSS handling: the NICCE image is a type-9 security section with
  mssc=2 trailing 512B signatures; __mss_index (physical efuse 0x5EC/0x5ED vs
  the OTP key tables) picks one, patched into the section body at offset 448,
  and the trailing signatures are excluded from the download size.

Also fixed a latent M1a bug: usb_pre_init used fabricated register addresses
(0x1A0/0x1A2/0x98 from an assistant table) that happened to be harmless for
efuse; the real ones (R_AX_USB_HOST_REQUEST_2=0x1078, USB_WLAN0_1=0x1174,
HCI_FUNC_EN=0x8380, RXDMA=BIT1/TXDMA=BIT0) are essential for the DMA path and
now verified against hci_reg_ax.h/mac_reg_ax.h. Reading the source directly
(vs. trusting derived tables) also caught PLE_INI_STATUS=0x9100 (not 0x9000)
and the H2C/FWDL_PATH_RDY bit positions.

HARDWARE VALIDATED on the TX20U Nano (35bc:0108): sudo tests/kestrelprobe_id.sh
fw 35bc:0108 -> "firmware booted (attempt 1)", kestrel.fw ok:true. Full green
init: power-on -> efuse (MAC = kernel's 3c:6a:d2:79:52:02) -> 343448-byte u3
NICCE image downloaded (170 section chunks) -> WCPU FW_INIT_RDY.

tests/kestrel_kernel_capture.sh added as an optional usbmon golden-reference
diff (vendor .ko vs devourer write stream); the port itself is source-driven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FrameParserKestrel.h — parses the G6 mac_ax RX descriptors (rxd_short 16B /
rxd_long 32B, fields verified against reference/rtl8852bu rxdesc.h + type.h):
rpkt_len/shift/drv_info_size/rpkt_type/long_rxd from dword0, rate/gi_ltf/bw
from dword1, freerun_cnt from dword2, a1_match/crc32/icv from dword3. Computes
the sub-packet payload offset (rxd + drv_info*8 + shift) and the 8-byte-aligned
next_offset for walking a bulk-IN aggregate. Routes rpkt_type
(WIFI/PPDU/CH_INFO/C2H) so the RX loop can separate 802.11 frames, per-frame
PHY status, and firmware C2H.

Header-only + self-contained so it unit-tests without hardware:
tests/kestrel_rxparse_selftest.cpp (ctest kestrel_rxparse) covers short/long
descriptors, drvinfo+shift offsets, CRC/ICV flags, 9-bit HE rate codes, C2H
routing, a two-frame aggregate walk, and truncated-buffer rejection.

The RX loop that feeds this (StartRxLoop) lands with M2a — the MAC TRX
bring-up (NIC-mode DLE/HFC + dmac_init/cmac_init RX path + promiscuous
rx_fltr + usb_rx_agg) that enables the chip to receive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nit passes

First slice of mac_trx_init ported from reference/rtl8852bu trxcfg.c, verified
register-by-register from source:
- dle_init_nic: re-init the DLE to the NIC (SCC) quota — wde_size25/ple_size33
  page config + wde_qt25 (WDE) and ple_qt74/ple_qt75 (PLE min/max, 11 fields)
  — replacing the DLFW quota used during firmware download.
- sta_sch_init: enable the station scheduler (R_AX_SS_CTRL SS_EN) and poll
  SS_INIT_DONE_1 — a real hardware checkpoint that the DLE + scheduler stood up.
- mpdu_proc_init: ACTION_FWD0 / TF_FWD / MPDU_PROC (APPEND_FCS | A_ICV_ERR) /
  CUT_AMSDU_CTRL. APPEND_FCS is why RX frames will carry the 4-byte FCS.
- sec_eng_init: SEC_ENG_CTRL clocks + TX-enc/RX-dec + MC/BC/mgmt decrypt.

kestrelprobe gains a "trx" stage (kestrel.trx event); tests/kestrelprobe_id.sh
grows the stage.

HARDWARE VALIDATED on 35bc:0108: `kestrelprobe trx` -> firmware booted, NIC DLE
WDE+PLE ready, sta-scheduler SS_INIT_DONE poll PASSES, kestrel.trx ok:true.
Empirical finding: the scheduler warm-init succeeds with the DLFW HFC still in
place — the full NIC HFC reprogramming (per-channel + public page quotas) is
NOT required for the DMAC init to come up green (noted in trx_dmac_init; it
becomes relevant only if data-plane flow needs it later). sec_info_tbl_init
(security CAM) skipped — not needed for monitor RX.

Next (M2a cont.): cmac_init (rx_fltr promiscuous / rmac / cmac_dma) + usb_rx_agg;
then M3 BB/RF/channel to actually receive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ome up green

Completes the MAC-layer TRX bring-up (cmac_init RX path, ported verbatim from
reference/rtl8852bu trxcfg.c + rx_filter.c):
- rx_fltr_init: MGNT/CTRL/DATA_FLTR = all-subtypes-to-host (0x55555555);
  R_AX_RX_FLTR_OPT = sniffer + accept A1/bc/mc/uc-cam/bc-cam (the vendor
  default sniffer profile).
- rmac_init: RESPBA_CAM SSN_SEL, DLK_PROTECT RX timeouts + reset-en, RCR
  CH_EN=0xF (the receiver channel-enable gate), RX_TIME_MON threshold, and the
  RX_FLTR_OPT max-MPDU-len field (31 = min(c0_rx_qta 178, 127)*128/512).
- cca_ctrl_init: R_AX_CCA_CONTROL clear-channel-assessment set/clear masks.
- cmac_com_init: TX subcarrier = 0, PTCL_RRSR1 OFDM+CCK enable.
- cmac_dma_init: clear RX full-mode ptr bits so RX DMA streams.
- usb_rx_agg_cfg: RX aggregation off (stream per frame for simple parsing).
The TX/protocol CMAC sub-inits (scheduler EDCA, NAV, spatial-reuse, tmac,
trxptcl, ptcl, addr-cam) are omitted — not needed for monitor RX.

HARDWARE VALIDATED on 35bc:0108: `kestrelprobe trx` -> firmware booted, DMAC
init, CMAC RX init, kestrel.trx ok:true. The MAC layer is now fully up; actual
reception needs the BB/RF/channel bring-up (M3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tools/extract_8852b_phy.py pulls the halbb + halrf raw_data register tables
(array_mp_8852b_phy_reg / _phy_reg_gain / _radioa / _radiob — {addr,value} u32
pairs with the phydm-style IF/ELSE/END/CHK conditional opcodes) from the
reference/rtl8852bu vendor source into hal/hal8852b_phy.{c,h}. Mirrors the
existing phydm extractors (extract_8822c_phy_tables.py).

Verified extraction counts: phy_reg 509 entries, gain 33, radioA 3607,
radioB 3711. The generated blob is wired into the build when the runtime table
walker (PhyTableLoaderKestrel, next) consumes it, alongside the halbb BB-init
sequence + halrf RF init + set_channel that together gate on-air RX.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PhyTableLoaderKestrel.h — walks the extracted {addr,val} register tables,
transcribed from the vendor walker (halbb_hwimg_8852b.c + halbb_hw_cfg.c):
headline {rfe,cut} variant selection (top-nibble-0xF entries) then the
IF/ELSE_IF/CHK/ELSE/END conditional opcodes, emitting a register write (via a
caller callback) only while the current branch matches. Opcodes (top nibble
4/8/9/a/b/f) never collide with real addresses (top nibble 0). Callback-based
so BB (write32) and RF (RF-serial write) share one walker, and header-only so
it unit-tests without hardware.

tests/kestrel_phytable_selftest.cpp (ctest kestrel_phytable): unconditional
writes, headline-derived IF/CHK matching (applied vs skipped by rfe), and ELSE
block termination.

Next: the BB init sequence (halbb_init: PLL/reset/path + apply phy_reg/gain via
this walker), halrf RF init (radioa/radiob), set_channel, then StartRxLoop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fix)

phy_bb_rf_init applies the halbb (phy_reg + gain) and halrf (radio-A/B) tables
via PhyTableLoaderKestrel — ~7500 register writes — and comes up green on
hardware without wedging the chip.

The critical fix: Realtek USB register addressing is (wIndex<<16)|wValue, and
the BB/RF register window lives at addr + bb0_cr_offset (0x10000) — i.e.
wIndex=1. devourer's 16-bit rtw_write path forces wIndex=0, so the first
attempt wrote BB values into the MAC/system space (wIndex=0) and wedged the
chip off the bus (recovered via uhubctl VBUS cycle). Added
IRtlTransport::write32_wide (32-bit address; USB splits it into wValue/wIndex,
default forwards to the 16-bit path) + RtlAdapter::rtw_write32_wide, and the
BB/RF emit now targets addr+0x10000. Root-caused by reading the vendor
"reg = (index << 16 | value)" — not a wire capture.

RF writes go through the same BB window (halbb_set_reg adds bb0_cr_offset), so
an RF write is a direct BB-window write, not a serial LSSI protocol:
0xe000(A)/0xf000(B) + ((addr&0xff)<<2) + 0x10000, low-20-bit value.

kestrelprobe gains a "phy" stage. HARDWARE VALIDATED on 35bc:0108:
`kestrelprobe phy` -> BB phy_reg + gain + radio-A/B applied, kestrel.phy
ok:true, chip stays alive. Next: set_channel (BB/RF tune) + StartRxLoop ->
ambient beacon = RX first light.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…channel set

The counterpart to write32_wide: masked BB writes (read-modify-write) and RF
reads over the halbb/halrf window (addr + 0x10000, wIndex=1) need a wide read.
USB override splits the address into wValue/wIndex and returns 0xFFFFFFFF on a
failed transfer (INVALID_RF_DATA sentinel); the default forwards to the 16-bit
read. Consumed by the channel-set machinery (set_channel) next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-to-end

set_channel ports the halbb ctrl_ch/ctrl_bw(20)/cck_en/bb_reset + the halrf
RF18 channel setting (DDV window), all over the wIndex=1 BB/RF window via the
new bb_rmw (masked RMW) / rf_read / rf_write helpers. RtlKestrelDevice::Init
now drives the whole monitor path — power -> efuse -> firmware -> MAC TRX ->
BB/RF tables -> channel tune -> StartRxLoop — and StartRxLoop walks the
bulk-IN aggregate with parse_rx_8852b (FrameParserKestrel), delivering WIFI
frames to the packet processor. SetMonitorChannel retunes live.

HARDWARE (35bc:0108, rxdemo on ch6): the entire chain executes green — "tuned
to ch6 bw20 (2.4G)", "starting RX loop", 8 RX URBs submitted, chip stays alive.
Frame decode is the remaining debug tail (the receiver is armed but not yet
delivering decoded beacons — next: the DAV/a-die RF path skipped here, RX
calibration (DACK/RXDCK), or BB DM/AGC/DIG init beyond the register tables).

tests/kestrel_rx_smoke.sh drives rxdemo through the bring-up and counts decoded
frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Log the first dozen bulk-IN completions (size + rxd dword0) so the RX-decode
debugging can distinguish "no data arrives" (RX-DMA/PHY not delivering) from
"data arrives but doesn't parse". Current finding on 35bc:0108 ch6: zero
bulk-IN completions — the receiver is armed but no frames flow, pointing at a
missing PHY/RX-delivery step rather than the parser.
…) — writes now land

Root-caused why RX never worked: the BB was held in reset the entire time, so
every halbb/halrf register write (the ~7500-write "tables applied" step) was
silently DROPPED — a BB register readback returned 0 even right after writing.

Ported set_enable_bb_rf (hw.c) into phy_bb_rf_init's prologue: R_AX_SYS_FUNC_EN
FEN_BBRSTB|FEN_BB_GLB_RSTN (BB reset release), SPS_DIG_ON ZCDC, the WLRF_CTRL
AFC_AFEDIG 1/0/1 toggle, XTAL_SI WL_RFC_S0/S1=0xC7, PHYREG_SET=XYN. All
MAC-space (wIndex=0).

VALIDATED on 35bc:0108: after the fix, BB 0x4004 reads back 0xCA014000 (its
exact phy_reg table value) and a scratch write round-trips — the baseband is
now actually configured. (Confirms the earlier wIndex=1 addressing was correct
all along; the BB was just asleep.) RX frame decode still needs the halbb DM
init (DIG/AGC) + RF calibration + RF18 tune verification — the remaining tail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rf_write_dav ports halbb_write_rf_reg_8852b_a — the a-die RF path via the BB
0x370 serial command register (0x174c busy-poll, w_reg = path<<28 | addr<<20 |
data for the full-20-bit case), all over the wIndex=1 BB window. set_channel
now writes RF18 to both the a-die (DAV) and d-die (DDV) windows + the 0xcf
re-latch, matching halrf_ch_setting.

RF18 readback still reads 0x400 (not the tuned channel) — so RF writes are not
round-tripping even though BB writes now do (BB 0x4004 reads its table value).
The RF interface (0xe000/0xf000 window) likely needs its own enable/select, or
RF18 returns synth/LCK status rather than the written value. Next: verify RF
register access with a non-synth round-trip and check for an RF-interface
enable in the halrf init.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…responding)

Read-only diag in set_channel confirms: after the BB-reset fix, BB registers
read back their table values (0x4004=0xCA014000) but RF register access is
dead — RF path-A reg 0x00 reads 0x00000, RF18 stuck at 0x400. So the radio
isn't responding through the 0xe000/0xf000 direct window despite WLRF_CTRL +
RFC LDO enable.

Two leads for the next pass:
1. The halrf radio tables use a DIFFERENT conditional walker than halbb
   (halrf_config_8852b_radio_a_reg: rfe/cut match via the v1 BIT(30) CHK and
   a leading 0xF00-header rfe enumeration, not the headline/top-nibble-CHK of
   the BB tables). apply_phy_table (the BB walker) mis-applies the RF tables —
   the RF needs its own walker so its power/mode registers actually get set.
2. RF may need a direct-access-mode enable / radio power-on beyond WLRF_CTRL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The halrf radio tables use a different conditional encoding than the halbb
tables (verified against halrf_config_8852b_radio_a_reg): bit31 entries carry
the {rfe,cut} IF/ELSE_IF/ELSE/END condition (rfe=v1[23:16], cut=v1[7:0],
0xFF=don't-care), a bit30 entry is the CHK, plain entries are writes gated by
the match — with a "highest matching cut wins" else-replay fallback. Ported it
verbatim as apply_rf_table and use it for radioa/radiob (the BB walker was
mis-selecting RF entries).

Does not by itself wake the radio: RF register access is still dead (RF path-A
0x00 reads 0 via the 0xe000 direct window). So the blocker is more fundamental
than the table variant — the radio isn't responding to the direct RF window.
Next: RF power-on / whether 8852BU-USB RF access must go through the DAV serial
window (0x174c/0x370) rather than the 0xe000 direct window, and the halrf init
sequence before the radio tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The usbmon golden-diff of the vendor 8852bu.ko bring-up settled how the
RF radio is actually programmed on USB: the vendor issues ZERO direct
BB/RF-window register writes and offloads every PHY register into H2C
batches the firmware replays on-chip (halbb_check_fw_ofld / halbb_fw_set_reg
-> rtw_hal_mac_add_cmd_ofld). Direct RF-window writes never reach the a-die
radio over USB, which is why the earlier direct-write path left RF00 dead.

This ports the two offload mechanisms:
- cmd_ofld (KestrelFw::ofld_begin/write/flush): 16-byte {src,type,path,
  offset,value,mask} commands batched into a class=FW_OFLD/func=CMD_OFLD_REG
  H2C, LC bit set on the last command per batch. Used for the RF radio
  tables (src=RF).
- radio-to-fw (KestrelFw::radio_page_to_fw): the packed (addr<<20|data)
  page cache as an OUTSRC H2C (class 8/9 = radio A/B, func = page).

Also:
- Fix the RF radio walker: the active vendor walker uses the BB
  headline/IF/CHK format (apply_phy_table), not the disabled bit31/bit30
  apply_rf_table variant.
- Add hfc_init_nic (usb_scc_8852b page/credit quotas) and the runtime
  dmac_func_en to trx_dmac_init.
- Generalize send_fwdl_packet into send_h2c_cmd(cat,class,func,...).

BB tables still applied via direct wIndex=1 writes (proven: BB 0x4004 reads
back 0xCA014000). Known remaining blocker: the running firmware does not
drain CH12 post-boot, so the offload H2C bulk-OUT times out (rc=-7) — a
post-fw-boot MAC-ready/H2C-arm handshake is still missing (see the kaeru
notes). Builds clean, 21/21 ctest pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…blocked)

Ports the missing runtime bring-up steps found by decoding the vendor
usbmon capture correctly:
- usb_init (usb_init_8852b / intf_init): endpoint NUMP burst config for
  EP5/6/7/9/10/11/12 + RX bulk size for the link speed + LFPS filter off,
  plus an HCI RXDMA/TXDMA re-kick after the DMAC/DLE init.
- hfc_init_nic now matches the vendor's USB h2c-only flow control
  (HCI_FC_CTRL = CH12_EN, FC_EN clear) instead of full FC.
- runtime dmac_func_en (0x8400 full engine enable) at the top of
  trx_dmac_init.

None of these unblock the CH12 IO-offload H2C yet — the running firmware
still does not drain CH12 post-boot (bulk-OUT NAKs -> rc=-7 timeout, which
is a DMA-drain/flow-control issue, not an endpoint STALL). Ruled out this
session: NIC HFC credits, dmac_func_en, USB NUMP, HFC h2c-only, HCI DMA
re-kick, DLE re-init, H2C batch size. The missing step is among the ~80
runtime register writes the vendor issues between fw-boot and its first
H2C; a golden-init replay is the next lever. Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure script

The fwcmd header TOTAL_LEN field must count the 8-byte fwcmd header plus the
content (h2cb->len in the vendor h2c_pkt_set_hdr), not just the content — the
firmware's H2C-queue parser uses it to find the next packet boundary, so an
8-byte-short value desyncs the queue. Matches the vendor's byte layout
(TOTAL_LEN == WD TXPKTSIZE == FWCMD_HDR_LEN + content).

Also adds tests/kestrel_self_capture.sh to usbmon-capture devourer's own
bring-up for diffing against the vendor .ko stream (WIP: the usbmon read
returns empty in this environment / the probe needs more robust device open
under the capture harness — to debug).

Does not yet unblock the CH12 H2C drain (the runtime firmware still NAKs the
offload bulk-OUT); this is a standalone correctness fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bset)

Replaces the cherry-picked "minimal RX" CMAC init with the complete vendor
cmac_init (trxcfg.c) transcribed verbatim for the 8852B band-0 ASIC path:
scheduler_init (SIFS/pre-backoff/beacon-EDCA), addr_cam_init (search range +
reset poll), nav_ctrl_init (two-nav 25ms), spatial_reuse_init, tmac_init
(TX underflow/FIFO MCS thresholds/preferred-AC), trxptcl_init (response
SIFS + RRSR 1M/legacy), ptcl_init (CMAC TX mode + AMPDU limits), alongside
the already-present rx_fltr/cca/rmac/cmac_com/cmac_dma. Ordered exactly as
the vendor cmac_init. rst_port_info + sec_info_tbl_init are host-side software
state (no chip registers) and are intentionally omitted.

Hardware (kestrelprobe phy, 8852BU): the full CMAC init runs green — the
ADDR_CAM reset poll completes (the CMAC is enabled and responding), and the
whole bring-up stays healthy. ~60 new register/bitfield constants transcribed
from mac_reg_ax.h + trxcfg.h. The CH12 H2C-offload blocker is unchanged
(confirming it is DMAC/firmware-side, not CMAC). Remaining trx_init pieces to
port next: mac_enable_imr (ser_imr_config) and coex_mac_init. Builds clean,
21/21 ctest pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports coex_mac_init_8852b verbatim: the LTE-coex indirect write helper
(mac_write_lte_8852b non-offload path — poll the LTE interface ready bit,
then WDATA + CTRL) plus the coex bring-up itself (disable LTE-coex CTRL and
CTRL_2, set the SYS_SDIO_CTRL coex bit). Wired into the trx bring-up after
dmac+cmac, matching mac_trx_init's order. On the WiFi+BT combo die the coex
block otherwise arbitrates the shared front end.

Hardware (kestrelprobe phy, 8852BU): runs green (LTE interface ready, no
timeout). CH12 H2C-offload blocker unchanged (DMAC/firmware-side). Remaining
mac_hal_init pieces to port: chip_func_en (OCP patch), mac_enable_imr
(ser_imr_config), feat_init/bacam_init, gpio_init. Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the 8852B chip_func_en OCP patch (SPS_DIG_ON_CTRL0 OCP_L1 field -> max)
to power_on's sys_init, alongside the existing DMAC/CMAC func_en. bacam_init
(mac_feat_init) is a no-op on 8852B (the vendor returns early for this chip),
so nothing to port there. Runs green on hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the full System-Error-Recovery interrupt-mask config (ser_imr_config_8852b
auto-gen) as a verbatim table: 16 DMAC-side masks (WDRLS/SEC/MPDU-tx/rx/
STA-sched/TXPKTCTL x2/WDE/PLE/PKTIN/host+cpu+other-dispatcher/CPUIO/BBRPT x2)
and 6 band-0 CMAC masks (DLE_CTRL/PTCL/SCHEDULE/PHYINFO/RMAC/TMAC), each doing
reg = (reg & ~clear_mask) | set_value with the auto-gen's documented values,
plus the phy-intf-timeout patch and mac_err_imr_ctrl(EN) (unmask the top-level
DMAC + CMAC0 error IMR). Wired into the mac_trx_init tail after coex.

With this the functional mac_hal_init is complete: power-on -> fwdl ->
dmac_init -> chip_func_en -> full cmac_init -> coex -> IMR -> usb_init. Only
gpio_init (LED/GPIO, non-functional) remains; bacam_init is a no-op on 8852B.

Hardware (kestrelprobe phy, 8852BU): full bring-up runs green. The CH12
H2C-offload blocker is unchanged, now against a fully-assembled MAC — which
localizes it decisively to the firmware's H2C servicing, not any host-side
init. Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n findings

Major diagnostic progress via usbmon capture of our OWN bring-up (captured
around the working kestrelprobe_id.sh): the firmware DOES service H2C — FWDL
completes and the first 2-3 runtime cmd_ofld batches succeed — then the CH12
H2C queue jams permanently (1s bulk-OUT timeouts). Root cause is NOT framing,
register state, HFC, or send-rate: the FW processes FWDL-class H2C but does
not drain/complete FW_OFLD-class (cmd_ofld) H2C, so the 32-page H2C pool fills
after ~2 batches (each ~16 pages) and never recovers. Smaller batches make it
worse (more leaked pages), confirming it's a processing failure, not a rate
issue.

Ports the vendor's flow-control handshake (poll_c2hreg / __recv_c2hreg): after
each cmd_ofld batch, poll R_AX_C2HREG_CTRL for the TRIGGER bit, drain data0..3,
and clear TRIGGER (the ack that releases the FW to return the H2C page). This
is correct vendor behavior, but currently the poll times out — the FW never
posts the IO_OFLD_RESULT, consistent with it not processing cmd_ofld at all.

Next: determine why the running FW ignores FW_OFLD-class H2C despite booting
from the same NICCE image the vendor uses (io_ofld capability enable / a
required H2CREG activation / MAC-ready signal). Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… dword1

Decisive diagnostic: routing a single BB register write through cmd_ofld to a
read-back-able register (BB 0x4004) leaves it UNCHANGED (0xca014000, not the
written 0x12345678) — with or without a concurrent bulk-IN drain. So the
running firmware does not process FW_OFLD-class (cmd_ofld) H2C at all; it only
accepts the packets into the H2C queue (which then leaks dry). This rules out
a C2H-packet-blocking cause and confirms the earlier page-leak finding.

Also fixes cmd_ofld dword1: the "base offset" field is the HIGH 16 bits of the
offset (offset>>16, = 0 for a 16-bit BB/MAC addr), per add_cmd's
GET_FIELD(offset, OFFSET) — was wrongly offset<<16 for the BB/MAC case (RF was
already correct at 0). Adds the KESTREL_TEST_OFLD diagnostic harness.

Next: why the fw's io_ofld handler is inactive despite booting from the NICCE
image the vendor processes cmd_ofld from — FWDL image/MSS byte-integrity, or a
missing runtime enable of the fw offload feature. Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full-header FWDL send (including the dynamic FWCAP header) broke FWDL
(FWDL_PATH_RDY timeout) — the vendor's fwdl_phase1 is also called with
(hdr_len - dynamic_hdr_len) (fwdl.c:1685/1746), so the dynamic header is NOT
part of FWHDR_DL; it is host-side cap metadata only. Restores the correct
minus-dynamic-header send and documents why. The cmd_ofld-not-processed
blocker is therefore NOT the missing dynamic header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the incrementing 3-bit H2C sequence number to send_h2c_cmd (the vendor
increments fwinfo->h2c_seq per H2C via h2c_pkt_set_hdr) — correct vendor
behavior, though it does not by itself unblock cmd_ofld processing.

Diagnostic milestone: byte-verified our extracted fw image (hal/hal8852b_fw.c
array_8852b_u3_nicce, 343448 bytes) is IDENTICAL to the vendor source
(reference .../hal8852b_fw.c) — same md5. So the cmd_ofld-not-processed
blocker is definitively NOT fw-image corruption; the fw is the exact image the
vendor kernel driver processes cmd_ofld from. It is a runtime-state difference.

Ruled out across this session: fw image (identical), dynamic-header download
(vendor excludes it too), H2C seq, C2H-packet blocking (bulk-IN drain),
send-rate/batch-size. Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more dead ends for the RF-programming blocker, both confirmed on hardware:
- cmd_ofld PKT variant (func 0x13, with bulk-IN drained) also leaves the test
  BB write unapplied — the fw ignores cmd_ofld regardless of REG/PKT response
  method.
- Direct a-die RF programming bypassing the fw: RF00 stays 0x00000 after both a
  DAV serial-command write (0x370) and a DDV-window write. The a-die radio does
  not respond to host-direct writes either.

So the radio can be programmed by neither cmd_ofld (fw ignores it) nor direct
host writes (a-die unresponsive) — both blocked by the same firmware boundary:
the running fw does not activate its io_ofld handler / the RF serial path that
programs the a-die, despite booting from the byte-identical NICCE image the
vendor uses. Keeps the KESTREL_TEST_OFLD diagnostic (now a direct-RF probe).
Builds clean, 21/21 ctest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch) — RF LIVE

THE BUG (found per "verbatim source must work"): the radio table lists each RF
register TWICE — once without BIT(16) (d-die, written via the DDV BB window
0xe000/0xf000) and once with BIT(16) set (a-die, written via the DAV serial
command 0x370). halbb_write_rf_reg_8852b dispatches on ad_sel=(addr>>16)&1:
set => a-die serial, clear => d-die window. My earlier code wrote EVERY entry
to the d-die BB window (masking addr&0xff), so the a-die radio synthesizer was
never programmed — RF00 read 0 and the radio was dead. This also sent me down a
long wrong path attributing it to the fw cmd_ofld/io_ofld handler.

Fix: apply the radioa/radiob tables via direct writes with proper BIT16
dispatch (rf_write_dav for a-die serial, rf_write for d-die window),
abandoning the cmd_ofld path entirely (which the fw was ignoring anyway — the
vendor's halbb_check_fw_ofld is false here, so it also uses direct writes).

kestrelprobe phy: fails=0 (no more CH12 jams), BB+RF direct tables applied
green. Builds clean, 21/21 ctest. RX validation next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
josephnef and others added 28 commits July 15, 2026 15:01
Adds the second Kestrel chip (RTL8852BU / C8852B, TP-Link Archer TX20U Nano
35bc:0108) to the supported-hardware table with measured MCS7/20 on-air TX
throughput: 2.4G 43, UNII-1 36, UNII-2/3 33 Mbps (SDR duty x PHY rate); 6 GHz —
(dual-band, no 6E front-end, unlike the tri-band 8832CU). Both Kestrel chips
verified working after this session's shared-code changes (band model, efuse
bridge, RF-tune unification): 8852BU 5 GHz RX decodes + TX airs on all three
bands; 8832CU RX/TX on 2.4/5/6 GHz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
set_channel gains the 160 MHz block/center derivation (8 20-MHz sub-channels,
center = block_start+14, pri_ch 1..8, band-aware grid origin: 5 GHz {36..64}->50,
6 GHz {1..29}->15); the vendored halrf_ctl_band_ch_bw_8852c (RF18[11:10]=00) +
halbb_ctrl_bw_ch_8852c (case CHANNEL_WIDTH_160 -> 160M ADC) already handle the RF
+ BB. AdapterCaps advertises kBw160 for the Kestrel generation; rx/tx demos parse
DEVOURER_BW=160 / DEVOURER_HOP_BW=160.

Validated on the RTL8832CU: 5 GHz 160 MHz on-air — tuned center ch50, synthLock=1,
RF18=0x10132 (5G + 160 encoding), SDR 49.3% duty -> ~66.6 Mbps (MCS7/160). build 0,
ctest 23/23.

6 GHz 160 MHz: the path tunes with the correct RF18 encoding (0x3000f = 6G + 160,
center ch15) but the synth does NOT lock on the tested TX50UH (synthLock=0, a
2-adapter TX->RX decode read 0) — a suspected 6E-160 front-end/synth constraint,
and un-SDR-confirmable since the 6G 160 block centers (>=6025 MHz) are above the
B210's 6 GHz ceiling. Not claimed as working; revisit with a >6 GHz SDR or a
different 6E adapter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rting

Dug into the earlier "6G 160 tunes but on-air unconfirmed" and resolved it
definitively with the B210. The 6 GHz 160 MHz *TX does not radiate* on the
C8852C, isolated cleanly (tests/kestrel_160_sdr_airs.sh, idle->TX duty):
  6G-20   0% -> 48.6%   airs
  6G-80   0% -> 45.2%   airs
  5G-160  1.8% -> 40.1% airs
  6G-160  0% -> 0.0%    SILENT
So it is specifically the 6G+160 TX combination — everything upstream is
correct: the RF synth locks (RF 0xb7[8]=0, exposed via the new
kestrel_halrf_read_rf diagnostic; RF18=0x3100f = 6G+160+ch15), center/pri
(15/1) match the vendor rtw_phl_get_center_ch, and the vendored
halbb_ctrl_bw_ch_8852c handles 6G-160. The vendor rtl8852cu.ko supports 6G-160
(phl_chan.c op_class 134), so this is an un-ported TX-enable gap, not a chip
limit. TX power ruled out (fixed-dBm 0x4594, no txpwr_lmt clamp). Remaining
candidate: MAC TXAGC-max BW160 clamp / a 6G-specific RF TX-path setting.

Corrections vs the earlier read: the BB 0xc5[15] "synthLock" bit is unreliable
(the real lock is RF 0xb7[8]); the 2-adapter A/B must be single-RX-launch +
per-run VBUS cold (the 8832CU comes up deaf on a 2nd soft-re-init); rx.txhit's
SA matcher is broken on Kestrel; VHT is invalid at 6 GHz (use HE).

Changes:
- kestrel_halrf_read_rf: read true synth-lock (RF 0xb7[8]) + RF18 readback,
  logged per C8852C tune.
- set_channel: warn that 6 GHz 160 MHz TX does not radiate (RX/monitor left
  available; use <=80 MHz for 6 GHz TX).
- Caps: advertise 160 MHz for the Kestrel generation properly — the demo
  adapter.caps emitter was dropping kBw160 (showed bw:[20,40,80]); GetTxCaps
  bw_max was stale at 80. Now bw:[20,40,80,160], bw_max 160 (5G-160 validated
  ~66.6 Mbps; 6G-160 TX gap documented in GetTxCaps).
- tests: 160 SDR airing probe, RF-lock probe, and a fixed single-RX-launch
  2-adapter A/B harness.

build 0, ctest 23/23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflect the SDR-backed finding: 160 MHz on 2.4/5 GHz is up (5G-160 ~66.6 Mbps),
but 6 GHz 160 MHz TX does not radiate on the C8852C (RF locks, RX-160 works;
un-ported 6G+160 TX-enable path), so 6 GHz TX tops out at 80 MHz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	CMakeLists.txt
#	README.md
…etries)

Every other generation's RX loop stops on g_devourer_should_stop (the
library-wide signal-flag pattern, see RtlJaguar2Device.h) — Kestrel only
checked its own _rx_stop, so a demo run under the harness's `timeout`
never exited: SIGTERM set the flag, nothing read it, and the async
bulk-IN loop ran forever. Same for the FWDL retry ladder on a dirty
chip (seconds of polling per attempt). Route both through the shared
flag: the RX/WP-drain loop predicates OR it in, and download_firmware
checks it between attempts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RE-era harness — golden usbmon captures (golden_c2h/vendor_ifup/
ifup/kernel/self + the capture_diff analyzer), the vendor register-dump
diff pair, and the closed-investigation probes (6G-160 synth lock,
160-airs duty, the ~103-frame TX-stall probe). We port vendor source,
we don't reverse-engineer it; the on-air suite is the regression
harness. The kept 6G-160 witness keeps its synth-lock fact without the
deleted probe's name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same monitor + method for both Kestrel chips: TX_ID=35bc:0108 points
the flood at the 8852BU instead of the default 8832CU.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/pyproject.toml is tracked; pin its resolution so uv sync
reproduces the bench environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A warm chip left by a previous run's clean Stop() brings up green but
delivers no bulk-IN — cold-cycle like every sibling harness. Also
grep -c prints "0" AND exits 1 on no matches, so the `|| echo 0`
fallback produced "0\n0" and broke the integer test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RX-diag register dump + 8852C EP4-diag at the end of set_channel,
the one-shot cmd_ofld hexdump, and the bulk-IN completion counters —
investigation aids from the RX-deafness/TX-silence bring-up, all
superseded by the on-air suite. The config-gated fw-log and CCA knobs
stay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…raming

Every register value that a usbmon "golden capture" comment justified is
re-justified from the vendor source it was ported from (mport.c, fwcmd.c,
fwdl.c, init.c, init_8852b.c, pwr_seq_func_8852b.c, hci_fc.c /
hci_fc_8852c.c, security_cam.c, _usb_8852b.c / _usb_8852c.c, dle.c). The
M0-M6 milestone labels and "[not yet ported]" status blocks describe a
bring-up that is finished — replaced with what each layer is; the dead
not_ported() machinery (zero callers) is deleted; kestrelprobe's header
now lists its real five stages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…halrf}/g6

Pure git mv (vendor file contents untouched — their ../../hal_headers_le.h
include depth is preserved) + the mechanical renames that follow: OBJECT
libs kestrel_halbb_8852c/kestrel_halrf_8852c -> kestrel_halbb/kestrel_halrf
and the gate defines DEVOURER_KESTREL_HALBB_8852C/HALRF_8852C ->
DEVOURER_KESTREL_HALBB/HALRF. The directory now hosts the G6 plane for the
whole generation; the 8852B backend joins it next to the 8852C one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vendored G6 core (from reference/rtl8852cu @ d0ec38d) is multi-chip
by design — BB_8852B_SUPPORT/RF_8852B_SUPPORT runtime dispatch exists
throughout — so the 8852B needs only its per-chip backend, copied
verbatim from reference/rtl8852bu @ 38fc5a3 by the new
tools/vendor_hal{bb,rf}_8852b.sh (dpk/txgapk compile in but stay masked
via support_ability: the rfk_ops_8852b pointer table references them as
data; psd + the regulatory pwr-table stay out). One kestrel_halbb /
kestrel_halrf OBJECT-lib pair now carries core + both backends, keyed
to the DEVOURER_KESTREL_* options, and builds on ALL compilers — the
GNU|Clang gate is gone (the vendor C is portable: audited, no GNU-only
constructs; /W0 /Gy on MSVC, -w -ffunction-sections elsewhere).

Shim frontier walked for the both-chips core: phy_hw_cap/dev_sw_cap
surface, hal_write8/16 RMW, the halrf->halbb cross-plane calls
(backup/restore_info + tx_mode_switch, faithful to hal_api_bb.c, via a
bridge bb_info pointer + newly vendored halbb_pmac_setting.c +
halrf_pmac.c), inert PMAC test-TX macros (gated hwtx/MP flows only),
and get_xsi beside set_xsi. Nothing is wired to the 8852B at runtime
yet — the glue dispatch lands next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…patch

The glue takes the chip at create (ic_type/chip_id feed the vendor
cores' own runtime dispatch; halrf_cmn_info_self_init wires the per-chip
rfk ops/backup tables) and the entry points go through the core
dispatchers where those are register-identical for the validated 8852C
(halbb_ctrl_bw_ch, halbb_ctrl_rx_path, halbb_gpio_setting_init,
halrf_iqk) with explicit per-chip forks only where the vendor has no
generic (gain load/apply, dac_cal, rx_dck — the trigger wrappers add
chlk_map/ops gates that can silently skip a cal — the RF tune: the
8852B has no ctl_band_ch_bw case and rides halrf_ctl_ch + halrf_ctl_bw,
its synth lock inside ctrl_ch). The RFK prologue now mirrors
halrf_dm_init including halrf_si_reset — a no-op for the 8852C (no
dispatch case) and real a-die SI work on the 8852B via the new bridge
XTAL-SI callbacks (HalKestrel xtal_si helpers; the shim macros became
routing inlines). The dead kestrel_halrf_tssi/dpk exports and their
commented-out call are gone — the EVM-settle evidence (duty 74->43%,
medEVM -70->-59) lives at the support_ability mask that enforces the
gate. C++ wrapper names go chip-neutral (vnd_bb_*/vnd_rf_*); the 8852C
call sites reach the same vendor functions with the same arguments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
phy_bb_rf_init's C8852C leg drops the C++ walker + cmd_ofld batching:
halbb_init_reg applies the BB phy_reg table, loads the gain table and
the efuse RX gain offsets; halrf_config_radio applies radio B then A
(vendor order) — the vendor's own loaders walking the vendored hwimg
arrays with the vendor's check-positive engine, over the bridge's
direct register planes. The radio-page H2Cs the loader emits
(halrf_fill_h2c_cmd, OUTSRC classes 8/9) now reach the fw through a
new bridge send_h2c callback filtered to exactly those classes and
encoded by the existing KestrelFw::radio_page_to_fw — every other
halrf H2C stays inert, as on the validated bring-up. The glue bringup
sheds its now-duplicate gain load; the vendor TPU/TSSI-ctrl MAC CR
init stays a helper no-op (the hand-ported fixed-dBm TXAGC owns the
power CRs). The 8852B leg keeps the C++ walker until its flip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tible)

The 8852B now runs the same vendored path the 8832CU validated: tables
via halbb_init_reg + halrf_config_radio (radio pages to the fw through
the bridge), per-channel BB via halbb_ctrl_bw_ch's 8852B backend
(replacing the hand-rolled ctrl_ch/ctrl_bw/CCK block and the gain/RPL
re-apply), the RF tune via the halrf_ctl_ch/ctl_bw generics (synth lock
inside halrf_ctrl_ch_8852b), DACK+RX-DCK via the full vendored cals
(MSBK/DADCK/biasK included — the hand-rolled ADDCK subset lacked them),
and the chip's first per-channel IQK behind the lazy RFK prologue
(NCTL microcode, LCK/RCK, a-die SI reset over the bridge XSI plane,
efuse trim + TSSI-DE). Per-channel RX-DCK is deliberately not re-run at
set_channel — the bring-up covers it, the shape validated on the 8852C.
FastRetune rides the same lean vendored pair on both chips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 MHz is 8852C-only (rtl8852c_halinit.c bw_sup has BW_CAP_160M;
rtl8852b_halinit.c tops at 80) — GetTxCaps/bw_mask report it per
variant and set_channel rejects 160 on the 8852B instead of mis-tuning
RF18. The generation mask also gains the 5/10 MHz widths both dies
declare (BW_CAP_5M|10M) — narrowband_ok already said so; the width set
now agrees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l stance

Four root causes found on the first true-cold bring-up (every prior session
ran on a chip the auto-probing kernel driver had pre-initialized — the
blacklist discipline exposed them all at once):

- power_on lacked the chip-generic mac_pwr_switch prologue (pwr.c:300..405):
  a USB chip from cold reads WLMAC_PWR_STE=MAC_ON (its AFSM auto-powers to
  enumerate), and the on-sequence applied over that half-on state leaves the
  WCPU bootrom FWDL_RDY without ever raising H2C_PATH_RDY. Force the MAC off
  first (EN_WLON + APFM_OFFMAC poll + the 8852B CMAC_CLK_SEL clear), plus the
  BOOT_MODE exit, now both-dies common.

- HALBB_DBCC/FW_OFLD/DBG_TRACE_SUPPORT were defined =0, but the vendor tests
  them with #ifdef — fw-offload was silently compiled IN, and the 8852B BBCR
  writer routes every table write through the fwofld wrapper (stubbed
  link-only), applying no BB registers at all. Off means undefined.

- coex_mac_init never armed B_AX_ENBT, so from cold the LTE indirect
  interface never reports ready, the LTECOEX writes time out, and the GNT
  arbitration stays at its cold default. Arm ENBT (mac_coex_init_8852b
  prologue) and pin the PTA to WiFi via mac_cfg_gnt_8852b's SW-force
  (gnt_wl=1, gnt_bt=0, both bands) — the Jaguar3 WiFi-only stance.

- the RFK prologue (NCTL/SI-reset/LCK/RCK/efuse-trim) ran lazily at first
  IQK, i.e. after the tune + BB channel config; the vendor runs it in
  halrf_dm_init position, right after the tables and before any channel
  work. Lazy placement lets the a-die SI reset tear down live RF state and
  the radio goes deaf both ways (bisected). Moved to phy_bb_rf_init.

Evidence-gated cal stance on the 8852B (same rule as TSSI/DPK): per-channel
IQK collapses on-air delivery under the fixed-dBm power model (paired cold
A/B, 5 GHz MCS0 flood, 8812AU monitor: 10/1 decodes with IQK vs 100/37
without) — 8852C-only until a TSSI-referenced power model exists. The
vendored DACK/RX-DCK run wrapped in the RF 0x0/0x1/0x5 save/restore the
hand-ported cal used (the vendor relies on later TRX flow to rewrite them).

Validated from real VBUS-cold on both dies: 8852B RX ch11 at baseline decode
counts + TX on-air (6M/MCS0 flood decoded, canonical txhits); 8832CU RX
2.4/5 GHz unchanged. 25/25 ctest.
- MSVC: __builtin_ctz in the BB mask_shift helper -> the existing portable
  ctz32 (the only compiler-specific construct in the Kestrel C++).
- macOS: ld64 rejects --gc-sections; use -Wl,-dead_strip on APPLE (MSVC gets
  /OPT:REF implied in Release via /Gy).
- MinGW/MSVC: linkers that resolve archive members before dead-stripping
  demand halrf_cmd_parser_init / halrf_dbg_setting_init (referenced by the
  un-called halrf_init/halrf_dm_init entry points) — link-only no-op stubs
  in kestrel_halrf_helpers.c.
…-rolled fallbacks

With both dies validated on the vendored halbb/halrf path, the compiled-but-
unreachable hand transcriptions go: the ofld table-walker leg, the gain/RPL
cache family, rf_ctrl_ch/bw, both dies' hand cal chains (8852B AFE/ADDCK/DACK
subset, 8852C DACK/RX-DCK), and the DEVOURER_KESTREL_HALBB/HALRF conditionals
themselves (the defines were unconditionally on for every Kestrel build).
CI grows kestrel-8852b-only / kestrel-8852c-only cells.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…racted copies

hal/hal8852{b,c}_phy.{c,h} (extract-generated, ~53K lines), the
PhyTableLoaderKestrel walker, its selftest and the extract_*_phy.py
generators duplicated the exact data the vendored halbb_hwimg_*.c /
halrf_hwimg_*.c ship — and since the vendored loaders own the table apply on
both dies, nothing read them. The array_mp_* rename defines existed solely
to avoid the duplicate-symbol clash and go in the same change. The fw blobs
(hal8852{b,c}_fw.c + extract_*_fw.py) stay — KestrelFw consumes them.

Verified: full + kestrel-only + 8852b-only + 8852c-only builds link clean,
ctest 24/24 (the phytable selftest retires with the walker), RX smoke both
dies unchanged from cold.
MSVC and MinGW resolve archive members before dead-stripping, so the
verbatim vendor TUs' references into the deliberately un-vendored
subsystems (regulatory power tables, PSD, BB debug port, MP counters,
TAS, RA reports) must resolve at link even though no runtime path calls
them. GNU --gc-sections hid all 69 on Linux; the no-gc-sections link now
passes locally as the cross-linker proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLAUDE.md Kestrel section describes the landed architecture: hand-ported
mac_ax plane + verbatim-vendored halbb/halrf BB/RF plane for BOTH dies
(core + per-chip backends + shim + glue under hal/hal{bb,rf}/g6/), the
evidence-gated cal stances (TSSI/DPK both dies, IQK 8852B), the dm_init-
position RFK prologue, the cold-boot facts (force-off prologue, ENBT/GNT,
rtw89-blacklist discipline), and 160 MHz correctly attributed to the 8852C
die only. README: Kestrel paragraph reflects the vendored BB/RF plane and
per-die bandwidth sets. tests/README.md gains the kestrel_* suite section.
kestrelprobe log labels use the stage names its CLI takes (id/power/fw/
trx/phy).
The demos call Stop() then libusb_exit, destroying the device object only
after — so the drain thread (started by InitWrite to recycle TX pages) was
still inside libusb_handle_events when libusb tore down, tripping libusb's
usbi_mutex_destroy assertion (SIGABRT at txdemo shutdown; 5/5 reproducible
on a narrowband flood, intermittent on plain floods). Kestrel had no Stop()
override; the join now happens at the ordered teardown point. Repro harness
0/6 abnormal after (was 5/5).
"${@:-36 6}" expands to the single argument "36 6" when no args are given,
so the default run drove DEVOURER_CHANNEL="36 6" and failed both cells.
Proper default array; explicit-args behavior unchanged.
README headline/intro say 11ac + 11ax, four generations; CLAUDE.md intro
likewise. Drop the noise qualifiers from the hardware table ("no 6 GHz" on
the TX20U Nano — no other adapter in the table has 6 GHz either; "first
8852C 6 GHz" on the TX50UH).
RTL8832BU (Wi-Fi-only SKU of the 8852B die) and RTL8852CU (the 8852
branding of the 8852C die) get not-benchmarked rows mirroring the other
same-die siblings (RTL8811AU/RTL8812BU/RTL8821CU/RTL8822CU/RTL8822EU
style) — both PID families are already in the KestrelUsbIds dispatch table.
@josephnef josephnef merged commit f738cb9 into master Jul 16, 2026
18 checks passed
@josephnef josephnef deleted the feat/kestrel-8852 branch July 16, 2026 09:27
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.

1 participant