rtl8733b: port FastRetune — intra-band hop with TSSI tracking kept live - #398
Conversation
The full SetMonitorChannel on this USB-HS part costs ~330-440 ms, profiled per-stage as ~165 ms TSSI disable/re-enable, ~90 ms band+bandwidth switches it does not need on a same-band same-width hop, and ~60 ms channel switch. Phy8733b::fast_retune keeps the subset a hop needs: the RF18 synth program from a compose cache (bandwidth bits preserved; primed by one read on the first fast hop after a full set), RF19 sub-band bits and the channel-keyed BB constants on bucket change only, then the BB reset + IGI toggle that restart the RX engine. TSSI tracking stays enabled across the hop with the per-channel rate-offset dwords rewritten in place when the plan differs — the OpenIPC#389 shape. Everything that can refuse is computed before the first chip write, so a declined hop (band/width change, cold radio) leaves the chip untouched and the device wrapper falls back to the full path per the IRtlDevice contract. AdapterCaps now states fastretune_ok. Measured on the validation unit (0bda:f72b, USB HS, 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 — a 1-in-20 tail), vs 70-100 ms radio-live through the full path. Channel-state readback parity held 7/7 hops; a 300-frame post-hop burst decoded 299/300 at an RTL8812AU witness, so the TSSI-live claim is air-verified. One unit, no SDR: radiated power across a hop stays uncharacterized, like every RF-domain quantity on this backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hri3rWJfbvw8iCyDDwXRit
PR Summary by QodoRTL8733B: add FastRetune for intra-band hops with live TSSI tracking
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
The TX-after-FastRetune coherence proof for the devourer 8733B port (OpenIPC/devourer#398): with both flags the injection moves after the settle cycles, so the frames air through whatever TSSI/channel state the fast hops left behind. A witness ear counting ~tx_frames accepted is the evidence a submit counter cannot give on a CCX-less die — measured 299/300 on the .181 BU after five fast hops. Without --tx, settle stays RX-only exactly as before. Claude-Session: https://claude.ai/code/session_01Hri3rWJfbvw8iCyDDwXRit Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Code Review by Qodo
1.
|
…n place Review finding on the first cut (qodo, PR OpenIPC#398): fast_retune kept TSSI tracking live but rewrote only the 0x3a00 rate-offset dwords, which are band-keyed and never change intra-band. The channel-varying TSSI calibration is the DE offsets from tssi_de_plan, whose 2.4 GHz buckets are ~3 channels wide — so nearly every hop crossed one and kept tracking with the previous channel's DE calibration until the next full set. prepare_tssi_offsets now records the applied DE plan and the EFUSE calibration that derives it; fast_retune recomputes the target plan before its first chip write (declining to the full path when it cannot) and, on bucket change, replays prepare's field sequence minus the 0x4318 tracking-disable write. disable_tssi_tracking drops the cache. Measured on the validation unit, 22 ch1->ch13 fast hops (every one bucket-crossing): DE readback parity 22/22, tracking-enable field held at 7 throughout, settle p50 unchanged at 10.4 ms. The selftest pins the bucket property the trigger depends on: plans equal within a bucket, different across a boundary. Also aligns the GetAdapterCaps timing comment with the documented measurements (~55 ms call / ~10 ms p50 radio-live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hri3rWJfbvw8iCyDDwXRit
…keeping on a failed hop The SCO fc tables, the ch14 TX shape and the AGC/SCO bucket ladders were duplicated verbatim between switch_channel and fast_retune — an edit to one that missed the other would be a full-vs-fast register parity divergence only a hardware parity run could catch. Hoisted to file-scope constexpr shared by both paths. A transport failure after fast_retune's first chip write used to propagate with _fr_plan still recording the old channel while the radio sat part-hopped; a later fast hop would then bucket-compare against a baseline the chip no longer holds and could skip a needed write. The catch now drops the fast-path bookkeeping so the next call declines to the full-path fallback, which reprograms everything. Also: disable_tssi_tracking resets _fr_tssi_power with its two siblings, and the FastRetune override no longer re-declares the cache_rf default (it binds at the IRtlDevice declaration, like the Jaguar overrides). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
josephnef
left a comment
There was a problem hiding this comment.
Reviewed against the full path, register by register:
- The DE rewrite sequence is
prepare_tssi_offsetsbyte-for-byte minus its0x4318tracking-disable write, including the repeated ht40 lane writes. - RF18/RF19 compose masks match
switch_channeland preserve the bandwidth bits (10/11), so the cached word carries width as claimed. - Every write the fast path skips is genuinely band-keyed or constant (
0x1ea8, the 2.4 GHz0x18acpair,spur_cancellation,0x2a38[27]— touched nowhere else, so already 0 after the prior full set the fast path requires). - Refuse-before-first-write holds: both TSSI plans and the cache-priming reads complete before the first chip write.
- The TSSI invariants hold:
_tssi_tracking == trueimplies bothprepare_tssi_offsets(readback-matched, DE plan + EFUSE recorded) andenable_tssi_tracking(offsets + path recorded) succeeded, with the same target cap at enable and hop. - The selftest addition pins exactly the bucket-boundary property the DE rewrite depends on.
Build + ctest 53/53 verified locally. The docs carry their adversarial counterparts (1-in-20 tail, one unit, no SDR) per the repo rule.
Two findings, both applied in 5c51692 rather than requested back:
- The SCO fc tables, ch14 TX shape and AGC/SCO bucket ladders were duplicated verbatim between
switch_channelandfast_retune— a one-sided edit would be a full-vs-fast parity divergence only a hardware parity run could catch. Hoisted to shared file-scope constexpr. - A transport failure after the first chip write propagated with
_fr_planstill recording the old channel while the radio sat part-hopped; a later fast hop would bucket-compare against a baseline the chip no longer holds. The catch now drops the fast-path bookkeeping so the next call declines to the full-path fallback.
Plus two nits in the same commit: disable_tssi_tracking resets _fr_tssi_power with its siblings, and the override no longer re-declares the cache_rf default (binds at the interface, like the Jaguar overrides).
🤖 Generated with Claude Code
Closes the runtime TX-power gap on the RTL8733B. Follow-up to the FastRetune port (#398); the issue is snokvist#1, which deferred these knobs out of #388's scope deliberately. ## Why now `Rtl8733bDevice` overrode none of the `IRtlDevice` power family, so every `SetTxPowerOffsetQdb` landed on `IRtlDevice.h:134` `(void)qdb; return 0;`. Measured on a CV610 bench 2026-08-14: **18 dB of commanded offset moved the air by nothing**, EVM bit-identical across every rung, while a matched 8822EU control on the same receiver moved 5.6 dB for a 6 dB command and then railed. That the knob was unported is documented. The defect is that `return 0` is indistinguishable from a successful zero-offset apply — the consumer's state read `{"applied_qdb":0,"saturated_low":false}`, which is exactly what a healthy actuator with travel remaining looks like. Reproduced at register level here: master writes the same all-zero TSSI target dwords whether asked for 0 or −48. ## What it does On a TSSI-offset PG unit the closed loop **is** the TX-power control, so the actuator is its per-rate target table: the five packed dwords at `0x3a00..0x3a10`, rewritten **in place with tracking left enabled**. That is the shape #389 validated and `fast_retune` already uses for its per-channel rewrite — not the ~165 ms disable/re-enable pair. Everything that can refuse is computed before the first chip write; a failed readback rolls back to the offsets the chip was carrying. The offset caps first and shifts second, so the calibrated per-rate spread survives the move; a lowered ceiling would move only the rates above it. It is sticky by construction — `configure_tx_power` re-folds it on every channel set and `FastRetune` passes it into the hop rewrite, without which a hop would recompute from the bare ceiling and silently walk the operator's backoff back up. Caps report the dBm-target model Kestrel already uses (`index_max = 0`, one qdB per step). Only the relative knob is ported: `SetTxPowerIndexOverride` is overridden **solely to log a refusal** (the base returns `void`, so silence would otherwise be the caller's only answer), and `SetTxPowerRateDiffs` / `ReApplyTxPower` keep their `false` defaults. `kSafeTxAgcIndex8733b` was witnessed unable to carry HT at all — MCS7, 300/300 submitted, 0 captured, twice. ## Range: the int8 delta field at both ends, `[-128, +127]` The `0x3a00` bytes are a signed int8 offset from the 64 qdBm anchor, so the field spans targets from -16 dBm to +47.75 dBm. Neither end is re-clamped at something softer, the same clamped-only-at-the-hardware-rail answer Jaguar1 (±126) and Jaguar3 (±127) give. `src/TxPower.h` is deliberate that headroom above the generated table belongs to the operator, and a per-unit EFUSE trimmed too cold is precisely what a bench calibration exists to correct. Both knees are **measured and documented rather than enforced**, because on this part both sit well inside the field and at points only a bench can find: - **~-96 qdB** — where the loop stops reducing power. Below it three successive rungs read 52.96 / 52.97 / 52.99. - **~+32 qdB** — where the PA compresses: 8.7 dB louder with EVM collapsed to -18. See the table below. An earlier cut of this PR clamped the floor at -64 (a 0 dBm target) on the assumption that a negative absolute target was meaningless. That was an assumption rather than a measurement, in a change whose subject is knobs that assume instead of checking, and it cost **~9 dB of working backoff** — power keeps falling 7.3 dB past the 0 dBm target with no sign wrap. Corrected in `03daab5`. ## Measured on air RTL8731BU DUT → RTL8812AU witness (chip-RSSI ground station — the SDR saturates at this range, per `tests/txpwr_offset_onair.sh`). Four independent 6-point passes, one of them on a cold re-enumeration: | pass | slope (dB/qdB) | span | rms | |---|---|---|---| | warm 1 | 0.222 | 14.2 dB | 1.02 | | warm 2 | 0.231 | 14.8 dB | 0.64 | | warm 3 (harness) | 0.227 | 14.5 dB | 1.02 | | **cold replug** | **0.253** | **16.2 dB** | **0.49** | **Do-nothing control, same session and geometry:** the pre-change binary measured **flat — 0.3 dB across the same 64 qdB** — because it had no actuator. Without that control the 14 dB would not be readable. `step_measured` stays **false**, now for a measured reason rather than an unexamined one: the step is not constant. The bottom 12 qdB delivered 0.125 and 0.126 dB/qdB — the one structure that reproduced exactly — while everything above −52 ran 0.233..0.242. Same call the 8822E gets, for the same reason. ### Overdrive buys ~3 dB and then the PA compresses — EVM is the only tell Sweeping UP from the clip (MCS0, ch36, witness reporting EVM beside RSSI): | offset | RSSI | EVM | SNR | |---|---|---|---| | 0 | 75.4 | −62.0 | 62.0 | | +16 (top of the PG table) | 78.2 | −50.2 | 63.7 | | +32 | 84.1 | **−18.0** | 57.9 | | +48 | 83.7 | −18.0 | 58.1 | | +64 | 83.8 | −18.0 | 58.2 | +16 is real gain that already cost 12 dB of EVM. +32 is **not** gain: 8.7 dB more energy with the constellation collapsed, and +48/+64 move nothing at all. **SNR held 58..64 throughout and never saw it** — the failure `docs/bench-testing-near-field.md` exists to warn about. So the vendor's PG table lands about where this part stops being linear: +16 qdB is the edge of *usable* overdrive even though the API allows +127. Below the clip EVM is flat at −58..−61 across all 16 dB of backoff. ## Verification - `ctest` **54/54**. Per-chip configs: 8733B-only **50/50**, 8733B-off **45/45** (the new tests correctly absent). ASan+UBSan selftest passes. Clean full build adds **zero** warnings — the 8 present are pre-existing in vendored headers, `chanmig`, `hopset` and `Radiotap.c`. - Cross-builds for armv7 musl (the CV610 craft that runs this adapter). - `tests/rtl8733b_txpwr_regcheck.sh` — **7/7 on the device, warm and cold**: caps, offset-0 parity vs a master build, the null control (master ignores a −48 request), the −24 shift, both rails, stickiness across `SetMonitorChannel` + `FastRetune`, and `hw_readback` after the hop. - `tests/rtl8733b_txpwr_selftest.cpp` in `ctest` — offset math, shape preservation, rails, and `kMaxPgTargetQdbm8733b` pinned against the generated table. Both mutations (drop the shift; implement it as a lowered ceiling) fail it. - CI note: GitHub Actions is billing-blocked account-wide on the fork, so the build matrix above was run locally instead. A red X there is not a content failure. ## Notes for review - **`GetTxPowerCaps()` is static and EFUSE-free** even though the actuator needs TSSI-offset PG mode. `IRtlDevice.h:194` contracts `GetAdapterCaps` as resolved at construction and callable before `Init`, EFUSE is not read until bring-up, and consumers cache it — so mutating caps mid-session would be worse. The refusal surfaces on the setter (returns 0, logs) and in `GetTxPowerState` instead. An offset latched before bring-up on such a unit is dropped loudly and zeroed by `configure_tx_power`, so reported state never claims an offset no register carries. That path has no hardware coverage — every unit seen is TSSI-offset PG — but it writes no registers. - **A positive session offset lifts bring-up targets above the safe clip** by design. That is a deliberate widening of what bring-up may program, not a hole in `enable_tssi_tracking`'s `max_target_qdbm > 64` guard, which still refuses a caller that tries to raise the ceiling argument itself. - **`tests/txpwr_offset_onair.sh` gained a `knob` column.** Every existing row passes `DEVOURER_TX_PWR`, exactly what the loop hardcoded before, so the Jaguar/Kestrel rows are unchanged. The 8733B row sweeps `DEVOURER_TX_PWR_OFFSET_QDB` because `DEVOURER_TX_PWR` drives the unported flat-index knob — verified a no-op on the device (0x10 vs 0x3f produce identical registers), so sweeping it would draw a flat line inside the test meant to catch flat lines. Its cells stop at 0: the overdrive half is not monotone in received power and cannot be scored by an RSSI slope. - **A separate `rtl8733b_txpwr_regcheck.sh`** rather than a row in `txpwr_offset_regcheck.sh`, because every cell there reads TXAGC indices this dBm-model family reports as −1. Kestrel is absent from that script for the same reason and has its own. - One physical unit, one witness, near-field geometry, integer-quantised RSSI, no SDR — as with every other on-air claim in this backend. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Ports
FastRetuneto the RTL8733B backend — the last generation without a fast path. Intra-band, same-width hops only; a band/width change or cold radio declines with the chip untouched and the wrapper falls back to the fullSetMonitorChannel, per theIRtlDevicecontract.AdapterCaps.fastretune_oknow states it.Why / how it was sized
Per-stage profiling of the full path on the USB-HS validation unit (
0bda:f72b, cut D): ~330–440 ms total = ~165 ms TSSI disable/re-enable + ~90 ms band/bandwidth switches + ~60 ms channel switch + HALMAC/readback. A same-band same-width hop needs almost none of that. The fast path keeps: RF18 synth program from a compose cache (bandwidth bits preserved; primed by one read on the first fast hop after each full set), RF19 sub-band bits and channel-keyed BB constants on bucket change only, BB reset + IGI toggle. TSSI tracking stays enabled across the hop, with the per-channel rate-offset dwords rewritten in place only when the plan differs — the in-place-with-tracking-live shape #389 validated. Everything that can refuse is computed before the first chip write.Measured (one unit — the standing 8733B caveat — no SDR)
read_channel_state().matches(plan)+ synth-ready held on 7/7 fast hops (5180→5805).Docs updated (
src/rtl8733b/CLAUDE.md, rootCLAUDE.mdfrequency-hopping section,docs/rtl8733b.mddeferred matrix) with the adversarial counterparts alongside the numbers, per the repo rule.🤖 Generated with Claude Code
https://claude.ai/code/session_01Hri3rWJfbvw8iCyDDwXRit