Skip to content

TX power: per-rate diff table on every generation (#332) - #347

Merged
josephnef merged 1 commit into
masterfrom
feat/rate-diffs-all-generations
Jul 27, 2026
Merged

TX power: per-rate diff table on every generation (#332)#347
josephnef merged 1 commit into
masterfrom
feat/rate-diffs-all-generations

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Closes #332.

SetTxPowerRateDiffs was wired for the 8822E only; every other generation returned false. That was an artificial gap — Jaguar1 and Jaguar2 already compute a per-rate TXAGC index in software, the Jaguar3 diff-table block is shared by both dies (the routine devourer ports is rtw8822c_set_write_tx_power_ref), and Kestrel already rewrites its BB power target between frames.

The contract, everywhere

The caller table replaces the calibrated per-rate shape: rate level = anchor + its diff (quantized to the family step) + the runtime offset, clamped to the rail — where the anchor is the level the chip's own default walk produces for HT MCS7.

So a zero table is a no-op at MCS7 and flattens every other rate onto it; rates the struct doesn't describe (MCS8+, VHT/HE, 2SS+) sit at the anchor. Diffs are not regulatory-clamped — the same operator's call the other two knobs make. Resolution is the family step, so 0.5 dB on Jaguar1/Jaguar2: an odd qdB rounds, and the setters log the applied step count.

Per generation

how
Jaguar1 one branch in ComputeTxPowerIndex, which every per-rate index already funnels through — the 8812A/8821A fanout and the 8814A packed port work unchanged, and the table is sticky by construction. Per-apply anchor cache keeps a 4-path 8814A walk at 4 EFUSE lookups.
Jaguar2 new write_txagc_diffs over 0x1d00/0x1d80, anchored on the HT 1SS index taken pre-offset and pre-rail. Added to the SetMonitorChannel/FastRetune re-apply gates — without that a channel change silently reverts to the calibrated shape.
Jaguar3 the 8822E-only compile guard around apply_rate_diffs was vestigial; the 8822C reaches the same refs + 0x3a00 table with its flat reference as the anchor. Its no-table path is untouched, so the SDR-validated CU power is bit-identical.
Kestrel no per-rate TXAGC table exists under the fixed-dBm model, so the frame's own rate diff folds into the target in send_packet, reusing the existing write-on-change guard. Flat atomic array (not the struct) so the TX path reads one entry lock-free.

GetTxPowerCaps().rate_diffs_hw_table separates Kestrel's software fold from a hardware table — different cost and different reach. Two Kestrel consequences are documented rather than papered over: HE/VHT frames carry no diff, and the target is global, so a hardware-timed beacon airing between host frames inherits the last frame's level.

Also: the knob was unreachable from txdemo, i.e. unusable from the TX path it exists for. DEVOURER_TX_RATE_DIFFS maps it, range-checking before the int8_t cast that would otherwise wrap a boost request into a cut.

Validation

Register/shadowtests/txpwr_rate_diffs_regcheck.sh now walks every DUT, gated on the device's own caps advertisement instead of a PID list, asserting the delta in family steps and the readback class each part can actually back. A write-only family proves state management, not radiated power, and the script says so. 50/50 across 8 DUTs.

On-airtests/txpwr_rate_diffs_onair.sh trims MCS0 only and checks 6M does not move, which is what separates a per-rate lever from a global one. Ground-station RSSI reference (an SDR front end hard-limits at this geometry). −32 qdB on MCS0, −8.0 dB nominal:

DUT MCS0 6M
RTL8812AU −8.0 dB 0.0
RTL8821AU (2.4 GHz) −7.0 dB 0.0
RTL8814AU −8.0 dB 0.0
RTL8822BU −9.0 dB 0.0
RTL8822CU −8.0 dB 0.0
RTL8822EU −9.0 dB (sign-only) −1.0
RTL8852BU −7.0 dB −1.0
RTL8832CU −8.0 dB 0.0

Plus a register-exact check on the 8812AU: BB 0xc2c byte 0 goes 0x160x0E (anchor 22 → 14, exactly −8 steps) with MCS1..3 untouched.

rate_diffs_measured stays false on the 8822E, whose TSSI-reshaped transfer means its cell can only assert sign — the same split step_measured already makes.

No-regressiontests/txpwr_offset_regcheck.sh 42/42 with canary parity byte-identical to master on all three 11ac generations, including the Jaguar2 per-section TXAGC log parity cell that covers the lambda split here. ctest 47/47; all 7 per-chip subset build configs green.

🤖 Generated with Claude Code

SetTxPowerRateDiffs was wired for the 8822E only; every other generation
returned false. That was an artificial gap — Jaguar1 and Jaguar2 already
compute a per-rate TXAGC index in software, the Jaguar3 diff-table block is
shared by both dies (the routine devourer ports is rtw8822c_set_write_tx_power
_ref), and Kestrel already rewrites its BB power target between frames.

One contract everywhere: the caller table REPLACES the calibrated per-rate
shape — rate level = anchor + its diff (quantized to the family step) + the
runtime offset, clamped to the rail, where the anchor is the level the chip's
own default walk produces for HT MCS7. A zero table is therefore a no-op at
MCS7 and flattens every other rate onto it; rates the struct does not describe
(MCS8+, VHT/HE, 2SS+) sit at the anchor. Diffs are not regulatory-clamped, the
same operator's call the offset and flat-override knobs already make.

Shared surface stays small: rate_diff_steps / rate_diff_qdb_for_rate /
clamp_rate_diffs in src/TxPower, each HAL keeping its own register write. The
rail, the saturation plumbing and the anchor derivation are family-specific, so
no "anchor + diff + offset + clamp" composer is worth the abstraction.

Per generation:
- Jaguar1: one branch in ComputeTxPowerIndex, which every per-rate index
  already funnels through, so the 8812A/8821A register fanout and the 8814A
  packed port both work unchanged and the table is sticky by construction. A
  per-apply anchor cache keeps a 4-path 8814A walk at 4 EFUSE lookups.
- Jaguar2: write_txagc_diffs over the 0x1d00/0x1d80 block, anchored on the HT
  1SS section index taken pre-offset and pre-rail (clamping first would rebuild
  the shape off a saturated base). The table joins the SetMonitorChannel /
  FastRetune re-apply gates, without which a channel change silently reverts to
  the calibrated shape. An unprogrammed EFUSE has no anchor: that path warns
  and leaves TXAGC alone rather than flattening to zero.
- Jaguar3: the 8822E-only compile guard around apply_rate_diffs was vestigial,
  and the 8822C reaches the same refs + 0x3a00 table with its flat reference as
  the anchor. Its no-table path is unchanged, so the SDR-validated CU power is
  bit-identical.
- Kestrel: no per-rate TXAGC table exists under the fixed-dBm model, so the
  diff for the frame's own rate folds into the target in send_packet, reusing
  the existing write-on-change guard. Stored as a flat atomic array rather than
  the struct so the TX path reads one entry without a lock. Two consequences
  are documented rather than papered over: HE/VHT frames carry no diff, and the
  target is global, so a hardware-timed beacon airing between host frames
  inherits the last frame's level. GetTxPowerCaps().rate_diffs_hw_table
  separates that software fold from a hardware table.

The knob was unreachable from txdemo, i.e. unusable from the TX path it exists
for; DEVOURER_TX_RATE_DIFFS maps it, range-checking before the int8_t cast that
would otherwise wrap a boost request into a cut.

Validation. Register/shadow: txpwr_rate_diffs_regcheck.sh now walks every DUT,
gated on the device's own caps advertisement instead of a PID list, asserting
the delta in family steps and the readback class each part can actually back —
a write-only family proves state management, not radiated power, and the script
says so. On-air: txpwr_rate_diffs_onair.sh trims MCS0 only and checks 6M does
not move, which is what separates a per-rate lever from a global one. Measured
against a ground-station RSSI reference (an SDR front end hard-limits at this
geometry), -32 qdB on MCS0: 8812AU -8.0, 8821AU -7.0 at 2.4 GHz, 8814AU -8.0,
8822BU -9.0, 8822CU -8.0, 8822EU -9.0, 8852BU -7.0, 8832CU -8.0 dB against -8.0
nominal, 6M unmoved on all eight. rate_diffs_measured stays false on the 8822E,
whose TSSI-reshaped transfer means its cell can only assert sign — the same
split step_measured already makes.

No-regression: txpwr_offset_regcheck.sh 42/42 with canary parity byte-identical
to master on all three 11ac generations, including the Jaguar2 per-section
TXAGC log parity cell that covers the lambda split here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit e16add3 into master Jul 27, 2026
19 checks passed
@josephnef
josephnef deleted the feat/rate-diffs-all-generations branch July 27, 2026 16:01
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.

TX power: generalize SetTxPowerRateDiffs (per-rate power diff table) beyond the 8822E to J1/J2/J3(/Kestrel)

1 participant