Skip to content

Pseudo preamble puncturing: keep a wide channel usable when one 20 MHz slice is dirty#159

Merged
josephnef merged 6 commits into
masterfrom
pseudo-preamble-puncturing
Jul 3, 2026
Merged

Pseudo preamble puncturing: keep a wide channel usable when one 20 MHz slice is dirty#159
josephnef merged 6 commits into
masterfrom
pseudo-preamble-puncturing

Conversation

@josephnef

@josephnef josephnef commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

A long-range link wants the widest channel it can get, but one dirty 20 MHz slice (an interferer, a neighbouring network) conventionally forces the whole 80 MHz block to be abandoned. Wi-Fi 7 solves this with preamble puncturing — transmit around the dirty slice — but that is 802.11be PHY signaling (EHT-SIG puncturing bitmap + TX IFFT tone nulls). The question this PR answers: how much of that capability can 802.11ac-era Jaguar silicon recover, and which mechanism actually carries the weight? Answered with ported vendor knobs + on-air measurements; the study lives in docs/pseudo-preamble-puncturing.md.

What lands

1. The RX tone-mask knobs — the only tone-level hardware the silicon has (src/ToneMask.h, all three generations):

  • DEVOURER_RX_CSI_MASK=<f_lo>[-<f_hi>][/wgt] — per-tone receive-equalizer de-weight over an arbitrary frequency range (the vendor phydm wrapper only notches one interferer; masking a 20 MHz slice needs the range form). 11ac: 0x874[0] + bit arrays 0x880-0x89C; Jaguar3: indexed table behind 0x1D94, per-tone 3-bit weight, enable 0xC0C[3].
  • DEVOURER_RX_NBI=<f_mhz> — the vendor-parity narrowband notch.
  • Validated on 8821AU, 8814AU, 8822BU, 8812CU, 8822EU (tests/tone_mask_regcheck.sh), demod-active not just readback (tests/tone_mask_rx_sanity.sh: masking the tuned channel collapses a live link 29→6 hits, off-frequency mask 29→30), tone math ctest-guarded (tone_mask_math).

2. The jammed-slice measurements — which lever works (tests/pseudo_puncture_onair.sh, B210 jamming one slice of a VHT80 link):

cell hits/15s
clean VHT80 61
jammed VHT80 31
jammed + CSI mask on the slice 33
jammed, TX on the clean 40 MHz half 48

The CSI mask is inert against a jammed slice — and the PR shows why: with DEVOURER_RX_KEEP_CORRUPTED=1 the jammed cell delivers zero FCS-failed frames, i.e. the loss is pre-FCS (sync/AGC), upstream of the equalizer and of sub-block salvage. The mask stays valuable for its designed purpose (in-band spurs on decodable frames), now exposed. TX-side avoidance is the working lever (78% of clean delivery), and a victim-link probe scan locates the dirty slice where per-tone SNR self-sounding cannot (it senses the peer's channel, not the victim's — tests/pseudo_puncture_detect.sh).

3. The avoidance lever measured precisely (tests/rx80_narrow_tx_probe.sh, tests/wide_rx_penalty_sweep.sh): an 80 MHz-tuned RX decodes narrower frames on its primary sub-channels without retuning and at 0 dB (≤1 dB) sensitivity cost — so TX bandwidth adaptation along the primary-nested ladder 20 ⊂ 40 ⊂ 80 is unilateral and per-packet, no coordination protocol needed.

4. The adaptive-link integration (tools/precoder/, 230 tests): bandwidth as an energy-ranked op-table dimension (ControllerConfig.bw_set, VHT 1SS rates incl. the 80 MHz rung, +3 dB/doubling noise cost per row); per-rung delivery sensing with zero new wire fields (a shared seq-derived probe schedule, ~3% duty per rung); rung blocking by delivery contrast (fires on interference, not on path loss); and a primary_dirty escalation flag for the one case no unilateral bandwidth choice can fix. Wire-compatible: legacy PROFILE bytes decode unchanged.

Also fixed en route: tests/sdr_interferer.py could not feed 20 MS/s from per-buffer numpy draws — the jam radiated only in underflow bursts and its gain knob silently did nothing (pre-generated fixed-seed noise ring now); WiFiDriverDemo accepts DEVOURER_BW=80.

Verdict

The honest Jaguar rendition of [clean][clean][skip][clean] is puncturing by primary-channel selection: probe-scan the slices with the victim link, transmit around the dirty one at the widest clean primary-nested bandwidth, escalate to a coordinated channel move only when the primary itself is dirty. The cost vs Wi-Fi 7 is the bandwidth quantization (60 MHz usable → 40).

Validation

  • CI green across the matrix (incl. the mingw selective-target fix for the new ctest binary); ctest 2/2; precoder pytest 230/230.
  • Hardware: 5-chip register-landing matrix, 3-cell mask sanity, 4-cell jammed-slice experiment (+ gain-85 repeat), probe-scan detection run, wide-RX penalty sweep — all scripts in tests/ are re-runnable on the 2-DUT + B210 bench.

🤖 Generated with Claude Code

josephnef and others added 6 commits July 3, 2026 20:31
Wi-Fi 7 preamble puncturing (use a wide channel, skip a dirty 20 MHz
slice) has no true rendition on Jaguar silicon — VHT preambles must be
contiguous and no generation exposes TX tone nulling. This ports the
closest thing the chips do have and measures what it is worth:

- src/ToneMask.h: phydm CSI-mask (per-tone receive-equalizer de-weight)
  + NBI notch, one shared header for all three generations
  (11ac bit-arrays 0x874/0x880-0x89C; Jaguar3 indexed table behind
  0x1D94 with per-tone weight, enable 0xC0C[3]). New env knobs
  DEVOURER_RX_CSI_MASK=<f_lo>[-<f_hi>][/wgt] (masks an arbitrary tone
  range — the vendor wrapper only notches one interferer frequency) and
  DEVOURER_RX_NBI=<f_mhz>, applied in each generation's StartRxLoop.
- Register landing validated on 8821AU / 8814AU / 8822BU / 8812CU /
  8822EU (tests/tone_mask_regcheck.sh); the mask provably gates
  demodulation (tests/tone_mask_rx_sanity.sh: 29->6 hits masking the
  tuned channel, 29->30 off-frequency). Tone math is ctest-guarded
  (tone_mask_math).
- Jammed-slice experiment (tests/pseudo_puncture_onair.sh, B210 AWGN on
  one slice of a VHT80 link): the CSI mask is inert against a jammed
  slice — with DEVOURER_RX_KEEP_CORRUPTED=1 the jammed cell delivers
  zero FCS-failed frames, so the loss is pre-FCS (sync/AGC), upstream
  of the equalizer. Dropping TX to the clean 40 MHz half recovers 78%
  of clean delivery; a victim-link probe scan locates the jammed slice
  (tests/pseudo_puncture_detect.sh). Study written up in
  docs/pseudo-preamble-puncturing.md.
- tests/sdr_interferer.py: pre-generated fixed-seed noise ring — the
  per-buffer numpy draw could not feed 20 MS/s, so the jam radiated
  only in underflow bursts and the gain knob did nothing.
- demo: DEVOURER_BW now accepts 80 (VHT80 monitor RX).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bench harnesses behind the adaptive-link bandwidth lever
(docs/pseudo-preamble-puncturing.md "puncture by primary selection"):

- tests/rx80_narrow_tx_probe.sh: an 80 MHz-tuned RX decodes 40 MHz VHT
  and legacy-20 frames sent on its primary sub-channels WITHOUT
  retuning — TX-side bandwidth adaptation is unilateral (per-packet
  radiotap, no RX coordination), as long as the TX keeps the RX's
  primary. The unilateral choice set is the nested ladder
  20(primary) < 40(primary half) < 80.
- tests/wide_rx_penalty_sweep.sh: the sensitivity cost of that
  wide-tuned reception, measured differentially (identical 40 MHz TX,
  only the RX tune differs) as the 50%-delivery crossing shift over a
  swept B210 noise floor: 0.0 dB — both tunes collapse at the same
  83.9 dB crossing (bounded by the ~1 dB capture-cliff width, so
  "<= 1 dB"). Measurement notes baked into the harness: gate the TX
  window on the RX's listening marker (per-bandwidth init variance
  otherwise aliases into hit counts — this artifact produced the
  apparent 25% penalty in the first probe), sweep noise gain rather
  than TXAGC (bench margin swallows the whole 24 dB TXAGC range), and
  retry a cell's TX once on the T3U's warm-DLFW abort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opens the channel-bandwidth lever the adaptive-link design listed but
never actuated, built on the two measured facts from the wide-RX
probes: the 20 c 40 c 80 primary-nested ladder is unilateral (TX picks
a rung per-packet via radiotap while the RX parks at the widest) and
costs the receiver nothing (wide-RX penalty 0 dB / <= 1 dB).

- energy_model: VHT 1SS rate tables (20/40/80 — the 80 MHz rung HT
  lacks) and bw_noise_db() (+3 dB of integrated noise per bandwidth
  doubling vs the 20 MHz reference).
- op_table.build_link_rows: opt-in bw_set/mode row dimensions; a wider
  row's snr_req carries its noise-bandwidth cost, and resolve()
  consistently hands the demodulator recv minus that same term.
- ControllerConfig.bw_set/mode (default None/ht — single-bw behaviour
  unchanged): the existing e_bit ranking then answers "drop bandwidth
  or drop MCS?" per tick. Swept: the controller rides VHT/80 from
  45 dB down to 14 dB (spending TXAGC to hold the wide rung — power is
  the weak energy lever, airtime the strong one) and sheds to 40 MHz
  only at 9 dB.
- adaptive_link.op_to_ladder: VHT rows map to VHT1SS_MCSn/bw ladder
  specs.

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

The sensing half of the bandwidth dimension — the "victim link is the
sensor" verdict from the pseudo-puncturing study, operationalized with
zero new wire fields:

- rc_proto: a shared bandwidth-probe schedule (slots 0/8/16 of each
  32-seq cycle fly the sorted rungs, ~3% duty per rung) that both ends
  derive from the video seq alone, and a v2 PROFILE byte encoding
  (mode/MCS/bw; legacy 0..7 bytes still decode as HT/20, and an HT/20
  encoding IS the legacy byte).
- score.RungWindow: per-rung delivery at the VRX — received seqs are
  attributed to their scheduled rung on arrival, missed seqs from the
  wrap-safe gaps between arrivals.
- Controller.report_rung_delivery: blocks (for a hold) a rung whose
  probed delivery contrasts badly with the best rung's — detection by
  contrast, so it fires on the interference signature (one rung's
  extra spectrum is dirty) and not on plain path loss (all rungs sag
  together). The narrowest rung is never blocked: when it, too, misses
  the target the controller raises `primary_dirty` — the condition no
  unilateral bandwidth choice can fix, which the embedding app
  escalates into a coordinated channel move (the one moment the
  4-slice probe scan earns its cost).
- AdaptiveVrx feeds the sensor into the controller each feedback tick
  and surfaces `primary_dirty`; AdaptiveVtx exposes probe_bw_for_seq()
  for injectors and builds its ladder from the decoded v2 profile via
  the now-shared ladder_spec().

230/230 precoder tests, incl. closed VRX-loop pins: kill exactly the
80-rung probe seqs -> the controller vacates 80 and 20/40 stay clean;
kill all rungs while video still flows -> primary_dirty fires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mingw job builds an explicit target list (the POSIX-only demos are
not mingw targets) and then runs the full ctest suite — so every
registered test's binary must be in that list. The new tone_mask_math
test failed there as "Not Run": ToneMaskSelftest was never built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The avoidance lever's two strongest measured properties landed after
the study was written: it is unilateral (RX@80 decodes primary-nested
narrower frames without retuning) and free at the receiver (wide-RX
penalty 0 dB / <= 1 dB), which is what makes it consumable by the
adaptive controller. State them where the lever is judged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef changed the title Pseudo preamble puncturing: RX tone-mask knobs + jammed-slice study Pseudo preamble puncturing: keep a wide channel usable when one 20 MHz slice is dirty Jul 3, 2026
@josephnef josephnef merged commit 92f7802 into master Jul 3, 2026
14 checks passed
@josephnef josephnef deleted the pseudo-preamble-puncturing branch July 3, 2026 19:05
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