Skip to content

rtl8733b: keep the send path free of register I/O, scope the TSSI rate-table knob - #396

Merged
josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:rtl8733b-tssi-send-path
Aug 14, 2026
Merged

rtl8733b: keep the send path free of register I/O, scope the TSSI rate-table knob#396
josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:rtl8733b-tssi-send-path

Conversation

@snokvist

@snokvist snokvist commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The two post-merge findings, rebased onto master and answering the review points on both. Fixes #389.

The send path no longer touches a register

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 / 136 USB round trips (#389). configure_tx_power now picks the curve once per channel set 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, never re-selected at runtime.

Measured on the DUT — alternating CCK/OFDM, 200 frames: 19.69 s → 2.82 s, 200 table switches → 0, TSSI still enabled. send_packet now has no conditional register-I/O path at all.

The TSSI loop itself stays on wherever the EFUSE is TSSI-offset PG, because there it is the TX-power control. An early 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.

No runtime table switch, and the measurement that closes it

The first revisions carried DEVOURER_TSSI_RATE_TABLE as an opt-in for per-crossing switching, on the argument that the hot thermal-delta regime is where the two curves are designed to differ. Both reviewers landed on "probably delete it". A heat soak turned that from a judgement call into a measurement, so the knob is deleted — it could never have done anything on this part.

The two curves are bit-identical until thermal delta +18. Reconstructing the plan against the vendor 64-entry layout: the swing ramp starts at index 18 (CCK swing 0 for 0..17, then 3,3,5…; the OFDM plan is all-zero), and every word either side of the baseline is zero in both tables. Below +18 the CCK table and the OFDM table are the same bits.

The part plateaus at +8. Five-minute max-duty MCS7 soak (DEVOURER_TX_GAP_US=0, ~5000 fps submission), thermal polled every 2 s:

raw 33 -> 40   delta +1 -> +8
trajectory: 33/1, 37/5, 38/6, 39/7, 39/7, 39/7, 40/8, 40/8, 40/8, 40/8, 40/8

It stopped climbing after about two minutes and held +8 for the remaining three — asymptotic, not still-rising-when-I-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.

Deleted with it: select_tssi_rate_table, the DeviceConfig field, the env translation, and _tx_fatal — whose only producer was that function's failure path, so keeping it would have left a permanently-null member guarding a dead branch. Re-verified on air after the deletion: MCS7 279/300, 1 Mbps CCK 300/300 — unchanged from 277/300 and 300/300 before it.

+8 is a property of this unit, not of the silicon. Bare module, open air, room ambient, and submission-rate duty rather than measured radiated duty. A sealed BL-M8733BU2 at high ambient could plausibly reach +18 where this one cannot. The +18 threshold is the durable half — it comes from the vendor table contents, not the unit. So the code and docs state the reasoning rather than the conclusion, and both point at #389 for the validated in-place implementation (13.8 ms, tracking left enabled) as the thing to revisit if a board ever does get hot enough; #390's heat-soak work now has a specific number to test against.

disable_cca

Took the reviewer's call — kept my wording naming both spellings, dropped theirs. One placement change on top: it sits 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 now that both existed. Verified on hardware: fires once, bring-up completes, TX proceeds.

Docs

src/rtl8733b/CLAUDE.md from #395 described the 84 ms as an unconditional per-frame cost and stated the table cannot be changed while tracking is enabled — both now wrong. Rewritten, pointing at #389.

Follow-up review pass also removed the drift this PR would otherwise have introduced: both CLAUDE.md sites still placed the disable_cca warning at InitWrite, and docs/rtl8733b.md still narrated two runs in terms of per-frame TSSI table switching — a path this tree deletes, so those runs are not reproducible as described. The rate-coverage and thermal evidence stays; the mechanism framing is gone.

The witness qualification you suggested

Cheap and worth it. Sweeping the RTL8812AU across the HT ladder, 300 frames each:

rate delivered witness RSSI
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

Flat at 100% through MCS5, so the witness is not measuring itself — a receiver at its cliff falls off progressively at the top rather than dipping at both ends. Both low readings also carried 5-7 units less RSSI than the flat middle, which places the variation in per-run link conditions.

Checks

53/53, RTL8733B-only 49/49, RTL8733B-only ASan+UBSan 49/49. Hardware verified on the same one-unit DUT: send-path switch count, the heat soak, the single disable_cca warning, and the ladder above.

Still no SDR on this part, so nothing here asserts an RF-domain magnitude.

Follow-up to OpenIPC#388/OpenIPC#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 (OpenIPC#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`.** OpenIPC#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) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

RTL8733B: pick TSSI thermal table at TX-power setup; gate per-frame switching

🐞 Bug fix ✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Remove USB register I/O from RTL8733B send_packet by fixing TSSI table selection at channel set.
• Add DEVOURER_TSSI_RATE_TABLE as an explicit, default-off opt-in for per-rate table switching.
• Move disable_cca warning to bring-up and update RTL8733B docs with witness qualification.
Diagram

graph TD
  A["Env vars"] --> B["DeviceConfig.tuning"] --> C["RTL8733B configure_tx_power"] --> F["Phy8733b TSSI setup"] --> G["USB reg I/O"]
  D["RTL8733B send_packet"] --> E{"tssi_rate_table enabled?"}
  E -- "no" --> H["No reg I/O in send path"]
  E -- "yes" --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Remove DEVOURER_TSSI_RATE_TABLE entirely
  • ➕ Eliminates an uncharacterized tuning surface and avoids accidental performance regressions
  • ➕ Reduces support/documentation burden
  • ➖ Blocks investigation of the hot thermal-delta regime where the curves are designed to differ
  • ➖ Requires reintroducing the mechanism later if heat-soak/SDR work finds divergence
2. Adaptive switching based on thermal delta (not per-frame)
  • ➕ Could capture any hot-regime benefit without paying per-boundary send-path cost
  • ➕ Preserves default vendor-like behavior at room temperature
  • ➖ Adds new policy logic (thresholds, hysteresis) and requires new validation data
  • ➖ Still risks occasional send-path stalls when switching triggers
3. Force table selection on explicit mode transitions only (SetTxMode/retune)
  • ➕ Keeps send_packet clean while still allowing dynamic table changes when the caller opts in
  • ➕ Simpler mental model than per-frame boundary switching
  • ➖ Does not help mixed-rate streams unless the caller actively coordinates mode changes
  • ➖ May diverge from vendor behavior if mode transitions are frequent

Recommendation: The PR’s approach is the best default: it matches vendor behavior (table chosen during setup) and removes high-latency USB register I/O from the TX hot path, which is a clear correctness/performance win. Keeping DEVOURER_TSSI_RATE_TABLE as an explicitly caveated, default-off investigation knob is reasonable given the unmeasured hot regime; if the project prefers fewer knobs, the clean alternative is to drop it until heat-soak/SDR data exists.

Files changed (6) +186 / -44

Enhancement (1) +9 / -0
TxDescriptor8733b.hAdd helper to detect CCK legacy rates for initial table choice +9/-0

Add helper to detect CCK legacy rates for initial table choice

• Adds 'is_cck_rate_500kbps()' to identify long-preamble CCK legacy rates (1/2/5.5/11 Mbps) in 500 kbps units. Used to pick the initial TSSI thermal-compensation curve during setup.

src/rtl8733b/TxDescriptor8733b.h

Bug fix (1) +45 / -15
Rtl8733bDevice.cppMove TSSI table selection to configure_tx_power; gate runtime switching on knob +45/-15

Move TSSI table selection to configure_tx_power; gate runtime switching on knob

• Changes RTL8733B TX-power configuration to select the CCK vs OFDM/HT thermal-compensation curve once at channel setup based on the configured default TX mode, mirroring vendor behavior. Updates 'select_tssi_rate_table' to early-return unless both EFUSE mode supports TSSI-offset PG and 'tssi_rate_table' is enabled, keeping 'send_packet' free of register I/O by default; also moves the disable_cca warning to 'bring_up_to_phy' so it fires once per bring-up (including RX-only sessions).

src/rtl8733b/Rtl8733bDevice.cpp

Documentation (2) +97 / -29
rtl8733b.mdDocument fixed-per-channel TSSI table selection and qualify witness results +72/-17

Document fixed-per-channel TSSI table selection and qualify witness results

• Updates RTL8733B validation notes to clarify that closed-loop TSSI remains the TX-power control on TSSI-offset PG units, while thermal-table switching is opt-in. Adds witness qualification data across the HT ladder and rewrites the prior per-frame switching cost explanation to reflect the new default behavior and the 'DEVOURER_TSSI_RATE_TABLE' caveat.

docs/rtl8733b.md

CLAUDE.mdUpdate RTL8733B backend notes on TSSI loop vs thermal-table switching +25/-12

Update RTL8733B backend notes on TSSI loop vs thermal-table switching

• Replaces outdated guidance that implied per-frame switching and constraints around tracking. Documents the new default (choose table once per channel set) and positions 'DEVOURER_TSSI_RATE_TABLE' as an opt-in investigative path with known send-path costs.

src/rtl8733b/CLAUDE.md

Other (2) +35 / -0
env_config.cppParse DEVOURER_TSSI_RATE_TABLE into DeviceConfig +3/-0

Parse DEVOURER_TSSI_RATE_TABLE into DeviceConfig

• Introduces a default-off environment flag that enables per-rate TSSI thermal-table switching for RTL8733B. Documents why it is default-off (to keep send path free of register I/O).

examples/common/env_config.cpp

DeviceConfig.hAdd tuning::tssi_rate_table knob with explicit constraints and caveats +32/-0

Add tuning::tssi_rate_table knob with explicit constraints and caveats

• Adds 'DeviceConfig::tuning::tssi_rate_table' (default false) and extensively documents its purpose, performance cost, and validation status. Clarifies that it controls thermal-table switching only (not whether TSSI runs) and that hot-regime benefit is unmeasured.

src/DeviceConfig.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. tssi_rate_table causes send I/O ✓ Resolved 📘 Rule violation ➹ Performance
Description
When tuning.tssi_rate_table is enabled, send_packet triggers select_tssi_rate_table, which
performs TSSI disable/enable and thermal-table programming on rate-class crossings. This violates
the requirement that the send_packet TX hot path must not perform per-frame register/sensor I/O.
Code

src/rtl8733b/Rtl8733bDevice.cpp[R191-193]

+  if (!_cfg.tuning.tssi_rate_table ||
+      _efuse.tx_power_mode != rtl8733b::TxPowerPgMode8733b::TssiOffset)
return true;
Evidence
PR Compliance ID 22 forbids register/sensor I/O in the send_packet TX path. The PR keeps a
configuration knob that, when enabled, lets send_packet call into select_tssi_rate_table() and
execute tracking disable/enable plus table programming during transmission-rate transitions.

CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path: CLAUDE.md: Do Not Perform Per-Frame Register/Sensor Reads in the send_packet TX Path
src/rtl8733b/Rtl8733bDevice.cpp[374-440]
src/rtl8733b/Rtl8733bDevice.cpp[186-216]
src/DeviceConfig.h[332-347]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The code still allows register I/O in the `send_packet` hot path when `DeviceConfig::tuning::tssi_rate_table` / `DEVOURER_TSSI_RATE_TABLE=1` is enabled, because `send_packet` calls `select_tssi_rate_table()` and that function can disable/re-enable TSSI tracking and reprogram thermal tables.
## Issue Context
Compliance requires that `send_packet` (and per-frame TX-path code) does not perform register/sensor I/O or other USB control-transfer-heavy operations.
## Fix Focus Areas
- src/rtl8733b/Rtl8733bDevice.cpp[186-216]
- src/rtl8733b/Rtl8733bDevice.cpp[374-440]
- src/DeviceConfig.h[332-363]
- examples/common/env_config.cpp[173-179]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. CLAUDE.md duplicates tssi_rate_table docs ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
src/rtl8733b/CLAUDE.md restates detailed behavior/characterization of
DeviceConfig::tuning::tssi_rate_table that is already documented in src/DeviceConfig.h. This
creates a drift risk and violates the rule to point to header doc-comments instead of duplicating
them.
Code

src/rtl8733b/CLAUDE.md[R63-66]

+  `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
Evidence
PR Compliance ID 10 forbids duplicating header doc-comments in CLAUDE.md. The updated CLAUDE.md
repeats the knob’s details (cost, characterization status, and guidance) that are already documented
in the DeviceConfig::tuning::tssi_rate_table header comment.

CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead): CLAUDE.md: Do Not Duplicate Header Doc-Comments in CLAUDE.md (Point to the Header Instead)
src/rtl8733b/CLAUDE.md[49-75]
src/DeviceConfig.h[332-363]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/rtl8733b/CLAUDE.md` duplicates detailed documentation that already exists as a header doc-comment for `DeviceConfig::tuning::tssi_rate_table`.
## Issue Context
Compliance requires CLAUDE.md to point to canonical header doc-comments instead of copying/paraphrasing them, to avoid documentation drift.
## Fix Focus Areas
- src/rtl8733b/CLAUDE.md[56-75]
- src/DeviceConfig.h[332-363]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Room-temp claim lacks caveat ✓ Resolved 📘 Rule violation ≡ Correctness
Description
docs/rtl8733b.md presents a favorable room-temperature measurement (table choice “costs nothing
measurable”) without immediately pairing it with the limiting counterpart that the hot/thermal-delta
regime is unmeasured. This violates the requirement that favorable measurements include an
adversarial/limiting counterpart in the same context.
Code

docs/rtl8733b.md[R155-158]

+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
Evidence
PR Compliance ID 11 requires pairing favorable measurements with an adversarial/limiting counterpart
in the same context. The new docs passage claims the room-temperature neutrality but does not
immediately include the corresponding limitation (hot regime unmeasured), even though that
limitation is documented elsewhere in the header comment.

CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context: CLAUDE.md: Performance/Measurement Claims Must Include Adversarial Counterpart in the Same Context
docs/rtl8733b.md[155-159]
src/DeviceConfig.h[349-356]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The documentation states a favorable result at room temperature without immediately stating the key limitation/adversarial counterpart (that elevated thermal delta behavior is unmeasured).
## Issue Context
Compliance requires favorable measurements to be accompanied by their limiting/adversarial counterpart in the same passage so readers do not overgeneralize.
## Fix Focus Areas
- docs/rtl8733b.md[155-159]
- src/DeviceConfig.h[349-356]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. TSSI table ignores SetTxMode 🐞 Bug ≡ Correctness
Description
Rtl8733bDevice::configure_tx_power() programs the TSSI thermal table from _tx_mode_default during
InitWrite, but SetTxMode() later only stores the new mode and never reprograms the already-selected
table. Callers that set a Legacy CCK default after InitWrite (as examples/tx does) can therefore run
CCK traffic with the OFDM/HT curve unless they enable the per-frame switching knob.
Code

src/rtl8733b/Rtl8733bDevice.cpp[R170-173]

+  const bool cck_table = _tx_mode_default.has_value() &&
+                         _tx_mode_default->mode ==
+                             devourer::TxMode::Mode::Legacy &&
+                         rtl8733b::is_cck_rate_500kbps(
Evidence
The PR makes the initial thermal-table choice depend on _tx_mode_default at InitWrite-time, but
SetTxMode() does not re-run prepare_tssi_thermal() or otherwise change the already-selected
curve. The tx demo calls InitWrite() before SetTxMode(), so _tx_mode_default is typically
unset when the table is chosen, making the selected curve potentially inconsistent with the caller’s
runtime default mode.

src/rtl8733b/Rtl8733bDevice.cpp[152-183]
src/rtl8733b/Rtl8733bDevice.cpp[425-440]
src/rtl8733b/Rtl8733bDevice.cpp[462-470]
examples/tx/main.cpp[998-1003]
examples/tx/main.cpp[1057-1059]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`configure_tx_power()` selects the RTL8733B TSSI thermal-compensation curve once, based on `_tx_mode_default` at InitWrite time. However, `SetTxMode()` is a runtime API and (per the repo's own tx demo) is often called *after* InitWrite; the current implementation only updates `_tx_mode_default` and never updates the programmed TSSI curve.
As a result, when the runtime default TX mode is changed to a Legacy CCK rate after InitWrite, the device can keep using the OFDM/HT thermal table for the entire session unless per-frame switching is enabled.
## Issue Context
- The send path intentionally avoids register I/O when `tssi_rate_table` is off, so the fix should keep `send_packet()` free of register work.
- Changing the thermal table while tracking is enabled requires disabling/re-enabling tracking (similar to the existing `select_tssi_rate_table()` transition body).
## Fix Focus Areas
- src/rtl8733b/Rtl8733bDevice.cpp[152-216]
- src/rtl8733b/Rtl8733bDevice.cpp[462-470]
- examples/tx/main.cpp[998-1060]
## Suggested approach
1. In `Rtl8733bDevice::SetTxMode()` (and potentially `ClearTxMode()`), if:
- EFUSE mode is `TssiOffset`,
- `_tssi_tracking` is true (TX power loop active), and
- `!_cfg.tuning.tssi_rate_table` (per-frame switching is OFF),
then compute the desired table (`cck_table`) from the new mode and, if it differs from `_tssi_cck`, perform a one-time table transition (disable tracking -> `prepare_tssi_thermal()` -> re-enable tracking) under `_reg_mu`.
- This keeps register I/O off the send path while ensuring the configured runtime default TX mode is honored.
2. Optionally update `examples/tx/main.cpp` to call `SetTxMode()` before `InitWrite()` to align demos with the intended “table chosen at setup” behavior, but still implement (1) to avoid relying on call order.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/rtl8733b/CLAUDE.md Outdated
Comment thread src/rtl8733b/Rtl8733bDevice.cpp Outdated
Comment thread docs/rtl8733b.md Outdated
Comment thread src/rtl8733b/Rtl8733bDevice.cpp
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) <noreply@anthropic.com>
@snokvist

Copy link
Copy Markdown
Contributor Author

Both bot findings addressed in 5a009ee, one by fixing it and one by disagreeing with the framing but fixing the real problem underneath.

1. CLAUDE.md duplicating the knob docs — valid, fixed. The root CLAUDE.md rule is explicit ("never duplicate what a header already doc-comments") and I had restated the room-temperature validation, the unmeasured hot regime and the not-a-neutral-toggle framing in the subtree file. The subtree file now keeps only what is genuinely subtree knowledge — the table is chosen once, the vendor does the same, and what the transition failure mode is and is not — and points at src/DeviceConfig.h for when the knob is worth paying for.

2. tssi_rate_table causes send I/O — by design, but it should not have been silent. Enabling the knob re-introducing register I/O on the send path is its documented purpose, so I do not think the invariant is violated: "nothing reads a register per frame on the send path" is a statement about the default configuration, the same way DEVOURER_DIS_CCA does not violate "carrier-sense is enabled by default". Deleting an opt-in because a rule about defaults reads unconditionally would be the wrong fix.

What the finding did surface is a real defect I had missed: the knob was silent. Someone who inherits DEVOURER_TSSI_RATE_TABLE=1 from an environment would meet the ~84 ms per rate crossing as an unexplained frame-rate ceiling, with nothing in the log connecting the two. InitWrite now warns once when the knob is active and TSSI is running, naming the cost and the issue — the same reasoning as the tuning.disable_cca warning one function above. Verified: warns once with the knob on, silent with it off.

@josephnef the standing offer from the PR description is unchanged and this does not pre-empt it — if you would rather not carry the knob at all, say so and I will drop it, and the capability should then come back (if #390 shows the hot case diverges) implemented as the validated in-place swap at 13.8 ms rather than the 84 ms teardown/rebuild it uses today. Deleting is a one-commit change; I have kept it only because #390 is the experiment that would justify or bury it.

53/53, RTL8733B-only 49/49, ASan+UBSan 49/49 on both commits.

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#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) <noreply@anthropic.com>
@snokvist

Copy link
Copy Markdown
Contributor Author

Ran the heat soak. The knob is deleted — it could never have done anything.

ba15a5d. You both landed on "probably delete it"; the soak turns that from a judgement call into a measurement.

The two curves are bit-identical until thermal delta +18. Reconstructing the plan against the vendor 64-entry layout: the swing ramp starts at index 18, and every word either side of the baseline is zero in both tables. Below +18 the CCK table and the OFDM table are the same bits.

The part plateaus at +8. Five-minute max-duty MCS7 soak (DEVOURER_TX_GAP_US=0, ~5000 fps submission), thermal polled every 2 s:

raw 33 -> 40   delta +1 -> +8
trajectory: 33/1, 37/5, 38/6, 39/7, 39/7, 39/7, 40/8, 40/8, 40/8, 40/8, 40/8

It stopped climbing after about two minutes and held +8 for the remaining three — asymptotic, not still-rising-when-I-stopped. That is less than half the delta needed for a single table entry to differ.

So a runtime switch would spend 84 ms and 136 USB register round trips, inside send_packet, to install a table bit-identical to the one already loaded. There is no temperature this unit reaches at which the knob does anything. It is deleted, along with select_tssi_rate_table, the DeviceConfig field, the env translation, and _tx_fatal — whose only producer was that function's failure path, so keeping it would have left a permanently-null member guarding a dead branch. send_packet now has no conditional register-I/O path at all.

Re-verified on air after the deletion: MCS7 279/300, 1 Mbps CCK 300/300 — unchanged from 277/300 and 300/300 before it.

What I deliberately did not claim

+8 is a property of this unit, not of the silicon. Bare module, open air, room ambient, and submission-rate duty rather than measured radiated duty. A sealed BL-M8733BU2 at high ambient could plausibly reach +18 where this one cannot. The +18 threshold is the durable half — it comes from the vendor table contents, not the unit.

So the code and docs now state the reasoning rather than just the conclusion, and both point at #389 for the validated in-place implementation (13.8 ms, tracking left enabled) as the thing to revisit if a board ever does get hot enough. I have added the same to #390 so the heat-soak work there has a specific number to test against.

53/53, RTL8733B-only 49/49, ASan+UBSan 49/49. The build warnings in the log are pre-existing (hal/basic_types.h, svctx, hopset, chanmig), not from this branch.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ba15a5d. The code change is good — deleting the knob on the table-identity evidence is the right call, and I verified the +18 claim against Phy8733b::tssi_thermal_plan (CCK swing 0 for 0..17, then 3,3,5…; OFDM plan all-zero — both sides of the baseline). Built clean, ctest 53/53 here too. Findings are all doc/consistency, two of them inline below, three here:

  1. The PR description is stale — it argues for keeping the knob the head commit deletes. The body still says "I kept it rather than deleting it… say so and I will drop it in this PR", but ba15a5d already dropped it. Please update the description before merge so the merge record matches the change — and the bit-identical-through-+17 evidence deserves to be in the body, it's the strongest part of the PR.

  2. Two CLAUDE.md sites still say the disable_cca warning fires "at InitWrite". The PR moves it to bring_up_to_phy, but root CLAUDE.md ("setting the config knob warns at InitWrite", ~line 337) and src/rtl8733b/CLAUDE.md ("InitWrite warns rather than dropping it", ~line 141) weren't updated. Given this PR exists partly to fix #395's doc drift, it shouldn't introduce the same shape of drift.

  3. docs/rtl8733b.md still narrates measurements of the removed mechanism as current behaviour: "submitted 250 frames through a CCK/OFDM TSSI table switch on every frame" (~line 95) and "1,000/1,000 frames through 118 CCK/OFDM TSSI table changes" (~line 125) describe a code path this PR deletes — those runs cannot be reproduced on this tree. The delivery/thermal evidence is worth keeping; the table-switch framing should go (docs are current-state only in this repo).

Nothing here touches the hardware-validated behaviour. Happy to approve once the doc drift and the PR body are fixed.

Comment thread src/rtl8733b/Rtl8733bDevice.cpp
Comment thread src/rtl8733b/Rtl8733bDevice.cpp Outdated
Review follow-ups on OpenIPC#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) <noreply@anthropic.com>
@snokvist

Copy link
Copy Markdown
Contributor Author

All five findings addressed in 70e95fc. No behavioural change in the commit — a dead member, two comments and three doc sites — so the hardware numbers from ba15a5d stand unchanged.

1. PR description. Rewritten. It no longer argues for keeping the knob it deletes, and the two pieces of evidence you called the strongest are now in the body rather than buried in a thread: the curves are bit-identical for deltas 0..+17 and first differ at +18 (CCK swing 0 through 17, then 3,3,5…; OFDM plan all-zero), and the soak trajectory that plateaus at +8. The "+8 is this unit, +18 is the silicon" split is stated there too.

2. disable_cca at InitWrite. Both sites fixed — root CLAUDE.md now says the knob warns once at bring-up including RX-only sessions, and src/rtl8733b/CLAUDE.md names bring_up_to_phy and why it sits there. You are right that this PR of all PRs should not have shipped that.

3. docs/rtl8733b.md narrating the removed mechanism. Both passages rewritten. The 250-frame run now reads as crossing the CCK/OFDM rate-class boundary per frame (which is what the run actually established — eight rate_hw values on air, valid FCS, 20 MHz, STBC/LDPC clear), and the 1,000-frame run as a mixed-rate run holding 32–33 raw against baseline 32. Both now carry an explicit line that they are rate-coverage and thermal evidence only and say nothing about the TX-power path, since at +0..+1 the two curves are the same bits either way.

4 and 5 answered inline: _tssi_cck deleted, and the ordering caveat written into the comment rather than reordering txdemo — with the reason, and the offer to do the reorder instead if you want the flow correct by construction.

53/53, RTL8733B-only ASan+UBSan 49/49.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five findings addressed in 70e95fc — verified against the diff, not just the replies. The comment at the table-selection site now states the InitWrite-before-SetTxMode ordering and why the gap is deliberately not chased; both CLAUDE.md sites track the warning's real location; docs/rtl8733b.md re-narrates the two runs without the deleted mechanism and pairs the evidence with its scope; _tssi_cck is gone. The commit is no-behavioural-change as claimed (only the two dead-member writes leave the .cpp), and the witness-ladder qualification closes the last open question about the delivery numbers. Approving.

@josephnef
josephnef merged commit 4b66e62 into OpenIPC:master Aug 14, 2026
26 of 32 checks passed
josephnef added a commit that referenced this pull request Aug 14, 2026
## The friction this removes

Merging #396 surfaced a gap in the gate's unblock flow: after resolving
the last Qodo thread, the reply-triggered green run lands **beside** the
stale red runs rather than superseding them — each trigger event type
creates its own workflow run, and the branch-protection rollup counts
every run of a required check on the head commit. The PR stayed
`BLOCKED` with a green latest run until all four red
`pull_request`-event runs were re-run by hand from the Checks tab.

## The fix

A passing run now sweeps: it lists this workflow's `failure`-conclusion
runs on the same head SHA and `gh run rerun --failed`s each (new
`actions: write` permission). Properties:

- **Convergent**: only a PASS triggers sweeps, and a re-run that passes
finds nothing red left to sweep.
- **Still a gate**: if threads are genuinely unresolved, the swept runs
go red again and the check keeps blocking — the sweep can only align the
rollup with the live resolution state, never bypass it.
- **Best-effort**: the sweep step tolerates individual `rerun` failures;
it cannot turn a PASS into a FAIL.

The failure message now points at the one remaining manual step (reply
in a thread to retrigger) instead of also asking for Checks-tab re-runs.

## The alternative, re-tested and still dead

Before adding machinery I re-verified the header's claim that GitHub
rejects the documented `pull_request_review_thread` trigger — if that
parsed, plain thread resolution would retrigger the gate and most of
this would be unnecessary. It still doesn't: a scratch workflow carrying
`pull_request_review_thread: {types: [resolved, unresolved]}` produced a
zero-job "workflow file issue" run on push (run 31778980528). The header
comment now records that this was re-verified empirically, not just
inherited.

## What this does not change

The gate's semantics are untouched: not pinned to head (the
anti-treadmill rationale stands), `skip-qodo-gate` label escape hatch,
pagination, thread-attribution logic — all as before. One net-new
permission (`actions: write`), scoped to the workflow's own runs by
construction (`--workflow qodo-gate --commit <head>`).

Validation: YAML parses; the sweep's list/filter pipeline is the same
query used to unblock #396 by hand. The self-referential part (a green
qodo-gate re-running red qodo-gates) can only be observed on a PR that
first goes red and then resolves — this PR itself will exercise the gate
normally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rtl8733b: CCK<->OFDM TSSI table switch costs 84 ms (136 USB register ops) inside send_packet

2 participants