Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions docs/rx-spectrum-sensing.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,15 @@ hardware it runs on:
|---|---|---|
| returns | `ChannelBusy` — busy airtime + energy-above-floor | `RxEnergy` — the phydm counter set |
| available on | any backend with a hardware busy-airtime counter | Realtek only |
| today | Jaguar1/2/3 (CCX CLM), MT7612U (MAC channel timers) | Jaguar1/2/3, Kestrel (floor only) |
| not available | Kestrel, RTL8733B — both report *no reading*, never zero | RTL8733B, MT7612U |
| today | Jaguar1/2/3 and RTL8733B (CCX CLM), MT7612U (MAC channel timers) | Jaguar1/2/3, Kestrel (floor only) |
| not available | Kestrel — reports *no reading*, never zero | RTL8733B, MT7612U |

Advertised statically by `AdapterCaps::busy_airtime_ok` /
`busy_airtime_measured` / `rx_energy_ok`. **Do not use a successful
`dynamic_cast<IRtlRadio*>` as the discriminator** — it was never correct: the
RTL8733B derives from `IRtlRadio` and implements no energy reader at all.
RTL8733B derives from `IRtlRadio` and implements no energy reader at all —
while nonetheless answering `GetChannelBusy()` through CLM, so the two flags
have to be read separately rather than inferred from each other.

`ChannelBusy` carries its own `source` (`Clm` or `ChTime`) because the two
facilities define busy differently: the MediaTek timers count TX+RX+NAV+EIFS,
Expand Down Expand Up @@ -360,6 +362,7 @@ The facilities differ by generation but all three read the same fields:
| Jaguar1 (8812/8821/8814) | yes | classic AC — FA 0xF48/0xA5C, CCA 0xF08, IGI 0xC50; NHM 0x994/0x990/0x998/0xfa8/0xfb4 |
| Jaguar2 (8822BU/8821CU) | yes | classic AC (FA/CCA sampled by the DIG thread; same NHM map) |
| Jaguar3 (8822CU/8822EU) | yes | newer BB — CCA 0x2c08, CCK-FA 0x1a5c, OFDM-FA 0x2d0x, IGI 0x1d70; NHM 0x1e60/0x1e40/0x1e44/0x2d40/0x2d4c |
| RTL8733B (8731BU/8733BU) | **CLM only** | the JGR3 CCX map (ctrl 0x1e60, period 0x1e40, result 0x2d88). No phydm FA/CCA block and no NHM here — CLM needs no IGI reference, so it ports alone |

## The armed busy window (`ArmChannelBusy`)

Expand Down Expand Up @@ -414,11 +417,11 @@ quick-connect decision has.
Three things spoil one, all measured, and each makes the reading come back
invalid with a reason (`ChannelBusy::spoil`) rather than plausible:

| spoiler | Jaguar1 (11AC) | Jaguar3 (JGR3) |
|---|---|---|
| an NHM read mid-window | survives, reads **+4 points high** (74.8 vs 70.9) | **destroyed** — returns the 2 ms re-arm (326 of 62500 ticks on the 250 ms window used for that measurement) |
| a retune mid-window | 60-62% where the channel was 71% | 44-47% where it was 61% |
| reading before it elapsed | the result register latches the PREVIOUS window; reads are non-destructive, so an early read is a stale number wearing a fresh timestamp |
| spoiler | Jaguar1 (11AC) | Jaguar3 (JGR3) | RTL8733B (JGR3) |
|---|---|---|---|
| an NHM read mid-window | survives, reads **+4 points high** (74.8 vs 70.9) | **destroyed** — returns the 2 ms re-arm (326 of 62500 ticks on the 250 ms window used for that measurement) | unreachable — no NHM reader on this die, so nothing can re-arm the shared engine |
| a retune mid-window | 60-62% where the channel was 71% | 44-47% where it was 61% | refused with `spoil=retuned` |
| reading before it elapsed | the result register latches the PREVIOUS window; reads are non-destructive, so an early read is a stale number wearing a fresh timestamp | as the other two: refused with `spoil=not-elapsed` |

The third row rests on one silicon behaviour nothing else here depends on:
**triggering CLM clears the ready bit**, so a window that has not finished
Expand Down Expand Up @@ -471,13 +474,28 @@ where a result at or above the period reports 100%.

### Coverage

`ArmChannelBusy` works on Jaguar1, Jaguar2 and Jaguar3 (CLM) and on the
MT7612U (channel timers). Under one flooder on one channel the three Realtek
families and the MediaTek independently measured the same load at 61-71% — the
spread is antenna and receiver gain, not a units disagreement. Kestrel and the
RTL8733B return 0 from the arm (the CCX engine is not wired up on either — the
RTL8733B has a working one, see the caps comment in its device source), and
their callers keep the sampled path.
`ArmChannelBusy` works on Jaguar1, Jaguar2, Jaguar3 and the RTL8733B (CLM)
and on the MT7612U (channel timers). Under one flooder on one channel the
Realtek families and the MediaTek independently measured the same load at
61-71% — the spread is antenna and receiver gain, not a units disagreement.
The RTL8733B and a Jaguar3 8812CU read the same load at 69% apiece on the same
flooder, across the two different device paths onto the same JGR3 map. Kestrel
returns 0 from the arm — its CCX engine is not wired up — and its callers keep
the sampled path.

The RTL8733B is the one backend where an armed window is the ONLY way to get a
number: it implements no `GetRxEnergy`, so its sampled path reports no reading
by design. `src/sensing/` cannot reach that reading yet — `SenseWindow` picks
its source by a non-null `IRtlRadio*` rather than by `rx_energy_ok`, the same
wrong discriminator `AdapterCaps.h` warns about, and `examples/chanscout` hits
it on this die. The gap and what a fix needs are in `src/sensing/CLAUDE.md`;
until then a caller on this die uses `IRadio::ArmChannelBusy`/`GetChannelBusy`
directly.

`tests/busy_window_probe.sh` skips its sampled and NHM arms for that reason,
gating on `rx_energy_ok` from the probe's own caps record rather
than on the sensor's USB VID — a Realtek chip with CCX and no phydm counters
is exactly what a VID test gets wrong.

Both harness runs above used an MT7612U flooder. With a Jaguar2 (8822BU)
`txdemo` flooder the same harness read a **valid 0%** for the first ~600 ms of
Expand Down
24 changes: 19 additions & 5 deletions src/AdapterCaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,30 @@ struct AdapterCaps {
* RTL8733B derives from IRtlRadio and implements no GetRxEnergy at all, so
* the cast reports a sensor that returns nothing.
*
* busy_airtime_ok: IRadio::GetChannelBusy returns a real busy-airtime
* reading — the Realtek CCX CLM engine (Jaguar1/2/3) or the MediaTek MAC
* channel timers. FALSE on Kestrel (its NHM rides the halbb glue, not
* NhmReader, so it has no CLM) and on the RTL8733B.
* The two flags below are also independent of each other, and the RTL8733B
* is where that stops being theoretical: it has a working CCX CLM engine
* and no phydm FA/CCA block, so busy_airtime_ok is true while rx_energy_ok
* is false. Do not read either from the other.
*
* busy_airtime_ok: the backend HAS a hardware busy-airtime engine that
* IRadio::GetChannelBusy can report — the Realtek CCX CLM engine
* (Jaguar1/2/3 and the RTL8733B, which the vendor phydm puts on the JGR3
* map) or the MediaTek MAC channel timers. FALSE on Kestrel, whose NHM
* rides the halbb glue rather than NhmReader, so it has no CLM.
*
* It does NOT promise that an unarmed GetChannelBusy() answers. On the
* RTL8733B it does not: that backend feeds its sampled path from
* GetRxEnergy, which it does not implement, so an unarmed call reports no
* reading and IRadio::ArmChannelBusy is the only way to get a number out of
* it. A consumer that wants a reading from an arbitrary backend should arm
* rather than sample; one that samples must handle "no reading" from a
* backend whose flag is true.
*
* busy_airtime_measured: that reading has been separated from a quiet
* channel ON AIR for this family, not merely implemented. The harness is
* tests/busy_window_probe.sh, which pits an armed window against the quiet
* floor under a known load, and against each way a window can be spoiled.
* True today on all four backends that set busy_airtime_ok. The flag stays
* True today on all five backends that set busy_airtime_ok. The flag stays
* because the two facts are independent: a port can land the engine before
* anyone has run it on air, and false-as-unmeasured is the house rule for
* that state.
Expand Down
77 changes: 75 additions & 2 deletions src/rtl8733b/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ untouched) and fall back to the full path.

## Not ported

`ReadTsf`/beacons, A-MPDU, CCX / `tx.report` per-frame TX outcomes,
`ReadTsf`/beacons, A-MPDU, CCX `tx.report` per-frame TX outcomes (the
TX-report engine — CCX **CLM** busy airtime IS ported, see below),
`FastSetBandwidth`, the flat-index / per-rate-diff TX-power knobs
(`SetTxPowerIndexOverride`, `SetTxPowerRateDiffs`, `ReApplyTxPower` — only the
relative `SetTxPowerOffsetQdb` is ported), `rx.path` per-chain telemetry,
Expand Down Expand Up @@ -369,7 +370,10 @@ The retry-12 shortfall from the ideal 13 may be passive-monitor loss or
genuinely fewer airings, so the ratio is reported as an observation, not an
exact hardware count.

**CCX / `tx.report` is NOT ported, and its root cause is unresolved.** The
**CCX `tx.report` is NOT ported, and its root cause is unresolved.** This is
the per-frame TX-outcome engine, a different thing from the CCX **CLM**
busy-airtime measurement documented under "Frame-free sensing" below, which
is ported and measured. The
descriptor and receive-side investigation narrows the problem but does not
prove a firmware defect: SPE_RPT is dword2[19] and SW_DEFINE dword6[11:0] via
the generic halmac
Expand Down Expand Up @@ -421,3 +425,72 @@ Headless coverage: `tests/rtl8733b_{efuse,phy_table,rx_parse,tx_desc}_selftest.c
in `ctest`. Hardware: `tests/rtl8733b_lifecycle_soak.sh` (bounded warm
lifecycle, explicitly not a true VBUS cycle) and `examples/rtl8733bprobe`
(staged identity → power/EFUSE → firmware → MAC/PHY → TSSI audit).

## Frame-free sensing: CLM busy airtime, and nothing else

`IRadio::ArmChannelBusy()` / `GetChannelBusy()` work on this die. The vendor
phydm puts the 8733B on JGR3 — at the pinned `reference/rtl8733bu-20230626`,
`hal/phydm/phydm_pre_define.h:513` lists `ODM_RTL8733B` in
`PHYDM_IC_SUPPORT_IFS_CLM`, and `:523-525` define
`PHYDM_IC_JGR3_SERIES_SUPPORT` when `RTL8733B_SUPPORT` is set. The engine
answers on that register map through
the shared `devourer::ClmWindow` — `with_ccx()` in `Rtl8733bDevice.h` lends it
`nhm_regs_jgr3()` under `_reg_mu` then the CCX mutex, exactly as Jaguar2/3 do.

The flag relationship and the armed-only behaviour are the contract, and it
lives at `src/AdapterCaps.h` on `busy_airtime_ok` / `rx_energy_ok` — the one
place it can be kept true. What is specific to this die:

- **Why CLM ports without a phydm block at all.** `arm_clm_only()` /
`read_clm_only()` take no IGI argument: busy airtime is a hardware tick
count, not a histogram referenced to the receiver's own noise floor the way
NHM's thresholds are. That is what makes it separable from FA/CCA here.
- There is no NHM read on this die, so the `Interrupted` spoiler is
unreachable by construction: nothing can re-arm the shared engine
mid-window. `Retuned` and `NotElapsed` both fire normally.

Measured with `tests/busy_window_probe.sh` (RTL8733BU sensor, MT7612U flooder,
ch165): **0.0% quiet, 69% under a steady load** with spread 0, and against a
50/450 ms burst (true duty ~9%) a mean of 6-10% over five windows with a
**19-point spread** — a 240 ms window inside a 500 ms burst period misses
whole bursts, so single windows read 0-19% and only the mean is a measurement. A Jaguar3 8812CU read
that same flooder at 69% as well, so the two device paths onto the JGR3 map
agree on one load. The retune and premature-read spoilers each refused with
their reason, and a re-armed window never returned the previous latched value.

**`src/sensing/` cannot reach this reading.** `SenseWindow` picks its source
by whether the `IRtlRadio*` is non-null rather than by `rx_energy_ok`, so on
this die it takes the phydm branch and never arms or reads CLM —
`examples/chanscout` on an RTL8733B reports neither. That is a sensing-layer
bug this die is merely the first to expose; it is described, with what a fix
needs, in `src/sensing/CLAUDE.md`. Until it lands, a caller on this die
reaches `IRadio::ArmChannelBusy`/`GetChannelBusy` directly.

**The CCA gate cannot bias the reading here.** CLM counts CCA-busy, so a
session with CCA disabled would under-report — but `SetCcaMode` throws "CCA
disable is not implemented by this backend" on this die, so that state is
unreachable rather than merely unlikely.

**`Stop()` forgets the window, and that is not obvious.** A stop is not a
retune, so nothing would spoil an armed window — but clearing `_phy_ready`
does not protect it either, because `SetMonitorChannel` and `FastRetune` both
call `bring_up_to_phy()`, which sets that flag true again. Without a reset in
`Stop()`, a window armed before a stop comes back to life on the revived chip
and the retune note hands the caller a `Retuned` spoil earned by a hardware
session that no longer exists: invalid either way, but the reason would be a
lie. The reset is scoped under the `_reg_mu` `Stop()` already holds.

Jaguar1/2/3 share this hole and have it worse — none of their `Stop()`
implementations resets the window either, and unlike this backend none of them
clears the flag their `with_ccx` gates on, so a `GetChannelBusy()` straight
after `Stop()` reads CCX registers on a deinitialised chip with no retune
needed. Not fixed here; noted so the asymmetry is not mistaken for an 8733B
quirk.

Retune notes live in `SetMonitorChannel` and `FastRetune`, both **scoped**:
`FastRetune` calls `SetMonitorChannel` on its declined path while already
holding `_reg_mu` (recursive, so that part is fine), and the CCX mutex is NOT
recursive — holding it across the tune would self-deadlock on that path. One
note per site suffices because `_reg_mu` is held across the whole tune and
`with_ccx` takes `_reg_mu` first, so a concurrent arm cannot interleave; that
is the Jaguar2/3 situation, not Jaguar1's.
Loading
Loading