Skip to content
15 changes: 11 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first):
only what an independent witness decoded — legacy OFDM + HT MCS0-7, BCC,
20/40 MHz on 2.4/5 GHz, plus long-preamble CCK on 2.4 GHz at 20 MHz.
Everything the backend has not ported (TSF/beacons, hardware ACK, A-MPDU,
the runtime TX-power levers) falls through to `IRtlDevice`'s
the flat-index and per-rate TX-power knobs) falls through to `IRtlDevice`'s
not-ported defaults rather than being faked, so read the base class before
assuming a cross-generation feature below applies here. `FastRetune` IS
ported (intra-band, TSSI kept live — `src/rtl8733b/CLAUDE.md`). SGI, LDPC, STBC, VHT
Expand Down Expand Up @@ -341,11 +341,18 @@ Behavioural traps the per-field docs can't carry:
separate null (`tests/dis_cca_onair.sh`).

**Runtime TX power** — the adaptive-link power lever, three knobs on the four
Jaguar/Kestrel generations (the RTL8733B ports none of them: it runs a fixed
safe closed-loop TSSI target): `SetTxPowerOffsetQdb` (relative,
Jaguar/Kestrel generations: `SetTxPowerOffsetQdb` (relative,
shape-preserving), `SetTxPowerIndexOverride` (flat absolute),
`SetTxPowerRateDiffs` (replace the
calibrated per-rate shape). The contract — how they compose, the MCS7-anchor
calibrated per-rate shape). The RTL8733B ports **only the first**, and on a
different mechanism: its closed-loop TSSI target table, not a TXAGC index, so
its caps report the dBm model (`index_max = 0`) over the int8 delta field's
`[-128, +127] qdB`, centred on a safe 16 dBm first-light target. Neither end is
re-clamped at something softer; where the chip stops responding — about
-96 qdB down, about +32 up, where the PA compresses and only EVM shows it — is
measured and documented rather than enforced. The flat index is refused there
because it was measured unable to carry HT at all. The contract — how they
compose, the MCS7-anchor
semantics, family step sizes, the write-only-family `hw_readback=false`
shadow, and Kestrel's software send-time fold — is documented at the
declarations in `src/TxPower.h`; the per-chip mechanics are in each
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,12 @@ if(DEVOURER_8733B)
)
target_link_libraries(Rtl8733bTxDescSelftest PRIVATE devourer)
add_test(NAME rtl8733b_tx_desc COMMAND Rtl8733bTxDescSelftest)

add_executable(Rtl8733bTxPwrSelftest
tests/rtl8733b_txpwr_selftest.cpp
)
target_link_libraries(Rtl8733bTxPwrSelftest PRIVATE devourer)
add_test(NAME rtl8733b_txpwr COMMAND Rtl8733bTxPwrSelftest)
endif()

# Headless guard for the per-UE RX attribution seed (src/cell/UeRxAttribution.h)
Expand Down
119 changes: 118 additions & 1 deletion docs/rtl8733b.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,116 @@ 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.

### The runtime lever

Because the loop is the power control, the runtime knob is the loop's target.
`IRtlDevice::SetTxPowerOffsetQdb` shifts every per-rate target that survives
the safe ceiling — cap first, then shift, so the calibrated per-rate spread
comes through the move intact — and writes it as the five packed dwords at
`0x3a00..0x3a10`, **in place, with tracking left enabled**. That is the same
shape `fast_retune` uses for its per-channel rewrite (#389); the alternative,
a TSSI disable/re-enable pair, costs ~165 ms and buys nothing here.

The capability report is the dBm model — `index_max = 0`, one qdB per step —
over `[-128, +127]`. Offset 0 is `kSafeTssiTargetQdbm8733b` (16 dBm), a
first-light clip sitting at or below this part's factory targets (18–20 dBm at
2.4 GHz, 16–19 at 5 GHz).

That range is asymmetric, and only one end is a hardware limit. The
`0x3a00..0x3a10` bytes hold a **signed int8 offset from the 64 qdBm anchor**, so
the field itself spans [−128, +127] — targets from −16 dBm to +47.75 dBm.

Both ends are that field, and neither is re-clamped at something softer — the
same clamped-only-at-the-hardware-rail answer Jaguar1 (±126) and Jaguar3 (±127)
give. Where the chip stops responding is measured and written down below rather
than enforced in the setter, because on this part both ends stop responding well
inside the field and at points only a bench can find.

The positive half is deliberately **not** re-clamped at the factory table.
`src/TxPower.h` states for every family that headroom above the generated
table is the operator's call, and a per-unit EFUSE trimmed too cold is exactly
what a bench calibration exists to correct — clamping here would make this the
one backend where a measured operating point cannot be commanded. What that
costs is measured below.

The offset is sticky: a full `SetMonitorChannel` re-folds it against the new
channel's targets, and `FastRetune` carries it into the hop's in-place rewrite.

The flat-index and per-rate-diff knobs are **not** ported. The flat index has
no measured dB-per-step slope on this part and, as above, cannot carry HT.

**Measured on air.** Two independent 6-point passes at ch36 against an
RTL8812AU witness (chip-RSSI ground station — the SDR saturates at this range,
see `tests/txpwr_offset_onair.sh`): the lever is monotone and worth
**14.2 / 14.8 dB** of received power for the full 16 dB of command, overall
slope **0.222 / 0.231 dB per qdB** against the 0.25 nominal. Its adversarial
counterpart, in the same breath: the step is not constant. The bottom 12 qdB
delivered **0.125 and 0.126** dB/qdB — the only structure that reproduced
exactly across both passes — while everything above −52 qdB ran 0.233..0.242
and the mid-range scattered 0.219..0.252 between passes. Near the floor the
loop returns about half the dB commanded, and `saturated_low` does not warn
about it (that clamp fires only at −64). `TxPowerCaps::step_measured`
therefore stays **false**, the same call the 8822E gets for the same reason: a
controller should calibrate its own dB-per-qdB, or close the loop on the
ground's RSSI.

The control that makes those numbers readable: the **pre-change binary, in the
same session and geometry, measured flat** — 0.3 dB across the same 64 qdB of
command, because it had no actuator to move. One unit, one witness, near-field
geometry, integer-quantised RSSI, no SDR. Harnesses:
`tests/rtl8733b_txpwr_regcheck.sh` (registers, including that null control),
`tests/txpwr_offset_onair.sh` (slope), `tests/rtl8733b_txpwr_selftest.cpp`
(the offset math, in `ctest`).

### The backoff floor is about −96 qdB, not the 0 dBm target

An earlier cut of this work clamped the offset at −64 qdB, reasoning that a
target below 0 dBm was meaningless. Sweeping past it says otherwise (MCS0,
ch36):

| offset | target | witness RSSI | witness EVM |
|---|---|---|---|
| −32 | 8.0 dBm | 69.40 | −60.8 |
| −64 | 0.0 dBm | 62.07 | −59.3 |
| −80 | −4.0 dBm | **54.80** | −54.0 |
| −96 | −8.0 dBm | 52.96 | −53.1 |
| −112 | −12.0 dBm | 52.97 | −52.8 |
| −128 | −16.0 dBm | 52.99 | −52.8 |

Power keeps falling past the 0 dBm target — another **7.3 dB** between it and
−4 dBm — with no sign wrap, and pins from about **−96 qdB**, where three
successive rungs agree within 0.03 dB. EVM softens from −59 to −53 across the
whole extension and the link stays decodable throughout. So usable travel is
~23 dB below the clip rather than the ~16 dB the first cut allowed, which
matters for near-field bench work and for a link that wants to sit quiet.

### Overdrive: about 3 dB, and then the PA compresses

Sweeping the other way — up from the clip, MCS0 at ch36, with the witness
reporting EVM beside RSSI:

| offset | witness RSSI | witness EVM | witness SNR |
|---|---|---|---|
| 0 | 75.4 | −62.0 | 62.0 |
| +16 (top of the PG table) | 78.2 | −50.2 | 63.7 |
| +32 | 84.1 | **−18.0** | 57.9 |
| +48 | 83.7 | −18.0 | 58.1 |
| +64 | 83.8 | −18.0 | 58.2 |

The +16 rung is real gain, +2.8 dB, though the constellation has already given
up 12 dB of EVM to get it. The +32 rung is **not** gain: the witness hears
8.7 dB more energy while EVM collapses to −18, and +48 and +64 change nothing
at all — RSSI and EVM both pinned. That is the PA in hard compression, and
`SNR never saw it` (58..64 throughout), which is the failure mode
`docs/bench-testing-near-field.md` exists to warn about: strong RSSI plus poor
EVM means back power off, the opposite of the weak-link response.

Two things follow. The vendor's PG table lands about where this part stops
being linear, so **+16 qdB is the edge of usable overdrive** even though the
API allows +127 — a caller sweeping for its own operating point should watch
EVM, not RSSI, and stop where EVM turns. And the negative half is clean by
comparison: EVM sits flat at −58..−61 across all 16 dB of backoff.

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 —
Expand Down Expand Up @@ -189,7 +299,14 @@ a request a caller may be making only through an inherited environment.
These results have **not** been claimed:

- No SDR was available, so occupied bandwidth, spectral mask, EVM and absolute
output power were not measured.
output power were not measured. The runtime power lever's slope is a
*relative* witness-RSSI measurement for the same reason: it says the lever
moves ~14 dB, not what any rung radiates in dBm.
- The TX-power offset's dB-per-qdB is not constant across its advertised range
(0.125 in the bottom 12 qdB vs 0.233..0.242 above −52), so
`step_measured` stays false and a controller must calibrate its own slope.
Raising `kSafeTssiTargetQdbm8733b` above 16 dBm is deferred — that wants a
conducted measurement, not a witness receiver.
- The experimental 5/10 MHz sequence has register-readback and normal-path RX
evidence only. Narrowband TX and decode by an independent narrowband peer are
deferred; `AdapterCaps::narrowband_ok` remains false.
Expand Down
84 changes: 80 additions & 4 deletions src/rtl8733b/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,68 @@ unrelated register map.
tail and its body in separate completions. The RX loop floors its URB size at
the same constant and a `static_assert` ties the two together — raising
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.
On a TSSI-offset PG unit the loop **is** the TX-power control, so it is not
- **TSSI closed loop.** Power runs from a safe ceiling
(`kSafeTssiTargetQdbm8733b` = 16 dBm). 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 runtime TX-power lever is that loop's target, and only the relative
knob is ported.** `SetTxPowerOffsetQdb` shifts every per-rate target below the
ceiling (`tssi_rate_offsets`: cap first, then shift — a lowered ceiling would
move only the rates above it and flatten the calibrated spread). The write is
the five packed dwords at `0x3a00..0x3a10`, rewritten **in place with tracking
live**, the same #389 shape `fast_retune` uses — not the ~165 ms
disable/re-enable pair. Caps therefore report the dBm model (`index_max = 0`,
one qdB per step) over `[-128, +127]` — the int8 delta field at both ends,
the same clamped-only-at-the-hardware-rail answer Jaguar1/3 give. The 0x3a00
bytes are a signed offset from the 64 qdBm anchor, so that field spans
targets from −16 to +47.75 dBm. Neither end is re-clamped at something
softer: not at the PG table on the way up (an EFUSE trimmed too cold is what
an operator calibrates their way out of), and not at a 0 dBm target on the
way down (an earlier cut did, and it cost ~9 dB of working backoff — see
below). Where the chip stops *responding* is measured and documented, not
enforced.
`SetTxPowerIndexOverride`, `SetTxPowerRateDiffs` and `ReApplyTxPower` stay
unported.
- **The backoff floor is ~−96 qdB, not the 0 dBm target.** Sweeping past the
old −64 clamp (MCS0, ch36): the 0 dBm target read 62.07, and −80 qdB
(−4 dBm) read **54.80 — another 7.3 dB down, with no sign wrap**. It pins
from about −96 qdB (−8 dBm): 52.96 / 52.97 / 52.99 at −96 / −112 / −128,
flat within 0.03 dB. So the usable travel is ~23 dB below the 16 dBm clip,
not the ~16 dB the first cut allowed, and EVM only softens from −59 to −53
across it. The lesson is the one this whole knob is about: the earlier floor
was a guess about what a negative absolute target *must* mean, and the guess
was worth 9 dB.
- **Overdrive above the clip buys ~3 dB and then the PA compresses, and EVM is
the only tell.** Sweeping UP from the clip (MCS0, ch36, witness reporting EVM
beside RSSI): +16 qdB — the top of the PG table — gave +2.8 dB with EVM
already down from −62 to −50; **+32 qdB read 8.7 dB louder with EVM collapsed
to −18**, and +48/+64 changed nothing at all (RSSI and EVM both pinned). More
energy, unusable constellation. **SNR held 58..64 throughout and never saw
it** — precisely the case `docs/bench-testing-near-field.md` is about. So the
counterpart to leaving the range open: the vendor's PG table lands about
where this part stops being linear, and +16 qdB is the edge of *usable*
overdrive even though the field allows +127. Below the clip EVM stays flat at
−58..−61 across all 16 dB of backoff — the negative half is clean.
- **The lever is worth ~14 dB, and it compresses at the bottom.** On-air
against an RTL8812AU witness (chip-RSSI ground station, the
`tests/txpwr_offset_onair.sh` method), two independent 6-point passes at
ch36: monotone, 14.2 / 14.8 dB of received power for the full 16 dB of
command, overall slope 0.222 / 0.231 dB per qdB against the 0.25 nominal.
The counterpart in the same breath: the step is **not** constant. The bottom
12 qdB delivered 0.125 and 0.126 dB/qdB — the one structure that reproduced
exactly across both passes — while everything above −52 qdB ran 0.233..0.242,
and the mid-range scattered 0.219..0.252 between passes. So the loop gives
about half the commanded dB as its target nears 0 qdBm, while
`saturated_low` still reads false (that clamp only fires at −64).
`step_measured` stays false for exactly that reason, the same call the 8822E
gets: calibrate your own dB-per-qdB or lean on the ground's RSSI. One unit,
one witness, near-field, integer-quantised RSSI, no SDR.
The pre-change binary measured **flat in the same session and geometry**
(0.3 dB across the same 64 qdB) — the do-nothing control that makes the
14 dB readable.
- **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,
Expand Down Expand Up @@ -162,14 +217,35 @@ untouched) and fall back to the full path.
## Not ported

`ReadTsf`/beacons, hardware ACK/BlockAck, A-MPDU,
`FastSetBandwidth`, the runtime TX-power knobs, `rx.path` per-chain telemetry,
`FastSetBandwidth`, the flat-index / per-rate-diff TX-power knobs
(`SetTxPowerIndexOverride`, `SetTxPowerRateDiffs`, `ReApplyTxPower` — only the
relative `SetTxPowerOffsetQdb` is ported), `rx.path` per-chain telemetry,
and CCA disable. These inherit `IRtlDevice`'s not-ported defaults (`false`,
`0`, or a full-path fallback) rather than being faked. `SetCcaMode` is the one
exception to the silent-default rule: it is pure virtual, so `true` throws
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.

The TX-power knobs are the other exceptions, in the same spirit:

- `SetTxPowerOffsetQdb` **refuses loudly and returns 0** on a unit whose EFUSE
carries no TSSI calibration, rather than reporting a successful zero-offset
apply. That indistinguishability is what this knob exists to end — a consumer
measured 18 dB of commanded offset moving nothing while its state read
`{"applied_qdb":0,"saturated_low":false}`, which is exactly what a healthy
actuator with travel remaining looks like. An offset latched *before*
bring-up on such a unit is **dropped loudly and zeroed** by
`configure_tx_power`, so the reported state never claims an offset no
register carries. (That path has no hardware coverage — every unit seen so
far is TSSI-offset PG — but it writes no registers, only a log and a reset.)
- `SetTxPowerIndexOverride` is overridden **solely to log a refusal**. The
`IRtlDevice` default returns `void` and ignores the value, so silence would
be the caller's only answer on the one backend where the flat index really is
unported — a knob that looks granted, in the PR that exists to abolish them.
`SetTxPowerRateDiffs` needs no such override: its `false` return already says
it.

`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. The warning
Expand Down
Loading
Loading