FastRetune on all three generations + active two-ended sounding sweep#186
Merged
Conversation
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>
ebcd480 to
f604b03
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 = fullSetMonitorChannel), 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:FastRetuneandSetMonitorChannelnow serialize on the coex-thread register mutex — in-session hops no longer race the housekeeping tick.CHANNELdrives a per-packet FastRetune on all three generations now (was Jaguar1-only).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
DEVOURER_RX_SWEEP_FULL=1forces the full path), and eachch=Nline reports the measuredretune_us.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_CHANNELSshare one bin-list grammar (src/SweepSpec.h): channel lists, channel ranges (36-48/4), and MHz ranges (5170-5250/5). Headlesssweep_spec_mathctest.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.pyrenders the map: per-bin median ofrssi_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
DumpCanaryported to Jaguar2 + Jaguar3 (same format, emitted from both the full and fast paths);tests/hop_parity_check.shis 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 WiFiDriverTxDemocleanup intests/*.shwas a silent no-op (leaked TX demos held adapters across scripts). Fixed to the real truncated comm across the harness.Testing
ctestgreen (5/5, incl. the newsweep_spec_math).--bins 5170-5250/5 --nb-bw 5recovered 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