From f509c03189b8676e9a390f30b7d29e709c2df32b Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 13 Aug 2026 06:35:18 +0200 Subject: [PATCH 1/4] rtl8733b: keep the send path free of register I/O, scope the TSSI knob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #388/#395, addressing @josephnef's post-merge review. **The TSSI thermal table is chosen once per channel set, not per frame.** This backend was the only one doing register I/O inside `send_packet`: it re-selected the CCK/OFDM thermal-compensation curve on every rate-class crossing, at 84 ms and 136 USB register round trips per crossing (#389). The vendor never does that — `_halrf_tssi_set_tmeter_tbl_8733b` is reachable only from full TSSI setup, keyed on `phydm_get_tx_rate` at that instant, and is never re-selected at runtime. `configure_tx_power` now picks the curve from the configured TX mode and leaves it. Measured on the DUT: an alternating CCK/OFDM stream drops from 19.69 s to 2.82 s per 200 frames, 200 switches to 0, TSSI still enabled. The closed loop itself stays on wherever the EFUSE is in TSSI-offset PG mode, because there it *is* the TX-power control. An earlier cut made TSSI opt-in with a flat-index fallback; witnessed, that could not carry HT at all — MCS7, 300/300 submitted, 0 captured, twice. Per-frame switching survives as `DeviceConfig::tuning::tssi_rate_table` / `DEVOURER_TSSI_RATE_TABLE=1`, and its declaration says what it is rather than selling it as a performance toggle: the default is validated at room temperature (1 Mbps CCK, 300/300 on the OFDM table vs 299/300 on the CCK table), the elevated-thermal-delta regime is unmeasured on this part, and if the hot case turns out not to diverge the honest outcome is to delete the knob. Raised in review — a knob shipped as neutral reads as though both settings had been characterized. **`tuning.disable_cca`.** #395 landed the same fix concurrently; per review, this keeps the wording that names both spellings (`DEVOURER_DIS_CCA / tuning.disable_cca`), since the env var is what an operator sets and the struct field is what a library caller sets, and a grep for either should find it. Sited in `bring_up_to_phy` rather than `InitWrite` so an RX-only session that set the knob is told too, and so it fires exactly once per bring-up rather than twice. **Docs.** `src/rtl8733b/CLAUDE.md` described the 84 ms as an unconditional per-frame cost and stated the table cannot be changed while tracking is enabled; both are now wrong, and the paragraph is rewritten. `docs/rtl8733b.md` gains the witness qualification review asked for: sweeping the RTL8812AU across the HT ladder gives 100% at MCS1/3/5 and 98.7% at MCS6, so the witness is nowhere near its cliff and the MCS0/MCS7 dips are per-run link variation, not modulation — MCS0 being the most robust rate and also the lowest reading is what rules a cliff out. 53/53, RTL8733B-only 49/49, RTL8733B-only ASan+UBSan 49/49. Co-Authored-By: Claude Opus 5 (1M context) --- docs/rtl8733b.md | 89 ++++++++++++++++++++++++++------ examples/common/env_config.cpp | 3 ++ src/DeviceConfig.h | 32 ++++++++++++ src/rtl8733b/CLAUDE.md | 37 ++++++++----- src/rtl8733b/Rtl8733bDevice.cpp | 60 +++++++++++++++------ src/rtl8733b/TxDescriptor8733b.h | 9 ++++ 6 files changed, 186 insertions(+), 44 deletions(-) diff --git a/docs/rtl8733b.md b/docs/rtl8733b.md index ac26cca..9c5869e 100644 --- a/docs/rtl8733b.md +++ b/docs/rtl8733b.md @@ -57,17 +57,42 @@ channel 6, 300 submitted frames per rate. The witness's hit counter reports the first ten hits then every hundredth, so these are floors at 100-frame granularity: -| requested | captured | FCS errors | +| requested | delivered | decoded as | FCS errors | +| --- | --- | --- | --- | +| 1 Mbps CCK | 298 / 300 | 1 Mbps | 0 | +| 2 Mbps CCK | 300 / 300 | 2 Mbps | 0 | +| 5.5 Mbps CCK | 300 / 300 | 5.5 Mbps | 0 | +| 11 Mbps CCK | 294 / 300 | 11 Mbps | 0 | +| 6 Mbps OFDM | 300 / 300 | 6 Mbps | 0 | +| HT MCS0 | 270 / 300 | MCS0 | 0 | +| HT MCS7 | 277 / 300 | MCS7 | 0 | + +Every frame decoded at the rate it was requested at, and no captured frame +carried an FCS error at any rate. These are single probes, so per this repo's +own guidance they are worth about +/-3 points each. + +The two HT readings sitting below the legacy ones are **not** a transmitter +effect, and the witness was qualified rather than assumed +(`tests/ground_station_qualify.sh` exists for exactly this question). Sweeping +the RTL8812AU witness across the HT ladder, 300 frames each: + +| rate | delivered | witness RSSI | | --- | --- | --- | -| 1 Mbps CCK | 300 / 300 | 0 | -| 6 Mbps OFDM | 300 / 300 | 0 | -| HT MCS7 | 300 / 300 | 0 | -| 11 Mbps CCK | >= 900 / 1000 | 0 | - -11 Mbps was re-run at 1,000 frames because its first result sat on a counter -boundary; both repetitions landed in the same bucket, so the small deficit -relative to the other three rates is repeatable rather than noise — expected, -since 11 Mbps is the least robust CCK rate. +| MCS0 | 270 / 300 | 57.8 | +| MCS1 | 300 / 300 | 65.0 | +| MCS3 | 300 / 300 | 65.3 | +| MCS5 | 300 / 300 | 65.2 | +| MCS6 | 296 / 300 | 65.1 | +| MCS7 | 277 / 300 | 60.3 | + +The ladder is flat at 100% through MCS5, so the witness is nowhere near its +modulation cliff and is fit to measure this rate range — a receiver measuring +itself would fall off progressively at the top, not dip at both ends. MCS0 is +the *most* robust rate in the set, so its being the lowest reading rules out a +cliff at either end. Both low readings also came with 5-7 units less RSSI than +the flat middle, which places the variation in the per-run link conditions +rather than in modulation. Treat the whole HT column as one flat band around +90-100% until something moves it by more than the probe noise. A mixed run alternating all eight rates (1/2/5.5/11 Mbps CCK, 6 Mbps OFDM, MCS0/4/7) submitted 250 frames through a CCK/OFDM TSSI table switch on every @@ -101,14 +126,37 @@ transition-churn run submitted 1,000/1,000 frames through 118 CCK/OFDM TSSI table changes; the witness captured 660 canonical frames across both rates, all with valid FCS, while the same 32–33 thermal range held. -Switching the TSSI thermal table between its CCK and OFDM/HT variants costs -84 ms — 136 USB register round trips, no sleeps — because the backend tears -down and rebuilds the whole TSSI front-end to change 16 BB dwords. -`select_tssi_rate_table` early-returns when the rate class is unchanged, so a -single-rate stream pays nothing; only a stream that alternates CCK and OFDM -rates is affected, and it is capped at roughly 11 fps. +## TX power control + +On a unit whose EFUSE is in TSSI-offset PG mode — the tested RTL8731BU is — +closed-loop TSSI *is* the TX-power control, so it runs by default. It is not +optional there: the flat fallback index (`kSafeTxAgcIndex8733b`) is a +conservative bring-up value, and on air it runs cold enough that HT rates do +not survive the link. Measured with the witness, 300 frames submitted at MCS7 +with the flat index: zero captured, twice. A unit whose EFUSE carries no TSSI +calibration has nothing to drive the loop and takes the flat path. + +What is opt-in is the *thermal-table* switching. The chip keeps two +thermal-compensation curves, one for CCK and one for OFDM/HT. The table is +chosen once per channel set from the configured TX mode and then left alone, +which is what the vendor driver does — `_halrf_tssi_set_tmeter_tbl_8733b` is +only ever called from full TSSI setup, keyed on `phydm_get_tx_rate` at that +instant, and never re-selected at runtime. + +`DEVOURER_TSSI_RATE_TABLE=1` re-selects it whenever a frame crosses the +CCK/OFDM boundary instead. That costs 84 ms — 136 USB register round trips, no +sleeps — *inside `send_packet`*, because changing 16 BB dwords tears down and +rebuilds the whole TSSI front-end, and it caps a mixed-rate stream at roughly +11 fps. No other generation does register I/O on the send path at all, which is +why it is off by default. [OpenIPC/devourer#389](https://github.com/OpenIPC/devourer/issues/389) tracks -the measurement and a validated in-place alternative. +the cost and a validated in-place alternative. + +Fixing the table costs nothing measurable at room temperature: the two curves +differ only in the swing ramp away from the baseline, so at a thermal delta +near zero they agree. Witnessed, 300 frames of 1 Mbps CCK: 300/300 delivered +with the OFDM table loaded, 299/300 with the CCK table, zero FCS errors either +way. A related property of the closed loop, worth knowing before reading power numbers from a fast rate-switching run: TSSI needs settling time. Alternating @@ -123,6 +171,13 @@ state this backend's MAC bring-up leaves programmed — succeeds as a no-op. gate has not been located and measured here, and the backend refuses rather than silently pretending. The refusal leaves the session running. +The construction-time form of the same knob — `DeviceConfig::tuning::disable_cca`, +which every other generation applies during bring-up — cannot be honoured +either, and bring-up emits one warning saying so rather than dropping it +silently or failing to initialise. Silence is the only outcome that would let a +refused knob look like a granted one; a hard failure would be a harsh answer to +a request a caller may be making only through an inherited environment. + ## Known gaps and deferred validation These results have **not** been claimed: diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 606d266..a62c3c3 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -173,6 +173,9 @@ devourer::DeviceConfig devourer_config_from_env() { /* Default-on knob: unset = tracking on; only "0" disables it. */ if (const char *e = env_str("DEVOURER_THERMAL_TRACK")) cfg.tuning.thermal_track = std::strcmp(e, "0") != 0; + /* Default-off knob (RTL8733B): unset = one TSSI thermal table per channel + * set, so the send path stays free of register I/O. */ + cfg.tuning.tssi_rate_table = env_flag("DEVOURER_TSSI_RATE_TABLE"); cfg.tuning.disable_cca = env_flag("DEVOURER_DIS_CCA"); if (env_long("DEVOURER_FASTRETUNE_FW", &v) && v >= 0) cfg.tuning.fastretune_fw = static_cast(v); diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index de8d8e3..85b3662 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -329,6 +329,38 @@ 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_TSSI_RATE_TABLE — RTL8733B per-rate TSSI thermal-table + * switching (default OFF; "1" enables). This is the *table* knob, not the + * TSSI loop: closed-loop TSSI itself stays on wherever the EFUSE is in + * TSSI-offset PG mode, because on those units it IS the TX-power control + * — a flat fallback index runs so cold that HT rates do not survive the + * link (measured: MCS7 undecodable by a witness at 300/300 submitted). + * + * The chip keeps two thermal-compensation curves, one for CCK and one for + * OFDM/HT. Off, the table is chosen once per channel set from the + * configured TX mode and left alone — which is exactly what the vendor + * driver does (`_halrf_tssi_set_tmeter_tbl_8733b` is only ever called from + * the full TSSI setup, keyed on `phydm_get_tx_rate` at that instant). On, + * the backend re-selects it whenever a frame crosses the CCK<->OFDM + * boundary, which costs ~84 ms / 136 USB register round trips *inside + * send_packet* (OpenIPC/devourer#389) and caps a mixed-rate stream at + * ~11 fps. No other generation does register I/O on the send path. + * + * This is NOT a neutral performance toggle, and the two settings are not + * equally characterized. The default is validated at room temperature and + * costs nothing there: the curves differ only in the swing ramp away from + * the baseline, so near zero thermal delta they agree (measured, 300 frames + * of 1 Mbps CCK: 300/300 delivered on the OFDM table, 299/300 on the CCK + * table, zero FCS errors either way). Whether they diverge enough to matter + * at elevated thermal delta is **unmeasured on this part** — that regime + * needs a heat soak and an SDR, neither of which has been on this silicon. + * + * So enable it only if you are deliberately investigating that hot regime + * on a mixed CCK/OFDM stream, and expect to pay the send-path cost above + * for a benefit nobody has yet demonstrated. If the hot case turns out not + * to diverge, the honest outcome is to delete this knob and keep the + * vendor's behaviour unconditionally. */ + bool tssi_rate_table = false; /* 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 Jaguar2 dies (8822B, 8821C) and the diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md index 11a9078..6cd5467 100644 --- a/src/rtl8733b/CLAUDE.md +++ b/src/rtl8733b/CLAUDE.md @@ -48,18 +48,31 @@ unrelated register map. either alone reintroduces the straddle, from opposite sides. - **TSSI closed loop.** Power runs from a fixed safe target (`kSafeTssiTargetQdbm8733b`); none of the runtime TX-power levers are ported. - The CCK and OFDM/HT variants of the thermal-compensation table are different - tables, and the table cannot be changed while tracking is enabled — so - `select_tssi_rate_table` disables tracking, rewrites, and re-enables, with - each stage verified by register readback. Note what the failure path is and - is not: `Phy8733b::enable_tssi_tracking` restores its own analog/BB snapshot - when its verdict fails, but the *transition* has no rollback to the previous - table — `select_tssi_rate_table` returns false with tracking left off, and - the caller responds by tearing the session down rather than transmitting at - an unverified power setting. That transition costs **84 ms / 136 USB - register round trips** (bench, one unit), so a stream alternating CCK and - OFDM rates is capped around 11 fps; a single-rate stream early-returns and - pays nothing. + On a TSSI-offset PG unit the loop **is** the TX-power control, so it is not + optional there — an attempt to make it opt-in with a fall back to the flat + `kSafeTxAgcIndex8733b` could not carry HT at all (witnessed: MCS7, 300/300 + submitted, 0 captured, twice). A unit whose EFUSE carries no TSSI calibration + has nothing to drive the loop and takes the flat path. +- **The thermal table is chosen once per channel set, not per frame.** The CCK + and OFDM/HT variants of the thermal-compensation table are different tables. + `configure_tx_power` picks one from the configured TX mode and leaves it, + which is what the vendor does: `_halrf_tssi_set_tmeter_tbl_8733b` is + reachable only from full TSSI setup, keyed on `phydm_get_tx_rate` at that + instant, and never re-selected at runtime. So nothing reads or writes a + register per frame on the send path, matching the other four HALs. + `DeviceConfig::tuning::tssi_rate_table` / `DEVOURER_TSSI_RATE_TABLE=1` + re-selects on each rate-class crossing instead, at **84 ms / 136 USB + register round trips** per crossing (bench, one unit), capping a mixed-rate + stream around 11 fps. Read that knob's declaration before enabling it: the + default is validated at room temperature, the hot regime is unmeasured, and + it is deliberately not presented as a neutral toggle. Cost breakdown and a + validated in-place alternative: OpenIPC/devourer#389. + Note what the opt-in path's failure mode is and is not: + `Phy8733b::enable_tssi_tracking` restores its own analog/BB snapshot when its + verdict fails, but the *transition* has no rollback to the previous table — + `select_tssi_rate_table` returns false with tracking left off, and the caller + responds by tearing the session down rather than transmitting at an + unverified power setting. - **The loop needs settling time, so a fast rate-switching run misreports power.** Alternating CCK and OFDM at a few ms per frame leaves CCK transmitting above its settled level until the loop converges; pacing the diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index 2caecdc..d0e7a95 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -73,6 +73,26 @@ void Rtl8733bDevice::bring_up_to_phy() { _phy_ready = true; _logger->info("RTL8733B factory path reached PHY-ready (cut {})", _chip.cut); + /* Every other generation applies tuning.disable_cca during bring-up + * (jaguar1/2/3, kestrel all call SetCcaMode there), so a caller setting + * DEVOURER_DIS_CCA=1 reasonably expects it to take effect. This backend has + * not located and measured the HALMAC 87xx carrier-sense gate, so it cannot + * honour the request — say so once, loudly, rather than dropping it in + * silence, which is the one way a refused knob can look like a granted one: + * the operator would otherwise believe carrier-sense was off and read the + * resulting deferral as a transmitter problem. + * + * Deliberately a warning and not the throw SetCcaMode(true) raises: the knob + * is on by default for the streamtx FPV downlink, and failing bring-up + * outright is a harsh answer to a request the caller may be making only via + * an inherited environment. The session runs with standard carrier-sense. + * + * Sited in bring_up_to_phy rather than InitWrite so an RX-only session that + * set the knob is told too, and so it fires exactly once per bring-up. */ + if (_cfg.tuning.disable_cca) + _logger->warn( + "RTL8733B: DEVOURER_DIS_CCA / tuning.disable_cca is not implemented by " + "this backend — carrier-sense stays ENABLED for this session"); } void Rtl8733bDevice::Init(Action_ParsedRadioPacket packetProcessor, @@ -111,17 +131,6 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { _tx_ready = true; _tx_submits = 0; _tx_fatal = nullptr; - /* Say so rather than dropping it. SetCcaMode(true) refuses loudly, so the - * config path must not be the one door where the same request vanishes - * without a word — the operator would otherwise believe carrier-sense was - * off and read the resulting deferral as a transmitter problem. Warn - * rather than throw: the knob is on by default for the streamtx FPV - * downlink, and refusing to bring TX up over an unported optimisation is a - * worse trade than airing with standard carrier-sense. */ - if (_cfg.tuning.disable_cca) - _logger->warn("RTL8733B: CCA disable (DEVOURER_DIS_CCA) is not " - "implemented by this backend; transmitting with " - "carrier-sense enabled"); /* One-shot thermal snapshot at bring-up — the PA-heating baseline for the * session, same as the Kestrel InitWrite snapshot. Logged, never acted on: * the meter is a PA-bias tracking index, not a calibrated °C sensor, and @@ -143,23 +152,44 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { _tssi_tracking = false; _tssi_cck = false; + /* Closed-loop TSSI is the TX-power control on a TSSI-offset PG unit, so it + * is not optional there: the flat fallback index below is a conservative + * bring-up value, and on-air it runs cold enough that HT rates do not + * survive the link (measured on the DUT: MCS7 undecodable by an RTL8812AU + * witness, 300/300 submitted, 0 captured). A unit whose EFUSE carries no + * TSSI calibration has nothing to drive the loop and takes the flat path. */ if (_efuse.tx_power_mode != rtl8733b::TxPowerPgMode8733b::TssiOffset) return _phy.set_flat_tx_power(rtl8733b::kSafeTxAgcIndex8733b); - /* Start in the OFDM/HT thermal table. A first CCK submission switches the - * table through select_tssi_rate_table() before its descriptor reaches USB. */ + /* Pick the thermal-compensation curve once, from the TX mode configured at + * this point, and leave it alone — the vendor's own setup keys the table on + * `phydm_get_tx_rate` at TSSI-setup time and never re-selects it at runtime. + * Re-selecting per frame is the opt-in DEVOURER_TSSI_RATE_TABLE path; the + * default keeps send_packet free of register I/O like every other + * generation. */ + const bool cck_table = _tx_mode_default.has_value() && + _tx_mode_default->mode == + devourer::TxMode::Mode::Legacy && + rtl8733b::is_cck_rate_500kbps( + _tx_mode_default->legacy_rate_500kbps); if (!_phy.prepare_tssi_bb(channel, _efuse) || - !_phy.prepare_tssi_thermal(_efuse, false) || + !_phy.prepare_tssi_thermal(_efuse, cck_table) || !_phy.prepare_tssi_offsets(channel, _efuse) || !_phy.enable_tssi_tracking( channel, _efuse, rtl8733b::kSafeTssiTargetQdbm8733b)) return false; _tssi_tracking = true; + _tssi_cck = cck_table; return true; } bool Rtl8733bDevice::select_tssi_rate_table(bool cck) { - if (_efuse.tx_power_mode != rtl8733b::TxPowerPgMode8733b::TssiOffset) + /* Default: the table was chosen once in configure_tx_power and stays put, so + * the send path does no register I/O — the vendor behaves the same way. + * Both conditions must be checked: gating only on the EFUSE PG mode would + * return false here, which send_packet treats as fatal and stops the card. */ + if (!_cfg.tuning.tssi_rate_table || + _efuse.tx_power_mode != rtl8733b::TxPowerPgMode8733b::TssiOffset) return true; if (!_tssi_tracking) return false; diff --git a/src/rtl8733b/TxDescriptor8733b.h b/src/rtl8733b/TxDescriptor8733b.h index f99a9d6..0fd6755 100644 --- a/src/rtl8733b/TxDescriptor8733b.h +++ b/src/rtl8733b/TxDescriptor8733b.h @@ -54,6 +54,15 @@ inline bool legacy_request_supported_8733b(unsigned bw_mhz, bool sgi, bool ldpc, return bw_mhz == 20 && !sgi && !ldpc && !stbc; } +/* Is a radiotap/TxMode legacy rate (500 kbps units) one of the four + * long-preamble CCK rates? 1M=2, 2M=4, 5.5M=11, 11M=22. Used to pick the TSSI + * thermal-compensation curve at setup, mirroring the vendor's rate-keyed + * table choice. */ +inline bool is_cck_rate_500kbps(uint8_t rate_500kbps) { + return rate_500kbps == 2 || rate_500kbps == 4 || rate_500kbps == 11 || + rate_500kbps == 22; +} + /* Whole-TxMode admission: the PPDU family plus its modulation parameters. VHT * and HE fall through to false on every band. */ inline bool tx_mode_supported_8733b(const devourer::TxMode &mode) { From 5a009ee6091d58c3536165d1fe4d57a52261feaf Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 13 Aug 2026 15:11:00 +0200 Subject: [PATCH 2/4] rtl8733b: stop duplicating the knob's docs, announce its cost when on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on the opt-in TSSI rate-table path. `src/rtl8733b/CLAUDE.md` restated the characterization that belongs to the `tssi_rate_table` declaration in src/DeviceConfig.h — the room-temperature validation, the unmeasured hot regime, the not-a-neutral-toggle framing. The root CLAUDE.md's own standing rule is to point at the header rather than copy it, so the subtree file now keeps only the subtree fact (the table is chosen once, the vendor does the same, what the transition's failure mode is) and points at the declaration for when the knob is worth paying for. Enabling the knob re-introduces register I/O on the send path. That is its documented purpose, so the invariant it appears to break — nothing reads a register per frame — is a statement about the default, not about every reachable configuration. What was genuinely wrong is that it was silent: a caller who inherited DEVOURER_TSSI_RATE_TABLE from an environment would meet the ~84 ms per rate crossing as an unexplained frame-rate ceiling. InitWrite now warns once when the knob is active and TSSI is running, naming the cost and the issue. The same reasoning as tuning.disable_cca one function above: a knob whose effect the operator cannot see is the one that gets misdiagnosed as a hardware fault. Co-Authored-By: Claude Opus 5 (1M context) --- src/rtl8733b/CLAUDE.md | 14 +++++++------- src/rtl8733b/Rtl8733bDevice.cpp | 10 ++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md index 6cd5467..5631061 100644 --- a/src/rtl8733b/CLAUDE.md +++ b/src/rtl8733b/CLAUDE.md @@ -60,13 +60,13 @@ unrelated register map. reachable only from full TSSI setup, keyed on `phydm_get_tx_rate` at that instant, and never re-selected at runtime. So nothing reads or writes a register per frame on the send path, matching the other four HALs. - `DeviceConfig::tuning::tssi_rate_table` / `DEVOURER_TSSI_RATE_TABLE=1` - re-selects on each rate-class crossing instead, at **84 ms / 136 USB - register round trips** per crossing (bench, one unit), capping a mixed-rate - stream around 11 fps. Read that knob's declaration before enabling it: the - default is validated at room temperature, the hot regime is unmeasured, and - it is deliberately not presented as a neutral toggle. Cost breakdown and a - validated in-place alternative: OpenIPC/devourer#389. + `DeviceConfig::tuning::tssi_rate_table` re-selects on each rate-class + crossing instead, at **84 ms / 136 USB register round trips** per crossing + (bench, one unit), capping a mixed-rate stream around 11 fps — read its + declaration in `src/DeviceConfig.h` for when that is worth paying, and + OpenIPC/devourer#389 for the cost breakdown and a validated in-place + alternative. Enabling it re-introduces send-path register I/O by design, + which is why it is default-off and announces itself at bring-up. Note what the opt-in path's failure mode is and is not: `Phy8733b::enable_tssi_tracking` restores its own analog/BB snapshot when its verdict fails, but the *transition* has no rollback to the previous table — diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index d0e7a95..e996404 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -131,6 +131,16 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { _tx_ready = true; _tx_submits = 0; _tx_fatal = nullptr; + /* An opt-in that re-introduces register I/O on the send path should not be + * silent about it: enabling this trades ~84 ms per CCK<->OFDM crossing for + * thermal-compensation accuracy nobody has yet shown to matter, and a + * caller who inherited the knob from an environment deserves to see the + * cost rather than discover it as a mysterious frame-rate ceiling. */ + if (_cfg.tuning.tssi_rate_table && _tssi_tracking) + _logger->warn( + "RTL8733B: DEVOURER_TSSI_RATE_TABLE is on — each CCK<->OFDM rate " + "crossing re-selects the TSSI thermal table inside send_packet at " + "~84 ms / 136 USB register round trips (OpenIPC/devourer#389)"); /* One-shot thermal snapshot at bring-up — the PA-heating baseline for the * session, same as the Kestrel InitWrite snapshot. Logged, never acted on: * the meter is a PA-bias tracking index, not a calibrated °C sensor, and From ba15a5d3bce5e2fc88b0b8265dd587e567aa67fa Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 13 Aug 2026 15:51:58 +0200 Subject: [PATCH 3/4] =?UTF-8?q?rtl8733b:=20delete=20the=20TSSI=20rate-tabl?= =?UTF-8?q?e=20knob=20=E2=80=94=20it=20cannot=20do=20anything?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heat soak settles it. The knob is gone, and so is everything that existed only to serve it: `select_tssi_rate_table`, the `DeviceConfig` field, the env translation, and `_tx_fatal` (whose sole producer was that function's failure path). `send_packet` now has no conditional register-I/O path at all. Why it can never matter on this silicon, rather than merely not mattering at room temperature: The CCK and OFDM/HT thermal-compensation curves are **bit-identical for thermal deltas 0..+17** and first differ at **+18** — the vendor swing ramp starts there and the words either side of the baseline are zero in both. A five-minute max-duty MCS7 soak on the validation unit climbed from +1 to +8, plateaued after two minutes and stopped, less than half the delta needed for a single table entry to differ. A runtime switch would therefore spend 84 ms and 136 USB register round trips, inside `send_packet`, to install a table bit-identical to the one already loaded. Room temperature agreed independently: 300 frames of 1 Mbps CCK delivered 300/300 with the OFDM table loaded versus 299/300 with the CCK table, zero FCS errors either way. Re-verified after the deletion: MCS7 279/300, 1 Mbps CCK 300/300, unchanged. The table is still chosen from the rate class at TSSI setup, because that costs nothing and is what the vendor does — but the code and docs now say plainly that the choice is not expected to be measurable, so nobody re-derives the switch from first principles. If a future board does reach +18 — a sealed module at high ambient might — OpenIPC/devourer#389 holds the validated in-place implementation (13.8 ms, tracking left enabled) to revisit, not the teardown/rebuild this deletes. `_tx_fatal` goes with it rather than being kept as scaffolding: it distinguished "the session was deliberately stopped" from "InitWrite has not run", and with the TSSI transition gone nothing can stop the card from inside `send_packet`, so it would have been a permanently-null member guarding a dead branch. Co-Authored-By: Claude Opus 5 (1M context) --- docs/rtl8733b.md | 52 +++++++++++---------- examples/common/env_config.cpp | 3 -- src/DeviceConfig.h | 32 ------------- src/rtl8733b/CLAUDE.md | 27 +++++------ src/rtl8733b/Rtl8733bDevice.cpp | 81 ++++----------------------------- src/rtl8733b/Rtl8733bDevice.h | 5 -- 6 files changed, 52 insertions(+), 148 deletions(-) diff --git a/docs/rtl8733b.md b/docs/rtl8733b.md index 9c5869e..622eb42 100644 --- a/docs/rtl8733b.md +++ b/docs/rtl8733b.md @@ -53,9 +53,8 @@ factory path: adapter-doctor checks. Per-rate delivery, witnessed by an RTL8812AU (`0bda:8812`) in monitor mode on -channel 6, 300 submitted frames per rate. The witness's hit counter reports the -first ten hits then every hundredth, so these are floors at 100-frame -granularity: +channel 6, 300 submitted frames per rate. Counted per frame from the witness's +own RX events, so these are exact rather than sampled: | requested | delivered | decoded as | FCS errors | | --- | --- | --- | --- | @@ -136,27 +135,32 @@ not survive the link. Measured with the witness, 300 frames submitted at MCS7 with the flat index: zero captured, twice. A unit whose EFUSE carries no TSSI calibration has nothing to drive the loop and takes the flat path. -What is opt-in is the *thermal-table* switching. The chip keeps two -thermal-compensation curves, one for CCK and one for OFDM/HT. The table is -chosen once per channel set from the configured TX mode and then left alone, -which is what the vendor driver does — `_halrf_tssi_set_tmeter_tbl_8733b` is -only ever called from full TSSI setup, keyed on `phydm_get_tx_rate` at that -instant, and never re-selected at runtime. - -`DEVOURER_TSSI_RATE_TABLE=1` re-selects it whenever a frame crosses the -CCK/OFDM boundary instead. That costs 84 ms — 136 USB register round trips, no -sleeps — *inside `send_packet`*, because changing 16 BB dwords tears down and -rebuilds the whole TSSI front-end, and it caps a mixed-rate stream at roughly -11 fps. No other generation does register I/O on the send path at all, which is -why it is off by default. -[OpenIPC/devourer#389](https://github.com/OpenIPC/devourer/issues/389) tracks -the cost and a validated in-place alternative. - -Fixing the table costs nothing measurable at room temperature: the two curves -differ only in the swing ramp away from the baseline, so at a thermal delta -near zero they agree. Witnessed, 300 frames of 1 Mbps CCK: 300/300 delivered -with the OFDM table loaded, 299/300 with the CCK table, zero FCS errors either -way. +The chip keeps two thermal-compensation curves, one for CCK and one for +OFDM/HT. The table is chosen once per channel set from the configured TX mode +and then left alone, which is what the vendor driver does — +`_halrf_tssi_set_tmeter_tbl_8733b` is only ever called from full TSSI setup, +keyed on `phydm_get_tx_rate` at that instant, and never re-selected at runtime. + +There is deliberately no runtime switch, and the reason is worth recording so +nobody adds one back. + +The two curves are **bit-identical for thermal deltas 0..+17** and first differ +at **+18** — the swing ramp starts there, and the words either side of the +baseline are zero in both tables. A five-minute max-duty MCS7 soak on the +validation unit plateaued at **+8** after two minutes and stopped climbing, +less than half the delta needed for the tables to differ by a single entry. So +on this silicon a runtime switch would spend 84 ms — 136 USB register round +trips, *inside `send_packet`*, capping a mixed-rate stream near 11 fps — to +install a table bit-identical to the one already loaded. + +An opt-in knob for it was written, measured on air and then deleted on that +evidence. Room temperature agreed too: 300 frames of 1 Mbps CCK delivered +300/300 with the OFDM table loaded and 299/300 with the CCK table, zero FCS +errors either way. +[OpenIPC/devourer#389](https://github.com/OpenIPC/devourer/issues/389) has the +cost breakdown and a validated in-place alternative (13.8 ms, tracking left +enabled) — if a future board does reach +18, that is the implementation to +revisit, not the teardown/rebuild. A related property of the closed loop, worth knowing before reading power numbers from a fast rate-switching run: TSSI needs settling time. Alternating diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index a62c3c3..606d266 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -173,9 +173,6 @@ devourer::DeviceConfig devourer_config_from_env() { /* Default-on knob: unset = tracking on; only "0" disables it. */ if (const char *e = env_str("DEVOURER_THERMAL_TRACK")) cfg.tuning.thermal_track = std::strcmp(e, "0") != 0; - /* Default-off knob (RTL8733B): unset = one TSSI thermal table per channel - * set, so the send path stays free of register I/O. */ - cfg.tuning.tssi_rate_table = env_flag("DEVOURER_TSSI_RATE_TABLE"); cfg.tuning.disable_cca = env_flag("DEVOURER_DIS_CCA"); if (env_long("DEVOURER_FASTRETUNE_FW", &v) && v >= 0) cfg.tuning.fastretune_fw = static_cast(v); diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 85b3662..de8d8e3 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -329,38 +329,6 @@ 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_TSSI_RATE_TABLE — RTL8733B per-rate TSSI thermal-table - * switching (default OFF; "1" enables). This is the *table* knob, not the - * TSSI loop: closed-loop TSSI itself stays on wherever the EFUSE is in - * TSSI-offset PG mode, because on those units it IS the TX-power control - * — a flat fallback index runs so cold that HT rates do not survive the - * link (measured: MCS7 undecodable by a witness at 300/300 submitted). - * - * The chip keeps two thermal-compensation curves, one for CCK and one for - * OFDM/HT. Off, the table is chosen once per channel set from the - * configured TX mode and left alone — which is exactly what the vendor - * driver does (`_halrf_tssi_set_tmeter_tbl_8733b` is only ever called from - * the full TSSI setup, keyed on `phydm_get_tx_rate` at that instant). On, - * the backend re-selects it whenever a frame crosses the CCK<->OFDM - * boundary, which costs ~84 ms / 136 USB register round trips *inside - * send_packet* (OpenIPC/devourer#389) and caps a mixed-rate stream at - * ~11 fps. No other generation does register I/O on the send path. - * - * This is NOT a neutral performance toggle, and the two settings are not - * equally characterized. The default is validated at room temperature and - * costs nothing there: the curves differ only in the swing ramp away from - * the baseline, so near zero thermal delta they agree (measured, 300 frames - * of 1 Mbps CCK: 300/300 delivered on the OFDM table, 299/300 on the CCK - * table, zero FCS errors either way). Whether they diverge enough to matter - * at elevated thermal delta is **unmeasured on this part** — that regime - * needs a heat soak and an SDR, neither of which has been on this silicon. - * - * So enable it only if you are deliberately investigating that hot regime - * on a mixed CCK/OFDM stream, and expect to pay the send-path cost above - * for a benefit nobody has yet demonstrated. If the hot case turns out not - * to diverge, the honest outcome is to delete this knob and keep the - * vendor's behaviour unconditionally. */ - bool tssi_rate_table = false; /* 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 Jaguar2 dies (8822B, 8821C) and the diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md index 5631061..931c8be 100644 --- a/src/rtl8733b/CLAUDE.md +++ b/src/rtl8733b/CLAUDE.md @@ -60,19 +60,20 @@ unrelated register map. reachable only from full TSSI setup, keyed on `phydm_get_tx_rate` at that instant, and never re-selected at runtime. So nothing reads or writes a register per frame on the send path, matching the other four HALs. - `DeviceConfig::tuning::tssi_rate_table` re-selects on each rate-class - crossing instead, at **84 ms / 136 USB register round trips** per crossing - (bench, one unit), capping a mixed-rate stream around 11 fps — read its - declaration in `src/DeviceConfig.h` for when that is worth paying, and - OpenIPC/devourer#389 for the cost breakdown and a validated in-place - alternative. Enabling it re-introduces send-path register I/O by design, - which is why it is default-off and announces itself at bring-up. - Note what the opt-in path's failure mode is and is not: - `Phy8733b::enable_tssi_tracking` restores its own analog/BB snapshot when its - verdict fails, but the *transition* has no rollback to the previous table — - `select_tssi_rate_table` returns false with tracking left off, and the caller - responds by tearing the session down rather than transmitting at an - unverified power setting. +- **The curve choice is inert at any temperature this part reaches, so do not + reintroduce runtime switching.** The CCK and OFDM/HT tables are + bit-identical for thermal deltas 0..+17 and first differ at **+18** (the + swing ramp starts there; the words either side of the baseline are zero in + both). A five-minute max-duty MCS7 soak on the validation unit plateaued at + **+8** after two minutes and stopped climbing — less than half the delta + needed for the tables to differ by a single entry. A runtime switch + therefore costs **84 ms / 136 USB register round trips** per crossing + (OpenIPC/devourer#389) to install a table that is bit-identical to the one + already loaded. An opt-in knob for it was written, measured and deleted on + that evidence. If a future board reaches +18 — a sealed module at high + ambient might — the switch is worth revisiting, but implement it as the + in-place rewrite validated in #389 (13.8 ms, tracking left enabled), not the + teardown/rebuild. - **The loop needs settling time, so a fast rate-switching run misreports power.** Alternating CCK and OFDM at a few ms per frame leaves CCK transmitting above its settled level until the loop converges; pacing the diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index e996404..dccd4f3 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -130,17 +130,6 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { throw std::runtime_error("RTL8733B monitor RX configuration failed"); _tx_ready = true; _tx_submits = 0; - _tx_fatal = nullptr; - /* An opt-in that re-introduces register I/O on the send path should not be - * silent about it: enabling this trades ~84 ms per CCK<->OFDM crossing for - * thermal-compensation accuracy nobody has yet shown to matter, and a - * caller who inherited the knob from an environment deserves to see the - * cost rather than discover it as a mysterious frame-rate ceiling. */ - if (_cfg.tuning.tssi_rate_table && _tssi_tracking) - _logger->warn( - "RTL8733B: DEVOURER_TSSI_RATE_TABLE is on — each CCK<->OFDM rate " - "crossing re-selects the TSSI thermal table inside send_packet at " - "~84 ms / 136 USB register round trips (OpenIPC/devourer#389)"); /* One-shot thermal snapshot at bring-up — the PA-heating baseline for the * session, same as the Kestrel InitWrite snapshot. Logged, never acted on: * the meter is a PA-bias tracking index, not a calibrated °C sensor, and @@ -174,9 +163,15 @@ bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { /* Pick the thermal-compensation curve once, from the TX mode configured at * this point, and leave it alone — the vendor's own setup keys the table on * `phydm_get_tx_rate` at TSSI-setup time and never re-selects it at runtime. - * Re-selecting per frame is the opt-in DEVOURER_TSSI_RATE_TABLE path; the - * default keeps send_packet free of register I/O like every other - * generation. */ + * It is never re-selected per frame, so send_packet does no register I/O, + * like every other generation. + * + * The curve choice is inert on this silicon at any temperature the part + * reaches: the CCK and OFDM/HT tables are bit-identical for thermal deltas + * 0..+17 and first differ at +18, while a five-minute max-duty MCS7 soak + * plateaued at +8 after two minutes and stopped climbing. It is set from the + * rate class anyway because that costs nothing and is what the vendor does — + * but do not expect it to be measurable. */ const bool cck_table = _tx_mode_default.has_value() && _tx_mode_default->mode == devourer::TxMode::Mode::Legacy && @@ -193,39 +188,6 @@ bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { return true; } -bool Rtl8733bDevice::select_tssi_rate_table(bool cck) { - /* Default: the table was chosen once in configure_tx_power and stays put, so - * the send path does no register I/O — the vendor behaves the same way. - * Both conditions must be checked: gating only on the EFUSE PG mode would - * return false here, which send_packet treats as fatal and stops the card. */ - if (!_cfg.tuning.tssi_rate_table || - _efuse.tx_power_mode != rtl8733b::TxPowerPgMode8733b::TssiOffset) - return true; - if (!_tssi_tracking) - return false; - if (_tssi_cck == cck) - return true; - - /* The vendor chooses this table from the current TX rate. The table cannot - * be changed while closed-loop tracking is enabled, so make the transition - * explicit and reversible: exact rollback, table readback, then a fresh - * capped enable verified by register readback. (prepare_tssi_thermal here is - * the vendor's TSSI thermal-*compensation table*, a hardware feature driven - * off the EFUSE baseline constant — not a live temperature read.) */ - if (!_phy.disable_tssi_tracking()) - return false; - _tssi_tracking = false; - if (!_phy.prepare_tssi_thermal(_efuse, cck) || - !_phy.enable_tssi_tracking( - _channel, _efuse, rtl8733b::kSafeTssiTargetQdbm8733b)) - return false; - _tssi_tracking = true; - _tssi_cck = cck; - _logger->info("RTL8733B TSSI rate table selected: {}", - cck ? "CCK" : "OFDM/HT"); - return true; -} - void Rtl8733bDevice::StartRxLoop( Action_ParsedRadioPacket packetProcessor) { Action_ParsedRadioPacket rx_packet_processor = std::move(packetProcessor); @@ -384,14 +346,7 @@ void Rtl8733bDevice::SetMonitorChannel(SelectedChannel channel) { bool Rtl8733bDevice::send_packet(const uint8_t *packet, size_t length) { std::lock_guard lock(_reg_mu); if (!_phy_ready || !_mac_ready || !_tx_ready) { - /* Name the reason: a retry loop must be able to tell "InitWrite has not run - * yet" from "the session was deliberately stopped and will not recover". */ - if (_tx_fatal != nullptr) - _logger->error("RTL8733B TX rejected: session stopped by {} — " - "re-run InitWrite to recover", - _tx_fatal); - else - _logger->error("RTL8733B TX rejected before InitWrite"); + _logger->error("RTL8733B TX rejected before InitWrite"); return false; } if (packet == nullptr) @@ -432,22 +387,6 @@ bool Rtl8733bDevice::send_packet(const uint8_t *packet, size_t length) { const uint8_t bandwidth = (usb_frame[0x14] >> 5) & 0x3; const uint8_t short_gi = (usb_frame[0x14] >> 4) & 0x1; const uint8_t ldpc = (usb_frame[0x14] >> 7) & 0x1; - if (!select_tssi_rate_table(rate_hw <= 3)) { - /* Fatal, not transient. The CCK<->OFDM table switch disables closed-loop - * TSSI tracking, rewrites the thermal table and re-enables tracking; a - * failure anywhere in there means the readback no longer describes the - * loop that is driving the PA. Continuing to inject would be transmitting - * at an unverified power setting, so tear the session down (MAC stop + - * card disable). send_packet keeps its bool contract — no other generation - * throws per frame — so the reason is latched in _tx_fatal instead: a bare - * `false` is indistinguishable from a short bulk write and would send a - * retry loop straight back into the same state. */ - _logger->error("RTL8733B TX aborted: TSSI rate-table transition failed — " - "closed-loop power state is unverified, stopping the card"); - _tx_fatal = "a failed TSSI rate-table transition"; - Stop(); - return false; - } const int sent = _device.bulk_send_sync_ep( endpoint, usb_frame.data(), usb_frame.size(), 100); if (sent != static_cast(usb_frame.size())) { diff --git a/src/rtl8733b/Rtl8733bDevice.h b/src/rtl8733b/Rtl8733bDevice.h index c3e8779..ff0bb47 100644 --- a/src/rtl8733b/Rtl8733bDevice.h +++ b/src/rtl8733b/Rtl8733bDevice.h @@ -50,7 +50,6 @@ class Rtl8733bDevice : public IRtlDevice { private: void bring_up_to_phy(); bool configure_tx_power(SelectedChannel channel); - bool select_tssi_rate_table(bool cck); size_t build_tx_block(const uint8_t *packet, size_t length, uint8_t *out, uint8_t packet_offset); @@ -69,10 +68,6 @@ class Rtl8733bDevice : public IRtlDevice { bool _mac_ready = false; bool _phy_ready = false; bool _tx_ready = false; - /* Non-null once a send-path failure was fatal enough to Stop() the card, so - * later rejections can say why instead of reading as "InitWrite never ran". - * Static string literal; cleared by InitWrite. */ - const char *_tx_fatal = nullptr; bool _tssi_tracking = false; bool _tssi_cck = false; std::atomic _rx_stop{false}; From 70e95fcba031d60888fc9b0a5fd543cfaa5ef5dc Mon Sep 17 00:00:00 2001 From: snokvist Date: Fri, 14 Aug 2026 06:04:56 +0200 Subject: [PATCH 4/4] rtl8733b: drop the dead _tssi_cck member, fix the doc drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on #396, all doc/consistency — no behavioural change. - `_tssi_cck` went write-only when `select_tssi_rate_table` was deleted: set in `configure_tx_power`, cleared in `Stop()`, read nowhere. Deleted rather than kept for a reader that does not exist. - The table-selection comment claimed vendor-equivalent keying without saying the canonical flow reaches it with no mode configured — txdemo calls `InitWrite` before `SetTxMode`, so `_tx_mode_default` is usually unset and a `DEVOURER_TX_RATE=1M` session loads the OFDM/HT table. Left as is (the two tables are the same bits below +18 and the part plateaus at +8), but the comment now says so instead of implying parity. - Root `CLAUDE.md` and `src/rtl8733b/CLAUDE.md` still placed the `disable_cca` warning at `InitWrite` after this PR moved it to `bring_up_to_phy`. - `docs/rtl8733b.md` narrated two runs in terms of per-frame TSSI table switching, a path this PR deletes and which cannot be reproduced on this tree. The rate-coverage and thermal evidence stays; the mechanism framing goes, with the tables' identity at those deltas stated instead. 53/53, RTL8733B-only ASan+UBSan 49/49. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 ++- docs/rtl8733b.md | 16 +++++++++------- src/rtl8733b/CLAUDE.md | 6 ++++-- src/rtl8733b/Rtl8733bDevice.cpp | 14 ++++++++++---- src/rtl8733b/Rtl8733bDevice.h | 1 - 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a14a95a..688a0f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -334,7 +334,8 @@ Behavioural traps the per-field docs can't carry: been located and measured there, so `SetCcaMode(true)` throws (loudly, but without tearing the session down) while `SetCcaMode(false)`, the state its MAC bring-up already leaves programmed, succeeds as a no-op; setting the - config knob warns at `InitWrite` and airs with carrier-sense. Does NOT apply + config knob warns once at bring-up (RX-only sessions included) and airs + with carrier-sense. Does NOT apply the vendor BB CCA-off writes (they deafen the RX). RX-decode side is a separate null (`tests/dis_cca_onair.sh`). diff --git a/docs/rtl8733b.md b/docs/rtl8733b.md index 622eb42..517e79a 100644 --- a/docs/rtl8733b.md +++ b/docs/rtl8733b.md @@ -94,9 +94,9 @@ rather than in modulation. Treat the whole HT column as one flat band around 90-100% until something moves it by more than the probe noise. A mixed run alternating all eight rates (1/2/5.5/11 Mbps CCK, 6 Mbps OFDM, -MCS0/4/7) submitted 250 frames through a CCK/OFDM TSSI table switch on every -frame. The witness decoded all eight `rate_hw` values on air with valid FCS, -every frame at 20 MHz with STBC and LDPC clear, matching the forced-BCC +MCS0/4/7) submitted 250 frames, crossing the CCK/OFDM rate-class boundary on +every frame. The witness decoded all eight `rate_hw` values on air with valid +FCS, every frame at 20 MHz with STBC and LDPC clear, matching the forced-BCC contract. RX aggregation is capped at 12 KiB (`rtl8733b::kRxAggregateBytes8733b`) because @@ -120,10 +120,12 @@ the meter is a PA-bias tracking index rather than a calibrated junction temperature, so it is not sound input for a TX gate (`docs/warm-tx-degradation.md`). During the mixed CCK/OFDM witness run the raw code stayed at 32–33 against baseline 32 across 650 submitted frames; 636 were -captured with valid FCS across all five requested rates. A separate -transition-churn run submitted 1,000/1,000 frames through 118 CCK/OFDM TSSI -table changes; the witness captured 660 canonical frames across both rates, -all with valid FCS, while the same 32–33 thermal range held. +captured with valid FCS across all five requested rates. A separate 1,000-frame +run alternating CCK and OFDM submitted 1,000/1,000; the witness captured 660 +canonical frames across both rates, all with valid FCS, while the same 32–33 +thermal range held. Both runs are rate-coverage and thermal evidence only — +neither says anything about TX-power tracking, and at these deltas (+0 to +1) +the two thermal-compensation curves are the same bits anyway. ## TX power control diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md index 931c8be..61ecd72 100644 --- a/src/rtl8733b/CLAUDE.md +++ b/src/rtl8733b/CLAUDE.md @@ -138,9 +138,11 @@ loudly — without tearing the session down, since an unported optional knob is not a hardware-safety event — while `false` succeeds as a no-op because that is the state MAC bring-up already leaves programmed. -`DeviceConfig::tuning::disable_cca` cannot be honoured either, and `InitWrite` +`DeviceConfig::tuning::disable_cca` cannot be honoured either, and bring-up warns rather than dropping it — a config knob must not be the one door where a -request the setter refuses loudly instead vanishes without a word. +request the setter refuses loudly instead vanishes without a word. The warning +sits in `bring_up_to_phy`, not `InitWrite`, so an RX-only session that set the +knob is told too, and so it fires exactly once per bring-up. ## Validation status diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index dccd4f3..3549461 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -150,7 +150,6 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { _tssi_tracking = false; - _tssi_cck = false; /* Closed-loop TSSI is the TX-power control on a TSSI-offset PG unit, so it * is not optional there: the flat fallback index below is a conservative * bring-up value, and on-air it runs cold enough that HT rates do not @@ -171,7 +170,16 @@ bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { * 0..+17 and first differ at +18, while a five-minute max-duty MCS7 soak * plateaued at +8 after two minutes and stopped climbing. It is set from the * rate class anyway because that costs nothing and is what the vendor does — - * but do not expect it to be measurable. */ + * but do not expect it to be measurable. + * + * Which is just as well, because this is weaker than the vendor's keying in + * the canonical demo flow: txdemo calls InitWrite before SetTxMode, so + * _tx_mode_default is usually unset here and the OFDM/HT table is what a + * DEVOURER_TX_RATE=1M session loads — the CCK branch is reached only by a + * caller that configures the mode first, or by a later SetMonitorChannel. + * The vendor keys on the rate in flight at setup instead. Nothing chases + * that gap, because closing it would buy a table identical to the one + * already loaded at every delta this part reaches. */ const bool cck_table = _tx_mode_default.has_value() && _tx_mode_default->mode == devourer::TxMode::Mode::Legacy && @@ -184,7 +192,6 @@ bool Rtl8733bDevice::configure_tx_power(SelectedChannel channel) { channel, _efuse, rtl8733b::kSafeTssiTargetQdbm8733b)) return false; _tssi_tracking = true; - _tssi_cck = cck_table; return true; } @@ -591,7 +598,6 @@ void Rtl8733bDevice::Stop() { } _tssi_tracking = false; } - _tssi_cck = false; _phy_ready = false; if (_mac_ready) { _mac.stop(); diff --git a/src/rtl8733b/Rtl8733bDevice.h b/src/rtl8733b/Rtl8733bDevice.h index ff0bb47..0304133 100644 --- a/src/rtl8733b/Rtl8733bDevice.h +++ b/src/rtl8733b/Rtl8733bDevice.h @@ -69,7 +69,6 @@ class Rtl8733bDevice : public IRtlDevice { bool _phy_ready = false; bool _tx_ready = false; bool _tssi_tracking = false; - bool _tssi_cck = false; std::atomic _rx_stop{false}; std::atomic _rx_active{false}; std::atomic _rx_configured_bw{0};