Skip to content

Kestrel: HE ER SU + DCM extended range; per-die RX drv_info unit fix#295

Merged
josephnef merged 1 commit into
masterfrom
kestrel-he-er-su
Jul 16, 2026
Merged

Kestrel: HE ER SU + DCM extended range; per-die RX drv_info unit fix#295
josephnef merged 1 commit into
masterfrom
kestrel-he-er-su

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

HE ER SU (802.11ax Extended Range) + DCM on both Kestrel dies — the standards-based range lever of the AX generation, full kit:

  • ER 242-tone (MCS0–2, NSS1): boosted/repeated HE-SIG-A preamble, full-width payload
  • ER 106-tone (DATA_BW_ER, MCS0): payload power concentrated in ~half the tones, ~3 dB more
  • DCM (dual-carrier modulation): stacks on ER or plain SU, excludes STBC

API follows the established devourer pattern — no new IRtlDevice method:

  • Per-packet radiotap: HE field FORMAT=EXT_SU, data5 RU code (6=106 / 7=242), data3 DCM bit
  • Session default: DEVOURER_TX_RATE=HE1SS_MCS0/ER[/DCM], /ER106 (HE-only suffixes); TxMode.he_er / TxMode.he_dcm programmatically
  • Caps: AdapterCaps.he_er_su_ok (adapter.capshe_er_su)
  • RX witness: the AX RXD PPDU-format nibble surfaces as RxAtrib.ppdu_type (7=HE_SU, 8=HE_ERSU; 0xff pre-AX) and in rx.txhit — payload rate alone cannot distinguish ER MCS0 from SU MCS0

Descriptor bits are vendor-verified: DATA_ER/DATA_BW_ER in wd_info dword0 on both dies; DCM moves with the rate word on the 8852C (DATA_DCM_V1, wd_body dword7 BIT30). Out-of-spec combos (NSS>1, MCS over the mode ceiling, DCM+STBC) clamp with a W diagnostic instead of stalling the AX scheduler. The SetTxMode fallback now expresses HE natively — it silently aired VHT before, which the new ppdu_type witness exposed.

Bug found while validating: 8852C RX payload offset

Judging the 8852C as receiver exposed a latent parser bug: the RX payload offset is rxd + shift*2 + drv_info_size*UNIT (+4 when mac_info_vld, non-PPDU-status), and UNIT is 16 bytes on the 8852C vs 8 on the 8852B (vendor hal_trx_8852{b,c}). With the B's unit the C die "receives" frames with correct lengths but unreadable bodies (fc=0x0000, zeroed SA/RSSI) — an SA-matching harness reads that as a deaf receiver. parse_rx_8852b now takes the per-die unit and applies the full vendor formula; selftests lock all three terms.

Also: the demos' DEVOURER_USB_BUS/PORT topology selection is now strict — matching no device is fatal instead of silently falling through to the unrestricted VID:PID open, which grabbed the wrong adapter in same-VID:PID pairs (the exact ambiguity the knob exists to resolve).

Validation

Headless: he_radiotap_selftest (grammar → radiotap → decode round-trip + clamp rules), kestrel_rxparse_selftest (ppdu_type nibble, per-die drv_info unit, shift×2, mac_info +4) — 24/24 ctest.

On-air (tests/he_er_su_cross_rx.sh, ch6, 15 s cells, 2× RTL8832CU + 1× RTL8852BU):

Pairing SU (pt7) ER-242 (pt8) ER-106 (pt8) ER+DCM (pt8)
8852C → 8852B 4600+ 5500 5600 5700
8852C → 8852C (same-die) 4600 4400 4300 3800
8852B → 8852C 3600 3700 3600 3500

ER MCS1/MCS2 decode in every pairing (3-adapter triangulation). One bench trap documented: a same-die pair at point-blank range fails QPSK at the default 20 dBm while BPSK passes — near-field saturation (DEVOURER_TX_PWR=6 restores delivery), not a protocol limit.

Docs: docs/he-extended-range.md (range ladder, API, hardware mechanics, measured matrix), docs/logging.md schema rows, README/CLAUDE.md notes.

🤖 Generated with Claude Code

HE ER SU (802.11ax Extended Range Single User) as a first-class TX/RX
feature on both Kestrel dies, full kit: ER 242-tone (MCS0-2), ER 106-tone
(DATA_BW_ER, MCS0), and DCM, each per-packet radiotap-driven
(HE FORMAT=EXT_SU / data5 RU code / data3 DCM bit) or as a session default
via the TX-mode grammar (/ER, /ER106, /DCM — HE rates only). Descriptor
bits follow the vendor split: DATA_ER/DATA_BW_ER in wd_info dword0 on both
dies, DCM in wd_info dword0 on the 8852B but DATA_DCM_V1 (BIT30) in the
wd_body dword7 rate word on the 8852C. Out-of-spec combos (NSS>1, MCS above
the mode ceiling, DCM+STBC) are clamped with a W diagnostic instead of
stalling the AX scheduler. The SetTxMode fallback now expresses HE natively
(it silently aired VHT before — exposed by the new RX-side witness).

RX surfaces the AX descriptor's PPDU-format nibble (7=HE_SU, 8=HE_ERSU) as
RxAtrib.ppdu_type (0xff pre-AX) and in the rx.txhit event — the only honest
on-air proof of the format, since the payload rate cannot distinguish ER
MCS0 from SU MCS0. AdapterCaps grows he_er_su_ok (adapter.caps: he_er_su).

Validating the 8852C as the RX side exposed a latent RX-descriptor parsing
bug: the payload offset is rxd + shift*2 + drv_info*UNIT (+4 when
mac_info_vld on non-PPDU-status packets), where UNIT is 8 bytes on the
8852B but 16 on the 8852C (vendor hal_trx_8852{b,c}). With the B's unit the
C die "receives" frames with correct lengths but unreadable bodies — an
SA-matching harness reads it as a deaf receiver. The parser now takes the
per-die unit and applies the full vendor formula.

The demos' DEVOURER_USB_BUS/PORT topology selection is now strict: matching
no device is fatal instead of silently falling through to the unrestricted
VID:PID open (which grabbed the wrong adapter in same-VID:PID pairs, the
exact ambiguity the knob exists to resolve).

On-air validation (tests/he_er_su_cross_rx.sh, ch6, 15 s cells,
2x RTL8832CU + 1x RTL8852BU): SU control classifies 7; ER-242, ER-106 and
ER+DCM deliver 3.3-5.7k hits/cell with uniform ppdu_type=8 in every
pairing — cross-die both directions and same-die 8852C->8852C — and ER
MCS1/MCS2 decode in every pairing (a same-die point-blank pair needs
DEVOURER_TX_PWR=6: near-field QPSK saturation, not a protocol limit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit 608e2bf into master Jul 16, 2026
18 checks passed
@josephnef
josephnef deleted the kestrel-he-er-su branch July 16, 2026 14:22
josephnef added a commit that referenced this pull request Jul 16, 2026
… discovery (#294) (#296)

Fixes the mechanical half of #294 (the deafness half was the
RX-descriptor walk fix already merged in #295 — full root-cause analysis
in the [issue
comment](#294 (comment))).

## Problem

`libusb_reset_device` on a Kestrel with running (or SIGKILL-orphaned)
firmware crashes it to bootrom: the device re-enumerates (`device
firmware changed`), the open dies with a stale handle, and the ROM path
can land in the dead ZeroCD DISK id (`0bda:1a2b` — `bInterfaceNumber`
255, unclaimable by libusb *and* kernel usbfs, so no userspace eject
exists; only a VBUS power-cycle recovers). Any harness cycling the demos
hit retry roulette; every zero read looked like a deaf receiver.

## Fix

- **`claim_interface_then_reset` skips the USB reset for any
`kestrel::variant_for_usb_id` match.** Kestrel `power_on` forces the MAC
off from any retained state — the reset added nothing but the ROM-drop
risk. Covers all demos at once. Bench: SIGKILL-churn 6/6 clean without
the reset vs ZeroCD roulette with it.
- **New `devourer::claim_interface_reset_reopen`** (used by
rxdemo/txdemo/streamtx): when a reset does re-enumerate (other chips /
forced), close the stale handle, wait bounded for the *same bus+port* to
return under the original VID:PID (transient ZeroCD ids are not a
match), re-open and re-claim **without** a second reset — the
re-enumeration itself reloaded the firmware.
- **Bounded topology discovery**: with `DEVOURER_USB_BUS/PORT` set, the
demos poll the named socket for up to 15 s instead of failing on the
first empty scan (a closed/killed predecessor leaves the chip
re-enumerating for seconds). Strict no-fall-through behavior unchanged.
- Selftest: locks the issue-#294 aggregate-walk regression —
`[PPDU_STS(drv_info, unit 16)][WIFI]`: the trailing WIFI frame must be
reached (with the old offset formula the walk broke after the phy-status
sub-packet → green bring-up, zero frames).

## Validation

- SIGKILL-churn ×6 on the 8832CU, default env: 6/6 bring-ups deliver
(700–1000 frames/15 s), **0 ZeroCD landings, 0 stale handles** — vs 5/6
failing before.
- From VBUS cold: clean.
- `tests/he_er_su_cross_rx.sh` same-die (8852C→8852C) and cross-die
(8852B→8852C), ch6: **8/8 cells pass on the first attempt**, zero
retries, zero VBUS cycles — vs all-day retry roulette.
- 24/24 ctest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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