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
31 changes: 25 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,31 @@ Behavioural traps the per-field docs can't carry:
Jaguar1/2/3; on Jaguar1 the enable is real work — its BB table parks the
EDCCA thresholds (`0x8a4`) at never-trigger, so bring-up programs the
vendor adaptivity operating point (IGI-coupled; the phydm watchdog
re-tracks it when running). The primary-CCA bit is the one that matters:
monitor injection is not CCA-free, it defers ~40–60% to a co-channel
802.11 transmitter, and clearing `[14]` recovers ~1.5–2.2× (on-air
8822EU/8812CU, `tests/dis_cca_tx_onair.sh`); the energy bit `[15]` alone
is null against a decodable preamble. **On by default on the streamtx FPV
downlink** (the link owns the channel — CSMA backoff only stutters it);
re-tracks it when running). **Which bit matters is family-specific and the
two measured families disagree — do not generalise either result.** On
Jaguar3, monitor injection defers to a co-channel 802.11 transmitter and
clearing `[14]` recovers it while the energy bit `[15]` alone is null
against a decodable preamble (on-air 8822EU/8812CU,
`tests/dis_cca_tx_onair.sh`, measuring the DUT's host-side `submitted`
rate). On Jaguar1 it inverts: with an 8812AU injecting on an idle channel
and two independent witnesses decoding, clearing `[15]` alone recovers
~95% while clearing `[14]` alone recovers little, because what stops this
family is the EDCCA its own bring-up turned on. The two are not in
conflict — they measure different things on different silicon — but
neither is the general answer.

Turning both gates off is WORSE than turning off the one that matters:
with EDCCA off and primary CCA left on, the same Jaguar1 injector delivers
95% on an idle channel and still 78% under a co-channel flooder; with both
gates off it collapses to 0.3%, because it stops waiting for a gap and
collides instead. `SetCcaGates` (`IRtlRadio`, Jaguar1 and Jaguar3) is the
one-bit-at-a-time form for exactly this; `SetCcaMode` remains the portable
all-or-nothing call and is `SetCcaGates(d, d)`. Both gate calls are
post-bring-up only and return false before it — see `src/IRtlRadio.h` for
the contract, and `tests/cca_gates_regcheck.sh` to reproduce the tables.

`DEVOURER_DIS_CCA` is **on by default on the streamtx FPV downlink** (the
link owns the channel — CSMA backoff only stutters it);
`DEVOURER_DIS_CCA=0` forces standard carrier-sense back. On Kestrel the
8852C runs the same enabled default (measured: full-rate TX, 2.4x flood
deferral); the 8852B TX bring-up still clears the gates and WARNS pending
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,15 @@ add_executable(chipstate
target_include_directories(chipstate PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(chipstate PUBLIC devourer PRIVATE PkgConfig::libusb)

# The in-tree caller for the carrier-sense gate split. Needs hardware, so it is
# a tool rather than an add_test — tests/cca_gates_regcheck.sh drives it and
# cross-checks the registers with chipstate.
add_executable(CcaGatesProbe
tests/cca_gates_probe.cpp
)
target_include_directories(CcaGatesProbe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(CcaGatesProbe PUBLIC devourer PRIVATE PkgConfig::libusb)

# Headless regression guard for the binary-stdin framing shared by the two
# stream demos above (examples/common/stream_stdin.h). No libusb, no radio — just the
# set_stdin_binary() + read_exact() path, so a text-mode regression (e.g. the
Expand Down
61 changes: 61 additions & 0 deletions src/IRtlRadio.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,67 @@ class IRtlRadio : public IRadio {
* tests/canary_diff.py. Reading a powered-down chip yields garbage or throws;
* interpreting that is the caller's job. No-op where unsupported (default). */
virtual void DumpChipState() {}
/* The MAC carrier-sense gate, one bit at a time.
*
* SetCcaMode is all-or-nothing, and on Jaguar1 and Jaguar3 it is two
* gates: 0x520[14] primary CCA (defers to a decodable preamble) and
* 0x520[15] EDCCA (defers to raw in-band energy). They answer different
* questions, and the two families measured so far DISAGREE about which one
* stops an injector — so a caller diagnosing a deferral has to tell them
* apart, and one that needs a single gate should not have to turn off
* both. CLAUDE.md summarises the on-air delivery figures and
* tests/dis_cca_tx_onair.sh is the harness behind them;
* tests/cca_gates_regcheck.sh is the register-level check that this
* contract holds, not a delivery measurement.
*
* `true` means DISABLED, matching SetCcaMode's argument sense and the
* register's own polarity (bit set = gate off). SetCcaMode is exactly
* SetCcaGates(d, d) and writes the same bytes it always did; it stays the
* portable call, and is all a backend without the split offers.
*
* CONTRACT, because both halves of this have bitten:
*
* - POST-BRING-UP ONLY. Both calls return false before Init/InitWrite:
* 0x520 is meaningless until the MAC is configured, so reading it would
* be a fabricated gate state and writing it would poke an uninitialised
* MAC. `false` therefore means EITHER "not ported on this backend" OR
* "not brought up yet"; a caller probing capability at construction
* cannot tell those apart and must re-ask after bring-up. On a refusal
* GetCcaGates leaves its out-parameters untouched.
*
* SetCcaMode is NOT the same, and the difference is pre-existing rather
* than something the split introduced: it returns void, so a
* pre-bring-up call cannot report anything, and what it does with one
* is per-backend. The way to ask for a gate state from bring-up is the
* tuning.disable_cca config knob, which Init applies once the MAC is
* up.
*
* - STICKINESS SURVIVES A RETUNE ON BOTH, BUT ONLY ONE OF THEM MEANS IT.
* Measured on an 8812AU and an 8822C, the gate state is intact after
* SetMonitorChannel AND after FastRetune, both within a band and across
* a 5 GHz/2.4 GHz change, at 0x520, 0x524 and Jaguar1's BB thresholds.
* The mechanisms are not equivalent: Jaguar3 records the pair and
* re-asserts it in SetMonitorChannel (its FastRetune fallback does not,
* and does not need to), while Jaguar1 records nothing and survives
* only because its channel path happens not to rewrite those registers.
* Do not build on the Jaguar1 case — re-read with GetCcaGates rather
* than assume. Bring-up IS a reset on Jaguar1: Init/InitWrite
* unconditionally re-run SetCcaMode(_cfg.tuning.disable_cca), so a
* re-Init puts the gates back to the configured default. */
virtual bool SetCcaGates(bool primary_disabled, bool edcca_disabled) {
(void)primary_disabled;
(void)edcca_disabled;
return false;
}

/* Current gate state, read back from the hardware rather than remembered.
* Same contract as SetCcaGates above: post-bring-up only, false where the
* split is unavailable, out-parameters untouched on a refusal. */
virtual bool GetCcaGates(bool &primary_disabled, bool &edcca_disabled) {
(void)primary_disabled;
(void)edcca_disabled;
return false;
}
};

#endif /* IRTL_RADIO_H */
38 changes: 27 additions & 11 deletions src/jaguar1/PhydmWatchdog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,37 @@ void PhydmWatchdog::TickOnce() {
* vendor recomputes the 0x8a4 L2H/H2L from IGI every adaptivity cycle —
* with DIG walking IGI above, a static threshold would drift off the
* operating point. Write-on-change only. */
if (_edcca_track.load(std::memory_order_relaxed)) {
const int8_t th_ini =
_eepromManager->version_id.ICType == CHIP_8814A ? -14 : -17;
const int8_t l2h = jaguar1_edcca_l2h(th_ini, _cur_ig_value);
if (static_cast<uint8_t>(l2h) != _edcca_last_l2h) {
_device.phy_set_bb_reg(0x8a4, 0xFF, static_cast<uint8_t>(l2h));
_device.phy_set_bb_reg(0x8a4, 0xFF00, static_cast<uint8_t>(l2h - 7));
_edcca_last_l2h = static_cast<uint8_t>(l2h);
_logger->info("PhydmWatchdog: EDCCA L2H/H2L re-tracked to {}/{} "
"(igi=0x{:02x})",
l2h, l2h - 7, _cur_ig_value);
{
/* Under _edcca_mu so a caller disabling the gate cannot have its park
* write raced by a tick that already passed the flag check. */
std::lock_guard<std::mutex> lk(_edcca_mu);
if (_edcca_track) {
const int8_t th_ini =
_eepromManager->version_id.ICType == CHIP_8814A ? -14 : -17;
const int8_t l2h = jaguar1_edcca_l2h(th_ini, _cur_ig_value);
if (static_cast<uint8_t>(l2h) != _edcca_last_l2h) {
_device.phy_set_bb_reg(0x8a4, 0xFF, static_cast<uint8_t>(l2h));
_device.phy_set_bb_reg(0x8a4, 0xFF00, static_cast<uint8_t>(l2h - 7));
_edcca_last_l2h = static_cast<uint8_t>(l2h);
_logger->info("PhydmWatchdog: EDCCA L2H/H2L re-tracked to {}/{} "
"(igi=0x{:02x})",
l2h, l2h - 7, _cur_ig_value);
}
}
}
}

void PhydmWatchdog::SetEdccaTrack(bool on) {
std::lock_guard<std::mutex> lk(_edcca_mu);
_edcca_track = on;
/* Turning tracking off hands 0x8a4 back to the caller, which parks it at
* the never-trigger value. Drop the write-on-change cache with it: the
* register no longer holds _edcca_last_l2h, so keeping it would let a
* later re-enable at the same IGI decide it had nothing to write. */
if (!on)
_edcca_last_l2h = 0x7f;
}

void PhydmWatchdog::ReadFaCountersAc(FaCnt &out) {
/* Port of `phydm_fa_cnt_statistics_ac` (phydm_dig.c:1421).
* Reads OFDM/CCK FA + CCA + CRC32 counters from page-F BB
Expand Down
25 changes: 18 additions & 7 deletions src/jaguar1/PhydmWatchdog.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <thread>

class RadioManagementModule;
Expand Down Expand Up @@ -53,12 +54,19 @@ class PhydmWatchdog {
/* Run one watchdog cycle synchronously on the calling thread. */
void TickOnce();

/* EDCCA threshold tracking (the SetCcaMode enable path): when on, each
* tick re-derives the BB 0x8a4 L2H/H2L from the IGI DIG just wrote —
* the vendor couples the EDCCA threshold to IGI per watchdog cycle
* (phydm_adaptivity). Off = leave 0x8a4 alone (SetCcaMode owns the
* parked/static value). */
void SetEdccaTrack(bool on) { _edcca_track.store(on, std::memory_order_relaxed); }
/* EDCCA threshold tracking (the SetCcaMode / SetCcaGates enable path):
* when on, each tick re-derives the BB 0x8a4 L2H/H2L from the IGI DIG
* just wrote — the vendor couples the EDCCA threshold to IGI per watchdog
* cycle (phydm_adaptivity). Off = leave 0x8a4 alone (the caller owns the
* parked/static value).
*
* Synchronous by contract: this returns only once no tick is inside the
* EDCCA block and none can enter, so a caller turning tracking OFF may
* then write 0x8a4 knowing the watchdog will not overwrite it. Without
* that, a tick landing between the park write and the flag store leaves
* live thresholds behind a disable the caller already asked for — which
* only becomes reachable once the gates are settable mid-session. */
void SetEdccaTrack(bool on);

/* Most-recent FA counter snapshot — exposed for diagnostics /
* future DIG integration. */
Expand Down Expand Up @@ -121,7 +129,10 @@ class PhydmWatchdog {
* just walk based on FA count). */
bool _digInitialised = false;
uint8_t _cur_ig_value = 0x20;
std::atomic<bool> _edcca_track{false};
/* Serialises the tick's EDCCA block against SetEdccaTrack. Held only
* across that block, never across a whole tick. */
std::mutex _edcca_mu;
bool _edcca_track = false; /* guarded by _edcca_mu */
uint8_t _edcca_last_l2h = 0x7f; /* parked sentinel — first tick writes */
uint8_t _dm_dig_max = 0x26; /* DIG_MAX_COVERAGR */
uint8_t _dm_dig_min = 0x1c; /* DIG_MIN_COVERAGE */
Expand Down
66 changes: 55 additions & 11 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -980,25 +980,70 @@ void RtlJaguarDevice::ClearAckResponder() {
(void)disarm_ack_responder();
}

bool RtlJaguarDevice::GetCcaGates(bool &primary_disabled, bool &edcca_disabled) {
/* The MAC register is meaningless before bring-up, and reporting whatever
* the bus returns as the gate state would be a fabricated measurement. */
if (!_brought_up)
return false;
const uint32_t v = _device.rtw_read<uint32_t>(0x0520);
primary_disabled = (v & (1u << 14)) != 0;
edcca_disabled = (v & (1u << 15)) != 0;
return true;
}

bool RtlJaguarDevice::SetCcaGates(bool primary_disabled, bool edcca_disabled) {
if (!_brought_up)
return false;
apply_cca(primary_disabled, edcca_disabled);
_logger->info("Jaguar1: CCA gates primary={} edcca={}",
primary_disabled ? "OFF" : "on",
edcca_disabled ? "OFF" : "on");
return true;
}

void RtlJaguarDevice::SetCcaMode(bool disabled) {
apply_cca(disabled, disabled);
_logger->info("Jaguar1: MAC carrier-sense {}",
disabled ? "DISABLED (dis_cca: CCA+EDCCA)"
: "enabled (default)");
}

void RtlJaguarDevice::apply_cca(bool primary_disabled, bool edcca_disabled) {
/* MAC carrier-sense gate: the same REG_TX_PTCL_CTRL bits as the HalMAC
* generations — the vendor's phydm_mac_edcca_state drives 0x520[15] on
* this family too; [14] is the primary-CCA defer. */
* this family too; [14] is the primary-CCA defer. A set bit disables. */
uint32_t v520 = _device.rtw_read<uint32_t>(0x0520);
if (disabled)
v520 |= (1u << 15) | (1u << 14);
if (primary_disabled)
v520 |= (1u << 14);
else
v520 &= ~((1u << 15) | (1u << 14));
v520 &= ~(1u << 14);
if (edcca_disabled)
v520 |= (1u << 15);
else
v520 &= ~(1u << 15);
_device.rtw_write<uint32_t>(0x0520, v520);

/* Stop the EDCCA tracker BEFORE touching 0x8a4, not after. The phydm
* watchdog owns that register while tracking, and it runs on its own
* thread from rtw_hal_init — i.e. already before bring-up's SetCcaMode.
* Clearing the flag last left a window in which a tick could re-derive
* L2H from IGI and overwrite the park, leaving live thresholds behind a
* disable the caller had asked for. SetEdccaTrack is synchronous, so once
* it returns the writes below are ours. The enable direction hands the
* register over only after it is programmed, at the end of this function.
* Harmless when no watchdog was built (the default config). */
if (edcca_disabled)
if (auto *wd = _halModule.phydm_watchdog())
wd->SetEdccaTrack(false);

/* BB EDCCA thresholds (rEDCCA_Jaguar 0x8a4: L2H byte0 / H2L byte1). The
* BB init table parks them at 0x7f/0x7f = never-trigger — the vendor's
* adaptivity-off default (CONFIG_RTW_ADAPTIVITY_EN 0). Parked, the BB
* never raises the EDCCA signal, so the MAC gate [15] has nothing to
* honour — enable must program the vendor operating point from the live
* IGI for EDCCA to exist at all; disable re-parks. */
const auto ic = _eepromManager->version_id.ICType;
if (disabled) {
if (edcca_disabled) {
_device.phy_set_bb_reg(0x8a4, 0xFFFF, 0x7f7f);
} else {
const int8_t th_ini = ic == CHIP_8814A ? -14 : -17;
Expand All @@ -1017,12 +1062,11 @@ void RtlJaguarDevice::SetCcaMode(bool disabled) {
l2h, l2h - 7, igi);
}
/* With the watchdog running, DIG walks IGI — hand it the re-track so the
* threshold follows (vendor couples them per adaptivity cycle). */
if (auto *wd = _halModule.phydm_watchdog())
wd->SetEdccaTrack(!disabled);
_logger->info("Jaguar1: MAC carrier-sense {}",
disabled ? "DISABLED (dis_cca: CCA+EDCCA)"
: "enabled (default)");
* threshold follows (vendor couples them per adaptivity cycle). Only the
* enable direction is done here; the disable ran above, before the park. */
if (!edcca_disabled)
if (auto *wd = _halModule.phydm_watchdog())
wd->SetEdccaTrack(true);
}

bool RtlJaguarDevice::SetAmpduMode(const devourer::AmpduMode &mode) {
Expand Down
8 changes: 8 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ class RtlJaguarDevice : public IRtlRadio {
* parked at never-trigger by the BB table, programmed to the vendor
* operating point on enable (EDCCA only exists once they are set). */
void SetCcaMode(bool disabled) override;
/* The two gates independently — see IRtlRadio. */
bool SetCcaGates(bool primary_disabled, bool edcca_disabled) override;
bool GetCcaGates(bool &primary_disabled, bool &edcca_disabled) override;
/* A-MPDU TX mode (IRadio contract; src/AmpduMode.h). Programs the
* Jaguar1 aggregate-fill timer (0x0456 — NOT the 0x0455 the HalMAC chips
* use) + the 8814A burst-mode gate (0x04BC), and records the descriptor
Expand Down Expand Up @@ -425,6 +428,11 @@ class RtlJaguarDevice : public IRtlRadio {
bool la_capture_wedged() const { return _la && _la->is_wedged(); }

private:
/* Programs 0x520[14]/[15] and, for the EDCCA gate only, the BB thresholds
* at 0x8a4. SetCcaMode is apply_cca(d, d) and writes exactly what it
* wrote before the split existed. */
void apply_cca(bool primary_disabled, bool edcca_disabled);

void StartWithMonitorMode(SelectedChannel selectedChannel);
bool NetDevOpen(SelectedChannel selectedChannel);

Expand Down
10 changes: 7 additions & 3 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1929,9 +1929,13 @@ int32_t RtlJaguar2Device::PinBeaconTbtt(int32_t offset_us) {
void RtlJaguar2Device::SetCcaMode(bool disabled) {
std::lock_guard<std::mutex> lk(_reg_mu);
/* Both MAC carrier-sense bits in REG_TX_PTCL_CTRL: primary CCA 0x520[14] +
* EDCCA [15], plus EDCCA_MSK_COUNTDOWN 0x524[11]. The primary-CCA bit is the
* one that stops TX deferring to a co-channel transmitter; 0x520
* is the same HalMAC layout as the on-air-validated Jaguar3. */
* EDCCA [15], plus EDCCA_MSK_COUNTDOWN 0x524[11]. 0x520 is the same HalMAC
* layout as the on-air-validated Jaguar3, which is why the register writes
* are shared. Which of the two bits actually stops an injector is NOT:
* Jaguar3 and Jaguar1 measure opposite answers (see CLAUDE.md), and this
* family has no measurement of its own, so nothing here should be read as
* one. Jaguar2 has not ported the per-gate split — SetCcaGates is the
* not-ported default and this stays all-or-nothing. */
uint32_t v520 = _device.rtw_read<uint32_t>(0x0520);
uint32_t v524 = _device.rtw_read<uint32_t>(0x0524);
if (disabled) { v520 |= (1u << 15) | (1u << 14); v524 &= ~(1u << 11); }
Expand Down
Loading
Loading