Skip to content

Beamforming self-sounding: MU report → per-subcarrier SNR#155

Open
josephnef wants to merge 3 commits into
masterfrom
bf-mu-sounding
Open

Beamforming self-sounding: MU report → per-subcarrier SNR#155
josephnef wants to merge 3 commits into
masterfrom
bf-mu-sounding

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

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

josephnef and others added 2 commits July 2, 2026 22:43
An SU report gives only per-tone steering direction + a per-stream scalar SNR.
An MU report additionally appends the MU Exclusive Beamforming Report — real
per-subcarrier SNR. Adds arm_beamformee_mu() (layers the MU group-table
registers 0x14C0/C4/C8/CC + entry 0x1684 on the SU responder base, programming
the group/user-position directly so self-sounding needs no over-the-air Group
ID Management handshake; recipe from hal_txbf_8822b_enter), wired on Jaguar-3
via DEVOURER_BF_ARM_BFEE_MU=1, plus the NDPA MU feedback bit via
DEVOURER_TX_NDPA_MU=1.

bf_report_decode.py parses the MU report: Realtek packs the SNR as 8-bit pairs
(series A = the per-tone SNR that swings with the channel), which the decoder
extracts, maps to dB and to a modulation per tone, and trims of devourer's
trailing chip-FCS/RX bytes. --operating-snr re-centres the measured per-tone
shape to a stated link budget to show the textbook per-tone QAM ladder.

Validated: 8822CU MU-beamformee reports run 153 B (vs 99 SU); series A is a
smooth 45-53 dB per-tone curve with ~8 dB frequency-selective swing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a live scrolling truecolor per-subcarrier SNR waterfall — pipe
WiFiDriverDemo's raw report stream into tools/bf_waterfall.py (or run the
whole rig via tests/bf_waterfall.sh) to watch the per-tone channel across
frequency (X) and time (Y), coloured by the modulation a rate-adaptive link
would pick per subcarrier. tools/bf_waterfall_svg.py renders the same as an
SVG for docs/img/bf_waterfall.svg (embedded in the doc).

Also corrects the MU per-tone SNR mapping: the values cross 128 (a stronger
tone reads e.g. 131 > 122), so the field is UNSIGNED (dB = -10 + 0.25*v); the
prior signed int8 reading wrapped the strongest tones to negative (spurious
navy cells). QAM thresholds recalibrated to the ~1e-3 uncoded-BER ballpark. A
measured bench capture now reads a realistic ~13-21 dB per-tone SNR (was a
too-high ~45-53) — 16-QAM on the stronger tones, QPSK on the weaker.

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

Copy link
Copy Markdown
Collaborator Author

Added: live per-subcarrier SNR waterfall + a decode correction

Waterfall. tools/bf_waterfall.py renders the MU per-tone SNR as a live scrolling truecolor spectrogram (frequency ×, time ↓, colour = the modulation a rate-adaptive link would pick per subcarrier). tests/bf_waterfall.sh drives the whole 3-adapter rig into it; tools/bf_waterfall_svg.py exports the same to an SVG, now embedded at the top of the doc:

waterfall

Correction (important). While building the waterfall I found the MU per-tone SNR field is unsigned, not signed int8. The per-tone values cross 128 (a stronger tone reads e.g. 131 > 122), so the old signed reading wrapped the strongest tones to negative dB — which is what produced spurious dark cells. Fixed to dB = -10 + 0.25·v, and recalibrated the QAM thresholds to the ~1e-3 uncoded-BER ballpark. The measured bench per-tone SNR is now a realistic ~13–21 dB (the earlier ~45–53 dB in this PR's description was the signed-mapping error) — 16-QAM on the stronger tones, QPSK on the weaker, straight from the real capture with no operating-point shift.

tools/bf_waterfall_gif.py renders the per-subcarrier SNR waterfall from a real
capture as an animated GIF styled as a live UI — DEVOURER-branded header, a
blinking LIVE indicator, a scrolling frequency×time heatmap that fades into the
past, a per-frame readout (peak/min SNR, best/worst modulation, a current-tone
profile bar), and the SNR→QAM legend. Embedded at the top of the doc in place
of the static SVG (bf_waterfall_svg.py stays for a diff-able static export).

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