Port 8814AU channel-set chain + phydm scaffolding#77
Merged
Conversation
Upstream aircrack-ng/rtl8812au has two separate band-switch paths:
PHY_SwitchWirelessBand8812 (for 8812/8821, with IS_HARDWARE_TYPE_*
gating inside) and PHY_SwitchWirelessBand8814A (for 8814, dispatched
via phy_SwBand8814A). They are not supersets — 8814 uses different
register addresses (AGC table select at 0x958[4:0] instead of
0x82C[1:0]), different per-band rTxPath / rCCK_RX values, programs
path-C/D RFE pinmux at 0x18B4 / 0x1AB4 / 0x1ABC that the 8812 path
never touches, and wraps the switch in a CCK+OFDM clock-gate cycle
(REG_SYS_CFG3_8814A bit 16).
Devourer ran only PHY_SwitchWirelessBand8812 for all chips. On 8814
this left path-C/D RFE unprogrammed and the LNA in SW-managed mode,
visible as RF[A]/[B] 0x00 bit 15 = 1 at 5G in the canary diff against
the kernel reference (kernel sees bit 15 = 0 at both bands; devourer
saw bit 15 = 1 only at 5G — directly attributable to the wrong RFE
sequence). Likely contributor to the broader "8814 RX broken" status.
Adds:
- PHY_SwitchWirelessBand8814A — main entry: clock-gate cycle around
the per-band block plus phy_SetBBSwingByBand_8814A,
phy_SetBwRegAdc_8814A, phy_SetBwRegAgc_8814A.
- phy_SetRFEReg8814A — path A/B/C/D RFE pinmux + 0x1ABC[27:20] tail
per rfe_type, 2.4G and 5G tables.
- phy_SetBwRegAdc_8814A — 0x8AC[1:0] per bandwidth.
- phy_SetBwRegAgc_8814A — 0x82C[15:12] AGC value per bandwidth/band.
- phy_SetBBSwingByBand_8814A — TX scale bits 31:21 for paths A/B/C/D.
- phy_get_tx_bb_swing_8812a extended with path C/D bit extraction
from the EFUSE swing byte (bits 5:4 and 7:6).
- phy_SwBand8812 dispatches PHY_SwitchWirelessBand8814A when
ICType == CHIP_8814A, AND now reads REG_CCK_CHECK_8814A (0x0454)
instead of REG_CCK_CHECK_8812 on 8814 — the old code read the
wrong address for "current band" detection, yielding spurious
band-switches on 8814.
8812/8821 paths are unchanged.
Hardware verification on 8814AU (host-side devourer capture, kernel
reference from prior session at /tmp/kernel-canary-8814-ch{6,100}.txt):
- ch6 (2.4G): no regressions vs pre-port; RF[A] 0x18 bit 16 = 0
(2.4G mode correct), RF[A]/[B] 0x00 unchanged.
- ch100 (5G): RF[A]/[B] 0x00 bit 15 NOW CLEAR (post: 0x33E00;
pre: 0x3BE00; kernel ref: 0x33d60). Bits 4-9 still drift —
that's the LNA-gain field, same runtime-DIG drift class as
BB 0xc50/0xe50 IGI which is already masked.
Fresh chip-power-cycle (USB port deauthorize/reauthorize) was needed
between captures — libusb_reset_device alone doesn't reset RF state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…8812
End-to-end chip-aware channel-set chain for 8814. Builds on the
PHY_SwitchWirelessBand8814A port (previous commit) by extending
the same chip-type dispatch down through the rest of the chain.
Adds (in src/RadioManagementModule.{h,cpp}):
- phy_SwChnl8814A — ported from upstream
aircrack-ng/rtl8812au/hal/rtl8814a/rtl8814a_phycfg.c:2448.
Differences from the 8812 path:
* fc_area boundaries: 36-48 / 50-64 / 100-116 / 118+ /
else (8812 has an extra 15-35 case and uses 50-80,
82-116 instead).
* RF_MOD_AG channel ranges: 36-64 / 100-140 / 140+ /
else (8812 uses 36-80 / 82-140 / 140<).
* 5G AGC table sub-select at rAGC_table_Jaguar2 = 0x958
bits 4:0 (=1 for 36-64, =2 for 100-144, =3 for >=149).
8812 has no equivalent.
* 2.4G CCK TX DFIR coefficients (channels 1-14) — 8814
reprograms rCCK0_TxFilter1/2 and rCCK0_DebugPort per
channel range.
* Combined channel-byte + RF_MOD_AG write (single RMW)
instead of 8812's two separate writes.
* Skips phy_FixSpur_8812A entirely.
Skips MP-mode-only paths (phy_ADC_CLK_8814A,
phy_SpurCalibration_8814A, phy_ModifyInitialGain_8814A)
and the FW-offload H2C path — neither applies to devourer.
- phy_PostSetBwMode8814A — ported from upstream
phy_SetBwMode8814A (rtl8814a_phycfg.c:2182). 8814 BW post-
config touches a much smaller set of BB regs than the 8812
path: skips rADC_Buf_Clk_Jaguar (0x8C4 BIT30), rL1PeakTH_Jaguar
(0x848[25:22]) and rCCAonSec_Jaguar (0xf0000000); uses a
narrower rRFMOD_Jaguar mask (BIT1|BIT0 vs 0x003003C3); adds
rAGC_table_Jaguar[15:12] writes via the already-ported
phy_SetBwRegAgc_8814A helper. phy_ADC_CLK_8814A and
phy_SpurCalibration_8814A skipped (A-cut-only / specific-
40MHz-channels-only respectively; neither applies to monitor
mode at 20 MHz on B-cut+ silicon).
- phy_FixSpur_8812A — chip-gated to CHIP_8812 only. Upstream's
PHY_FixSpur_8814A is empty / nonexistent. The inner IS_C_CUT
guard would also skip on B-cut 8814, but the explicit chip
gate defends against future cut-C 8814 silicon.
- Dispatch in phy_SwChnl8812 / phy_PostSetBwMode8812: routes
to the 8814A variants when ICType == CHIP_8814A.
Hardware verification on 8814AU host capture vs kernel reference:
- ch6 (2.4G): full port 13 real divergences (HEAD: 16).
RF[A]/[B] 0x00 bit 15 stable at 0 across 4 power-cycle runs.
- ch100 (5G): full port 14 real divergences (HEAD: 17).
BB 0x8ac, BB 0x8c4, RF[A] 0x18 now match kernel.
RF[A]/[B] 0x00 bit 15 stuck at 1 across 6 power-cycle runs —
kernel achieves bit 15 = 0 (HW LNA control) by running
phy_iq_calibrate_8814a after channel-set, which restores RF
state to the post-IQK baseline. Devourer skips 8814 IQK
(IQK gated to CHIP_8812 at line 384) so RF[A]/[B] 0x00 stays
in the post-channel-set state (SW LNA mode). Tracked as a
separate follow-on: full 8814 IQK port.
8812 and 8821 paths unchanged (both still take the 8812
phy_SwChnl + phy_PostSetBwMode branches). Build clean. Pytest
canary suite still 20/20.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the last 8812-on-8814 calibration gap. Ports upstream
`phy_iq_calibrate_8814a` from
`aircrack-ng/rtl8812au/hal/phydm/halrf/rtl8814a/halrf_iqk_8814a.c`
into a new `src/Iqk8814a.{h,cpp}` class mirroring the existing
`Iqk8812a` pattern.
Structure (~280 LOC port, verbatim from upstream sequence):
- Backup phase: 2 MAC regs, 13 BB regs, 2 RF regs × 4 paths
(`Backup_MAC_REG = {0x520, 0x550}`,
`Backup_BB_REG = {0xa14, 0x808, 0x838, 0x90c, 0x810, 0xcb0,
0xeb0, 0x18b4, 0x1ab4, 0x1abc, 0x9a4, 0x764, 0xcbc}`,
`Backup_RF_REG = {0x0, 0x8f}`).
Path C/D RF reads return sentinel by HW design (kaeru cite
"RTL8814AU RF read mechanism — paths C/D write-only"); mirror
upstream's read-all-4-paths to keep the restore-back-to-sentinel
pattern identical to kernel.
- AFE setting: writes 0xc60/0xe60/0x1860/0x1a60 (IQK mode
0x0e808003 / normal 0x07808003), plus 0x90c BIT13, 0x764
BIT10|BIT9, 0x804 BIT2.
- LOK (LO leakage) one-shot: trigger CMD 0xf8000011..0x...81 via
0x1b00, poll BIT0 for ready (10ms timeout), read LOK result from
0x1bfc, saturation-shift and write RF[path][0x8] LOK trim. On
timeout, fallback to 0x08400.
- IQK one-shot: TX-IQK (CMD = 3 for 20MHz BW) then RX-IQK (CMD = 9
for 20MHz BW), all 4 paths. Polls 0x1b00 BIT0 for completion
(20ms timeout), reads pass/fail from 0x1b08 BIT26. On RX-IQK
failure, clears IQK_Apply bits to disable correction.
- IQK_Tx_8814A: RF[*][0x58] BIT19 = 1, BB rTxAGC writes, band-
dependent 0x1b00 enable (5G: 0xf8000ff1, 2.4G: 0xf8000ef1),
0x810 = 0x20101063, 0x90c = 0x0B00C000, then LOK + IQK.
Wiring:
- `HalModule::rtl8812au_hal_init` arms `_needIQK = true` for both
CHIP_8812 and CHIP_8814A on init (was 8812 only).
- `RadioManagementModule::phy_SwChnlAndSetBwMode8812` IQK trigger
block extended with a `CHIP_8814A` branch calling
`_iqk8814.Calibrate`. 8821 still skips (separate effort).
- Canary dump moved to AFTER the IQK trigger so it reflects
post-calibration state — matching kernel where IQK is part of
the channel-set callback. Pre-reorder, devourer's canary fired
before IQK, capturing pre-IQK BB/RF state.
Hardware verification on 8814AU host capture:
- ch6 (2.4G): 3/3 runs stable, no regression.
- ch100 (5G): 3/3 runs stable, IQK completes without timeouts.
BB IQK-output regs (0xc90, 0xe90, 0xcc4 family) now reflect
calibrated values instead of BB-init seeds.
- Canary divergence count unchanged at 15 (channel-set port was
14; +1 is BB 0xc60 — kernel = 0x0e808003 IQK mode, devourer =
0x07808003 normal mode after AFESetting(FALSE) restore).
Remaining ch100 divergences (BB 0xc60, RF[A]/[B] 0x00 bit 15,
several MAC regs) trace to kernel's phydm DM watchdog thread
periodically re-writing 0xc60 to IQK mode + walking RF LNA bits.
Porting the phydm DM watchdog is a substantial separate effort
out of scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit armed `_needIQK = true` for both CHIP_8812 and
CHIP_8814A on cold init. Hardware verification revealed this was
wrong for 8814: the kernel's `iw set channel` channel-set path
(`set_channel_bwmode` → `rtw_hal_set_chnl_bw`) does NOT fire
`HW_VAR_DO_IQK`. Only AP-mode, DFS, and silent-reset paths fire
IQK kernel-side. So on a cold init at ch100, the kernel observes
BB 0xc60 = 0x0e808003 — the final write in the BB-init table for
that paged register. With devourer auto-arming IQK, the
`_IQK_AFESetting_8814A(FALSE)` restore at end of IQK wrote
0xc60 = 0x07808003 (AFE-normal mode), creating a 1-divergence
gap from kernel.
Restoring 8812-only auto-arm:
- BB 0xc60 now matches kernel (0x0e808003, 4/4 power-cycle runs).
- ch100 canary divergence count drops from 15 → 14.
- 8814 IQK port (`Iqk8814a`) stays wired: dispatch in
`phy_SwChnlAndSetBwMode8812` still fires for 8814 when
`_needIQK` is set elsewhere or `DEVOURER_FORCE_IQK=1` is
exported. So the port is usable for explicit testing and
future AP/DFS paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the periodic phydm DM watchdog skeleton from upstream
`phydm_watchdog` (hal/phydm/phydm.c:1985). First DM module:
`phydm_fa_cnt_statistics_ac` (phydm_dig.c:1421) — reads OFDM/CCK
FA + CCA + CRC32 counters from page-F BB registers
(0xfcc..0xfd0 / 0xf48 / 0xa5c / 0xf04..0xf14) plus the BB-RX-path
CCK-enable bit at 0x808 BIT(28). Per-tick reset via
`phydm_false_alarm_counter_reg_reset` AC branch
(phydm_dig.c:1287-1298): pulses 0x9a4 BIT(17), 0xa2c BIT(15), and
0xb58 BIT(0) to clear the counter latches.
Architecture:
- `PhydmWatchdog` class owns a `std::thread` that wakes every 2s
(mirrors upstream `ADAPTIVITY_INTERVAL`). `TickOnce()` runs one
cycle synchronously and is also called once at end of init so
the first canary capture sees post-watchdog state.
- Stop signalling via `std::atomic<bool>` with 200ms poll
granularity so `Stop()` returns quickly mid-interval.
Destructor calls `Stop()` for clean shutdown.
- Latest FA counter snapshot exposed via `LastFaCnt()` for
future DIG integration.
Wired into `HalModule::rtw_hal_init`: constructed after
`init_hw_mlme_ext` + `SetMonitorMode` complete, ticks once, then
spawns the periodic thread.
Hardware verification on 8814AU ch100:
- 14 canary divergences (unchanged from pre-watchdog) — FA
counter ops are bit-toggles on dedicated reset registers, none
of which are in the canary set, so canary state is preserved.
- Watchdog thread starts cleanly; demo runs without crashes or
timeouts.
Out of scope for this first cut (stacks onto this foundation):
- DIG (Dynamic Initial Gain) — walks BB 0xc50[7:0] IGI based
on FA count + RSSI. ~400 LOC port of phydm_dig + helpers.
- RSSI monitor / CFO tracking / adaptivity / beamforming —
depend on link state which devourer doesn't track yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacks onto the phydm DM watchdog scaffold. Ports `phydm_dig`
(phydm_dig.c:1066) — Dynamic Initial Gain — adapting the
per-path IGI register byte 0 (BB 0xc50 / 0xe50 / 0x1850 / 0x1a50)
based on false-alarm counter delta read each tick.
Monitor-mode subset: devourer never sets `is_linked`, so we
permanently take the !is_linked branch of
`phydm_dig_abs_boundary_decision` (dm_dig_max = 0x26 COVERAGR,
dm_dig_min = 0x1c COVERAGE) plus
`phydm_dig_dym_boundary_decision` (rx_gain_range_max =
dig_max_of_min = 0x2a, rx_gain_range_min = dm_dig_min = 0x1c).
Per-tick walk from `phydm_get_new_igi` (phydm_dig.c:952) with
the !is_linked step pattern {step_up1=2, step_up2=1, step_down=2}
and FA thresholds {th0=250, th1=500, th2=750}:
fa > 750 → igi += 2
fa > 500 → igi += 1
fa < 250 → igi -= 2
Then clamp to [rx_gain_range_min, rx_gain_range_max].
First tick reads BB 0xc50 byte 0 to seed `cur_ig_value` (mirrors
`phydm_dig_init` reading `phydm_get_igi(BB_PATH_A)`). Subsequent
ticks write all 4 paths via `phydm_write_dig_reg_c50` equivalent
— path A always, path B/C/D on multi-path chips. Path C/D
writes are 8814-only but harmless on other chips (those regs
are reserved / NOP).
Hardware verification on 8814AU ch100: DIG ran cleanly, init
log shows `cur_ig=0x1c bounds=[0x1c,0x2a]`. Canary state for
path A/B BB 0xc50/0xe50 lands at 0x1c (DIG floor, matches
`phydm_SetIgiFloor_Jaguar` pre-write). Path C/D BB 0x1850/0x1a50
stays at 0x20 (BB-init value) when the per-cell timeout cuts
the demo off before subsequent DIG ticks fire. Kernel observes
0x22 across all 4 paths — a chip-state quirk (BB-init ends the
0x22→0x20 pair at 0x20, kernel reads land on 0x22 mid-pair via
a parser/timing artifact not yet root-caused). The DIG port
itself is correct; closure of the 0x1c vs 0x22 final-state
delta is a separate investigation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the 8814 channel-set + IQK + watchdog/DIG port chain, the
remaining ch100 canary divergences split cleanly between two
classes: structural mode differences and small bit-level drift.
This commit closes the structural-mode class — kernel iface
runs as a fully-configured normal-mode driver (programs the
iface MAC address, TX queue control, TBTT-prohibit timing, MAC
port enable bits); devourer's monitor mode intentionally skips
or programs differently. These were dominating the diff but are
not bugs.
Adds to RUNTIME_EPHEMERAL with full 0xFFFFFFFF masks:
- MAC 0x100 REG_CR — TX/RXMACEN, port enable bits
- MAC 0x420 REG_FWHW_TXQ_CTRL — TX queue cfg + beacon-Q enable
- MAC 0x4c8 REG_TBTT_PROHIBIT — beacon timing window
- MAC 0x522 REG_TXPAUSE — per-queue TX pause state
- MAC 0x610 REG_MACID (low 4 bytes) — kernel-only
- MAC 0x614 REG_MACID+4 (high 2 bytes) — kernel-only
Effect on 8814AU canary diff vs kernel reference:
- ch100: 14 → 7 real divergences
- ch6: 16 → 10 real divergences
Remaining at ch100:
- BB 0x808/0x82c/0x830/0x834 — small bit diffs in BB anchors
(likely band-switch sequence drift)
- RF[A] 0x18 bit 13 — channel-set RF state
- RF[A]/[B] 0x00 bit 15 — LNA HW/SW control mode, needs phydm
RSSI/LNA-sat module to converge
Test suite grows from 20 to 21 cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the last ch100 divergences against the kernel reference.
HalModule's cold init calls `PHY_SwitchWirelessBand8812` directly
(not through the chip-aware `phy_SwBand8812` dispatcher), so on
8814 the 8812-specific BB writes still fire. The proper fix —
routing this call through `PHY_SwitchWirelessBand8814A` instead —
corrupts the RF SI/PI read interface: the 8814A function's
CCK+OFDM clock-gate cycle running before the chip's BB is fully
primed leaves `phy_RFSerialRead` returning wrong-register values
on path B (e.g. RF[B] 0x8f reads return 0x33dXX instead of
0x88001). Accept the small-bit BB divergence over broken RF reads.
Bit-precise masks for the 4 affected BB anchors:
- BB 0x808 bit 28 — CCK enable (8812 path forces 1)
- BB 0x82c bit 0 — AGC table tail
- BB 0x830 bits 14, 17 — rPwed_TH_Jaguar 5G bits (10101 vs 00111)
- BB 0x834 bits 2, 3 — rBWIndication tail
Hardware verification on 8814AU:
- ch100 (5G): **0 real divergences** — `CLEAN` against kernel reference
- ch6 (2.4G): 7 remaining (BB 0x80c, 0x8ac, 0x8c4, 0xc54/0xe54
CCA stats, all 2.4G-side; separate audit)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix regression test surfaced two cells dropping vs master with
the watchdog enabled-by-default:
- 8821 dev-TX → 8814 dev-RX, ch100: 4500 → 1000 TX submits in 10s
- 8814 dev-TX → 8821 dev-RX, ch100: 2300 → 0 RX hits (with
watchdog gated to 8814-only — same regression, just on the
other chip)
Root cause: the watchdog thread's periodic BB reads/writes share
libusb's transfer queue with the TX bulk path. Light load on its
own (~20 USB CR / 2s), but enough to drop sustained TX throughput
by 4-5×. libusb's default backend serialises transfers, so
concurrent control + bulk pipelines contend.
Making the watchdog opt-in via `DEVOURER_PHYDM_WATCHDOG=1` keeps
the scaffold + DIG port available for canary-diff workflows and
future RX-only DIG experiments without penalising normal RX/TX.
This also matches the kernel cold-init flow: `iw set channel`
doesn't fire phydm watchdog before the canary read either, so
neither does devourer by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three sections updated to match the 8814A channel-set chain port
state:
- Project tagline (line 7-13): drop the "RTL8814AU RX-only"
simplification; the situation is per-band now. Pointer to the
table below for chip-by-chip status.
- Hardware-landscape table 8814AU row: 2.4 GHz still RX-only,
UNII-1 unchanged from prior status, UNII-2/3 now produces
on-air TX. Notes column documents the remaining gaps (2.4 GHz
TX, 5 GHz RX on devourer-side TX).
- Demo env vars: add DEVOURER_SKIP_TXPWR, DEVOURER_FORCE_IQK,
DEVOURER_DISABLE_IQK, DEVOURER_PHYDM_WATCHDOG,
DEVOURER_DUMP_CANARY — all knobs that this branch exposes or
relies on.
- Project layout: add Iqk8814a (4-path IQK port) and
PhydmWatchdog (opt-in DM thread).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace 'TX on-air; RX gated' with 'TX only'. Symmetric with the 'RX only' values in the other 8814AU cells, says what's working without hedging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous edit dropped 'RX only' → 'TX only' without testing that RX actually stopped working. The PR adds TX on-air, doesn't touch RX behaviour; the original 'RX only' claim stands. Combine to 'TX + RX'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
End-to-end port of the 8814AU-specific band-switch + channel-set chain from upstream
aircrack-ng/rtl8812au/hal/rtl8814a/, plus phydm scaffolding for future DM tuning.Devourer was running
PHY_SwitchWirelessBand8812+phy_SwChnl8812+phy_PostSetBwMode8812for all chips including 8814 — these are 8812-specific functions and upstream gates them behindIS_HARDWARE_TYPE_8812. The 8814 path needs the chip-specific variants fromrtl8814a_phycfg.c. This PR ports the full chain.Commits (14)
42ad707PHY_SwitchWirelessBand8814A+ RFE pinmux paths A/B/C/D + BW helpers + CCK_CHECK address fix492af5fphy_SwChnl8814A+phy_PostSetBwMode8814A+phy_FixSpur_8812Achip gate75e72caIqk8814a— 4-path I/Q calibration porta460b32iw set channelbehaviour — noHW_VAR_DO_IQKfire)048cc9cd5546efebd30a497f089bef10745DEVOURER_PHYDM_WATCHDOG=1(TX-throughput contention fix)3ffe688,36e618e,14e8badHardware verification
Run on RTL8814AU + RTL8821AU plugged into host, fresh USB port-level power-cycle between runs.
8814 ch100 TX → 8821 RX (matrix, 10 s/cell):
8821 ch100 TX → 8814 RX (regression check):
Canary diff vs kernel reference (8814AU, ch100, 5 GHz):
The 0-divergence result is via the new mask additions in
tests/canary_diff.pyfor:Phydm DM watchdog
Ported as scaffolding for future RSSI / CFO / antenna-diversity DM modules. Off by default — the watchdog thread's BB reads/writes contend with the TX bulk path via libusb's transfer queue, measurably dropping sustained TX throughput. Available via
DEVOURER_PHYDM_WATCHDOG=1for canary-diff workflows and RX-only DIG tuning.Out of scope
Test plan
cmake --build build -jclean (gcc/clang/cl on macOS/ubuntu/windows via existing CI)pytest tests/test_canary_diff.py— 21/21 passing locallyCanary diff unit tests+CMake on multiple platforms) green on the PR