Skip to content

FastRetune on all three generations + active two-ended sounding sweep#186

Merged
josephnef merged 14 commits into
masterfrom
issue149-jaguar3-fastretune-sounding
Jul 5, 2026
Merged

FastRetune on all three generations + active two-ended sounding sweep#186
josephnef merged 14 commits into
masterfrom
issue149-jaguar3-fastretune-sounding

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Completes the narrowband noise/sounding sweep (#149): the fast retune the sweep dwells on, and the active two-ended sounding mode on top of the #177 spectrum map. Closes #149.

FastRetune on every generation

IRtlDevice::FastRetune(channel) is now the generation-agnostic lean intra-band retune (default = full SetMonitorChannel), with a real fast path on all three generations — the RF channel write plus channel-keyed constants on bucket change, everything bandwidth/band-keyed untouched, full-path fallback on a band change:

DUT full path fast
RTL8812AU ~251 ms ~1.6 ms
RTL8822BU ~62 ms ~18 ms
RTL8821CU ~29 ms ~5.6 ms
RTL8822CU ~11 ms ~3.6 ms
RTL8812EU ~11 ms ~4.3 ms
  • Jaguar2 (both variants): one cached full-register RF18 write collapses the full path's read-modify-write rounds (three on the 8821C); AGC index / CFO fc / RF 0xBE VCO band / ch144 RF 0xDF / spur + CCK-filter regs written on bucket change; per-hop RX kick kept.
  • Jaguar3 (both variants): RF18 inside its 3-wire bracket + force-anapar + BB reset; lazily-primed 8822c RF18 cache (primed on the first fast hop — init-time IQK rewrites RF18 after the channel set, so priming earlier would cache a stale value); 5/10 MHz narrowband dividers are preserved across hops (no per-dwell re-clock + DAC-FIFO reset). FastRetune and SetMonitorChannel now serialize on the coex-thread register mutex — in-session hops no longer race the housekeeping tick.
  • Radiotap CHANNEL drives a per-packet FastRetune on all three generations now (was Jaguar1-only).
  • The subtle rule the parity oracle taught on hardware: init-time calibration rewrites BW-keyed state after the channel set (halrf clears the 40 MHz TX_CCK_IND bit in the 8822e's RF 0x1a; the NB recipe owns 0x808[6:4]), and the full path re-imposes it per retune — so the fast path re-asserts that state once per cache epoch. Correctness is end-state parity with the full path.

Sweep upgrades

  • Dwell retunes ride FastRetune (DEVOURER_RX_SWEEP_FULL=1 forces the full path), and each ch=N line reports the measured retune_us.
  • Per-dwell frame stats on the bin line: frames/rssi_mean/rssi_max/snr_mean/snr_min/evm_mean, drained at dwell start so retune transients never leak into a bin. DEVOURER_RX_AGG_SA=canon|<mac> restricts the aggregate to one transmitter's SA.
  • DEVOURER_RX_SWEEP / DEVOURER_HOP_CHANNELS share one bin-list grammar (src/SweepSpec.h): channel lists, channel ranges (36-48/4), and MHz ranges (5170-5250/5). Headless sweep_spec_math ctest.

Active two-ended sounding — coarse H(f) of the link

The passive map is blind to fading of our own path; the active mode probes it. tests/sounding_sweep.sh: one end hops fixed-rate canonical-SA probe beacons across the bins via FastRetune, the other sweeps the same bins with the SA filter — synchronization is asymmetric-duty (RX dwell ≥ ~2 TX cycles), no control channel. tests/sounding_map.py renders the map: per-bin median of rssi_max (off-channel bleed decodes weaker, never stronger — bleed-robust), NOTCH/DEAD flags, optional Spearman cross-check against a B210 capture (hop_rx_probe.py --bin-power-csv).

Register-parity tooling

DumpCanary ported to Jaguar2 + Jaguar3 (same format, emitted from both the full and fast paths); tests/hop_parity_check.sh is family-aware (Jaguar1 keeps its TX-power/IQK exclusions plus the live RF 0x00/0x42 registers the full-vs-full control only catches probabilistically; Jaguar2/3 exclude nothing).

Test-harness fix

The kernel truncates comm to 15 chars, so every pkill -x WiFiDriverTxDemo cleanup in tests/*.sh was a silent no-op (leaked TX demos held adapters across scripts). Fixed to the real truncated comm across the harness.

Testing

  • Build clean on the full config and the per-chip subsets; ctest green (5/5, incl. the new sweep_spec_math).
  • Register parity (full-vs-fast, with full-vs-full control): PASS on 8812AU (5G/40), 8822BU (2.4G + 5G), 8821CU (2.4G + 5G), 8822CU (2.4G/20, 5G/20, 5G/40, NB5), 8812EU (2.4G/20, 5G/40, NB5). The oracle caught two real fast-path bugs on the 8812EU before they shipped (the end-state rule above).
  • B210 on-air hop order: PASS on 8822CU (12 cycles), 8812EU (23), Archer T3U (6), and the radiotap-driven per-packet mode.
  • Sweep A/B (CW tone on ch6): fast and full dwells localize the same peak at 20 MHz and at 5 MHz NB on both Jaguar3 variants. NB caveat captured in the docs: a CW at an NB bin's own centre hides in the receiver's DC null and registers on the adjacent bins (identical on both paths — chip physics).
  • Sounding: 8812AU↔8822CU at 2.4 GHz (flat bench map, correct null case, B210 cross-check wired); 8822CU→8812EU at --bins 5170-5250/5 --nb-bw 5 recovered a real H(f) — a smooth ~15 dB notch centred at 5230 MHz with monotone roll-in/out across all 17 five-MHz bins; reverse direction verified at 20 MHz (this 8812EU specimen's NB emission is marginal — same unit with the dead 2.4 GHz RX).

🤖 Generated with Claude Code

josephnef and others added 14 commits July 5, 2026 21:07
freq_to_chan (radiotap CHANNEL consumer, RtlJaguarDevice) and chan_to_freq
(txdemo radiotap builder) were per-file statics; the Jaguar3 radiotap-CHANNEL
hop path is about to need the same mapping, so make it a generation-neutral
header both sides share.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lean hop retune was a Jaguar1-only concrete method, so the txdemo hop
loop needed a DEVOURER_HAVE_JAGUAR1-guarded downcast and every other caller
was stuck with the full SetMonitorChannel. Hoist it into the interface with
a full-path default (SetMonitorChannel at the current width/offset), so
hoppers/sweepers call it unconditionally on any chip; generations with a
real fast path override it. The cache_rf default argument lives only on the
interface declaration (virtual defaults bind statically).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the Jaguar1 fast_retune recipe (docs/frequency-hopping.md) to the
Jaguar3 HAL: an intra-band, same-bandwidth hop writes RF18 inside its
3-wire bracket + force-anapar and toggles the BB reset — everything
bandwidth-keyed (clock/DFIR block, RXBB, MAC BW/TXSC, NB dividers,
DAC-FIFO reset) and band-keyed (CCK-RxIQ, RF 0xdf) is untouched, and the
channel-keyed constants (SCO fc, TX DFIR, AGC table) are written only
when their bucket moves. The 8822c RF18 read-modify-write becomes one
lazily-primed cached read (primed on the first fast hop, after init-time
IQK has settled RF18); the 8822e composes RF18 from scratch (its full
path is a plain write). In 5/10 MHz mode the TX-DFIR write is skipped
entirely — the narrowband re-clock owns 0x808 on the 8822e — so fast
hops preserve the NB divider state without re-running the recipe.

The full set_channel_bwmode now shares the central/pri, RF18, SCO, AGC
and TX-DFIR computations with the fast path (so they cannot diverge) and
invalidates the fast-path caches whenever it runs.

RtlJaguar3Device::FastRetune and SetMonitorChannel both serialize on
_reg_mu against the coex runtime thread — retunes during an active TX
session no longer race the housekeeping tick (previously a documented
contract violation for hop TX).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the Jaguar1 contract in the Jaguar3 send_packet: a radiotap
CHANNEL field whose frequency differs from the current channel triggers
a lean FastRetune before the descriptor is built, making frequency a
per-packet property on this generation too (DEVOURER_HOP_RADIOTAP now
works on 8822C/8822E with zero demo changes). The retune serializes on
_reg_mu; a frame without a CHANNEL field keeps the lock-free hot path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give Jaguar2 the same lean hop retune as Jaguar1/Jaguar3 instead of the
full-path interface default. HalJaguar2::fast_retune (variant-dispatched
like set_channel_bw) runs only the per-hop essentials: one cached
full-register RF18 write (a single direct-window read primes the cache —
collapsing the full path's read-modify-write rounds, three of them on
the 8821C), the channel-keyed constants written only when their bucket
moves (AGC table index, CFO fc, 8822B RF 0xBE VCO band / ch144 RF 0xDF
flag / 2G spur regs, 8821C 2G CCK filter), and the per-hop RX kick
(8822B RF 0xb8 + RX-path toggles; IGI toggle on both variants).
Bandwidth-keyed (0x8ac/0x8c4, RX DFIR, CCA thresholds) and band-keyed
(RFE pins, 8821C switch-band/RF-set) work stays untouched; a band change
falls back to the full path. Concurrency model unchanged from
SetMonitorChannel (no lock; the DIG thread runs only in RX sessions and
its IGI interleaving is benign).

The full path now shares the central-channel and RF-0xBE computations
with the fast path and invalidates the fast-path caches whenever it
runs. stream_tx_demo's hop branch switches to the IRtlDevice virtual
(the txdemo one already had).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweep dwells now retune via IRtlDevice::FastRetune (full-path escape
hatch: DEVOURER_RX_SWEEP_FULL=1) and report the measured cost as a
retune_us= field on each bin line. The per-frame RSSI/SNR aggregate
feeds in sweep mode too, gains an EVM mean (frames that report one), is
drained at each dwell start so retune-transient frames don't leak into
the bin, and lands on the <devourer-energy>ch=N line as
frames/rssi_mean/rssi_max/snr_mean/snr_min/evm_mean — the RX half of the
two-ended sounding map. DEVOURER_RX_AGG_SA=canon|<mac> restricts the
aggregate to the sounding probe's SA so ambient traffic doesn't pollute
per-bin link stats (default: every frame, as before). All fields are
appended, so the existing key=value consumers keep parsing.

The pre-sweep wait now watches for the first RX frame (up to 10 s)
instead of a fixed 2.5 s, so a slow Jaguar3 bring-up (DACK/IQK) isn't
raced by the first retune.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the DumpCanary register dump (the fast-vs-full parity oracle from
docs/frequency-hopping.md) to both newer generations, emitting the same
grep format inside the same markers, from both the full and fast channel
paths — the fast path must dump its own canary or the diff compares a
stale full-set snapshot. The canary sets cover the channel/BW-relevant
registers each generation's channel set touches and exclude live
counters (IGI/FA).

tests/hop_parity_check.sh picks the chip family from TX_PID (FAMILY=
overrides): jaguar1 keeps its TX-power/IQK exclusion lists; jaguar2/3
exclude nothing beyond the full-vs-full run-variance control — their
channel sets have no TX-power stage, so every dumped register must
match. NB_BW=5|10 exercises the Jaguar3 narrowband divider-preservation
case; TX_VID supports OEM-VID dongles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DEVOURER_RX_SWEEP and DEVOURER_HOP_CHANNELS now share one bin-list
grammar (src/SweepSpec.h): plain channel lists as before, channel ranges
("36-48/4"), and centre-frequency MHz ranges ("5170-5250/5" — the
issue's narrowband-map spelling; values >= 1000 are MHz, stepping on the
5 MHz channel grid). One spec now drives both sides of a sounding pair.
Headless ctest guard: sweep_spec_math.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/sounding_sweep.sh orchestrates the active sounding pair: the
prober hops fixed-rate canonical-SA beacons across the bin list via
FastRetune while the sounder sweeps the same bins with the SA-filtered
per-dwell aggregate (DEVOURER_RX_AGG_SA=canon). Synchronization is
asymmetric-duty with no control channel — the script sizes the RX dwell
to ~2 TX full cycles so every dwell overlaps a probe visit — and one
SweepSpec drives both sides (incl. "1-13/1 --nb-bw 5", the 5 MHz
Jaguar3 mode the issue headlines).

tests/sounding_map.py renders the recovered coarse H(f): per-bin median
of rssi_max (off-channel bleed decodes weaker, never stronger, so the
dwell max is the bleed-robust probe level) with rssi_mean/snr/evm/hits
columns, NOTCH (>= 6 dB below the across-bin median) and DEAD (no probe
frames while other bins hit) flags, and an optional Spearman
rank-correlation against a B210 wideband capture — hop_rx_probe.py
grows --bin-power-csv to export exactly that per-bin ground truth
(--sdr on the orchestrator wires it up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 8812EU parity run caught two real breaks in the lean path, both the
same class: init-time halrf calibration rewrites BW-keyed state AFTER
the channel set, so "skip BW-keyed work" preserved the calibration's
values while the full path re-imposes the channel-set ones on every
retune. (1) IQK clears the 40 MHz TX_CCK_IND bit in RF 0x1a — the fast
path now re-asserts the RXBB write once per cache epoch, inside the
3-wire bracket, in each variant's full-path order. (2) In 5/10 MHz mode
the divider recipe owns 0x808[6:4]=1 and the full path re-imposes it per
dwell — the previous skip-0x808-in-NB rule preserved the calibration's
value instead; the fast path now writes the full path's END-state DFIR
pair (NB on the 8822e forces [6:4]=1). RXBB/RF-window writes are now
shared helpers so the paths cannot diverge.

hop_parity_check.sh: exclude the two live registers in the Jaguar1
canary (RF 0x00 AGC-driven mode/gain word, RF 0x42 thermal meter) the
full-vs-full control only catches probabilistically — observed flipping
run to run.

Parity now passes on 8812AU (5G/40), 8822CU (2.4G/20, 5G/20, 5G/40,
NB5) and 8812EU (2.4G/20, 5G/40, NB5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The kernel truncates comm to 15 chars (TASK_COMM_LEN), so every
`pkill -x WiFiDriverTxDemo` (16 chars) in the test harness matched
nothing — pkill warns and exits 1, the || true swallowed it, and TX
demos leaked past their scripts' cleanup traps (bit as a CW-tone holder
still owning the adapter a later sounding run needed). A few scripts
had a 14-char "WiFiDriverTxDe" variant with the same zero-match fate.
Use the actual truncated comm, WiFiDriverTxDem; the pkill -f full-path
forms already worked and are untouched.

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

DEVOURER_TX_MAXFAIL=0 on the prober: a retune can wedge one in-flight
frame into a bulk-OUT timeout (hardware-observed on the 8812EU at 5 MHz
NB on BOTH the full and fast retune paths — the BB reset lands while a
frame drains); a scattered lost probe frame is noise to the map, but the
demo's consecutive-failure bail was killing the prober mid-sweep.

sounding_map.py: skip the SDR rank-correlation when the recovered map is
flat (bench near-field has no rank structure — rho over ties is
meaningless) and render int/float medians uniformly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same contract as the Jaguar1/Jaguar3 paths: a radiotap CHANNEL field
whose frequency differs from the current channel triggers the lean
retune before the descriptor is built, completing the per-packet
frequency story on all three generations (DEVOURER_HOP_RADIOTAP now
works on 8822BU/8821CU too). B210-validated: hop order PASS on the
Archer T3U.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
frequency-hopping.md gains the per-generation port results (measured
switch_us table, what each lean path writes/skips, the lazy RF18 cache,
NB divider preservation) and the end-state-parity rule the Jaguar3 port
hardware-taught: init-time calibration rewrites BW-keyed state after the
channel set, so "skip BW-keyed work" must mean "re-assert once per
fast-hop epoch". rx-spectrum-sensing.md documents the FastRetune sweep
(retune_us, per-dwell frame fields, DEVOURER_RX_AGG_SA/_SWEEP_FULL,
SweepSpec grammar, the NB DC-null CW caveat) and the new active
two-ended sounding section with the measured 5 MHz H(f) result.
CLAUDE.md/README track the same surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef force-pushed the issue149-jaguar3-fastretune-sounding branch from ebcd480 to f604b03 Compare July 5, 2026 19:40
@josephnef josephnef merged commit bd44411 into master Jul 5, 2026
15 checks passed
@josephnef josephnef deleted the issue149-jaguar3-fastretune-sounding branch July 5, 2026 19:43
josephnef added a commit that referenced this pull request Jul 5, 2026
…5 ms on every generation) (#187)

Follow-up to #186: bring every generation's hop latency down to the
RTL8812AU's level, so a robust FHSS layer can sit on top. FastRetune
medians (1/6/11 hop set):

| DUT | before | after | USB op cost | fast ops/hop |
|-----|--------|-------|-------------|--------------|
| RTL8812AU | 1.6 ms | 1.6 ms (was at floor) | ~0.8 ms | 2 |
| RTL8822BU | 18.2 ms | **2.5 ms** | ~1.0 ms | 2 |
| RTL8821CU | 5.6 ms | **0.55 ms** | ~0.5 ms | 1 |
| RTL8822CU | 3.6 ms | **1.9 ms** | ~0.21 ms | 9 |
| RTL8812EU | 4.3 ms | **2.4 ms** | ~0.27 ms | 9 |

## The cost model

`DEVOURER_HOP_PROF=1` (new, `src/HopProf.h`) emits per-stage timing for
every fast hop. It reduced the problem to one equation: **hop cost = USB
op count × the chip's EP0 control-transfer latency**. A register read or
write is one synchronous control transfer; a masked `phy_set_bb_reg` is
secretly two (read-modify-write). EP0 latency is a per-chip firmware
property varying 5× across the family (0.21 ms/op on the 8822CU, ~1
ms/op on the Archer T3U), so the only code lever is op count — the
8812AU's famous 1.6 ms was never exotic, it is simply *two ops per hop*.

## What changed

- **Compose cache** (the Jaguar1 cached-write trick generalised to every
masked write): the fast path primes the full dwords of everything it
touches once per epoch — lazily, on the first fast hop, since init-time
calibration rewrites registers after the full set — then composes bit
changes in memory and writes whole dwords. The steady hop is write-only:
nine transfers on Jaguar3 (3-wire bracket, RF18 window A/B, anapar A/B,
BB-reset triple), one or two RF18 LSSI writes on Jaguar2. Untouched bits
are written back as read — correct by construction. Caches invalidate
wherever those registers are written outside the channel paths.
- **No per-hop RX kick on Jaguar2**: the vendor `switch_channel` tail
(RF 0xb8 toggle, RX-path toggle, IGI toggle) was 17 of the first port's
19 transfers. Hardware A/B on both variants, both directions: a hopping
receiver catches a parked beacon at identical per-dwell medians with and
without it (no decay over ~850 kickless retunes, zero dead dwells), and
hopping-TX delivery is unchanged. The kick stays in the full path only —
a hop needs the channel write, not the state-machine kick.
- **Parity-oracle hardening**: the 8822C's RF 0x1a bit17 floats
(observed flipping between two full-path control runs — it is the
8822E's RXBB register, unused on the C); statically excluded for C PIDs
only, still checked on the E where it is real config.

## Testing

- Build clean (full config + per-chip subsets); `ctest` green (5/5).
- **Register parity** (full-vs-fast with full-vs-full control): PASS on
8812AU (5G/40), 8822BU (2.4G/20), 8821CU (2.4G + 5G), 8822CU (2.4G/20 ×3
repeats, 5G/40), 8812EU (2.4G/20, 5G/40) — the composed writes reproduce
the full path's end state bit-for-bit.
- **B210 on-air hop order**: PASS on all four newer DUTs at the new
speeds (~100 observed cycles each vs 6–23 before) plus the radiotap
per-packet mode.
- **FHSS soaks**: 12,000 (8822CU) and 9,000 (8822BU) consecutive dwell-1
per-packet hops at 300–400 hops/s — zero bulk-OUT failures, schedules
ran to completion. 4-minute kickless hopping-RX soak on the T3U:
first-quarter median == last-quarter median (126 frames/dwell), zero
dead dwells across ~850 retunes.
- Sounding harness re-run on the composed paths: map recovered, all bins
live.

🤖 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.

Frequency-resolved link quality (1/3): narrowband noise/sounding sweep — coarse spectrum map for the adaptive link

1 participant