Beamforming self-sounding: per-subcarrier CSI over the air (Jaguar-1 + Jaguar-3) by josephnef · Pull Request #154 · OpenIPC/devourer · GitHub
Skip to content

Beamforming self-sounding: per-subcarrier CSI over the air (Jaguar-1 + Jaguar-3)#154

Merged
josephnef merged 3 commits into
masterfrom
bf-selfsounding-probe
Jul 2, 2026
Merged

Beamforming self-sounding: per-subcarrier CSI over the air (Jaguar-1 + Jaguar-3)#154
josephnef merged 3 commits into
masterfrom
bf-selfsounding-probe

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

Recovers per-subcarrier channel state from Realtek Jaguar silicon that the chip otherwise terminates in hardware, by driving both ends of a link with devourer as an 802.11ac beamforming sounder + beamformee.

A beamformee estimates the channel H(k) per subcarrier from a sounding NDP, compresses the steering matrix V(k) into Givens angles, and transmits it back over the air as a VHT Compressed Beamforming report. The chip can't read back its own estimate — but the report is addressed to the beamformer, so a monitor RX captures it. With two adapters we sound our own link on demand and decode per-subcarrier CSI.

How

  • src/BeamformingSounder.h — generation-neutral helper. The MAC sounding registers (0x718/0x6E4/0x6F4/0x714/0x42C) are byte-identical across the Realtek AC family, so arm_sounder() is fully shared. Per-generation beamformee differences (sounding-control 0xCB vs 0xDB; the 0x9B4 BB CSI-content register that is the narrowband clock divider on Jaguar-3 and must not be written there; the RX-filter + own-AID gates) sit behind BfeeConfig presets. Recipes transcribed from the vendor hal_txbf_jaguar_enter / hal_txbf_8822b_enter.
  • Library hooks in RtlJaguarDevice (Jaguar-1) and RtlJaguar3Device (Jaguar-3), env-gated and no-op by default like the other DEVOURER_* debug knobs.
  • tools/bf_report_decode.py — chip-agnostic decoder: parses the MIMO-control field, extracts per-stream avg SNR, unpacks the angle bitstream, reconstructs the per-tone V(k). Auto-selects the bit-split by cross-frame stability and flags a flat channel. (Realtek uses a compact 10-bit/subcarrier codebook — 6-bit φ, 4-bit ψ — not the textbook 12/16-bit VHT sizes.)
  • docs/beamforming-self-sounding.md — mechanism, recipe, report byte layout.

Env vars (opt-in)

var side effect
DEVOURER_BF_ARM_SOUNDER=1 beamformer arm the MAC sounding engine
DEVOURER_TX_NDPA=1 beamformer mark the injected frame as an NDPA in the TX desc
DEVOURER_TX_NDPA_RA=<mac> beamformer WiFiDriverTxDemo builds a VHT NDPA to this beamformee
DEVOURER_BF_ARM_BFEE=<mac> beamformee arm the HW CSI responder (no association)
DEVOURER_BF_DETECT_REPORT=1..4 monitor report detector in WiFiDriverDemo (1 summary … 4 raw for the decoder)

Validation (on hardware, ch100)

  • Jaguar-1: 8812AU sounder → 8821AU beamformee → 8814AU sniffer — thousands of VHT Compressed Beamforming reports, SA = the beamformee, Nc=1 Nr=2; zero when unarmed.
  • Jaguar-3: same sounder → 8822CU / 8822EU beamformee — thousands of reports, full Nc=2 Nr=2; zero when unarmed.
  • Decoder cross-checks: LSB-first bit order confirmed (MSB-first blows cross-frame variance ~70×); a frequency-selective 8822CU capture decodes with adjacent-tone correlation ~0.86, independent evidence the per-tone reconstruction is real.

ctest green. All new library code is behind env gates and compiles into every per-chip build subset. Jaguar-2 (8822BU) is a straightforward follow-up (the recipe is already the shared kBfeeJaguar23) once that port lands.

🤖 Generated with Claude Code

josephnef and others added 3 commits July 2, 2026 22:19
802.11ac explicit sounding leaks per-subcarrier channel state: a beamformee
estimates H(k) from a sounding NDP, compresses the steering matrix V(k) into
Givens angles, and transmits it back over the air as a VHT Compressed
Beamforming report. On Jaguar silicon that pipeline is hardware-terminated
(a chip can't read back its own estimate), but the report is addressed to the
beamformer — so with devourer driving both link ends, a monitor RX captures
per-subcarrier CSI the chip otherwise hides.

Adds a generation-neutral BeamformingSounder helper: the MAC sounding registers
(0x718/0x6E4/0x6F4/0x714/0x42C) are byte-identical across the Realtek AC family,
so arm_sounder() is fully shared; per-generation beamformee differences (0xCB vs
0xDB, the 0x9B4 BB-CSI-content that is the narrowband clock divider on Jaguar-3,
the RX-filter/own-AID gates) sit behind BfeeConfig presets (kBfeeJaguar1,
kBfeeJaguar23). Recipes transcribed from the vendor hal_txbf_jaguar_enter /
hal_txbf_8822b_enter.

Env-gated, no-op by default (like the other DEVOURER_* debug knobs):
  DEVOURER_BF_ARM_SOUNDER   arm the MAC sounding engine (beamformer)
  DEVOURER_TX_NDPA          mark the injected frame as an NDPA in the TX desc
  DEVOURER_TX_NDPA_RA=mac   txdemo builds a VHT NDPA to this beamformee
  DEVOURER_BF_ARM_BFEE=mac  arm the hardware CSI responder (beamformee), no
                            association — wired for Jaguar-1 and Jaguar-3
  DEVOURER_BF_DETECT_REPORT report detector in WiFiDriverDemo (1 summary /
                            2 frame survey / 3 CSI hexdump / 4 raw for decode)

Validated on hardware: 8812AU/8821AU (Jaguar-1) and 8822CU/8822EU (Jaguar-3)
beamformees reply with VHT Compressed Beamforming reports to an unassociated
sounder; unarmed control yields zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tools/bf_report_decode.py decodes captured VHT Compressed Beamforming reports
into per-subcarrier CSI: parses the MIMO-control field (Nc/Nr/BW/Ng/codebook),
extracts per-stream avg SNR, unpacks the Givens angle bitstream, and
reconstructs the per-tone steering vector V(k). The angle bit-split is chosen
by cross-frame stability and validated against the standard phi=psi+2 Givens
structure; it also flags a flat (frequency-nonselective) channel. Realtek's
reports use a compact 10-bit/subcarrier codebook (phi=6, psi=4) rather than the
textbook 12/16-bit VHT sizes. The decoder is chip-agnostic — it reads
over-the-air standard frames, so the same tool decodes Jaguar-1 and Jaguar-3
reports.

Validation harnesses (out-of-band, like tests/regress.py):
  bf_ndpa_probe.py / bf_ndpa_onair.sh   B210 burst-pair probe: does the TX-desc
                                        NDPA bit make the MAC emit a HW NDP?
  bf_selfsound_onair.sh                 B210 burst-chain check of the responder
  bf_report_sniff.sh                    3-adapter decode-level confirmation
  bf_selfsound_jaguar3.sh               cross-generation (8822C/E) beamformee

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the NDPA/NDP/report exchange, why the register recipe is
generation-neutral, the per-generation beamformee differences, how to drive it
via the demo env vars, and the measured 2x1 report byte layout (52 subcarriers
x 10-bit compact codebook) that the decoder consumes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit fa57b5a into master Jul 2, 2026
12 checks passed
@josephnef josephnef deleted the bf-selfsounding-probe branch July 2, 2026 19:24
josephnef added a commit that referenced this pull request Jul 3, 2026
## What

Follow-up to #154 (SU self-sounding). An SU beamforming report carries
only the per-tone steering **direction** plus a per-stream **scalar**
SNR. An **MU** report additionally appends the *MU Exclusive Beamforming
Report* — genuine **per-subcarrier SNR** — which is the per-tone channel
quality that maps to a per-tone modulation order (the textbook "256-QAM
on clean tones, QPSK on faded tones" picture).

## How

- **`arm_beamformee_mu()`** (`src/BeamformingSounder.h`) layers the MU
group-table registers (`0x14C0` MU-TX-CTL, `0x14C4`/`0x14C8`/`0x14CC`
group/user-position, entry `0x1684`) on top of the SU responder base.
For *self*-sounding we program the group/user-position directly, so no
over-the-air VHT Group ID Management handshake is needed. Recipe from
the vendor `hal_txbf_8822b_enter()` MU branch (identical across
8822B/C/E).
- Wired on Jaguar-3 via **`DEVOURER_BF_ARM_BFEE_MU=1`**; the NDPA MU
feedback bit (STA-info bit 12) via **`DEVOURER_TX_NDPA_MU=1`** in
`WiFiDriverTxDemo`.
- **`tools/bf_report_decode.py`** parses the MU report. Realtek packs
the SNR not as the spec's 4-bit deltas but as 8-bit values in pairs —
series A (even bytes) is the per-tone SNR that swings with the channel.
The decoder extracts it, maps to dB (`22 + 0.25·int8`) and to a
modulation per tone, and trims devourer's trailing chip-FCS/RX bytes
where the smooth series collapses. `--operating-snr N` re-centres the
*measured* per-tone shape to a stated link budget, turning the real
frequency-selectivity into the textbook QAM ladder even on a strong
bench link.

## Result (8822CU MU-beamformee, ch100, 20 MHz)

Reports grow 99 → **153 bytes**; the per-tone SNR series A is a smooth
**45–53 dB** curve with ~8 dB of real frequency-selective swing:

```
as measured:   SNR 53..45 dB  →  all 256-QAM (link is uniformly strong)
--operating-snr 26 (same shape, weaker link):
   SNR:  30 29 29 28 28 28 27 27 27 26 26 26 25 24 24 24 24 23 23 22  dB
   QAM:  +  +  +  :  :  :  :  :  :  :  :  :  :  :  :  :  :  :  :  :     (+ 16-QAM / : QPSK)
```

Note: `DEVOURER_TX_PWR` does **not** attenuate the link — the NDP is
hardware-generated by the sounding engine, not sent through
`send_packet` — hence the operating-point model rather than a physical
power sweep.

`ctest` green; all new code is env-gated and no-op by default.

🤖 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