From 2818c5783edce92f888c7e37fc23dff8a75b64f9 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:30:29 +0300 Subject: [PATCH 1/3] Jaguar3: extend the firmware channel-switch fast path to 8822C/8822E MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Jaguar3 vendor drivers wire the same H2C 0x1D fw switch, so DEVOURER_FASTRETUNE_FW now covers the generation: the fw block in RadioManagementJaguar3::fast_retune mirrors the Jaguar2 port (fire-and-confirm-later, one RF-window read at the next hop, compose caches invalidated under the fw's writes), and the H2C rides HalJaguar3::send_h2c_raw via an injected sender so it shares the HMEBOX box counter with the coex runtime thread's H2Cs under the same lock. 8822E spur channels keep declining every fast path, fw included. On-air (8812CU, kchansw oracles): 36↔40 fw 2.28 ms med / p99 5.94 vs sw 2.40 / 5.19 — a tie on air (the 8822C's RF settle dominates), with ~3× lower per-hop host cost (0.6 ms H2C vs ~1.9 ms composed writes); cross-band 36↔6 through the firmware 2.63 ms med (n=379) vs the ~90 ms full path. Hopping-RX decode identical fw vs sw. The 8822E shares the code path, not yet on-air-validated (no E-die on the rig). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 10 ++-- docs/kernel-channel-switch-offload.md | 13 +++++ src/DeviceConfig.h | 15 +++--- src/jaguar3/CLAUDE.md | 7 +++ src/jaguar3/RadioManagementJaguar3.cpp | 67 ++++++++++++++++++++++++-- src/jaguar3/RadioManagementJaguar3.h | 18 +++++++ src/jaguar3/RtlJaguar3Device.cpp | 13 +++++ 7 files changed, 129 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5398d21c..f5fe2b1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -317,10 +317,12 @@ temporal layer and injects each at its ladder's rate `IRtlDevice::FastRetune(channel)` — lean intra-band, same-bandwidth retune on every generation (RF channel switch only, write-only from a compose cache); falls back to full `SetMonitorChannel` on a band change. FHSS-grade: ~0.5–2.5 ms -per hop depending on chip. On the 8822B, `DEVOURER_FASTRETUNE_FW=1` hands the -hop to the chip firmware instead (H2C 0x1D, fire-and-confirm-later): ~1.4 ms -dead air, and `=2` extends it to **cross-band** hops (~1.9 ms vs the ~90 ms -full path) — protocol + bench: `docs/kernel-channel-switch-offload.md`. `send_packet` honours a radiotap `CHANNEL` field, so +per hop depending on chip. On the 8822B and 8822C/8822E, +`DEVOURER_FASTRETUNE_FW=1` hands the hop to the chip firmware instead (H2C +0x1D, fire-and-confirm-later): ~1.4 ms dead air on the 8822B (a tie on-air on +the 8822C, but ~3× cheaper host-side), and `=2` extends it to **cross-band** +hops (~2–2.6 ms vs the ~90 ms full path) — protocol + bench: +`docs/kernel-channel-switch-offload.md`. `send_packet` honours a radiotap `CHANNEL` field, so hopping is per-packet and radiotap-driven like rate. Demos hop via `DEVOURER_HOP_CHANNELS` (SweepSpec grammar: `1,6,11`, `36-48/4`, `5170-5250/5` MHz) + `DEVOURER_HOP_DWELL_FRAMES` / `_ROUNDS` / `_FAST` / `_RADIOTAP` / diff --git a/docs/kernel-channel-switch-offload.md b/docs/kernel-channel-switch-offload.md index c4ef3229..a9c54412 100644 --- a/docs/kernel-channel-switch-offload.md +++ b/docs/kernel-channel-switch-offload.md @@ -133,6 +133,19 @@ across 400 consecutive band changes (no silent-TX). Hopping RX is unaffected: identical decode counts over identical hop-RX runs with the path on and off. +**Jaguar3** wires the same H2C in both vendor drivers +(`rtl8822c/e_phy.c`), and the port covers it through the same knob (the +H2C rides `HalJaguar3`'s HMEBOX counter, shared with the coex thread's +H2Cs under the same lock). On the 8822C (8812CU, same rig/oracles): +36↔40 fw 2.28 ms med / p99 5.94 versus sw 2.40 / 5.19 — a tie on air, +because the 8822C's RF settle dominates its dark time whichever engine +sequences it; the fw win there is the per-hop host cost (~0.6 ms H2C +submit vs ~1.9 ms of composed USB writes). Cross-band 36↔6 through the +firmware: **2.63 ms median** (n=379, p99 11.0) versus the ~90 ms full +path. Hopping-RX decode identical fw vs sw. The 8822E shares the code +path (not yet on-air-validated — no E-die on the rig this session); its +spur channels conservatively decline the fast path, fw included. + ## Go/no-go for the series **Go — and the port target is devourer, not the kernel.** The offload is diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 52076af2..8af1e68e 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -206,13 +206,14 @@ struct DeviceConfig { * MIX_MODE swing compensation (0xc94/0xe94 TXAGC + 0xc1c/0xe1c BB scale) * so on-air power holds flat as the PA heats over a sustained TX link. */ bool thermal_track = true; - /* env: DEVOURER_FASTRETUNE_FW — Jaguar2 8822B FastRetune firmware fast - * path (H2C 0x1D SINGLE_CHANNELSWITCH_V2, the switch the vendor driver - * gates behind rtw_ch_switch_offload): 0 = off (software compose path), - * 1 = firmware switch for intra-band 20/40 MHz hops, 2 = additionally - * accept cross-band hops (the firmware reprograms the band block; TXAGC - * baseline stays the bring-up band's — active power knobs re-fold). - * Bench + protocol: docs/kernel-channel-switch-offload.md. */ + /* env: DEVOURER_FASTRETUNE_FW — FastRetune firmware fast path (H2C 0x1D + * SINGLE_CHANNELSWITCH_V2, the switch the vendor drivers gate behind + * rtw_ch_switch_offload) on the 8822B (Jaguar2) and 8822C/8822E + * (Jaguar3): 0 = off (software compose path), 1 = firmware switch for + * intra-band 20/40 MHz hops, 2 = additionally accept cross-band hops + * (the firmware reprograms the band block; TXAGC baseline stays the + * bring-up band's — active power knobs re-fold). Bench + protocol: + * docs/kernel-channel-switch-offload.md. */ int fastretune_fw = 0; /* env: DEVOURER_DIS_CCA — Jaguar2/3 MAC carrier-sense disable at bring-up * (primary CCA 0x520[14] + EDCCA [15]): injected/beacon TX stops deferring to diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index bb3c7882..5e83d393 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -23,6 +23,13 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` dividers (vs the `0x8ac` block the Jaguar1/2 chips share). 80 MHz works, incl. a 40-in-80 frame via TX-descriptor DATA_SC. See `docs/narrowband.md`. - halrf calibration: DACK/IQK/TXGAPK/thermal tracking. +- **Firmware channel switch** (H2C 0x1D via `fastretune_fw`, both dies): the + fw-switch H2C must ride `HalJaguar3::send_h2c_raw`'s HMEBOX box counter — + the coex runtime thread shares it (both callers hold `_reg_mu`); a second + counter would corrupt the mailbox rotation. On the 8822C the fw and sw + fast paths tie on-air (~2.3 ms — RF settle dominates); the fw win is ~3× + lower per-hop host/USB cost, plus ~2.6 ms cross-band. 8822E spur channels + decline every fast path, fw included. - `DEVOURER_TX_WITH_RX=thread` must be set **before** `InitWrite` — the bring-up keeps the RX filters open; retrofitting RX later is unreliable. - The rtl8822e's hardware-bisected constraints (DPDT/pin-mux front end, diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index ac28d928..57701472 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -638,6 +638,27 @@ void RadioManagementJaguar3::invalidate_fast_caches() { _last_agc_key = -1; _rxbb_asserted = false; _cw_primed = false; + _fw_sw_pending = 0; /* a full set supersedes any in-flight fw switch */ +} + +bool RadioManagementJaguar3::fw_switch_confirm() { + if (!_fw_sw_pending) + return true; + const uint8_t want = _fw_sw_pending; + _fw_sw_pending = 0; + /* One RF-window read (path A RF18 mirror) — a dwell has passed since the + * H2C, so a live firmware has long since landed the channel field. The + * landed dword re-primes the window half of the compose cache. */ + const uint32_t win_a = + _device.rtw_read32(static_cast(0x3c00 + (0x18 << 2))); + if ((win_a & 0xffu) == want) { + _cw_rfwin_a = win_a; + return true; + } + _logger->warn("Jaguar3: fw channel switch to central {} never landed " + "(RF18 win=0x{:08x}) — resyncing via the full path", + want, win_a); + return false; } bool RadioManagementJaguar3::fast_retune(uint8_t channel, @@ -646,14 +667,15 @@ bool RadioManagementJaguar3::fast_retune(uint8_t channel, if (_last_channel == 0) return false; /* never tuned — unknown band, cold BW/band state */ const bool cur_2g = _last_channel <= 14; - if (cur_2g != (channel <= 14)) - return false; /* band change needs RFE/AGC/CCK-RxIQ — full path only */ + const bool band_change = cur_2g != (channel <= 14); if (channel == _last_channel) return true; /* no-op hop */ /* Spur channels (phydm_spur_eliminate_8822e) carry per-channel NBI/CSI-mask * state that only the full path programs — a lean hop into OR out of one * would leave a stale notch/mask. Decline; the caller falls back to the - * full SetMonitorChannel. 8822E only (the spur port is 8822E-gated). */ + * full SetMonitorChannel. 8822E only (the spur port is 8822E-gated). + * Applies to the fw path too: whether the 8822E firmware programs its own + * notch on these channels is uncharacterized — stay conservative. */ if (_variant == ChipVariant::C8822E) { uint8_t cc, cp, tc, tp; central_and_pri(_last_channel, channel_offset, bwmode, cc, cp); @@ -662,6 +684,45 @@ bool RadioManagementJaguar3::fast_retune(uint8_t channel, return false; } + /* Firmware fast path (DEVOURER_FASTRETUNE_FW): the same H2C 0x1D + * SINGLE_CHANNELSWITCH_V2 the 8822B port uses — both Jaguar3 dies wire it + * in their vendor drivers (rtl8822c/e_phy.c switch_chnl_and_set_bw_by_fw). + * Fire-and-confirm-later (see the Jaguar2 port: polling RF during the + * switch contends with the firmware's RF-bus writes); mode 2 additionally + * hands over band changes. 20/40 MHz only. */ + if (_cfg.tuning.fastretune_fw > 0 && _send_h2c && + (!band_change || _cfg.tuning.fastretune_fw >= 2) && + (bwmode == CHANNEL_WIDTH_20 || bwmode == CHANNEL_WIDTH_40)) { + devourer::HopProf prof(_logger->events(), _cfg.debug.hop_prof, "j3fw", + channel); + if (!fw_switch_confirm()) + return false; /* previous fw switch never landed — full-path resync */ + prof.mark("confirm"); + uint8_t central, pri; + central_and_pri(channel, channel_offset, bwmode, central, pri); + const uint8_t bw_code = (bwmode == CHANNEL_WIDTH_40) ? 1 : 0; + const uint32_t msg = + 0x1du | (static_cast(central) << 8) | + (static_cast((pri & 0xf) | (bw_code << 4)) << 16) | + (0x02u << 24); /* IQK_UPDATE_EN; PWR_IDX_UPDATE stays 0 */ + _send_h2c(msg, 0); + prof.mark("fw"); + _fw_sw_pending = central; + _cw_primed = false; /* the fw rewrites RF18 under the compose cache */ + if (band_change) { + _last_sco = 0xffffffff; + _last_dfir = 0xffffffff; + _last_agc_key = -1; + } + _last_channel = channel; + DVR_DEBUG(_logger, "Jaguar3: fw fast retune -> ch {} (central {})", + channel, central); + return true; + } + + if (band_change) + return false; /* band change needs RFE/AGC/CCK-RxIQ — full path only */ + devourer::HopProf prof(_logger->events(), _cfg.debug.hop_prof, "j3", channel); uint8_t central, pri; diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index 14edd488..5f3902a6 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -2,6 +2,7 @@ #define RADIO_MANAGEMENT_8822C_H #include +#include #include "logger.h" #include "RtlAdapter.h" @@ -50,6 +51,16 @@ class RadioManagementJaguar3 { bool fast_retune(uint8_t channel, uint8_t channel_offset, ChannelWidth_t bwmode, bool cache_rf); + /* Firmware fast-path enabler (H2C 0x1D SINGLE_CHANNELSWITCH_V2 — protocol + * + bench: docs/kernel-channel-switch-offload.md). The sender is injected + * by the device layer so the H2C rides HalJaguar3's HMEBOX box counter — + * shared with the coex runtime thread's H2Cs and serialized by the same + * _reg_mu that serializes FastRetune against the coex tick. Unset = + * fastretune_fw is inert on this radio. */ + void set_h2c_sender(std::function fn) { + _send_h2c = std::move(fn); + } + /* Channel/BW register-canary dump for tests/hop_parity_check.sh — the same * grep format as the Jaguar1 DumpCanary (BB/MAC/RF[A|B] ADDR = VALUE inside * === DEVOURER_DUMP_CANARY === markers). Emitted by both the full and fast @@ -195,6 +206,13 @@ class RadioManagementJaguar3 { uint32_t _cw_c30 = 0, _cw_808 = 0; uint32_t _cw_rfwin_a = 0, _cw_rfwin_b = 0; /* 0x3c60/0x4c60 full dwords */ void invalidate_fast_caches(); + + /* fw fast-path state (fastretune_fw): the pending central channel of a + * fire-and-confirm-later H2C 0x1D switch, corroborated at the next hop by + * one RF-window read (fw_switch_confirm). */ + std::function _send_h2c; + uint8_t _fw_sw_pending = 0; + bool fw_switch_confirm(); /* BW-keyed RXBB state, re-asserted on the first fast hop of each epoch: * the init-time halrf calibration rewrites it after the channel set * (hardware-observed on the 8812EU — IQK clears the 40 MHz TX_CCK_IND bit diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index b73c4568..5231325f 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -44,6 +44,11 @@ RtlJaguar3Device::RtlJaguar3Device(RtlAdapter device, Logger_t logger, : _device{device}, _cfg{std::move(cfg)}, _logger{logger}, _variant{variant}, _hal{device, logger, variant, _cfg}, _radioManagement{device, logger, variant, _cfg}, _phydm{device, logger} { + /* fastretune_fw: the fw-switch H2C must ride the HAL's HMEBOX box counter + * (the coex runtime thread sends its H2Cs through the same one; both + * callers serialize on _reg_mu). */ + _radioManagement.set_h2c_sender( + [this](uint32_t msg, uint32_t ext) { _hal.send_h2c_raw(msg, ext); }); _logger->info("RtlJaguar3Device constructed ({})", variant == jaguar3::ChipVariant::C8822E ? "8822E/EU" : "8822C/CU"); } @@ -1182,9 +1187,17 @@ void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { std::lock_guard lk(_reg_mu); if (channel == _channel.Channel) return; + const bool band_change = (_channel.Channel <= 14) != (channel <= 14); if (_radioManagement.fast_retune(channel, _channel.ChannelOffset, _channel.ChannelWidth, cache_rf)) { _channel.Channel = channel; + /* Only the fw fast path (DEVOURER_FASTRETUNE_FW=2) accepts a band + * change; the firmware retunes the chip but power folding is host-side — + * re-fold active knobs against the new band's tables (the same gating + * SetMonitorChannel applies). */ + if (band_change && _brought_up && + (_tx_pwr_offset_steps != 0 || _tx_pwr_override >= 0)) + apply_tx_power_current(/*full=*/true); return; } /* Fast path declined (band change / never tuned) — full channel set at the From b8667db6bd0efca3db852bad1d88bae4b7ab669d Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:37:18 +0300 Subject: [PATCH 2/3] docs: frequency-hopping ports table gains the firmware fast path column Cross-links the H2C 0x1D mode into the per-generation table (host-cost column, like-for-like hop.dwell medians) with the on-air dead-time and cross-band numbers pointing at kernel-channel-switch-offload.md. Co-Authored-By: Claude Opus 4.8 --- docs/frequency-hopping.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index a11f947f..5bb8e8b7 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -329,13 +329,13 @@ constants once); every subsequent same-band hop is ~1.5 ms. point (default = the full `SetMonitorChannel` at the current width/offset), and every generation overrides it with a lean path built from the tricks above: -| DUT | full path | fast (cached) | USB op cost | fast ops/hop | -|-----|-----------|---------------|-------------|--------------| -| RTL8812AU (Jaguar1) | ~277 ms | **~1.6 ms** | ~0.8 ms | 2 | -| RTL8822BU (Jaguar2) | ~65 ms | **~2.5 ms** | ~1.0 ms | 2 | -| RTL8821CU (Jaguar2) | ~30 ms | **~0.55 ms** | ~0.5 ms | 1 | -| RTL8822CU (Jaguar3) | ~12 ms | **~1.9 ms** | ~0.21 ms | 9 | -| RTL8812EU (Jaguar3) | ~12 ms | **~2.4 ms** | ~0.27 ms | 9 | +| DUT | full path | fast (cached) | fast (fw, host cost) | USB op cost | fast ops/hop | +|-----|-----------|---------------|----------------------|-------------|--------------| +| RTL8812AU (Jaguar1) | ~277 ms | **~1.6 ms** | — | ~0.8 ms | 2 | +| RTL8822BU (Jaguar2) | ~65 ms | **~2.5 ms** | ~2.6 ms | ~1.0 ms | 2 | +| RTL8821CU (Jaguar2) | ~30 ms | **~0.55 ms** | — | ~0.5 ms | 1 | +| RTL8822CU (Jaguar3) | ~12 ms | **~1.9 ms** | **~0.8 ms** | ~0.21 ms | 9 | +| RTL8812EU (Jaguar3) | ~12 ms | **~2.4 ms** | (same path, unvalidated) | ~0.27 ms | 9 | (Median `hop.dwell` switch_us over a 1/6/11 hop set; per-stage numbers from `DEVOURER_HOP_PROF=1`. Every hop microsecond is USB round-trips: one register @@ -345,6 +345,20 @@ lever is op count. FHSS-soak-validated: 12,000 (8822CU) and 9,000 (8822BU) consecutive dwell-1 per-packet hops, zero bulk-OUT failures; a kickless hopping receiver holds a constant catch rate over ~850 retunes.) +**The firmware fast path** (`DEVOURER_FASTRETUNE_FW`, 8822B + 8822C/8822E): +instead of the composed register sequence, one H2C 0x1D hands the whole +retune to the chip firmware, fire-and-confirm-later (the previous hop is +corroborated by a single RF18 read at the next one). Host cost above is the +H2C submit + confirm; the number that matters — **on-air dead time** — is +measured separately with the channel-switch oracles: on the 8822B the fw +path nearly halves it (1.44 ms median vs the sw path's 2.75), on the 8822C +the two tie (~2.3 ms — that die's RF settle dominates) but the fw path is +~3× cheaper on the bus. Mode `=2` extends the hop set **across the 2.4/5 GHz +band boundary** (~2–2.6 ms measured, vs the ~90 ms full-path fallback the sw +fast path needs) — mixed-band FHSS plans become practical. Protocol, bench +method and full distributions: +[kernel-channel-switch-offload.md](kernel-channel-switch-offload.md). + Two techniques carried the newer generations to the table above, beyond the original tricks: From 371e6ea8e8e84079968db1a7f7931119d91deaa8 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:39:06 +0300 Subject: [PATCH 3/3] docs: drop changelog narration from the channel-switch notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "now" dates a sentence to its writing; state the current shape instead — and the port section's opening had gone stale (it predated the Jaguar3 coverage described two paragraphs below it). Co-Authored-By: Claude Opus 4.8 --- docs/kernel-channel-switch-baseline.md | 4 ++-- docs/kernel-channel-switch-offload.md | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/kernel-channel-switch-baseline.md b/docs/kernel-channel-switch-baseline.md index 7e620fa3..024d5f0c 100644 --- a/docs/kernel-channel-switch-baseline.md +++ b/docs/kernel-channel-switch-baseline.md @@ -229,8 +229,8 @@ hopping primitive. ### TDLS / MCC offload paths TDLS channel-switch offload stays **blocked-hardware-absent** end-to-end -(needs two associated STAs under an AP), but the target is now fully -named from source + build evidence: `rtw_hal_ch_sw_oper_offload` +(needs two associated STAs under an AP), but the target is fully named +from source + build evidence: `rtw_hal_ch_sw_oper_offload` (`hal/hal_com.c`) composes **H2C 0x1C `CHNL_SWITCH_OPER_OFFLOAD`**, built and runtime-armed in the measured module. The MCC attempt got further than expected (capability matrix): everything up to `mcc_enable=1` works; diff --git a/docs/kernel-channel-switch-offload.md b/docs/kernel-channel-switch-offload.md index a9c54412..93adc2c7 100644 --- a/docs/kernel-channel-switch-offload.md +++ b/docs/kernel-channel-switch-offload.md @@ -105,9 +105,11 @@ Reading: ## The devourer port (`DEVOURER_FASTRETUNE_FW`) -The offload is now a `FastRetune` fast path in the Jaguar2 HAL -(`HalJaguar2::fw_channel_switch`, 8822B only): the H2C rides the classic -HMEBOX mailboxes devourer already drives, and completion is +Devourer ships the offload as a `FastRetune` fast path +(`HalJaguar2::fw_channel_switch` on the 8822B; +`RadioManagementJaguar3::fast_retune`'s fw block on the 8822C/8822E): the +H2C rides the classic HMEBOX mailboxes devourer already drives, and +completion is **fire-and-confirm-later** — polling RF18 during the switch measurably *stretched* it (the PI reads contend with the firmware's RF-bus writes; on-air dead time tripled), and the vendor's C2H wait needs an RX drain a