diff --git a/CLAUDE.md b/CLAUDE.md index 688a0f6..f5df669 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,9 +59,10 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first): only what an independent witness decoded — legacy OFDM + HT MCS0-7, BCC, 20/40 MHz on 2.4/5 GHz, plus long-preamble CCK on 2.4 GHz at 20 MHz. Everything the backend has not ported (TSF/beacons, hardware ACK, A-MPDU, - FastRetune, the runtime TX-power levers) falls through to `IRtlDevice`'s + the runtime TX-power levers) falls through to `IRtlDevice`'s not-ported defaults rather than being faked, so read the base class before - assuming a cross-generation feature below applies here. SGI, LDPC, STBC, VHT + assuming a cross-generation feature below applies here. `FastRetune` IS + ported (intra-band, TSSI kept live — `src/rtl8733b/CLAUDE.md`). SGI, LDPC, STBC, VHT and HE are refused outright. Scope, one-unit validation record and the deferred matrix: `docs/rtl8733b.md`. @@ -380,10 +381,13 @@ temporal layer and injects each at its ladder's rate ## Frequency hopping `IRtlDevice::FastRetune(channel)` — lean intra-band, same-bandwidth retune on -the four Jaguar/Kestrel generations (RF channel switch only, write-only from a -compose cache); falls back to full `SetMonitorChannel` on a band change, and to -the full path entirely on the RTL8733B, which has no fast path yet. -FHSS-grade: ~0.5–2.5 ms per hop depending on chip. On the Jaguar2 dies (8822B, 8821C) and Jaguar3 +all five generations (RF channel switch only, write-only from a +compose cache); falls back to full `SetMonitorChannel` on a band change. +FHSS-grade on the Jaguar/Kestrel dies: ~0.5–2.5 ms per hop depending on chip. +On the RTL8733B the hop keeps TSSI tracking live (per-channel rate-offset +dwords rewritten in place) and costs ~55 ms call / ~10 ms p50 radio-live on +the USB-HS validation unit — vs its ~330-440 ms full path — with a measured +1-in-20 40 ms radio-live tail; one unit, no SDR (`src/rtl8733b/CLAUDE.md`). On the Jaguar2 dies (8822B, 8821C) and Jaguar3 (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/8822E, but ~3× cheaper host-side), and `=2` extends it diff --git a/docs/rtl8733b.md b/docs/rtl8733b.md index 517e79a..c1ab80e 100644 --- a/docs/rtl8733b.md +++ b/docs/rtl8733b.md @@ -202,8 +202,18 @@ These results have **not** been claimed: - SGI remains disabled: a descriptor with its short-GI bit set submitted successfully, but an independent RTL8812AU decoded both broadcast and unicast probes as long GI. STBC and LDPC remain rejected. ACK/BlockAck - response, A-MPDU, fast retune and throughput were not independently + response, A-MPDU and throughput were not independently validated and remain unadvertised or unsupported by this backend. +- Fast retune is now ported and independently witnessed (intra-band, + same-width; `src/rtl8733b/CLAUDE.md` has the measured contract and its + counterparts): channel-state readback parity 7/7 hops, a 299/300 post-hop + burst decoded by an RTL8812AU witness, ~55 ms call / ~10 ms p50 radio-live + on this unit. The channel-bucketed TSSI-DE offsets are rewritten in place + across the hop with tracking live (the first cut skipped them — caught in + review): DE readback parity 22/22 across ch1→ch13 hops, tracking-enable + field held at 7 throughout, settle p50 unchanged at 10.4 ms. Radiated + power across a hop remains SDR-gated like every other RF-domain quantity + here. - A vendor-kernel-driver cross-test was attempted with the 2024 source on Linux 6.14, but its module did not build: its Makefile omitted required include paths, and after supplying the main include path compilation stopped on an diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md index 61ecd72..fd2387f 100644 --- a/src/rtl8733b/CLAUDE.md +++ b/src/rtl8733b/CLAUDE.md @@ -127,9 +127,41 @@ The SGI refusal is the one worth re-opening: the descriptor bit was set and the frame submitted, but the witness decoded long GI. Descriptor and air disagree, and the reason is not yet understood. +## FastRetune + +Ported: intra-band, same-width hops (`Phy8733b::fast_retune`). The full +`SetMonitorChannel` on this USB-HS part costs ~330-440 ms — profiled as +~165 ms TSSI disable/re-enable, ~90 ms band+bandwidth switches, ~60 ms +channel switch — and the fast path keeps only what a hop needs: the RF18 +synth program (bandwidth bits preserved via a compose cache primed on the +first fast hop after a full set), RF19 sub-band bits and the channel-keyed +BB constants on bucket change only, then BB reset + IGI toggle. **TSSI +tracking stays enabled across the hop**, with the per-channel rate-offset +dwords AND the channel-bucketed TSSI-DE offsets rewritten in place when +their plans differ — the in-place shape #389 validated; every full set +still runs the disable/re-enable pair. The DE rewrite is the one that +matters intra-band: the rate offsets are band-keyed and never change +across a 2.4 GHz hop, while the DE buckets are ~3 channels wide +(boundaries at ch 2/5/8/11/14, trim at 7/14), so most hops cross one — +skipping them leaves the loop tracking with the previous channel's +calibration (found by review on the first cut of this port). The rewrite +replays prepare_tssi_offsets' field sequence minus its tracking-disable +write; measured across 22 ch1→ch13 hops on the validation unit: readback +parity 22/22, tracking-enable field stayed 7 throughout, settle p50 +unchanged (10.4 ms). Measured +on the validation unit (20-cycle settle harness, 1 kHz witness emitter): +call ~55 ms, radio-live 10.0 ms p50 from hop start (min 3.6 / p90 12.9 / +**max 40.3 ms — a 1-in-20 tail, not noise**), vs 70-100 ms radio-live +through the full path. Channel-state readback parity held 7/7 hops, and a +300-frame post-hop burst decoded 299/300 at an independent witness — the +TSSI-live claim is air-verified. The counterparts: one physical unit, like +every on-air claim in this subtree; no SDR, so radiated power ACROSS a hop +is uncharacterized; and cross-band or width-change hops decline (chip +untouched) and fall back to the full path. + ## Not ported -`ReadTsf`/beacons, hardware ACK/BlockAck, A-MPDU, `FastRetune`, +`ReadTsf`/beacons, hardware ACK/BlockAck, A-MPDU, `FastSetBandwidth`, the runtime TX-power knobs, `rx.path` per-chain telemetry, and CCA disable. These inherit `IRtlDevice`'s not-ported defaults (`false`, `0`, or a full-path fallback) rather than being faked. `SetCcaMode` is the one diff --git a/src/rtl8733b/Phy8733b.cpp b/src/rtl8733b/Phy8733b.cpp index fabcb72..7b2f9c9 100644 --- a/src/rtl8733b/Phy8733b.cpp +++ b/src/rtl8733b/Phy8733b.cpp @@ -694,6 +694,50 @@ void Phy8733b::spur_cancellation() { set_bb(0x1908, 0xf0u, 9); } +/* Channel-keyed constants shared by switch_channel (which writes them on + * every full set) and fast_retune (which writes them only when their bucket + * changes across the hop). One copy, so full-vs-fast register parity cannot + * silently diverge. */ +namespace { + +constexpr uint32_t kScoFc2a38_2g[15] = { + 0, 0x1cfea, 0x1d0e1, 0x1d1d7, 0x1d2cd, 0x1d3c3, 0x1d4b9, + 0x1d5b0, 0x1d6a6, 0x1d79c, 0x1d892, 0x1d988, 0x1da7f, + 0x1db75, 0x1ddc4}; +constexpr uint32_t kScoFc2a3c_2g[15] = { + 0, 0x27de3, 0x27f35, 0x28088, 0x281da, 0x2832d, 0x2847f, + 0x285d2, 0x28724, 0x28877, 0x289c9, 0x28b1c, 0x28c6e, + 0x28dc1, 0x290ed}; + +constexpr uint32_t agc_bucket_5g(uint8_t center) { + return center <= 64 ? 6u : center <= 144 ? 7u : 0u; +} + +constexpr uint32_t sco_bucket_0c30(uint8_t center) { + return center >= 173 ? 0x411u + : center >= 120 ? 0x412u + : center >= 112 ? 0x452u + : center >= 56 ? 0x453u + : center >= 52 ? 0x493u + : center >= 16 ? 0x494u + : center >= 13 ? 0x969u + : center >= 11 ? 0x96au + : 0x9aau; +} + +constexpr std::array tx_shape_2g(uint8_t center) { + return {center == 14 ? 0x452484u : 0x7847cfu, + center == 14 ? 0x0fe3c8u : 0x57a6b1u, + center == 14 ? 0u : 0x1f2af412u, + center == 14 ? 0u : 0x09717du, + center == 14 ? 0u : 0xfb9003u, + center == 14 ? 0u : 0xfb1fa5u, + center == 14 ? 0u : 0xfe2fcau, + center == 14 ? 0u : 0xffcff3u}; +} + +} // namespace + bool Phy8733b::switch_channel(const ChannelPlan8733b &plan) { uint32_t rf18 = read_rf(0, 0x18); uint32_t rf19 = read_rf(0, 0x19); @@ -722,44 +766,18 @@ bool Phy8733b::switch_channel(const ChannelPlan8733b &plan) { set_bb(0x18ac, 0x000001f0u, 4); } else { set_bb(0x1ea8, 1u << 7, 0); - const uint32_t agc = plan.center <= 64 ? 6 : plan.center <= 144 ? 7 : 0; - set_bb(0x18ac, 0x000001f0u, agc); + set_bb(0x18ac, 0x000001f0u, agc_bucket_5g(plan.center)); } - static constexpr uint32_t sco38[15] = { - 0, 0x1cfea, 0x1d0e1, 0x1d1d7, 0x1d2cd, 0x1d3c3, 0x1d4b9, - 0x1d5b0, 0x1d6a6, 0x1d79c, 0x1d892, 0x1d988, 0x1da7f, - 0x1db75, 0x1ddc4}; - static constexpr uint32_t sco3c[15] = { - 0, 0x27de3, 0x27f35, 0x28088, 0x281da, 0x2832d, 0x2847f, - 0x285d2, 0x28724, 0x28877, 0x289c9, 0x28b1c, 0x28c6e, - 0x28dc1, 0x290ed}; if (plan.center <= 14) { set_bb(0x2a38, 1u << 27, 0); - set_bb(0x2a38, 0x07ffff00u, sco38[plan.center]); - set_bb(0x2a3c, 0x000fffffu, sco3c[plan.center]); + set_bb(0x2a38, 0x07ffff00u, kScoFc2a38_2g[plan.center]); + set_bb(0x2a3c, 0x000fffffu, kScoFc2a3c_2g[plan.center]); } - const uint32_t sco = plan.center >= 173 ? 0x411 - : plan.center >= 120 ? 0x412 - : plan.center >= 112 ? 0x452 - : plan.center >= 56 ? 0x453 - : plan.center >= 52 ? 0x493 - : plan.center >= 16 ? 0x494 - : plan.center >= 13 ? 0x969 - : plan.center >= 11 ? 0x96a - : 0x9aa; - set_bb(0x0c30, 0xfffu, sco); + set_bb(0x0c30, 0xfffu, sco_bucket_0c30(plan.center)); if (plan.is_2g) { - const uint32_t shape[8] = { - plan.center == 14 ? 0x452484u : 0x7847cfu, - plan.center == 14 ? 0x0fe3c8u : 0x57a6b1u, - plan.center == 14 ? 0u : 0x1f2af412u, - plan.center == 14 ? 0u : 0x09717du, - plan.center == 14 ? 0u : 0xfb9003u, - plan.center == 14 ? 0u : 0xfb1fa5u, - plan.center == 14 ? 0u : 0xfe2fcau, - plan.center == 14 ? 0u : 0xffcff3u}; + const auto shape = tx_shape_2g(plan.center); for (unsigned i = 0; i < 8; ++i) set_bb(static_cast(0x1a00 + i * 4), i == 2 ? kDwordMask : 0x00ffffffu, shape[i]); @@ -1315,6 +1333,11 @@ bool Phy8733b::enable_tssi_tracking(SelectedChannel channel, _tssi_digital_snapshot = digital_snapshot; _tssi_analog_snapshot = analog_snapshot; _tssi_is_2g = channel_cfg->is_2g; + /* fast_retune bookkeeping: the offsets the chip now carries and the RF + * path bit (RFE routing — static per session), for the in-place + * per-channel rewrite. */ + _fr_tssi_offsets = capped->rate_offsets; + _fr_tssi_path = path; _logger->info( "RTL8733B TSSI tracking enabled: ch={} path={} ceiling={} " "rates={:08x}/{:08x}/{:08x}/{:08x}/{:08x}", @@ -1365,6 +1388,9 @@ bool Phy8733b::disable_tssi_tracking() { _tssi_digital_snapshot.reset(); _tssi_analog_snapshot.reset(); } + _fr_tssi_offsets.reset(); + _fr_tssi_de.reset(); + _fr_tssi_power.reset(); _logger->info("RTL8733B TSSI tracking disabled: rollback={}", ok); devourer::Ev(_logger->events(), "rtl8733b.tssi_tracking") .f("enabled", false) @@ -1428,6 +1454,13 @@ bool Phy8733b::prepare_tssi_offsets(SelectedChannel channel, const TssiDeState8733b state = read_tssi_de_state(); const bool ok = state.matches_disabled(*de); + if (ok) { + /* fast_retune bookkeeping: the DE plan the chip now carries and the + * EFUSE calibration that derives it, for the in-place per-channel + * rewrite on bucket-crossing hops. */ + _fr_tssi_de = *de; + _fr_tssi_power = efuse.tssi_power; + } _logger->info( "RTL8733B TSSI-DE: ready={} enabled={} ch={} cck={}/{} ht40={}/{} " "ofdm={}/{} ht20={}/{}", @@ -1489,6 +1522,15 @@ bool Phy8733b::set_channel(SelectedChannel channel) { const ChannelState8733b state = read_channel_state(); const bool ok = state.matches(*plan); + /* fast_retune bookkeeping: record where the radio is, and invalidate the + * RF compose cache — the switch functions above rewrote both words, so a + * cached copy is stale until the next fast hop re-primes it. */ + if (ok) + _fr_plan = *plan; + else + _fr_plan.reset(); + _fr_rf18.reset(); + _fr_rf19.reset(); _logger->info( "RTL8733B channel: ready={} primary={} center={} width={} offset={} " "RF18={:05x}/{:05x} RF19={:05x}/{:05x} BB9B0={:08x} " @@ -1513,6 +1555,146 @@ bool Phy8733b::set_channel(SelectedChannel channel) { return ok; } +bool Phy8733b::fast_retune(SelectedChannel channel, bool tssi_live, + uint8_t max_target_qdbm, bool cache_rf) { + const auto plan = channel_plan(channel); + if (!_initialized || !plan || !_fr_plan) + return false; + const ChannelPlan8733b cur = *_fr_plan; + if (plan->is_2g != cur.is_2g || plan->width != cur.width || + plan->offset != cur.offset) + return false; + if (plan->center == cur.center && plan->primary == cur.primary) + return true; + + /* Everything that can refuse is computed BEFORE the first chip write, so a + * declined hop leaves the radio untouched for the caller's full-path + * fallback rather than half-hopped. */ + std::optional tssi; + std::optional de; + if (tssi_live && _fr_tssi_offsets) { + tssi = tssi_bb_plan(_tx_power_targets, channel.Channel, _rfe_type, + _fr_tssi_path, max_target_qdbm); + if (!tssi) + return false; + } + if (tssi_live && _fr_tssi_de && _fr_tssi_power) { + de = tssi_de_plan(*_fr_tssi_power, channel.Channel); + if (!de) + return false; + } + if (!cache_rf || !_fr_rf18) { + const uint32_t rf18 = read_rf(0, 0x18); + if (rf18 == 0xffffffffu) + return false; + _fr_rf18 = rf18; + } + if (!cache_rf || !_fr_rf19) { + const uint32_t rf19 = read_rf(0, 0x19); + if (rf19 == 0xffffffffu) + return false; + _fr_rf19 = rf19; + } + + uint32_t rf18 = *_fr_rf18; + uint32_t rf19 = *_fr_rf19; + if (plan->is_2g) { + rf18 &= ~((3u << 16) | (3u << 8) | 0xffu); + rf18 |= plan->center; + } else { + rf18 &= ~((1u << 17) | (1u << 9) | 0xffu); + rf18 |= (1u << 16) | (1u << 8) | plan->center; + rf19 &= ~((1u << 19) | (1u << 18)); + if (plan->center > 144) + rf19 |= 1u << 19; + else if (plan->center > 80) + rf19 |= 1u << 18; + } + /* From the first chip write onward a transport failure leaves the radio + * part-hopped, so the catch drops the fast-path bookkeeping: the next call + * declines (stale _fr_plan gone) and the caller's full-path fallback + * reprograms everything, instead of a later fast hop bucket-comparing + * against a baseline the chip no longer holds. */ + try { + if (!program_synth(rf18)) { + _fr_plan.reset(); + return false; + } + _fr_rf18 = rf18; + if (rf19 != *_fr_rf19) { + write_rf(0, 0x19, kRfMask, rf19); + write_rf(1, 0x19, kRfMask, rf19); + _fr_rf19 = rf19; + } + + /* Channel-keyed constants from switch_channel, written only when their + * bucket changes across the hop. Band-keyed constants (0x1ea8, the + * 2.4 GHz 0x18ac pair, spur cancellation) were set by the last full set + * at this band and are untouched — that is the fast path's contract. */ + if (plan->is_2g) { + if (plan->center <= 14) { + set_bb(0x2a38, 0x07ffff00u, kScoFc2a38_2g[plan->center]); + set_bb(0x2a3c, 0x000fffffu, kScoFc2a3c_2g[plan->center]); + } + if ((plan->center == 14) != (cur.center == 14)) { + const auto shape = tx_shape_2g(plan->center); + for (unsigned i = 0; i < 8; ++i) + set_bb(static_cast(0x1a00 + i * 4), + i == 2 ? kDwordMask : 0x00ffffffu, shape[i]); + } + } else { + if (agc_bucket_5g(plan->center) != agc_bucket_5g(cur.center)) + set_bb(0x18ac, 0x000001f0u, agc_bucket_5g(plan->center)); + } + if (sco_bucket_0c30(plan->center) != sco_bucket_0c30(cur.center)) + set_bb(0x0c30, 0xfffu, sco_bucket_0c30(plan->center)); + if ((plan->center == 13) != (cur.center == 13)) + set_bb(0x0808, 0x7fu, plan->center == 13 ? 0x30 : 0x40); + + bb_reset(); + igi_toggle(); + + /* TSSI: tracking stays enabled; the per-channel rate-offset dwords and + * the channel-bucketed DE offsets are rewritten, in place, each only + * when the new channel's plan differs (the #389 shape). Intra-band the + * rate offsets are band-keyed and never change — the DE buckets, ~3 + * channels wide at 2.4 GHz, are the calibration a hop actually moves. + * The rollback snapshots are untouched — they record the pre-enable + * state, which these rewrites do not change. */ + if (tssi && tssi->rate_offsets != *_fr_tssi_offsets) { + for (size_t i = 0; i < tssi->rate_offsets.size(); ++i) + set_bb(static_cast(0x3a00 + i * 4), kDwordMask, + tssi->rate_offsets[i]); + _fr_tssi_offsets = tssi->rate_offsets; + } + if (de && !(*de == *_fr_tssi_de)) { + /* The prepare_tssi_offsets field sequence, minus its 0x4318 + * tracking-disable write — the loop stays live across the rewrite. */ + set_bb(0x433c, 0x0ff00000u, static_cast(de->cck[0])); + set_bb(0x434c, 0x0ff00000u, static_cast(de->cck[1])); + set_bb(0x4334, 0x0ff00000u, static_cast(de->ht40[0])); + set_bb(0x4344, 0x0ff00000u, static_cast(de->ht40[1])); + set_bb(0x43b0, 0x000000ffu, static_cast(de->ofdm[0])); + set_bb(0x43b0, 0x0000ff00u, static_cast(de->ht40[0])); + set_bb(0x43b0, 0x00ff0000u, static_cast(de->ht40[0])); + set_bb(0x43b4, 0x0000ff00u, static_cast(de->ofdm[1])); + set_bb(0x43b4, 0x000000ffu, static_cast(de->ht40[1])); + set_bb(0x43b4, 0x00ff0000u, static_cast(de->ht40[1])); + set_bb(0x43b4, 0xff000000u, static_cast(de->ht40[1])); + set_bb(0x43b0, 0xff000000u, static_cast(de->ht20[0])); + set_bb(0x43b8, 0x000000ffu, static_cast(de->ht20[1])); + _fr_tssi_de = *de; + } + _fr_plan = *plan; + return true; + } catch (...) { + _fr_plan.reset(); + _fr_rf18.reset(); + _fr_rf19.reset(); + throw; + } +} + bool Phy8733b::initialize(uint8_t cut, const EfuseInfo &efuse) { if (!efuse.valid || efuse.rfe_type == 0xff || efuse.xtal == 0xff) { _logger->error("RTL8733B PHY: refusing initialization without EFUSE data"); diff --git a/src/rtl8733b/Phy8733b.h b/src/rtl8733b/Phy8733b.h index 13b8aef..5069bc6 100644 --- a/src/rtl8733b/Phy8733b.h +++ b/src/rtl8733b/Phy8733b.h @@ -93,6 +93,8 @@ struct TssiDePlan8733b { std::array ht40{}; std::array ofdm{}; std::array ht20{}; + + bool operator==(const TssiDePlan8733b &) const = default; }; struct TssiDeState8733b { @@ -194,6 +196,28 @@ class Phy8733b { bool enable_tssi_tracking(SelectedChannel channel, const EfuseInfo &efuse, uint8_t max_target_qdbm); bool disable_tssi_tracking(); + /* Lean intra-band, same-bandwidth hop — the FastRetune core (see + * docs/frequency-hopping.md; profile that sized it: full set_channel on + * this USB-HS part is ~330 ms, of which ~165 ms is the TSSI + * disable/re-enable dance and ~90 ms the band/bandwidth switches a hop + * does not need). The subset a hop DOES need: the RF18 synth program + * (bandwidth bits preserved from the cached word), RF19 sub-band bits and + * the channel-keyed BB constants (AGC bucket, SCO fc) only when their + * bucket changes, then the BB reset + IGI toggle that restart the RX + * engine. TSSI tracking stays ENABLED across the hop; the per-channel + * rate-offset dwords and the channel-bucketed TSSI-DE offsets are + * rewritten in place, each only when the plan for the new channel + * differs — the in-place-rewrite-with-tracking-live shape validated in + * #389. 2.4 GHz hops also rewrite the per-channel SCO fc words and the + * ch13/ch14-keyed constants when crossed. + * + * cache_rf=true runs write-only from words primed by one read each on the + * first fast hop after a full set; cache_rf=false re-reads every hop (the + * A/B knob measuring the read penalty). Returns false — chip untouched — + * on a band or width change or when the radio was never tuned; the caller + * falls back to the full set_channel. */ + bool fast_retune(SelectedChannel channel, bool tssi_live, + uint8_t max_target_qdbm, bool cache_rf); bool prepare_tssi_offsets(SelectedChannel channel, const EfuseInfo &efuse); TssiDeState8733b read_tssi_de_state(); uint8_t read_thermal(); @@ -248,6 +272,28 @@ class Phy8733b { std::optional _tssi_analog_snapshot; bool _tssi_is_2g = true; bool _initialized = false; + + /* fast_retune state. _fr_plan is the channel the radio is actually on, + * recorded by every successful set_channel/fast_retune — the base for the + * same-band/same-width admission and the bucket-change comparisons. + * _fr_rf18/_fr_rf19 are the compose cache (primed on the first fast hop; + * invalidated by every full set_channel, whose switch functions rewrite + * both words). _fr_tssi_offsets is the rate-offset table the chip + * currently carries, recorded by enable_tssi_tracking and by every + * in-place fast-hop rewrite; _fr_tssi_path is the RF path bit read at + * enable time (RFE routing — static per session). _fr_tssi_de is the DE + * plan the chip currently carries and _fr_tssi_power the EFUSE + * calibration that derives it, recorded by prepare_tssi_offsets — the DE + * buckets are only ~3 channels wide at 2.4 GHz, so most hops cross one + * and must rewrite the DE registers or track with the previous + * channel's calibration. */ + std::optional _fr_plan; + std::optional _fr_rf18; + std::optional _fr_rf19; + std::optional> _fr_tssi_offsets; + uint8_t _fr_tssi_path = 0; + std::optional _fr_tssi_de; + std::optional _fr_tssi_power; }; } // namespace rtl8733b diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index 3549461..32b1cc2 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -350,6 +350,23 @@ void Rtl8733bDevice::SetMonitorChannel(SelectedChannel channel) { } } +void Rtl8733bDevice::FastRetune(uint8_t channel, bool cache_rf) { + std::lock_guard lock(_reg_mu); + if (_phy_ready && channel == _channel.Channel) + return; + SelectedChannel target = _channel; + target.Channel = channel; + if (_phy_ready && + _phy.fast_retune(target, _tssi_tracking, + rtl8733b::kSafeTssiTargetQdbm8733b, cache_rf)) { + _channel = target; + return; + } + /* Fast path declined (band/width change, cold radio) — full channel set at + * the current width/offset, under the same recursive lock. */ + SetMonitorChannel(target); +} + bool Rtl8733bDevice::send_packet(const uint8_t *packet, size_t length) { std::lock_guard lock(_reg_mu); if (!_phy_ready || !_mac_ready || !_tx_ready) { @@ -642,6 +659,11 @@ devourer::AdapterCaps Rtl8733bDevice::GetAdapterCaps() { caps.tune_5g = {true, 5180, 5885}; caps.characterized_5g = {true, 5180, 5825}; caps.hw_rx_timestamp = true; + /* Lean FastRetune override exists (Phy8733b::fast_retune): intra-band, + * same-width hops with TSSI tracking kept live. Measured on the + * validation unit: ~55 ms call / ~10 ms p50 radio-live, vs the + * ~330-440 ms full path (USB HS). */ + caps.fastretune_ok = true; return caps; } diff --git a/src/rtl8733b/Rtl8733bDevice.h b/src/rtl8733b/Rtl8733bDevice.h index 0304133..558e754 100644 --- a/src/rtl8733b/Rtl8733bDevice.h +++ b/src/rtl8733b/Rtl8733bDevice.h @@ -32,6 +32,12 @@ class Rtl8733bDevice : public IRtlDevice { void StartRxLoop(Action_ParsedRadioPacket packetProcessor) override; void StopRxLoop() override { _rx_stop = true; } void SetMonitorChannel(SelectedChannel channel) override; + /* Lean intra-band, same-bandwidth hop (see Phy8733b::fast_retune — the + * profile that sized it and the TSSI in-place contract live there). Falls + * back to the full SetMonitorChannel on a band/width change or a cold + * radio, per the IRtlDevice contract. The cache_rf default binds at the + * interface declaration. */ + void FastRetune(uint8_t channel, bool cache_rf) override; bool send_packet(const uint8_t *packet, size_t length) override; void SetTxMode(const devourer::TxMode &mode) override; void ClearTxMode() override; diff --git a/tests/rtl8733b_phy_table_selftest.cpp b/tests/rtl8733b_phy_table_selftest.cpp index 93d8b55..8a27135 100644 --- a/tests/rtl8733b_phy_table_selftest.cpp +++ b/tests/rtl8733b_phy_table_selftest.cpp @@ -235,6 +235,15 @@ int main() { tssi_ch6->ht40[0] == 5 && tssi_ch6->ht40[1] == 3 && tssi_ch6->ofdm[0] == 3 && tssi_ch6->ofdm[1] == 1 && tssi_ch6->ht20 == tssi_ch6->ofdm); + /* The fast_retune DE rewrite triggers on plan inequality across a hop. + * Pin the property it depends on: the 2.4 GHz buckets are ~3 channels + * wide, so plans differ across a bucket boundary and match within one. */ + const auto tssi_ch7 = rtl8733b::Phy8733b::tssi_de_plan(tssi_power, 7); + const auto tssi_ch11 = rtl8733b::Phy8733b::tssi_de_plan(tssi_power, 11); + expect("TSSI DE plan matches within a 2.4 GHz bucket", + tssi_ch7 && *tssi_ch6 == *tssi_ch7); + expect("TSSI DE plan differs across a 2.4 GHz bucket boundary", + tssi_ch11 && !(*tssi_ch6 == *tssi_ch11)); tssi_power.path_a_de[24] = -127; tssi_power.trim[7][0] = -8; const auto tssi_ch177 = rtl8733b::Phy8733b::tssi_de_plan(tssi_power, 177);