Skip to content

FHSS-grade hop latency: write-only compose cache + kick diet (0.55-2.5 ms on every generation)#187

Merged
josephnef merged 5 commits into
masterfrom
fhss-hop-latency
Jul 5, 2026
Merged

FHSS-grade hop latency: write-only compose cache + kick diet (0.55-2.5 ms on every generation)#187
josephnef merged 5 commits into
masterfrom
fhss-hop-latency

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

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

josephnef and others added 5 commits July 5, 2026 23:28
One machine-parseable <devourer-hop-prof> line per fast hop with
per-stage microseconds — the instrument behind the hop-latency work
(every hop microsecond is USB control transfers; the profile is what
localises them). Zero overhead when unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A masked phy_set_bb_reg is a read-modify-write — two USB control
transfers — and the hop's cost is purely transfer count x EP0 latency
(DEVOURER_HOP_PROF-measured ~0.21 ms/op on the 8822CU). Prime the full
dwords of everything the hop touches once per epoch (lazily, on the
first fast hop — init calibration rewrites registers after the full
set), compose bit changes in memory, and write whole dwords: the steady
hop is nine write-only transfers (3-wire bracket, RF18 window A/B,
anapar A/B, BB-reset triple), with SCO/DFIR as composed single writes on
bucket change (DFIR's two nibbles in one dword). Top bits ride along
from the prime, so untouched fields are written back as read — correct
by construction. The compose caches invalidate wherever those registers
are written outside the channel paths (TXAGC-gate writers, the NB
divider recipe).

Measured (1/6/11 hop set): 8822CU 3.6 -> 1.9 ms, 8812EU 4.3 -> 2.4 ms
median. Register parity PASS on both variants (20/40 MHz + NB shapes);
B210 hop order PASS (~100 cycles); 12,000 consecutive dwell-1 per-packet
hops with zero bulk-OUT failures.

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

The first Jaguar2 fast path spent 17 of its 19 USB transfers on the
vendor switch_channel tail (RF 0xb8 toggle, RX-path toggle, IGI toggle
— each masked write a read+write round-trip, at the T3U's ~1 ms EP0
latency). Hardware A/B on both variants, both directions, shows a hop
does not need that kick: 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.

The rest goes write-only via the same compose cache as Jaguar1/Jaguar3:
RF18 primed once and composed per hop (the 8821CU hop is a single LSSI
write), AGC/fc/RF-0xBE as composed writes on bucket change.

Measured (1/6/11 hop set): 8822BU 18.2 -> 2.5 ms, 8821CU 5.6 -> 0.55 ms
median. Register parity PASS both variants (2.4/5 GHz); B210 hop order
PASS (~100 cycles); 9,000 consecutive dwell-1 per-packet hops with zero
bulk-OUT failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RF 0x1a is not written by any channel path on the 8822C (it is the
8822E's RXBB register) and its bit17 floats — observed flipping between
two full-path control runs. Static exclusion for the C PIDs only; on the
E it is real RXBB config and stays checked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ports table gains the per-DUT USB-op cost and op count (the whole
cost model: transfer count x EP0 latency, which varies 5x across the
family), the two techniques that carried the newer generations to
0.55-2.5 ms (the compose cache generalising the cached-write trick to
every masked register write; the hardware-measured finding that a hop
needs no per-hop RX kick), and the FHSS soak results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit 3ec1ab1 into master Jul 5, 2026
15 checks passed
@josephnef josephnef deleted the fhss-hop-latency branch July 5, 2026 20:43
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