Skip to content

IRtlRadio: address the two carrier-sense gates separately - #427

Merged
josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:feat/cca-gates
Sep 13, 2026
Merged

josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:feat/cca-gates

Conversation

@snokvist

Copy link
Copy Markdown
Collaborator

SetCcaMode is all-or-nothing, and on these families it is two gates doing different jobs: 0x520[14] primary CCA defers to a decodable preamble, 0x520[15] EDCCA defers to raw in-band energy. This adds SetCcaGates / GetCcaGates so a caller can address them one at a time, with not-ported defaults, and reduces SetCcaMode to SetCcaGates(d, d).

Why split them

They do not behave the same way — and on Jaguar1 they behave oppositely to what tests/dis_cca_tx_onair.sh found.

That test is Jaguar3. It uses the 8812AU only as its flooder and has never run a Jaguar1 as the DUT. Running one — RTL8812AU injecting 300 broadcast frames at 6M on an otherwise idle ch6, fresh radio open per arm so nothing latches across points, two independent receivers, repeated:

Gate state delivered
both gates on (the default) 0.0%, 1.7%
EDCCA off only 94.3%, 94.7%
primary CCA off only 13.7%, 2.7%
both off 94.3%, 95.3%

On Jaguar1 the energy bit is everything and the preamble bit is nearly null. Without the split there is no way to discover that, and no way to act on it except dis_cca, which turns off both.

Acting on it beats dis_cca

Same DUT, against a saturating co-channel flooder:

Arm flooder delivered
EDCCA off, primary CCA ON no 95.3%
EDCCA off, primary CCA ON yes 78.0%
both gates off yes 0.3%

Carrier sense still defers (95% → 78% when a real transmitter takes the channel), and turning both gates off is worse for the injector's own delivery — it transmits into the flood and collides.

No behaviour change for existing callers

SetCcaMode's two states write exactly the bytes they wrote before:

  • Jaguar1apply_cca(d, d) is the old body, with the BB threshold half at 0x8a4 keyed on the EDCCA argument.
  • Jaguar30x524[11] moves with the pair and only with the pair. What that bit does on its own is undocumented and unmeasured, so a mixed state leaves it at the enabled value rather than guessing. Jaguar3 also tracks the two gates separately so SetMonitorChannel re-asserts what the caller asked for, not just the all-or-nothing pair.

Verification

Hardware: RTL8812AU (Jaguar1) and RTL8822C (Jaguar3, an 8812CU). All four gate states read back from 0x520 on both, and the Jaguar3 state survives a retune.

Defaults checked against a build without this change, alternating the two builds on the same bench so ambient drift shows as baseline-vs-baseline disagreement:

RX, ambient frames in 5 s on ch6      baseline        patched
  RTL8812AU                             0 / 0          0 / 0
  RTL8822C                             99 / 58        59 / 86
  MT7612U                             172 / 138      142 / 139     zero bridge drops throughout

TX, default carrier sense, 300 frames @ 6M, best independent witness
  RTL8812AU                          3.7% / 3.0%    5.0% / 7.7%
  RTL8822C                         100.0% / 100.0%  100.0% / 100.0%
  MT7612U                          100.0% / 99.7%   100.0% / 100.0%

Every difference is inside the baseline-to-baseline spread. cmake --build clean and ctest 63/63 on this branch.

Not verified here: Jaguar2, Kestrel and RTL8733B have no hardware on this bench and stay on the not-ported default, as does MediaTek, which is not an IRtlRadio at all.

A note on the CLAUDE.md claim

The current text reads as general:

The primary-CCA bit is the one that matters … the energy bit [15] alone is null against a decodable preamble.

It is accurate for Jaguar3 and inverted on Jaguar1. Happy to add the scoping and the Jaguar1 numbers in this PR or a follow-up, whichever you prefer.

Separately, this bench found that Jaguar1's bring-up enables EDCCA — programming 0x8a4 off its parked never-trigger default, which the code comment itself identifies as the vendor's adaptivity-off default (CONFIG_RTW_ADAPTIVITY_EN 0) — and hard-codes th_l2h_ini and the H2L gap, both of which the vendor driver exposes as runtime module parameters. Measured cost of that default on an 8812AU injector is the 94% above. That is a policy question rather than a bug, so it is not in this PR; glad to raise it as an issue if useful.

SetCcaMode is all-or-nothing, and on these families it is two gates doing
different jobs: 0x520[14] primary CCA defers to a decodable preamble,
0x520[15] EDCCA defers to raw in-band energy. This adds SetCcaGates /
GetCcaGates so a caller can address them one at a time, with not-ported
defaults and SetCcaMode reduced to SetCcaGates(d, d).

Why it is worth splitting: they do not behave the same way, and on Jaguar1
they behave OPPOSITELY to what tests/dis_cca_tx_onair.sh found.

That test is Jaguar3 — it uses the 8812AU only as its flooder and has never
run a Jaguar1 as the DUT. Running one (RTL8812AU injecting 300 broadcast
frames at 6M on an otherwise idle ch6, fresh radio open per arm so nothing
latches, two independent receivers, repeated):

  both gates on (the default)    0.0%   1.7%
  EDCCA off only                94.3%  94.7%
  primary CCA off only          13.7%   2.7%
  both off                      94.3%  95.3%

On Jaguar1 the energy bit is everything and the preamble bit is nearly null.
Without the split there is no way to find that out, and no way to act on it
except dis_cca, which turns off both.

Acting on it matters, because leaving primary CCA on is strictly better than
dis_cca. Same DUT against a saturating co-channel flooder:

  EDCCA off, primary CCA ON, no flooder   95.3%
  EDCCA off, primary CCA ON, flooding     78.0%
  both gates off,            flooding      0.3%

Carrier sense still defers, and both-off is worse for the injector's own
delivery because it transmits into the flood and collides.

Behaviour is unchanged for every existing caller. SetCcaMode's two states
write exactly the bytes they wrote before: on Jaguar1 apply_cca(d, d) is the
old body with the BB threshold half keyed on the EDCCA argument, and on
Jaguar3 0x524[11] moves with the pair and only with the pair — what that bit
does on its own is undocumented and unmeasured, so a mixed state leaves it
at the enabled value rather than guessing. Jaguar3 tracks the two gates so
SetMonitorChannel re-asserts what the caller asked for rather than only the
all-or-nothing pair.

Verified on hardware: RTL8812AU (Jaguar1) and RTL8822C (Jaguar3, an 8812CU).
All four states read back from 0x520 on both, and the Jaguar3 state survives
a retune. Defaults checked against a build without this change, alternating
builds on the same bench: ambient RX rates, bridge drop counts and
default-path TX delivery all inside the baseline-to-baseline spread, with
the Jaguar3 injector at 100% on both.

Not verified here: Jaguar2, Kestrel and RTL8733B have no hardware on this
bench and are left on the not-ported default, as is MediaTek, which is not
an IRtlRadio at all.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Control primary CCA and EDCCA gates independently

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds independent primary CCA and EDCCA controls for supported Realtek families.
• Preserves legacy SetCcaMode behavior by changing both gates together.
• Reapplies Jaguar3 mixed gate states after channel retunes.
Diagram

graph TD
  Caller["Radio caller"] --> API["IRtlRadio gates"] --> J1["Jaguar1 device"] --> MAC[("MAC gates")]
  API --> J3["Jaguar3 device"] --> MAC
  J1 --> BB[("EDCCA thresholds")]
  J3 --> Sticky["Retune state"] --> MAC
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a named CCA gate state structure
  • ➕ Avoids ambiguous adjacent boolean arguments
  • ➕ Provides a natural return value for readback
  • ➕ Can accommodate future gate properties without adding parameters
  • ➖ Adds a new public type for two hardware bits
  • ➖ Requires slightly more call-site ceremony
  • ➖ Provides limited immediate benefit with only two stable fields
2. Expose the controls through IRadio
  • ➕ Makes gate control available without a Realtek-specific cast
  • ➕ Creates one uniform radio-facing API
  • ➖ Forces unsupported semantics onto non-Realtek backends
  • ➖ Leaks chipset-specific gate behavior into the vendor-neutral contract
  • ➖ Expands implementation obligations across unrelated radios

Recommendation: Keep the feature scoped to IRtlRadio and preserve SetCcaMode as the portable all-or-nothing control. The current paired API is appropriately small, though a named state structure would be preferable if additional callers or gate properties are expected because the two disabled-polarity booleans are easy to transpose.

Files changed (5) +114 / -17

Enhancement (5) +114 / -17
IRtlRadio.hExpose independent Realtek CCA gate controls +29/-0

Expose independent Realtek CCA gate controls

• Adds virtual SetCcaGates and GetCcaGates methods for controlling primary CCA and EDCCA separately. Unsupported implementations return false, leaving SetCcaMode as the portable fallback.

src/IRtlRadio.h

RtlJaguarDevice.cppImplement separate Jaguar1 CCA and EDCCA programming +27/-8

Implement separate Jaguar1 CCA and EDCCA programming

• Reads and writes the two 0x520 gate bits independently through a shared helper. EDCCA alone controls the 0x8a4 thresholds and watchdog tracking, while SetCcaMode retains its prior paired behavior.

src/jaguar1/RtlJaguarDevice.cpp

RtlJaguarDevice.hDeclare Jaguar1 gate APIs and shared helper +8/-0

Declare Jaguar1 gate APIs and shared helper

• Declares the independent gate overrides and private apply_cca helper used by both the split and legacy controls.

src/jaguar1/RtlJaguarDevice.h

RtlJaguar3Device.cppPersist independent Jaguar3 gate states across retunes +41/-9

Persist independent Jaguar3 gate states across retunes

• Adds synchronized gate readback and independent writes to 0x520 while conservatively handling 0x524[11]. Tracks each requested gate separately so mixed states are restored after channel changes without altering legacy SetCcaMode writes.

src/jaguar3/RtlJaguar3Device.cpp

RtlJaguar3Device.hDeclare Jaguar3 gate controls and sticky state +9/-0

Declare Jaguar3 gate controls and sticky state

• Adds independent gate overrides, per-gate state fields, and a locked register helper for retune-safe application.

src/jaguar3/RtlJaguar3Device.h

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

qodo-free-for-open-source-projects Bot commented Sep 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Startup ignores queued gate changes ✓ Resolved 📘 Rule violation ≡ Correctness
Description
SetCcaGates caches both requested gate states and returns true, but only calls
apply_cca_gates_locked when _brought_up is already true, while neither Init nor InitWrite
replays the cached values. When callers configure the public radio before RX or TX initialization,
the hardware retains its initialization defaults until another setter or a channel retune applies
the state.
Code

src/jaguar3/RtlJaguar3Device.cpp[R1221-1222]

+  if (_brought_up)
+    apply_cca_gates_locked(primary_disabled, edcca_disabled);
Evidence
The setter explicitly records pre-bring-up requests but conditionally skips the register write,
despite compliance rule 8 prohibiting successful feature support when the required operation is
silently omitted. Both Jaguar3 bring-up paths set _brought_up without replaying the cached
split-gate state; the factory and existing examples show that configuring the public radio between
construction and initialization is legitimate, and the only later replay in SetMonitorChannel does
not correct the initial hardware state.

CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior: CLAUDE.md: Unsupported Backend Features Must Use Explicit Not-Ported Behavior
src/jaguar3/RtlJaguar3Device.cpp[1214-1225]
src/jaguar3/RtlJaguar3Device.cpp[57-102]
src/jaguar3/RtlJaguar3Device.cpp[790-887]
src/jaguar3/RtlJaguar3Device.cpp[94-102]
src/jaguar3/RtlJaguar3Device.cpp[796-800]
src/jaguar3/RtlJaguar3Device.cpp[884-887]
src/jaguar3/RtlJaguar3Device.cpp[1257-1260]
src/WiFiDriver.cpp[294-319]
examples/tx/main.cpp[856-882]

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

## Issue description
`SetCcaGates` reports success and caches primary and EDCCA gate requests made before Jaguar3 bring-up, but `Init` and `InitWrite` never apply those recorded states, leaving the hardware at its defaults.
## Fix Focus Areas
- src/jaguar3/RtlJaguar3Device.cpp[94-102]
- src/jaguar3/RtlJaguar3Device.cpp[796-800]
- src/jaguar3/RtlJaguar3Device.cpp[884-887]
- src/jaguar3/RtlJaguar3Device.cpp[1214-1225]
## Recommended Fix
After each Jaguar3 bring-up sequence reaches a stable hardware state, replay the cached primary and EDCCA gate states, particularly when either gate was requested disabled. Perform the replay after register-initialization steps that could overwrite the gate registers and preserve the existing configuration-level `disable_cca` precedence; if queued pre-bring-up operation is intentionally unsupported instead, return `false` rather than reporting success.

ⓘ 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 start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Reading 0x520 on a chip that was never brought up returns whatever the bus
gives back, and reporting that as the gate state is a fabricated
measurement; writing it pokes an uninitialised MAC. Jaguar3's SetCcaGates
already guards on _brought_up — this is the Jaguar1 equivalent, on both the
read and the write side.

Found by review, then confirmed against an RTL8812AU: opened but not tuned,
the op now refuses instead of answering.

@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.

Verified locally on the head: builds clean, ctest 63/63. The API shape is right — not-ported defaults, SetCcaMode reduced to SetCcaGates(d, d), and the Jaguar1 byte-for-byte claim holds (apply_cca(d, d) is the old body with the BB 0x8a4 half keyed on the EDCCA argument). The Jaguar1 measurement is the valuable part of this PR and the inversion is worth acting on.

Three blocking items, all on Jaguar3, all invisible to a Jaguar1-only bench:

  1. 0x524[11] is not undocumented — it is BIT_EDCCA_MSK_CNTDOWN_EN and it is EDCCA-scoped, so it should follow edcca_disabled rather than the pair.
  2. phydm's edcca_track argument is still keyed on the all-or-nothing _cca_disabled, so the watchdog keeps tracking EDCCA in a mixed state.

Together those two mean the arm this PR recommends — EDCCA off, primary CCA on — is only half applied on Jaguar3. The on-air evidence here is all Jaguar1, where the path is correct, so the bench could not have caught it.

  1. The pre-bring-up contract diverges between the two backends, and GetCcaGates on Jaguar3 has no _brought_up guard — the exact fabricated-gate-state hazard the Jaguar1 follow-up commit fixed.

Non-blocking: stickiness across SetMonitorChannel differs between the two backends and is undocumented (and Jaguar1 bring-up unconditionally re-runs SetCcaMode(_cfg.tuning.disable_cca), clobbering anything set before Init); there is no in-tree caller, env knob or regcheck script, so nothing in the repo reproduces the tables in the description — the tests/txpwr_*_regcheck.sh shape would cover the four gate states plus retune survival.

On the CLAUDE.md question: please scope it in this PR rather than a follow-up. The current text states the Jaguar3 result as general and it is now measured inverted on Jaguar1, and the new API is not mentioned in Configuration at all. Pair the Jaguar1 table with the flooder arm (95% → 78%, both-off 0.3%) in the same section — turning both gates off being worse for the injector is the half that keeps the first half from being read as "disable everything".

On Jaguar1's bring-up enabling EDCCA off its parked default with a hard-coded th_l2h_ini: agreed it is a policy question and out of scope here, but the 94% makes it a real one — worth an issue.

Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.cpp
Comment thread src/IRtlRadio.h Outdated
Comment thread src/jaguar1/RtlJaguarDevice.cpp Outdated
Two Jaguar3 defects from review, both invisible on a Jaguar1 bench because
every on-air number in the original description came from a Jaguar1 DUT.

0x524[11] is BIT_EDCCA_MSK_CNTDOWN_EN (REG_RD_CTRL), the same name and bit on
8822B, 8822C and 8822E in the vendor HALMAC headers, so the meaning is
family-stable. Being EDCCA-scoped it follows edcca_disabled alone. Moving it
with the pair — on the reasoning that an unmeasured bit should not be guessed
at — was the wrong kind of caution, and it left the EDCCA-off arm this work
recommends with EDCCA still masking the backoff countdown. Measured on an
8812CU, exactly one of the six states changes, and SetCcaMode's two states
write what they always wrote.

phydm's edcca_track was keyed on the all-or-nothing flag, so with EDCCA off
and primary CCA on the ~2 s tick went on running PhydmRuntimeJaguar3::edcca()
and rewriting the BB thresholds at 0x84c. It now follows the EDCCA gate, the
way Jaguar1 already did via SetEdccaTrack at the end of apply_cca. Sampling
0x84c cannot detect this — the tracker recomputes the same th_l2h from a
static IGI, so an active tracker writes identical bytes — so it is measured by
poking the register with a value it would never choose and seeing whether it
is restored.

Jaguar3's GetCcaGates and SetCcaGates now refuse before bring-up, caching
nothing, as Jaguar1 already did. Reading 0x520 on an unconfigured MAC returned
a plausible-looking state that depended on whatever the previous session left
behind, and the setter reported success for a write that nothing replayed.

_cca_disabled is removed: once both readers moved to the per-gate flags it was
write-only, and a write-only field invites a future reader to assume it means
something.

The contract moves to the declaration in IRtlRadio.h, since FastRetune and
SetMonitorChannel are the normal operating pattern for this lever: both calls
are post-bring-up only, `false` means either "not ported" or "not brought up",
a refusal leaves GetCcaGates' out-parameters alone, and the state survives a
retune on both families — but by different mechanisms, so the Jaguar1 case is
documented as incidental rather than guaranteed. That last point corrects the
review: measured on an 8812AU, the gates and the BB thresholds are intact
across a same-band retune, a band change, and FastRetune.

Jaguar1 readability: the re-aliased `disabled` local is gone and the four uses
name the gate, and the braceless same-line if/else pairs are braced.

tests/cca_gates_probe.cpp is the in-tree caller the split lacked, and
tests/cca_gates_regcheck.sh the register-level check, in the shape of the
txpwr regchecks: the four gate states, the pre-bring-up refusal, both legacy
SetCcaMode states, the 0x524[11] scoping, EDCCA-tracking shutdown, and retune
survival by both channel paths — cross-checking the API's readback against a
raw chipstate peek so an API that lies about the silicon fails rather than
passes. 23 cells across an RTL8812AU, an RTL8822C and an RTL8733BU; reverting
either fix above makes the matching cell fail.

CLAUDE.md no longer states the Jaguar3 result as general. Both families are
given as disagreeing measurements of different things, the flooder arm is
paired in so the Jaguar1 figure cannot be read as "disable everything", and
SetCcaGates is named in Configuration. The three roundings of one run are
replaced by one citation of it.

Verified on RTL8812AU (Jaguar1), RTL8822C (Jaguar3) and RTL8733BU (an
IRtlRadio that ports neither, confirming the not-ported defaults refuse on
real silicon). The 8822EU/8812EU arms of the original tables are unchanged and
not re-run.
@snokvist

Copy link
Copy Markdown
Collaborator Author

Thanks — both blocking Jaguar3 items were real, and you were right that the
bench here could not have caught them: every on-air number in the original
description came from a Jaguar1 DUT. An 8812CU is now attached, so all three
are fixed and measured rather than argued.

1. 0x524[11] is BIT_EDCCA_MSK_CNTDOWN_EN, and moving it with the pair was
wrong.
Confirmed against the vendor headers independently of your citation —
BIT_EDCCA_MSK_CNTDOWN_EN_8822C BIT(11) at halmac_bit_8822c.h:13368, the same
name at 8822b:11458 and 8822e:13531, and REG_RD_CTRL_8822C 0x0524. It now
follows edcca_disabled alone. Measured on the 8812CU, reading the chip in
every state:

state 0x520[14] 0x520[15] 0x524[11] before after
primary on, EDCCA on 0 0 1 1
primary on, EDCCA off 0 1 1 0
primary off, EDCCA on 1 0 1 1
primary off, EDCCA off 1 1 0 0
SetCcaMode(true) 1 1 0 0
SetCcaMode(false) 0 0 1 1

Exactly one row moves and it is the recommended arm, so the byte-identical
property for SetCcaMode survives — as you predicted.

My original comment called the bit undocumented. That was the wrong kind of
caution: I treated "I have not measured it" as a reason to guess a conservative
coupling rather than as a reason to go and look. It was named in the tree
already — RtlJaguar2Device.cpp:1932 calls it EDCCA_MSK_COUNTDOWN.

2. edcca_track now follows the EDCCA gate. Both tick sites pass
!_cca_edcca_disabled, and the stale comment at the housekeeping site is
rewritten. Worth recording how this has to be measured: sampling 0x84c cannot
see the bug, because PhydmRuntimeJaguar3::edcca() recomputes the same
th_l2h from a static IGI — an active tracker writes identical bytes and looks
exactly like an idle one. Poking the register with a value the tracker would
never choose and watching whether it is restored does discriminate:

arm before after
both gates on (default) restored — tracking restored — tracking
EDCCA off, primary CCA on restored — tracking survived — stopped
both gates off survived — stopped survived — stopped

3. Pre-bring-up contract aligned, not documented-as-divergent. Jaguar3's
GetCcaGates and SetCcaGates now refuse before bring-up exactly as Jaguar1
does, caching nothing. Verified on both: the call returns false and
GetCcaGates leaves the caller's out-parameters untouched. This also settles
qodo's finding on the same function — it reported success for a write that
never happened, which is the same defect seen from the other side.

Worth noting what the unguarded read actually did, since it is less dramatic
than "returns garbage" and more awkward: on this 8812CU it returned a
plausible state, and which state depended on what the previous session had
left on the chip rather than on anything the caller had set. Reading a
freshly-reset part gave bits 14 and 15 clear — indistinguishable from a
correctly-brought-up default. A fabricated reading that looks right is the
version of this that survives review, which is the argument for refusing
rather than for sanitising.


Non-blocking items:

Stickiness — your assertion needs a correction, and it is in my favour, so
please check me.
I wrote "Jaguar1 records nothing so a retune drops it" into
the contract on your word, then measured it and it is not what the hardware
does. With EDCCA off and primary CCA on, an 8812AU keeps 0x520[15] set and
its BB thresholds parked at 7f/7f across a same-band retune and across a
5 GHz→2.4 GHz band change; an 8822C does the same. The mechanisms differ
exactly as you said — Jaguar3 re-asserts in SetMonitorChannel, Jaguar1 has no
re-assert at all — but Jaguar1 survives because nothing in its channel path
rewrites those registers. IRtlRadio.h now states the measured outcome and
warns that the Jaguar1 case is incidental rather than guaranteed. Bring-up is
a reset there, via the SetCcaMode(_cfg.tuning.disable_cca) calls you cited.

In-tree caller and regcheck. tests/cca_gates_probe.cpp (built as
CcaGatesProbe) is the caller; tests/cca_gates_regcheck.sh is the
register-level check in the shape you pointed at. It covers the four gate
states, the pre-bring-up refusal, the two legacy SetCcaMode states, the
0x524[11] scoping and retune survival, cross-checking the API's readback
against a raw chipstate --no-claim peek so an API that lies about the silicon
fails rather than passes. Currently 23 passed / 0 failed / 1 skipped across the
8812AU, 8812CU and an RTL8733BU; reverting either of your two fixes makes it
fail on the corresponding cell, which is how I checked the cells can fail at
all rather than assuming it. One limit worth stating: the cntdown cell
discovers whether a backend drives 0x524[11] by comparing the two
SetCcaMode states against the split, so it catches the two paths
disagreeing, but a backend that stopped writing the bit altogether would be
reported rather than failed — deciding it should move would need a per-chip
expectation table. The track cell covers the functional half of that arm
independently.

I did not add an env knob — that is new public configuration
surface and this PR is already under review; say the word if you would rather
have DEVOURER_DIS_PRIMARY_CCA / DEVOURER_DIS_EDCCA alongside the existing
DEVOURER_DIS_CCA and I will add it.

CLAUDE.md is scoped into this PR. The Jaguar3 result is no longer stated
as general; both families' results are given as disagreeing measurements of
different things, the flooder arm is paired in (95% idle → 78% flooded with
primary CCA kept on, against 0.3% with both gates off), and SetCcaGates is
named in Configuration. On the three numbers: I removed the restated ones and
cite the runs once each, since ~40-60%/41-45%/~1.5-2.2x were three
roundings of one run.

IRtlRadio.h now names Jaguar1/Jaguar3 instead of "this family", and
states the two meanings of false plus the out-parameter rule. It also
documents that SetCcaMode does not share the contract — it returns void,
so a pre-bring-up call cannot report anything — and points at
tuning.disable_cca as the from-bring-up path.

Jaguar1 nits applied: the disabled alias is gone and the four uses read
edcca_disabled, and the braceless same-line if/else pairs are braced.

Also removed: _cca_disabled on Jaguar3, which this PR left write-only once
both readers moved to the per-gate flags.

The EDCCA-at-bring-up policy question — Jaguar1 enabling EDCCA off its
parked 0x7f7f default with a hard-coded th_l2h_ini, where the vendor ships
CONFIG_RTW_ADAPTIVITY_EN 0 and exposes the thresholds as module parameters —
I will raise separately as an issue rather than widen this PR.

Devices actually verified for the above: RTL8812AU (Jaguar1), RTL8822C
(Jaguar3) and RTL8733BU (as an unported IRtlRadio, confirming the
not-ported defaults refuse on real silicon). The 8822EU/8812EU arms of the
original tables are unchanged and not re-run — I do not have those parts.

Pushed as 68cb3f8. Build is clean and ctest 63/63 from a fresh configure of
the branch.

josephnef
josephnef previously approved these changes Sep 13, 2026

@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 three blocking items are fixed, and I checked the mechanism rather than the claim. 0x524[11] now follows edcca_disabled alone; both _phydm.tick sites pass !_cca_edcca_disabled; Jaguar3's SetCcaGates/GetCcaGates refuse before bring-up and the combined _cca_disabled is gone with no readers left behind. _brought_up = true precedes SetCcaMode(_cfg.tuning.disable_cca) on both families, so the config path still applies. Build clean, ctest 63/63.

I ran tests/cca_gates_regcheck.sh on my own bench, and it picked up an arm you said you could not cover:

0xa81a  RtlJaguar3Device, chip-id 0x17 — 8822E     12 passed
0xc812  RtlJaguar3Device, chip-id 0x13 — 8822C     12 passed
0xb812  Jaguar2 8822BU        not ported, refuses cleanly
0xb733  RTL8733BU             not ported, refuses cleanly
35bc:0101  Kestrel C8852C     not ported, refuses cleanly   (rc=5)

== 25 passed, 0 failed, 0 skipped ==

cntdown reads 00:1 01:0 10:1 11:0 on both Jaguar3 dies — so 0x524[11] follows EDCCA alone on the 8822E as well as the 8822C, and the two SetCcaMode states still bracket it. The track cell passes on both. I have no Jaguar1 plugged, so that family is still only your bench.

The correction on stickiness is a good catch and I was wrong: I asserted a mechanism where you measured an outcome. The header's wording — survives on both, means it on only one, don't build on the Jaguar1 case — is the right shape.

No blocking items. Seven things worth a pass before merge, none of which need another review round.

1. Jaguar1 apply_cca clears the tracker last, so a mid-session EDCCA-disable can race the watchdog. src/jaguar1/RtlJaguarDevice.cpp parks 0x8a4 at 0x7f7f and then calls SetEdccaTrack(false). PhydmWatchdog is started in rtw_hal_init, i.e. before bring-up's SetCcaMode, and its tick rewrites both 0x8a4 bytes from the IGI-derived L2H while _edcca_track is still set. A tick landing in that window leaves live thresholds behind the disable the caller just asked for — the same defect as round-one finding #2, reached through a window instead of a wrong argument. It is inert at bring-up and gated on DEVOURER_PHYDM_WATCHDOG=1, but SetCcaGates is the first API that makes this a runtime call. Hoisting SetEdccaTrack(false) above the park (and leaving SetEdccaTrack(true) after the enable programming) closes it.

Related: Jaguar1's SetCcaGates/GetCcaGates take no lock while Jaguar3's take _reg_mu, and the 0x520/0x8a4 read-modify-writes share the bus with that same watchdog thread.

2. The track cell hard-codes 0x84c, which is Jaguar3's register — Jaguar1's is 0x8a4. On a Jaguar1 DUT the cell pokes an unrelated BB register, sees no restore, and prints no EDCCA tracker running in the default arm, which is false: PhydmWatchdog::SetEdccaTrack drives 0x8a4. The EDCCA-off arm then reports SKIP: no tracker to stop in this configuration. I suspect that is the 1 skipped in your 23/0/1, which would mean the functional half of fix #2 is unverified on the family whose measurement motivated the PR. Keying the address off the family turns that skip into a real cell — and a harness that quietly applies the Jaguar3 register to Jaguar1 is the exact generalisation this PR is about not making.

3. tests/cca_gates_regcheck.sh:221elif true; then makes the else at 231-233 unreachable, and that dead note duplicates line 217 verbatim. Plain else.

4. The cntdown cell disappears silently when fewer than four arms report. The [ ... -eq 4 ] at line 204 has no else, so a single failed arm drops the whole cell with no PASS/FAIL/SKIP and the counters simply do not move. A skip in the else keeps the ledger honest about what was not measured.

5. stop_hold cannot stop the probe. start_hold backgrounds sudo -n "$BUILD/CcaGatesProbe", so $! is a root-owned process; run as documented (sudo -v && tests/cca_gates_regcheck.sh) the plain kill "$probe_pid" gets EPERM and is swallowed by 2>/dev/null, and wait then blocks for the probe's entire hold walk. That is why a run is minutes per adapter — the two Jaguar3 cells took ~25 minutes here. sudo -n kill fixes it. The EXIT trap is fine, its pkill is already under sudo -n.

6. The retune cells can only see half the gate state. GetCcaGates reads 0x520 alone, so retune/fast-retune cannot detect loss of 0x524[11] or of the BB thresholds. IRtlRadio.h claims survival "at 0x520, 0x524 and Jaguar1's BB thresholds"; peek32 is already there, so one extra read after the retune would put the 0x524 half of that claim in the harness instead of in the prose.

7. Two smaller ones. src/jaguar2/RtlJaguar2Device.cpp:1932 still says "The primary-CCA bit is the one that stops TX deferring to a co-channel transmitter" on a family with no measurement of its own — the last unscoped copy of the claim you just scoped in CLAUDE.md, one file over. And in CLAUDE.md the new text now runs straight into On by default on the streamtx FPV downlink, which lands immediately after the SetCcaGates sentence and reads as though it describes the new API rather than DEVOURER_DIS_CCA; re-anchoring it on the env var restores the referent.

On the env knob: I would leave it out, as you did. DEVOURER_DIS_CCA covers the shipped default and the split has an in-tree caller now; adding two more vars before anything in the tree wants them is surface for its own sake.

Scoping the Jaguar3 dis_cca figure as the DUT's host-side submitted rate, next to the Jaguar1 numbers that are decoded delivery from two witnesses, is the part of this description I would keep verbatim — the two results only stop looking contradictory once the metrics are named.

Approving. The list above is all follow-up-sized; #1 and #2 are the two I would do before merge, since both are the same bug class the first round found.

…to fail

Review follow-ups on the gate split. The first is a real defect of the same
class the split already fixed once; the rest are the harness not being able
to catch it.

Jaguar1 apply_cca cleared the EDCCA tracker AFTER parking 0x8a4. The phydm
watchdog owns that register while tracking and runs on its own thread from
rtw_hal_init, so a tick landing between the park write and the flag store
re-derived L2H from IGI and overwrote the park — live thresholds behind a
disable the caller had asked for. Harmless while the gates could only be set
at bring-up; SetCcaGates is what makes it a mid-session call.

Reordering alone only narrows the window, so SetEdccaTrack is now
synchronous: the tick's EDCCA block and the flag store share a mutex, and
turning tracking off also drops the write-on-change cache, since the register
no longer holds the value that cache names. apply_cca stops the tracker
before it writes and hands the register back only after programming it.

The harness could not have found that, because its track cell poked 0x84c on
every family. That is Jaguar3's threshold register; Jaguar1's is 0x8a4. On a
Jaguar1 DUT the cell poked something unrelated, saw no restore, and reported
"no EDCCA tracker running" — a false negative on the family whose measurement
motivated the split, and the one skipped cell in the original run. The probe
now reports its generation from AdapterCaps and the cell picks the register
from that, skipping a generation it has no address for rather than guessing.

Also in the harness:

  - The retune cells only ever saw 0x520, because that is all GetCcaGates
    reads. They now peek 0x524[11] while the state is held. Negative control
    on an 8812CU with the countdown write deliberately broken: the API still
    reported ret=1 primary=1 edcca=0 while 0x524[11] read 0, so this is the
    half of the claim the API cannot speak for.
  - stop_hold could not stop anything: the probe is started through sudo, so
    a plain kill from an unprivileged shell got EPERM silently and wait then
    blocked for the probe's whole hold walk. Stops go through sudo now.
  - `elif true` made the cntdown cell's else unreachable, and the cell
    vanished with no verdict at all when fewer than four arms reported.

Two scoping fixes. RtlJaguar2Device.cpp still asserted that primary CCA is
the bit that stops an injector, on a family with no measurement of its own —
the last unscoped copy of the claim this work scoped everywhere else. And in
CLAUDE.md "on by default on the streamtx FPV downlink" had come to sit after
the SetCcaGates sentence, where it read as describing the new API instead of
DEVOURER_DIS_CCA.

Verified: build clean, ctest 63/63, jaguar1-only config builds.
tests/cca_gates_regcheck.sh 26 passed / 0 failed / 0 skipped over an 8822E
(0xa81a), an 8822C (0xc812), an 8822BU and an RTL8733BU. No Jaguar1 on this
bench, so that family's arm is still the one in the PR description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192ViRjaiFT9vTiiJpohZGT
@josephnef

Copy link
Copy Markdown
Collaborator

Pushed 2848c5a applying the review findings myself, so this can go in without another round.

The first one turned out to be a real defect, not a nit. Jaguar1 apply_cca cleared the EDCCA tracker after parking 0x8a4, and PhydmWatchdog owns that register while tracking — running on its own thread from rtw_hal_init, i.e. before bring-up's SetCcaMode. A tick landing between the park write and the flag store re-derived L2H from IGI and overwrote the park, leaving live thresholds behind a disable the caller had asked for. Same failure as the edcca_track one, reached through a window instead of a wrong argument, and inert until SetCcaGates made the gates settable mid-session.

Reordering alone only narrows the window, so SetEdccaTrack is now synchronous — the tick's EDCCA block and the flag store share a mutex, and turning tracking off drops the write-on-change cache too, since the register no longer holds the value _edcca_last_l2h names. apply_cca stops the tracker before it writes and hands the register back only after programming it.

Your harness could not have caught that, and the reason is worth stating: the track cell poked 0x84c on every family. That is Jaguar3's threshold register — Jaguar1's is 0x8a4. On a Jaguar1 DUT it poked something unrelated, saw no restore, and reported no EDCCA tracker running in the default arm. That is almost certainly your one skipped cell, and it means the functional half was unverified on the family whose measurement motivated the split. The probe now reports its generation from AdapterCaps and the cell takes the register from that, skipping a generation it has no address for rather than guessing.

The retune cells got the same treatment. They only ever saw 0x520, because that is all GetCcaGates reads, so they now peek 0x524[11] while the state is held. I negative-controlled it on the 8812CU with the countdown write deliberately broken:

GATES after-retune   ret=1 primary=1 edcca=0     <- API says intact
0x524[11] = 0                                    <- new cell fails

Which is the point: the API cannot speak for that half.

Rest as described — stop_hold now stops through sudo (a plain kill on a sudo-launched probe was getting EPERM silently, which is why a run took ~25 min an adapter here), the dead elif true branch is gone, and the cntdown cell reports a SKIP instead of vanishing without a verdict. Plus the two scoping fixes: RtlJaguar2Device.cpp was the last unscoped copy of the primary-CCA claim, and CLAUDE.md's "on by default on the streamtx FPV downlink" had drifted to where it read as describing SetCcaGates rather than DEVOURER_DIS_CCA.

Verified: build clean, ctest 63/63, jaguar1-only config builds, and the regcheck is 26 passed / 0 failed / 0 skipped over an 8822E, an 8822C, an 8822BU and an RTL8733BU. The skip is gone because the cell now names its register:

note: 0xc812 track: tracker IS running at 0x84c in the default arm (as expected)

Still no Jaguar1 on this bench, so that family's on-air table remains yours. Given the tracker fix lands on exactly that path, a re-run of the regcheck against your 8812AU would be worth doing — the track cell should now produce a real verdict there instead of a skip.

Merging.

@josephnef
josephnef merged commit 025164f into OpenIPC:master Sep 13, 2026
33 checks passed
josephnef added a commit that referenced this pull request Sep 13, 2026
The harness from #427 could not check Jaguar1 at all, for two independent reasons that both produced a clean-looking non-result rather than a failure.

The probe built a default `DeviceConfig`, so `tuning.phydm_watchdog` was false — and on Jaguar1 that watchdog IS the EDCCA tracker, so `SetEdccaTrack` was never reached and the cell reported "no tracker running" about a path that had never been instantiated. A `--phydm-watchdog` flag builds it; Jaguar3 ignores the field.

Separately, `peek32`/`poke32` addressed `chipstate` by `--pid` alone while the probe got `--vid`, so any adapter under a vendor VID rather than 0x0bda failed every register cell.

Also fixes a teardown race the flag made reachable: `rtw_hal_deinit()` cleared `REG_CR`, zeroed `REG_RCR` and ran the card-disable sequence with the watchdog thread still doing BB reads and writes. `Stop()` now runs first, before any MAC-disable or power-off write.

This retires the unvalidated caveat on the tracker-ordering fix from #427 — the race reproduces on an RTL8821AU once the window is widened and the disable lands while DIG is still walking IGI, and does not with the fix.

Hardware: RTL8821AU (Jaguar1) 11 passed / 0 failed / 0 skipped; RTL8822E, RTL8822C, RTL8822BU and RTL8733BU 26 passed / 0 failed / 0 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192ViRjaiFT9vTiiJpohZGT
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.

2 participants