From 6a7a752a24bf76a2f0d5207da1d885d9d6c9be92 Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 25 Oct 2026 12:18:14 +0100 Subject: [PATCH 1/5] jaguar3: make the coex/phydm runtime thread optional (tuning.j3_runtime_thread) After bring-up RtlJaguar3Device starts coex_runtime_loop. With no RX loop running it keeps a 16 KiB bulk-IN read posted to drain firmware C2H reports, and every ~2 s it writes coex, TX-power-tracking and DIG/EDCCA registers. A host that owns bulk-IN itself (raw C2H transfers) or needs fixed gain (LA-mode capture) loses packets to that read and sees its IGI rewritten. tuning.j3_runtime_thread (default true; env DEVOURER_J3_RUNTIME=0) lets such a host skip the thread. Every join is already guarded by joinable(). Measured on an 8812CU (8822C cut 5): host C2H 16 KiB went from 4-32 packets per run (the reads that won the race) to 1600/1600 with the thread off. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WAgKg5Azt5HiWAvzGdGnuW --- examples/common/env_config.cpp | 2 ++ src/DeviceConfig.h | 7 +++++++ src/jaguar3/RtlJaguar3Device.cpp | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 21748e95..7fa06924 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -230,6 +230,8 @@ devourer::DeviceConfig devourer_config_from_env() { cfg.tuning.skip_trx_reassert = env_flag("DEVOURER_SKIP_TRX_REASSERT"); cfg.tuning.skip_rfe_init = env_flag("DEVOURER_SKIP_RFEINIT"); cfg.tuning.skip_coex = env_flag("DEVOURER_SKIP_COEX"); + if (const char *e = std::getenv("DEVOURER_J3_RUNTIME")) + cfg.tuning.j3_runtime_thread = std::string(e) != "0"; cfg.tuning.skip_dig = env_flag("DEVOURER_SKIP_DIG"); /* Default-on knob: unset = tracking on; only "0" disables it. */ if (const char *e = env_str("DEVOURER_THERMAL_TRACK")) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index a5949432..0c56bfeb 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -365,6 +365,13 @@ struct DeviceConfig { bool skip_trx_reassert = false; /* env: DEVOURER_SKIP_TRX_REASSERT (J2) */ bool skip_rfe_init = false; /* env: DEVOURER_SKIP_RFEINIT (J2) */ bool skip_coex = false; /* env: DEVOURER_SKIP_COEX (J2) */ + /* env: DEVOURER_J3_RUNTIME ("0" disables) — Jaguar3: start the coex / + * phydm watchdog runtime thread after bring-up (default ON). With no RX + * loop running, that thread also keeps a bulk-IN read posted to drain + * firmware C2H reports, and every ~2 s it writes coex, TX-power tracking + * and DIG/EDCCA registers. A host that owns bulk-IN itself or needs fixed + * gain (raw C2H / LA-mode capture) turns it off. */ + bool j3_runtime_thread = true; bool skip_dig = false; /* env: DEVOURER_SKIP_DIG (J2) */ /* env: DEVOURER_THERMAL_TRACK — Jaguar2 thermal TX-power tracking * (default ON; "0" disables). A ~2 s tick reads the RF 0x42 thermal diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 1adde54e..84445768 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1177,7 +1177,10 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { * adapter's transfer counter, so anything emitted after it would count * that thread's register and H2C traffic as bring-up. */ timer.total(); - _coex_thread = std::thread([this] { coex_runtime_loop(); }); + if (_cfg.tuning.j3_runtime_thread) + _coex_thread = std::thread([this] { coex_runtime_loop(); }); + else + _logger->info("Jaguar3: coex/phydm runtime thread not started (tuning.j3_runtime_thread)"); if (_cfg.rx.ack_responder && !SetAckResponder(*_cfg.rx.ack_responder)) /* DEVOURER_ACK_RESPONDER */ throw std::runtime_error( From 4afa386d9a32b115d89664a7b3ceb977bea43c7c Mon Sep 17 00:00:00 2001 From: snokvist Date: Sun, 25 Oct 2026 12:51:35 +0100 Subject: [PATCH 2/5] jaguar3: honour skip_dig in the phydm watchdog; scope j3_runtime_thread docs Review on #498: with j3_runtime_thread off, StartRxLoop still starts its own phydm worker, whose tick() runs DIG every ~2 s, so the flag alone does not give fixed gain during an RX session. Keep that worker (ordinary RX sessions want it) and let tuning.skip_dig (DEVOURER_SKIP_DIG, so far Jaguar2-only) skip the DIG step on Jaguar3 too, in both tick() callers. The j3_runtime_thread comment now says what it covers. 8812CU ch36, rxdemo 14 s per arm: runtime off -> 'Jaguar3 dig: IGI=0x1e' written by the RX worker; runtime off + DEVOURER_SKIP_DIG=1 -> no DIG step; default unchanged. ctest 83/83. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WAgKg5Azt5HiWAvzGdGnuW --- src/DeviceConfig.h | 11 ++++++++--- src/jaguar3/PhydmRuntimeJaguar3.cpp | 5 +++-- src/jaguar3/PhydmRuntimeJaguar3.h | 5 +++-- src/jaguar3/RtlJaguar3Device.cpp | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 0c56bfeb..3b436375 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -369,10 +369,15 @@ struct DeviceConfig { * phydm watchdog runtime thread after bring-up (default ON). With no RX * loop running, that thread also keeps a bulk-IN read posted to drain * firmware C2H reports, and every ~2 s it writes coex, TX-power tracking - * and DIG/EDCCA registers. A host that owns bulk-IN itself or needs fixed - * gain (raw C2H / LA-mode capture) turns it off. */ + * and DIG/EDCCA registers. A host that owns bulk-IN itself (raw C2H) + * turns it off. This flag covers only that post-bring-up thread: + * StartRxLoop still runs its own phydm watchdog (DIG included) when the + * thread is off, so fixed gain during an RX session also needs skip_dig. */ bool j3_runtime_thread = true; - bool skip_dig = false; /* env: DEVOURER_SKIP_DIG (J2) */ + /* env: DEVOURER_SKIP_DIG — J2: no DIG at bring-up; J3: the phydm + * watchdog (runtime thread and StartRxLoop worker) skips its DIG step, + * so IGI stays where the host put it. */ + bool skip_dig = false; /* env: DEVOURER_THERMAL_TRACK — Jaguar2 thermal TX-power tracking * (default ON; "0" disables). A ~2 s tick reads the RF 0x42 thermal * meter, computes the delta vs the efuse baseline, and writes the vendor diff --git a/src/jaguar3/PhydmRuntimeJaguar3.cpp b/src/jaguar3/PhydmRuntimeJaguar3.cpp index d7e0b545..e289b710 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.cpp +++ b/src/jaguar3/PhydmRuntimeJaguar3.cpp @@ -226,9 +226,10 @@ void PhydmRuntimeJaguar3::edcca(uint8_t igi) { } } -void PhydmRuntimeJaguar3::tick(uint8_t channel, bool edcca_track) { +void PhydmRuntimeJaguar3::tick(uint8_t channel, bool edcca_track, bool dig_track) { const FaStats fa = fa_statistics_and_reset(); - dig(fa, is_dfs_channel(channel)); + if (dig_track) + dig(fa, is_dfs_channel(channel)); if (fa.cck_enabled) cck_pd(fa); if (edcca_track) diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index 31c6eef0..f4f09b49 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -32,8 +32,9 @@ class PhydmRuntimeJaguar3 { /* One watchdog pass: FA/CCA statistics + reset, DIG, CCK-PD (2 GHz), * EDCCA. `channel` = current center channel (band + DFS decision), * `edcca_track` = false leaves 0x84c untouched (DEVOURER_DIS_CCA / - * SetCcaMode owns the thresholds then). */ - void tick(uint8_t channel, bool edcca_track); + * SetCcaMode owns the thresholds then); `dig_track` = false skips DIG, so + * the IGI the host set is kept (DEVOURER_SKIP_DIG). */ + void tick(uint8_t channel, bool edcca_track, bool dig_track = true); /* A channel/bandwidth change invalidates the CCK-PD applied-level cache * (the kernel re-checks bw/nrx from registers each apply; the level is diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 84445768..6fdef9ee 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -300,7 +300,7 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { std::lock_guard lk(_reg_mu); /* edcca_track follows the EDCCA gate alone — see the housekeeping * tick below for why the all-or-nothing flag is the wrong input. */ - _phydm.tick(_channel.Channel, !_cca_edcca_disabled); + _phydm.tick(_channel.Channel, !_cca_edcca_disabled, !_cfg.tuning.skip_dig); } catch (...) { break; /* chip gone — the RX loop will wind down too */ } @@ -578,7 +578,7 @@ void RtlJaguar3Device::coex_runtime_loop() { * thresholds at 0x84c every ~2 s in the EDCCA-off/primary-on arm, * undoing the disable the caller asked for. Jaguar1 does the same * thing via SetEdccaTrack(!edcca_disabled) at the end of apply_cca. */ - _phydm.tick(_channel.Channel, !_cca_edcca_disabled); + _phydm.tick(_channel.Channel, !_cca_edcca_disabled, !_cfg.tuning.skip_dig); _hal.fw_update_wl_phy_info(); _hal.fw_set_pwr_mode_active(); _hal.fw_coex_query_bt_info(); From ca7cdf722cd05ed4b5199721efc3c64e13f15f6c Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 26 Oct 2026 07:03:28 +0100 Subject: [PATCH 3/5] DeviceConfig: skip_dig does not cover the J3 wide-TX IGI floor The floor still raises IGI on the first 40/80 MHz frame and clamps it on leaving; say so, and point at DEVOURER_WIDE_TX_IGI_FLOOR=0 for fully fixed IGI. Co-Authored-By: Claude Opus 5.5 --- src/DeviceConfig.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 3b436375..49cf330d 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -375,8 +375,11 @@ struct DeviceConfig { * thread is off, so fixed gain during an RX session also needs skip_dig. */ bool j3_runtime_thread = true; /* env: DEVOURER_SKIP_DIG — J2: no DIG at bring-up; J3: the phydm - * watchdog (runtime thread and StartRxLoop worker) skips its DIG step, - * so IGI stays where the host put it. */ + * watchdog (runtime thread and StartRxLoop worker) skips its DIG step. + * On J3 the wide-TX IGI floor (wide_tx_igi_floor) still writes IGI: the + * first 40/80 MHz frame raises a lower IGI to the floor, and leaving the + * floor clamps IGI down to the DIG window top. For IGI that stays exactly + * where the host put it, also set DEVOURER_WIDE_TX_IGI_FLOOR=0. */ bool skip_dig = false; /* env: DEVOURER_THERMAL_TRACK — Jaguar2 thermal TX-power tracking * (default ON; "0" disables). A ~2 s tick reads the RF 0x42 thermal From 2282164e096c2979288ff335f33daa5db0bbcf0e Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 26 Oct 2026 10:40:27 +0100 Subject: [PATCH 4/5] jaguar3: document what j3_runtime_thread=false costs a TX-only session; review nits - DeviceConfig.h / src/jaguar3/CLAUDE.md: with the thread off, a TX-only session gets no tx.report (nothing drains C2H), no pwr_track and no coex/FW heartbeats; CLAUDE.md's coex bullet names the opt-out. - env_config: DEVOURER_J3_RUNTIME via env_str, like the other default-on knobs. - PhydmRuntimeJaguar3::tick: no default for dig_track (both callers pass it). Co-Authored-By: Claude Opus 5.5 --- examples/common/env_config.cpp | 4 ++-- src/DeviceConfig.h | 7 ++++++- src/jaguar3/CLAUDE.md | 3 +++ src/jaguar3/PhydmRuntimeJaguar3.h | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 7fa06924..762ee2c6 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -230,8 +230,8 @@ devourer::DeviceConfig devourer_config_from_env() { cfg.tuning.skip_trx_reassert = env_flag("DEVOURER_SKIP_TRX_REASSERT"); cfg.tuning.skip_rfe_init = env_flag("DEVOURER_SKIP_RFEINIT"); cfg.tuning.skip_coex = env_flag("DEVOURER_SKIP_COEX"); - if (const char *e = std::getenv("DEVOURER_J3_RUNTIME")) - cfg.tuning.j3_runtime_thread = std::string(e) != "0"; + if (const char *e = env_str("DEVOURER_J3_RUNTIME")) + cfg.tuning.j3_runtime_thread = std::strcmp(e, "0") != 0; cfg.tuning.skip_dig = env_flag("DEVOURER_SKIP_DIG"); /* Default-on knob: unset = tracking on; only "0" disables it. */ if (const char *e = env_str("DEVOURER_THERMAL_TRACK")) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 49cf330d..a12d85bb 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -372,7 +372,12 @@ struct DeviceConfig { * and DIG/EDCCA registers. A host that owns bulk-IN itself (raw C2H) * turns it off. This flag covers only that post-bring-up thread: * StartRxLoop still runs its own phydm watchdog (DIG included) when the - * thread is off, so fixed gain during an RX session also needs skip_dig. */ + * thread is off, so fixed gain during an RX session also needs skip_dig. + * A TX-only session with the thread off loses all of it: nothing drains + * C2H, so no tx.report (CCX) is emitted; pwr_track (thermal TX-power + * compensation, which sustains upper 5 GHz) stops; and the coex re-apply + * and FW heartbeats stop (src/jaguar3/CLAUDE.md: sustained 5 GHz TX + * needs them). */ bool j3_runtime_thread = true; /* env: DEVOURER_SKIP_DIG — J2: no DIG at bring-up; J3: the phydm * watchdog (runtime thread and StartRxLoop worker) skips its DIG step. diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index ed8bc30c..c18f85a9 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -19,6 +19,9 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` `InitWrite`): sustained 5 GHz TX needs it — without its ~2 s WiFi-only coex re-apply + FW heartbeats, the combo chip's coex firmware silences the antenna. It also drains C2H, so TX-only sessions still see `tx.report`. + Opt-out: `tuning.j3_runtime_thread = false` (`DEVOURER_J3_RUNTIME=0`), for + a host that owns bulk-IN itself. A TX-only session then gets no + `tx.report`, no `pwr_track` and no coex/FW heartbeats. - **5/10 MHz narrowband**: the re-clock lives in the `0x9b0`/`0x9b4` dividers (vs the `0x8ac` block the Jaguar1/2 chips share). 80 MHz works, incl. a 40-in-80 frame via TX-descriptor DATA_SC. See `docs/narrowband.md`. diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index f4f09b49..b9cc567c 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -34,7 +34,7 @@ class PhydmRuntimeJaguar3 { * `edcca_track` = false leaves 0x84c untouched (DEVOURER_DIS_CCA / * SetCcaMode owns the thresholds then); `dig_track` = false skips DIG, so * the IGI the host set is kept (DEVOURER_SKIP_DIG). */ - void tick(uint8_t channel, bool edcca_track, bool dig_track = true); + void tick(uint8_t channel, bool edcca_track, bool dig_track); /* A channel/bandwidth change invalidates the CCK-PD applied-level cache * (the kernel re-checks bw/nrx from registers each apply; the level is From 49a9fbf2734d9e09c6280ca668546d95c3ec7a76 Mon Sep 17 00:00:00 2001 From: snokvist Date: Mon, 26 Oct 2026 10:55:06 +0100 Subject: [PATCH 5/5] jaguar3: skip_dig and tick() docs say what they cover; root CLAUDE.md names the opt-out - DeviceConfig.h: on J2, skip_dig stops StartRxLoop's DIG thread (not a bring-up step, as 4afa386 said). - PhydmRuntimeJaguar3::tick: dig_track = false skips DIG, but the wide-TX IGI floor still writes IGI (as the skip_dig comment says since ca7cdf7). - CLAUDE.md (Jaguar3 bullet): sustained 5 GHz TX needs the coex thread; name the j3_runtime_thread opt-out and point to src/jaguar3/CLAUDE.md. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 +++- src/DeviceConfig.h | 5 +++-- src/jaguar3/PhydmRuntimeJaguar3.h | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 627b8f56..10d0bb1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,9 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first): and rtl8822e (RTL8812EU/8822EU, chip-id `0x17`). 5/10 MHz narrowband, 80 MHz (incl. a 40-in-80 frame), halrf calibration. Sustained 5 GHz TX needs the coex runtime thread (started in `InitWrite`) — without it the - combo chip's coex firmware silences the antenna. 8822e constraints: + combo chip's coex firmware silences the antenna. A host that owns bulk-IN + itself can opt out (`tuning.j3_runtime_thread`, see `src/jaguar3/CLAUDE.md` + for what that costs). 8822e constraints: `docs/8822e-quirks.md`. - **Kestrel** (`src/kestrel/`): the Wi-Fi 6 / 802.11ax generation — RTL8852BU/8832BU (variant C8852B) and RTL8852CU/8832CU (C8852C), one HAL diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index a12d85bb..1759a60c 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -379,8 +379,9 @@ struct DeviceConfig { * and FW heartbeats stop (src/jaguar3/CLAUDE.md: sustained 5 GHz TX * needs them). */ bool j3_runtime_thread = true; - /* env: DEVOURER_SKIP_DIG — J2: no DIG at bring-up; J3: the phydm - * watchdog (runtime thread and StartRxLoop worker) skips its DIG step. + /* env: DEVOURER_SKIP_DIG — J2: StartRxLoop starts no DIG thread; J3: the + * phydm watchdog (runtime thread and StartRxLoop worker) skips its DIG + * step. * On J3 the wide-TX IGI floor (wide_tx_igi_floor) still writes IGI: the * first 40/80 MHz frame raises a lower IGI to the floor, and leaving the * floor clamps IGI down to the DIG window top. For IGI that stays exactly diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index b9cc567c..4b89794e 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -32,8 +32,9 @@ class PhydmRuntimeJaguar3 { /* One watchdog pass: FA/CCA statistics + reset, DIG, CCK-PD (2 GHz), * EDCCA. `channel` = current center channel (band + DFS decision), * `edcca_track` = false leaves 0x84c untouched (DEVOURER_DIS_CCA / - * SetCcaMode owns the thresholds then); `dig_track` = false skips DIG, so - * the IGI the host set is kept (DEVOURER_SKIP_DIG). */ + * SetCcaMode owns the thresholds then); `dig_track` = false skips DIG + * (DEVOURER_SKIP_DIG; the wide-TX IGI floor still applies, see + * set_igi_floor). */ void tick(uint8_t channel, bool edcca_track, bool dig_track); /* A channel/bandwidth change invalidates the CCK-PD applied-level cache