diff --git a/CLAUDE.md b/CLAUDE.md index 17e023d..9754624 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,9 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**): Sustained 5 GHz TX needs the **coex runtime thread** (`RtlJaguar3Device::coex_runtime_loop`, started in `InitWrite`) — without its ~2 s WiFi-only coex re-apply + FW heartbeats, the combo chip's coex firmware - silences the antenna. + silences the antenna. The rtl8822e's hardware-bisected constraints (DPDT/ + pin-mux front end, single-path 1SS TX, spur channels, LCK, the 2.4 GHz TX + kernel-parity limitation) live in `docs/8822e-quirks.md`. Naming traps: **RTL8821AU is Jaguar1** (not Jaguar2, despite the Jaguar2 RTL8821C's similar name); RTL8822**B**U (Jaguar2) ≠ RTL8822**C**U (Jaguar3). @@ -224,9 +226,7 @@ Knob-specific facts that aren't obvious from the field docs: - `DEVOURER_TX_WITH_RX=thread` (concurrent TX+RX on one claimed handle: `InitWrite` once, then `StartRxLoop` on a thread) must be set **before** `InitWrite` on Jaguar3 — the bring-up keeps the RX filters open; retrofitting - RX later is unreliable. On the 8822E, TX+RX mode leaves the path-B OFDM TXAGC - reference (0x41e8) at table default — any nonzero value there desenses the - EU's RX to near-deaf (hardware-bisected, value-independent). This is the + RX later is unreliable. This is the single-radio beamforming self-sounding station: pair with `DEVOURER_BF_ARM_SOUNDER` / `DEVOURER_TX_NDPA` / `DEVOURER_BF_DETECT_REPORT` (`docs/beamforming-self-sounding.md`). Non-`thread` values select a diff --git a/CMakeLists.txt b/CMakeLists.txt index c0347c7..c9ffc35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -270,6 +270,7 @@ if(DEVOURER_JAGUAR3) src/jaguar3/HalmacJaguar3MacInit.cpp src/jaguar3/HalmacJaguar3MacInit.h src/jaguar3/HalmacJaguar3Regs.h src/jaguar3/RadioManagementJaguar3.cpp src/jaguar3/RadioManagementJaguar3.h + src/jaguar3/PhydmRuntimeJaguar3.cpp src/jaguar3/PhydmRuntimeJaguar3.h src/jaguar3/FrameParserJaguar3.h src/jaguar3/PhyTableLoaderJaguar3.cpp src/jaguar3/PhyTableLoaderJaguar3.h src/jaguar3/Phy8822cTables.cpp diff --git a/README.md b/README.md index 7911772..320789e 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Bandwidth cells are devourer's measured on-air TX throughput (Mbps, HT MCS7, | **RTL8821CU** | 1T1R + BT | — | — | — | rides the 8811CU (8821C) code path. 5/10 MHz capable | | **RTL8812CU** | 2T2R | 65 | 60 | 60 | LB-LINK WDN1300H (`0bda:c812`). 5/10 MHz capable | | **RTL8822CU** | 2T2R + BT | — | — | — | not benchmarked (`0bda:c82c`). 5/10 MHz capable | -| **RTL8812EU** | 2T2R | 8 | 51 | 47 | LB-LINK BL-M8812EU2 (`0bda:a81a`); bare 5 GHz FPV module. 5/10 MHz capable | +| **RTL8812EU** | 2T2R | ‡ | 51 | 47 | LB-LINK BL-M8812EU2 (`0bda:a81a`); bare 5 GHz FPV module. 5/10 MHz capable. ‡ 2.4 GHz TX airs energy but no receiver decodes it — the vendor kernel driver behaves identically on this module ([quirks](docs/8822e-quirks.md)) | | **RTL8822EU** | 2T2R + BT | — | — | — | not benchmarked. 5/10 MHz capable | | **RTL8821CE** (PCIe) | 1T1R + BT | — | — | — | Radxa X4 onboard Wi-Fi (`10ec:c821`); not benchmarked | diff --git a/docs/8822e-quirks.md b/docs/8822e-quirks.md new file mode 100644 index 0000000..96afdb2 --- /dev/null +++ b/docs/8822e-quirks.md @@ -0,0 +1,166 @@ +# RTL8822E (RTL8812EU / RTL8822EU) — definitive quirks + +The rtl8822e family (WiFi-only **RTL8812EU** `0bda:a81a`, BT-combo **RTL8822EU** +`0bda:e822`, chip-id `0x17`, Jaguar3) as devourer drives it today. Every entry +states what the chip needs, what devourer does about it, the residual cost if +any, and the reproducer that proves it. Reference hardware: LB-LINK +BL-M8812EU2 (`0bda:a81a`, rfe-type 21); the BT-combo part and a second board +for variance remain untested (see **Untested**). + +The methodology that closed most of these: vendor-kernel ground truth on the +same unit (`tests/eu_kernel_mcs_probe.sh`), full MAC+BB end-state diff +kernel-vs-devourer (`tests/eu_bb_endstate_diff.sh` — kernel via `/proc` +`read_reg` live, devourer via `DEVOURER_BB_DUMP`), then subset bisection with +the Jaguar3 golden-replay hook (`DEVOURER_REPLAY_WSEQ`). + +## Front-end pin-mux is load-bearing (DPDT / PAPE / pads) + +The chip's RFE control lines (DPDT antenna transfer switch, PA-enable, pad +routing) ride MAC GPIO/LED pin-mux registers that the halmac "Config PIN Mux" +family programs. Three of these must hold, or TX breaks in distinctive ways: + +- **`REG_LED_CFG 0x4c[24]` = `BIT_DPDT_WLBT_SEL`** (with `[22]` clear): routes + the DPDT switch to WL control. Without it, every PPDU faster than ~26 Mbps + PHY (HT MCS4+, legacy 48M/54M) airs at full duty as garbage no receiver can + even sync to, while MCS0–3 / ≤24M leak through at a degraded EVM floor + (~−30 dB vs kernel-parity −50s). Power/calibration/payload-invariant — + bench-bisected to this single register. Written post-coex in `InitWrite` + (the FW H2C steps run earlier). NB `0x204c` is a readable mirror of `0x4c` + — end-state diffs surface it at either address. +- **OFDM 1SS TX path must be single-path** (`0x820[7:0]`: `0x32` = 1ss-B at + 5 GHz, `0x31` at 2.4 GHz; `0x1e2c=0x0400`): kernel semantics give 1SS frames + ONE chain (2SS gets both). The old 1ss-on-both mapping (`0x33`) interacts + with the DPDT fix — MCS0 TX stalls (bulk NAK wedge) and MCS7 delivery + collapses. Path B is selected at 5 GHz on measured merit (MCS7 ~2.5× the + delivery of 1ss-A at ~3 dB better EVM on this module); the 2.4 GHz choice is + the kernel default, unvalidated (see **2.4 GHz TX**). +- **`PAD_CTRL1 0x64[29:28]`**: halmac pre-init sets both; the bring-up's + FW/coex steps clear bit 29 — re-asserted post-coex. + +Reproducers: `tests/eu_mcs7_txagc_fix.sh` (parametrized TX-rate/power cell +bench, EU→CU), `tests/eu_bb_endstate_diff.sh`. Bench state: full ladder clean +at ch36 — MCS0/MCS4/MCS7/54M ≈ 5k frames / 12 s at EVM −26/−32/−48/−45. + +## TX+RX mode and the path-B TXAGC reference (0x41e8) + +Path-B OFDM TXAGC (`0x41e8`) is written unconditionally in every mode, +including TX+RX. (An earlier structural skip existed because any nonzero write +appeared to near-deafen the EU's RX; that desense was a downstream artifact of +the DPDT/pin-mux mis-config above and does not reproduce on the fixed driver: +`tests/eu_41e8_desense_recheck.sh`, −2% = noise.) Full-duplex proof with +path-B power applied: `tests/eu_fullduplex_pathb_check.sh` — 24k RX frames +and 14.3k clean MCS7 at the ground simultaneously. + +`DEVOURER_TX_WITH_RX=thread` must still be set **before** `InitWrite` +(bring-up keeps the RX filters open; retrofitting RX later is unreliable). + +## RF register 0x0 needs the legacy FON write port + +RF reg `0x0` (mode register) writes through the direct `0x3c00`/`0x4c00` BB +window silently no-op — the kernel special-cases it through the FON port +`0x1808`/`0x4108` (`addr<<20 | data`). Devourer does the same in the halrf +code and the radio-table loader (the vendor tables carry reg-0 entries). +Reads stay direct-window for every register. Same rule on the 8822C. + +## Channel-switch obligations (run at EVERY switch) + +Ported straight from the kernel's `switch_channel` tail; skipping any of these +was a real failure on the bench: + +- **IGI toggle** (`0x1d70` −0x202/restore): forces the BB to send the 3-wire + command so the RF re-enters RX mode — the BB does not do this on its own + after path/channel/BW changes. Symptoms when missing: intermittent + post-switch RX deafness and a first-cell TX wedge (~50 frames then bulk + NAK). Applies to both Jaguar3 variants. Liveness reproducer: + `tests/cu_2g_ground_liveness.sh`. +- **Spur elimination**: per-channel NBI notch + CSI mask + packet-detection + for the 14 channel/BW combos whose synthesizer harmonics land in-band + (153/161/169@20, 151/159/167@40, 155/171@80, 54/102/118@40, 58/106/122@80), + explicit spur-free default elsewhere. `FastRetune` declines hops into or + out of these combos (the lean hop can't reprogram the notch state). +- **CCK TX shaping filter** (2.4 GHz, per channel; ch14 special set) + per-band + TX backoff/scaling. +- 2.4 GHz RX needs the RF-write force-update brackets (`0x1830[29]`/ + `0x4130[29]`) around the RF18 tune — the BB-window write path does not push + the analog front-end shadow on its own. + +## Runtime obligations (the ~2 s watchdog cadence) + +devourer runs the vendor watchdog's monitor-mode dynamic mechanisms +(`PhydmRuntimeJaguar3`, both Jaguar3 variants) — from the coex thread in +TX/TX+RX sessions, from a dedicated housekeeping thread in RX-only sessions +(register I/O must not run on the bulk-IN event thread): + +- **FA/CCA window statistics** (read + reset per tick) feeding +- **DIG**: unlinked IGI stepping by false-alarm level (thresholds + 2000/4000/5000 per window; DFS channels pin `0x20`), written to `0x1d70` + per path. The coverage window floors at `0x1e`, NOT phydm's generic + `DIG_MIN_COVERAGE 0x1c`: at IGI `0x1c` the 8822CU's MCS4+/dense-QAM RX + decodes nothing (0 of 65k kernel-injected MCS7 frames) while `0x1e` is + transparent (65.9k/65k) — hardware-bisected, value-specific, the same + class of per-IC "For HW setting" floor exception the kernel carries for + other ICs. +- **CCK packet detection** (type4, 2.4 GHz): CCK-FA moving average drives the + PD/CS level ladder in `0x1ac8/0x1acc/0x1ad0`. +- **EDCCA tracking**: `th_l2h = max(IGI+8, 48)`, `th_h2l = th_l2h − 8` into + `0x84c` (matches the kernel end state). `SetCcaMode`'s EDCCA-disable knob + suppresses this tracking. + +The kernel's remaining watchdog mechanisms self-disable without a link and +are intentionally absent: CFO tracking (returns unless associated), rate +adaptation and the beamforming watchdog (no station entries), antenna +diversity (off on 2T2R). + +## Coex / thermal (the same tick) + +- **Coex re-apply + FW heartbeats**: without the WiFi-only coex re-apply the + combo firmware silences the antenna during sustained 5 GHz TX (shared + Jaguar3 behavior). +- **Thermal swing tracking**: RF `0x42[6:1]` meter → delta-swing table → + `0x18a0/0x41a0[7:0]`. The chip runs THERMAL mode (the kernel's TSSI + machinery is compiled in but its efuse mode-select is forced to thermal; + `0x1e7c[30]`=0 on both drivers — the 2600-line TSSI subsystem is *not* a + devourer gap). +- **LCK synthesizer re-lock**: when the averaged thermal drifts ≥4 units from + the LCK baseline, re-run AACK+RTK (RF-A `0xca[0]` / `0xcc[18]` pulse+poll — + the SYN lives on path A). Validated live: 22-minute max-duty soak + (`tests/eu_heat_soak_lck.sh`) — 1.10 M clean MCS7 frames, re-lock fired at + drift=4, EVM drift +3 dB. +- The FW power-mode/coex H2Cs **rewrite the OFDM TXAGC refs wholesale** during + bring-up — devourer's authoritative TXAGC apply runs after them (the coex + runtime ticks do not re-clobber; watchable via the coex-tick register trace + under `DEVOURER_LOG_LEVEL=debug`). + +## 2.4 GHz TX: undecodable on this module — kernel parity + +The module airs 2.4 GHz TX energy (SDR duty ≈ expected) but **no receiver +decodes any of it** — CCK and OFDM alike, at any power, under **both** +devourer and the vendor kernel driver (`tests/eu_kernel_2g_verified.sh`: the +same verified ground decodes an 8812AU control at −63 EVM while 51k +kernel-injected EU frames yield zero). Treat 2.4 GHz TX as unusable on the +BL-M8812EU2 until a second board proves it module-specific vs family-wide. +2.4 GHz **RX** works. (The old README "8 Mbps @ ch6" figure was SDR +duty×rate — energy, not decodable throughput.) + +## EFUSE / bring-up constraints + +- **OTP/efuse is only reliably readable at init** (burst mode + sw-power-cut, + one-shot); post-bring-up probes are refused by design. +- Efuse supplies: per-channel-group per-path TXAGC base refs (`0x22`/`0x4C` + logical), rfe-type (`0xca`), thermal baselines (`0xd0`/`0xd1`). +- **DPK is force-bypassed** on rfe 21/22 — by the kernel too; both drivers run + DPK-bypassed. +- **IQK clears the 40 MHz `TX_CCK_IND` RF bit** — calibration-order fix-up in + the channel path. +- The TXAGC ref→power transfer is **not a constant 0.25 dB/step** on the E + (accelerating ~0.3→0.9 dB/idx up the range; TSSI/kfree trims reshape it) — + `GetTxPowerCaps().step_measured` stays false; controllers should calibrate + their own dB-per-step or use ground RSSI. + +## Untested + +- **RTL8822EU (`0bda:e822`, BT combo)**: entirely untested — including whether + it needs more than the WiFi-only coex handling the CU-style combo parts get. +- **Board variance**: all of the above is one LB-LINK BL-M8812EU2 (rfe 21). + rfe 22–24 boards differ in RFE pin mapping and DPK policy; the 2.4 GHz TX + verdict especially needs a second board. diff --git a/examples/tx/main.cpp b/examples/tx/main.cpp index 0e6c779..2c85a26 100644 --- a/examples/tx/main.cpp +++ b/examples/tx/main.cpp @@ -523,6 +523,17 @@ int main(int argc, char **argv) { if (const char *p = std::getenv("DEVOURER_TX_PWR")) rtlDevice->SetTxPower(static_cast(std::strtol(p, nullptr, 0))); + /* DEVOURER_TX_PWR_OFFSET_QDB=N — quarter-dB offset relative to the + * efuse-calibrated per-rate/per-path default (SetTxPowerOffsetQdb). + * Unlike DEVOURER_TX_PWR (flat: both paths forced to ONE index, per-rate + * diffs zeroed) this PRESERVES the per-path trim and by-rate shape — + * e.g. -44 on an 8822e shifts both OFDM refs down 11 dB while keeping + * the path-A/path-B calibration spread. Recorded now, applied at + * bring-up. */ + if (const char *p = std::getenv("DEVOURER_TX_PWR_OFFSET_QDB")) + rtlDevice->SetTxPowerOffsetQdb( + static_cast(std::strtol(p, nullptr, 0))); + /* DEVOURER_TX_PKT_OFSET=N — (Jaguar2 8822B/8821C) default per-packet * TXPWR_OFSET LUT step written into every TX descriptor: 0=none, 1=-3dB, * 2=-7dB, 3=-11dB, 4=+3dB, 5=+6dB. The measurement driver for the descriptor diff --git a/src/jaguar3/HalJaguar3.cpp b/src/jaguar3/HalJaguar3.cpp index 709854c..d299c2e 100644 --- a/src/jaguar3/HalJaguar3.cpp +++ b/src/jaguar3/HalJaguar3.cpp @@ -262,15 +262,21 @@ void HalJaguar3::config_channel_8822e(uint8_t channel) { _device.phy_set_bb_reg(0x81c, 0x001fc000, 0x4); /* Tx scaling */ } - /* phydm_config_tx_path_8822e (BB_PATH_AB): OFDM + CCK TX antenna routing. The - * phy_reg table leaves 0x820 at its default (0x11111111 = path-A-only on [7:0]); - * the vendor runtime overrides it to the AB mapping. Without this the OFDM TX - * path is not routed to both antennas — on this module the 5 GHz output collapses - * (the antenna for 5 GHz is not on the default path) while 2 GHz still works. - * OFDM 0x820[7:0]=0x33 (AB 2ss/1ss) + 0x1e2c[15:0]=0x0404 + /* phydm_config_tx_path_8822e: OFDM + CCK TX antenna routing. The phy_reg + * table leaves 0x820 at its default (0x11111111 = 2ss/1ss both path-A on + * [7:0]); the vendor runtime overrides it. Kernel semantics + * (rtw_hal_runtime_trx_path_decision): 2SS rates drive both chains, 1SS + * rates (every HT MCS / legacy OFDM frame) drive ONE selected path — a + * 1ss-on-both mapping (0x33/0x0404) sums two chains and interacts with + * the DPDT pin-mux write in RtlJaguar3Device::InitWrite (MCS0 TX stalls, + * MCS7 delivery collapses). At 5 GHz 1SS selects path B: bench + * head-to-head on the BL-M8812EU2 it delivers ~2.5x the MCS7 frames of + * 1ss-A at ~3 dB better EVM. At 2.4 GHz 0x31 is the kernel's default + * (unvalidated on-air here — see docs/8822e-quirks.md, 2.4 GHz TX). + * OFDM 0x820[7:0]=0x31/0x32 (2ss AB, 1ss A@2G / B@5G) + 0x1e2c=0x0400 * CCK 0x1a04[31:28]=0xc (AB) */ - _device.phy_set_bb_reg(0x820, 0xff, 0x33); - _device.phy_set_bb_reg(0x1e2c, 0xffff, 0x0404); + _device.phy_set_bb_reg(0x820, 0xff, is_2g ? 0x31 : 0x32); + _device.phy_set_bb_reg(0x1e2c, 0xffff, 0x0400); _device.phy_set_bb_reg(0x1a04, 0xf0000000, 0xc); /* phydm_tx_triangular_shap_cfg_8822e: CFR + triangular TX shaping (both bands). */ @@ -903,6 +909,15 @@ void HalJaguar3::apply_bb_rf_agc_tables() { case 0xffe: std::this_thread::sleep_for(std::chrono::milliseconds(50)); return; case 0xfe: std::this_thread::sleep_for(std::chrono::microseconds(100)); return; case 0xffff: std::this_thread::sleep_for(std::chrono::microseconds(1)); return; + case 0x0: + /* RF reg 0x0 (mode register) can't be written through the direct + * window — it silently no-ops (hardware-observed on the 8822e). The + * kernel's table load routes it through config_phydm_write_rf_reg, + * i.e. the legacy FON write port 0x1808 (A) / 0x4108 (B), addr in + * [27:20] (0 here), data in [19:0]. The radio tables DO carry reg-0 + * entries (0x00010000 / 0x0003001F), so this branch is load-bearing. */ + _device.rtw_write32(base == 0x4c00 ? 0x4108 : 0x1808, data & RFREG_MASK); + return; default: _device.phy_set_bb_reg(static_cast(base + ((addr & 0xff) << 2)), RFREG_MASK, data); diff --git a/src/jaguar3/Halrf8822c.cpp b/src/jaguar3/Halrf8822c.cpp index aedd27c..b19810a 100644 --- a/src/jaguar3/Halrf8822c.cpp +++ b/src/jaguar3/Halrf8822c.cpp @@ -82,11 +82,28 @@ uint32_t Halrf8822c::rf_read(uint8_t path, uint16_t addr, uint32_t mask) { return bb_get(direct, mask & RFREG_MASK); } -/* config_phydm_direct_write_rf_reg_8822c: RF write via the same window. */ +/* config_phydm_write_rf_reg_8822c: RF reg 0x0 (the mode register) canNOT be + * written through the direct 0x3c00/0x4c00 window — the kernel routes it + * through the legacy FON write port 0x1808 (A) / 0x4108 (B), addr in [27:20], + * data in [19:0] (same split as the 8822e, where the direct-window write was + * hardware-observed to silently no-op). Every other register goes through the + * direct window (config_phydm_direct_write_rf_reg_8822c). */ void Halrf8822c::rf_write(uint8_t path, uint16_t addr, uint32_t mask, uint32_t val) { + mask &= RFREG_MASK; + if ((addr & 0xff) == 0x0) { + uint32_t data = val; + if (mask != RFREG_MASK) { + uint32_t orig = rf_read(path, addr, RFREG_MASK); + data = (orig & ~mask) | ((val << mask_shift(mask)) & mask); + } + uint32_t data_and_addr = + (((addr & 0xffu) << 20) | (data & 0x000fffffu)) & 0x0fffffffu; + _device.rtw_write32(path & 1 ? 0x4108 : 0x1808, data_and_addr); + return; + } uint16_t direct = static_cast(RF_WIN[path & 1] + ((addr & 0xff) << 2)); - bb_set(direct, mask & RFREG_MASK, val); + bb_set(direct, mask, val); } /* _iqk_nctl_8822c — load the IQK calibration-engine microcode (generated table diff --git a/src/jaguar3/Halrf8822e.cpp b/src/jaguar3/Halrf8822e.cpp index a71e1a9..3b08fa2 100644 --- a/src/jaguar3/Halrf8822e.cpp +++ b/src/jaguar3/Halrf8822e.cpp @@ -54,8 +54,26 @@ uint32_t Halrf8822e::rf_read(uint8_t path, uint16_t addr, uint32_t mask) { } void Halrf8822e::rf_write(uint8_t path, uint16_t addr, uint32_t mask, uint32_t val) { + /* config_phydm_write_rf_reg_8822e: RF reg 0x0 (the mode register) canNOT be + * written through the direct 0x3c00/0x4c00 window — that write silently + * no-ops (hardware-observed: TXGAPK's gain-index select via RF 0x0 never + * took, so the 5 GHz gain-table readback came back all-zero). It must go + * through the legacy FON write port 0x1808 (A) / 0x4108 (B), addr in + * [27:20], data in [19:0]. Reads stay direct for every register. */ + mask &= RFREG_MASK; + if ((addr & 0xff) == 0x0) { + uint32_t data = val; + if (mask != RFREG_MASK) { + uint32_t orig = rf_read(path, addr, RFREG_MASK); + data = (orig & ~mask) | ((val << mask_shift(mask)) & mask); + } + uint32_t data_and_addr = + (((addr & 0xffu) << 20) | (data & 0x000fffffu)) & 0x0fffffffu; + _device.rtw_write32(path & 1 ? 0x4108 : 0x1808, data_and_addr); + return; + } uint16_t direct = static_cast(RF_WIN[path & 1] + ((addr & 0xff) << 2)); - bb_set(direct, mask & RFREG_MASK, val); + bb_set(direct, mask, val); } /* --- calibration (Phase C: ported incrementally, hardware-iterated) --- */ @@ -1158,6 +1176,22 @@ void Halrf8822e::thermal_track_8822e() { sum += _therm_avg[p][i]; uint8_t avg = static_cast(sum / _therm_avg_cnt[p]); + /* LCK track (halrf_lck_track_8822e): the RF synthesizer's VCO drifts + * with temperature — when the averaged thermal moves >= 4 units from the + * LCK baseline, re-run the synthesizer calibration (AACK+RTK) and + * re-base. Without it, hours of sustained TX let the LO wander with chip + * heating (spectral regrowth / EVM drift). The kernel runs this from the + * same ~2 s watchdog as the swing tracking. */ + if (_lck_base[p] < 0) { + _lck_base[p] = avg; /* first valid average = baseline */ + } else if (avg - _lck_base[p] >= 4 || _lck_base[p] - avg >= 4) { + _logger->info("Jaguar3(8822e): LCK re-lock (path{} thermal avg={} " + "lck_base={})", + p, avg, _lck_base[p]); + lck_trigger(); + _lck_base[0] = _lck_base[1] = avg; + } + int delta = avg > base ? (avg - base) : (base - avg); if (delta >= D_S) delta = D_S - 1; @@ -1177,6 +1211,31 @@ void Halrf8822e::thermal_track_8822e() { } } +/* phy_lc_calibrate_8822e — the LCK trigger: AACK (RF-A 0xca[0] pulse, poll + * 0xc9[5] clear) then RTK (RF-A 0xcc[18] pulse, poll 0xce[11] clear, then + * de-assert). Path A only — the synthesizer lives on path A. ~ms-scale; the + * kernel fires it live from its watchdog, so a brief mid-TX glitch is the + * vendor-sanctioned behavior. */ +void Halrf8822e::lck_trigger() { + /* AACK */ + rf_write(0, 0xca, 1u << 0, 0x0); + rf_write(0, 0xca, 1u << 0, 0x1); + for (int i = 0; i < 100; ++i) { + delay_ms(1); + if (rf_read(0, 0xc9, 1u << 5) != 0x1) + break; + } + /* RTK */ + rf_write(0, 0xcc, 1u << 18, 0x0); + rf_write(0, 0xcc, 1u << 18, 0x1); + for (int i = 0; i < 100; ++i) { + delay_ms(1); + if (rf_read(0, 0xce, 1u << 11) != 0x1) + break; + } + rf_write(0, 0xcc, 1u << 18, 0x0); +} + /* Coex / antenna control — port of the 8822c WiFi-only coex (Halrf8822c). Even * though the EU parts have no BT combo, the chip still powers up with the * LTE/BTC coex arbitration ENABLED, which gates the WL TRX path: with it left at diff --git a/src/jaguar3/Halrf8822e.h b/src/jaguar3/Halrf8822e.h index 16bed0b..7b5c280 100644 --- a/src/jaguar3/Halrf8822e.h +++ b/src/jaguar3/Halrf8822e.h @@ -241,6 +241,11 @@ class Halrf8822e : public Jaguar3Calibration { uint8_t _therm_avg_idx[2] = {0, 0}; uint8_t _therm_avg_cnt[2] = {0, 0}; int _last_swing[2] = {0x7fff, 0x7fff}; /* log-on-change sentinel */ + /* LCK (synthesizer re-lock) thermal baseline — halrf_lck_track_8822e: + * re-run AACK+RTK when the averaged thermal drifts >= 4 units from this + * base (VCO temperature coefficient); -1 = capture on first tick. */ + int _lck_base[2] = {-1, -1}; + void lck_trigger(); /* phy_lc_calibrate_8822e: AACK + RTK, path A (SYN) */ RtlAdapter _device; Logger_t _logger; diff --git a/src/jaguar3/PhydmRuntimeJaguar3.cpp b/src/jaguar3/PhydmRuntimeJaguar3.cpp new file mode 100644 index 0000000..58f843c --- /dev/null +++ b/src/jaguar3/PhydmRuntimeJaguar3.cpp @@ -0,0 +1,213 @@ +#include "PhydmRuntimeJaguar3.h" + +namespace jaguar3 { + +namespace { +/* phydm_dig.h coverage bounds — the unlinked DIG operating window. */ +/* phydm's generic unlinked floor is DIG_MIN_COVERAGE 0x1c, but the kernel + * carries per-IC "For HW setting" exceptions (e.g. 0x1e on the 8197F) — and + * the Jaguar3 parts need one: at IGI 0x1c the 8822CU's MCS4+ RX decodes + * NOTHING (0 of 65k kernel-injected MCS7 frames) while 0x1e is transparent + * (65.9k of 65k) — hardware-bisected, value-specific. Floor at 0x1e. */ +constexpr uint8_t DIG_MIN_COVERAGE = 0x1e; +constexpr uint8_t DIG_MAX_OF_MIN_COVERAGE = 0x22; +constexpr uint8_t DIG_MIN_DFS = 0x20; +/* phydm_adaptivity.h normal-mode EDCCA constants. */ +constexpr int TH_L2H_DIFF_IGI = 8; +constexpr int EDCCA_TH_L2H_LB = 48; +constexpr int EDCCA_HL_DIFF_NORMAL = 8; +} // namespace + +/* IGI lives in 0x1d70, 7-bit field per RX path (A [6:0], B [14:8]) — + * phydm_write_dig_reg_jgr3 / phydm_get_igi_reg_val_jgr3. */ +uint8_t PhydmRuntimeJaguar3::get_igi() { + return static_cast(_device.rtw_read32(0x1d70) & 0x7f); +} +void PhydmRuntimeJaguar3::set_igi(uint8_t igi) { + _device.phy_set_bb_reg(0x1d70, 0x0000007f, igi); + _device.phy_set_bb_reg(0x1d70, 0x00007f00, igi); +} + +/* phydm_fa_cnt_statistics_jgr3 + phydm_false_alarm_counter_reg_reset: read + * the per-window false-alarm and CCA counters, then clear and re-arm them so + * the next tick sees a fresh window. */ +PhydmRuntimeJaguar3::FaStats PhydmRuntimeJaguar3::fa_statistics_and_reset() { + FaStats s; + uint32_t v; + + v = _device.rtw_read32(0x2d04); + uint32_t ofdm_fail = (v >> 16) & 0xffff; /* parity fail */ + v = _device.rtw_read32(0x2d08); + ofdm_fail += (v & 0xffff) + ((v >> 16) & 0xffff); /* rate illegal + crc8 */ + v = _device.rtw_read32(0x2d10); + ofdm_fail += (v & 0xffff) + ((v >> 16) & 0xffff); /* mcs fail (HT + VHT) */ + v = _device.rtw_read32(0x2d0c); + ofdm_fail += v & 0xffff; /* crc8 fail (VHT SIG-A) */ + v = _device.rtw_read32(0x2d20); + ofdm_fail += (v & 0xffff) + ((v >> 16) & 0xffff); /* fast_fsync + sb_search */ + + s.cck_fa = _device.rtw_read32(0x1a5c) & 0xffff; + + v = _device.rtw_read32(0x2c08); + const uint32_t ofdm_cca = (v >> 16) & 0xffff; + const uint32_t cck_cca = v & 0xffff; + + /* CCK contributes only when its RxIQ weighting is enabled (2.4 GHz — + * 0x1a14[9:8] == 0), mirroring the channel path's cck_rxiq toggle. */ + s.cck_enabled = ((_device.rtw_read32(0x1a14) >> 8) & 0x3) == 0; + s.fa_all = ofdm_fail + (s.cck_enabled ? s.cck_fa : 0); + s.cca_all = ofdm_cca + (s.cck_enabled ? cck_cca : 0); + + /* Counter reset: CCK FA (0x1a2c[15:14] 0->2) and CCK CCA ([13:12] 0->2), + * then the OFDM/CCA block via the page-2 counter reset (0x1eb4[25] pulse) + * bracketed by the RX clock-gate bit (0x1d2c[31]). */ + _device.phy_set_bb_reg(0x1a2c, 0x0000c000, 0); + _device.phy_set_bb_reg(0x1a2c, 0x0000c000, 2); + _device.phy_set_bb_reg(0x1a2c, 0x00003000, 0); + _device.phy_set_bb_reg(0x1a2c, 0x00003000, 2); + _device.phy_set_bb_reg(0x1d2c, 1u << 31, 0); + _device.phy_set_bb_reg(0x1eb4, 1u << 25, 1); + _device.phy_set_bb_reg(0x1eb4, 1u << 25, 0); + _device.phy_set_bb_reg(0x1d2c, 1u << 31, 1); + return s; +} + +/* phydm_dig, unlinked path: step the IGI by the per-window false-alarm level + * — fa_th {2000, 4000, 5000} steps {+2, +1} up / -2 down (DFS band: + * {250, 1000, 2000} with the IGI pinned at DIG_MIN_DFS by the boundary + * rules), clamped to the coverage window. */ +void PhydmRuntimeJaguar3::dig(const FaStats &fa, bool is_dfs) { + int igi = get_igi(); + + uint32_t th0 = 2000, th1 = 4000, th2 = 5000; + uint8_t lo = DIG_MIN_COVERAGE, hi = DIG_MAX_OF_MIN_COVERAGE; + if (is_dfs) { + th0 = 250; th1 = 1000; th2 = 2000; + lo = DIG_MIN_DFS; hi = DIG_MIN_DFS; + } + + if (fa.fa_all > th2) + igi += 2; + else if (fa.fa_all > th1) + igi += 1; + else if (fa.fa_all < th0) + igi -= 2; + + if (igi < lo) + igi = lo; + if (igi > hi) + igi = hi; + + if (igi != get_igi()) + set_igi(static_cast(igi)); + if (igi != _last_igi_logged) { + _last_igi_logged = igi; + _logger->info("Jaguar3 dig: IGI=0x{:02x} (fa={} cca={})", igi, fa.fa_all, + fa.cca_all); + } +} + +/* phydm_cckpd_type4, unlinked: 75%-smoothed CCK-FA moving average drives a + * two-level packet-detection ladder (>1000 -> LV1, <500 -> LV0, else hold). + * The per-level PD/CS values derive from the BB-table defaults in + * 0x1ac8/0x1acc/0x1ad0 plus the 8822C/E step ladder + * (phydm_cck_pd_init_type4); the level is applied per current bw/nrx + * (phydm_set_cck_pd_lv_type4 / phydm_write_cck_pd_type4). */ +void PhydmRuntimeJaguar3::cck_pd(const FaStats &fa) { + if (!_cck_tbl_valid) { + const uint32_t r0 = _device.rtw_read32(0x1ac8); /* PD limit, BW20 per nrx */ + const uint32_t r1 = _device.rtw_read32(0x1acc); /* PD limit, BW40 per nrx */ + const uint32_t r2 = _device.rtw_read32(0x1ad0); /* CS ratio fields */ + static const uint8_t pw_step[5] = {0, 9, 12, 14, 17}; + static const uint8_t cs_step[5] = {0, 0, 1, 1, 1}; + for (int lv = 0; lv < 5; ++lv) { + _cckpd_tbl[0][0][0][lv] = static_cast((r0 & 0xff) + pw_step[lv]); + _cckpd_tbl[1][0][0][lv] = static_cast((r1 & 0xff) + pw_step[lv]); + _cckpd_tbl[0][0][1][lv] = + static_cast((r2 & 0x1f) + cs_step[lv]); + _cckpd_tbl[1][0][1][lv] = + static_cast(((r2 >> 20) & 0x1f) + cs_step[lv]); + _cckpd_tbl[0][1][0][lv] = + static_cast(((r0 >> 8) & 0xff) + pw_step[lv]); + _cckpd_tbl[1][1][0][lv] = + static_cast(((r1 >> 8) & 0xff) + pw_step[lv]); + _cckpd_tbl[0][1][1][lv] = + static_cast(((r2 >> 5) & 0x1f) + cs_step[lv]); + _cckpd_tbl[1][1][1][lv] = + static_cast(((r2 >> 25) & 0x1f) + cs_step[lv]); + } + _cck_tbl_valid = true; + } + + if (_cck_fa_ma == 0xffffffff) + _cck_fa_ma = fa.cck_fa; + else + _cck_fa_ma = (_cck_fa_ma * 3 + fa.cck_fa) >> 2; + + uint8_t lv; + if (_cck_fa_ma > 1000) + lv = 1; + else if (_cck_fa_ma < 500) + lv = 0; + else + return; /* hold */ + + /* nrx (0x1a2c[18:17]+1) and CCK bandwidth (0x9b0[3:2]) are re-checked at + * every apply like the vendor's; only 20/40 MHz have table entries. */ + const uint8_t nrx = + static_cast(((_device.rtw_read32(0x1a2c) >> 17) & 0x3) + 1); + const uint8_t bwc = + static_cast((_device.rtw_read32(0x9b0) >> 2) & 0x3); + if (bwc > 1 || nrx < 1 || nrx > 2) + return; + if (_cck_lv_valid && lv == _cck_lv && bwc == _cck_bw && nrx == _cck_nrx) + return; + _cck_lv = lv; + _cck_bw = bwc; + _cck_nrx = nrx; + _cck_lv_valid = true; + _cck_fa_ma = 0xffffffff; /* CCK_FA_MA_RESET on level apply */ + + const uint8_t pd = _cckpd_tbl[bwc][nrx - 1][0][lv]; + const uint8_t cs = _cckpd_tbl[bwc][nrx - 1][1][lv]; + if (bwc == 0) { /* BW20 */ + _device.phy_set_bb_reg(0x1ac8, nrx == 1 ? 0x000000ffu : 0x0000ff00u, pd); + _device.phy_set_bb_reg(0x1ad0, nrx == 1 ? 0x0000001fu : 0x000003e0u, cs); + } else { /* BW40 */ + _device.phy_set_bb_reg(0x1acc, nrx == 1 ? 0x000000ffu : 0x0000ff00u, pd); + _device.phy_set_bb_reg(0x1ad0, nrx == 1 ? 0x01f00000u : 0x3e000000u, cs); + } + _logger->info("Jaguar3 cckpd: lv={} pd=0x{:02x} cs=0x{:02x} (fa_ma window)", + lv, pd, cs); +} + +/* phydm_edcca_thre_calc_jgr3, normal (non-adaptivity-regulation) mode: + * th_l2h = max(igi + 8, 48), th_h2l = th_l2h - 8, written to 0x84c as + * biased bytes (value + 0x80): L2H in [23:16], H2L in [31:24]. */ +void PhydmRuntimeJaguar3::edcca(uint8_t igi) { + const int th_l2h = + (igi + TH_L2H_DIFF_IGI > EDCCA_TH_L2H_LB) ? igi + TH_L2H_DIFF_IGI + : EDCCA_TH_L2H_LB; + const int th_h2l = th_l2h - EDCCA_HL_DIFF_NORMAL; + _device.phy_set_bb_reg(0x84c, 0x00ff0000, + static_cast(th_l2h + 0x80) & 0xff); + _device.phy_set_bb_reg(0x84c, 0xff000000, + static_cast(th_h2l + 0x80) & 0xff); + if (th_l2h != _last_l2h_logged) { + _last_l2h_logged = th_l2h; + _logger->info("Jaguar3 edcca: L2H={} H2L={}", th_l2h, th_h2l); + } +} + +void PhydmRuntimeJaguar3::tick(uint8_t channel, bool edcca_track) { + const FaStats fa = fa_statistics_and_reset(); + /* DFS-band decision as the vendor driver's rtw_is_dfs_ch: 5 GHz ch 52-144. */ + const bool is_dfs = channel > 14 && channel >= 52 && channel <= 144; + dig(fa, is_dfs); + if (fa.cck_enabled) + cck_pd(fa); + if (edcca_track) + edcca(get_igi()); +} + +} // namespace jaguar3 diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h new file mode 100644 index 0000000..1d75749 --- /dev/null +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -0,0 +1,79 @@ +#ifndef DEVOURER_JAGUAR3_PHYDM_RUNTIME_H +#define DEVOURER_JAGUAR3_PHYDM_RUNTIME_H + +#include + +#include "logger.h" +#include "RtlAdapter.h" + +namespace jaguar3 { + +/* Port of the phydm dynamic mechanisms the vendor kernel runs from its ~2 s + * watchdog in a no-link (monitor) session — the RX-side adaptation loop: + * + * phydm_false_alarm_counter_statistics (phydm_fa_cnt_statistics_jgr3) + * phydm_dig (unlinked algorithm, IGI in 0x1d70) + * phydm_cck_pd_th (type4, 2.4 GHz CCK packet detection) + * phydm_adaptivity (EDCCA thresholds in 0x84c) + * + * The other watchdog mechanisms self-disable without a link and are not + * ported: CFO tracking returns unless is_linked && one entry, RA and the + * beamforming watchdog have no station entries, antenna diversity is off on + * the 2T2R parts. + * + * tick() is called from the coex/housekeeping thread every ~2 s under the + * device register lock. All state lives here; nothing is read from the + * environment. */ +class PhydmRuntimeJaguar3 { +public: + PhydmRuntimeJaguar3(RtlAdapter device, Logger_t logger) + : _device{device}, _logger{logger} {} + + /* 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); + + /* A channel/bandwidth change invalidates the CCK-PD applied-level cache + * (the kernel re-checks bw/nrx from registers each apply; the level is + * re-applied when they change). */ + void on_channel_change() { _cck_lv_valid = false; } + +private: + /* --- false-alarm / CCA statistics (this tick's window) --- */ + struct FaStats { + uint32_t fa_all = 0; + uint32_t cca_all = 0; + uint32_t cck_fa = 0; + bool cck_enabled = false; + }; + FaStats fa_statistics_and_reset(); + + void dig(const FaStats &fa, bool is_dfs); + void cck_pd(const FaStats &fa); + void edcca(uint8_t igi); + + uint8_t get_igi(); + void set_igi(uint8_t igi); + + RtlAdapter _device; + Logger_t _logger; + + /* DIG state */ + int _last_igi_logged = -1; + + /* CCK-PD state (phydm_cckpd_type4) */ + bool _cck_tbl_valid = false; + uint8_t _cckpd_tbl[2][2][2][5] = {}; /* [bw20/40][1r/2r][pd/cs][lv0..4] */ + uint32_t _cck_fa_ma = 0xffffffff; /* CCK_FA_MA_RESET sentinel */ + bool _cck_lv_valid = false; + uint8_t _cck_lv = 0, _cck_bw = 0, _cck_nrx = 0; + + /* EDCCA state (log-on-change) */ + int _last_l2h_logged = 0x7fff; +}; + +} // namespace jaguar3 + +#endif /* DEVOURER_JAGUAR3_PHYDM_RUNTIME_H */ diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index b7e0ce1..ac28d92 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -203,6 +203,13 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, _device.phy_set_bb_reg(0x1c80, 0x3F000000, 0x22); } + /* CCK TX shaping filter (phydm_cck_tx_shaping_filter_8822e): per-channel + * 16-tap shaping coefficients + CCK/OFDM TX backoff + scaling, written at + * every 2 GHz channel switch (ch14 gets its own tighter set for the Japan + * band edge). 8822E only — the 8822C uses a different shaping scheme. */ + if (is_2g && _variant == ChipVariant::C8822E) + cck_tx_shaping_8822e(central); + /* SCO tracking f_c (phydm_sco_trk_fc_setting_8822c): BB 0xc30[11:0]. Keyed * on the central channel (shared with fast_retune). */ _device.phy_set_bb_reg(0xc30, 0xfff, sco_for(central)); @@ -213,11 +220,31 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, /* MAC-side bandwidth + TX sub-channel (halmac cfg_bw / cfg_pri_ch_idx). */ set_mac_bw_txsc(bwmode, pri); + /* Spur elimination (phydm_spur_eliminate_8822e): NBI notch + CSI-mask + * setup for the channels whose synthesizer harmonics land in-band + * (5760/5280/5600 MHz spurs), and the explicit spur-free default state + * everywhere else — run at every switch_channel so no stale NBI/CSI + * state survives a channel change. 8822E only (the 8822C table differs). */ + if (_variant == ChipVariant::C8822E) + spur_eliminate_8822e(central, bwmode); + /* phydm_bb_reset_8822c: toggle the BB reset (MAC reg 0x0 BIT16, 1->0->1) to * (re)start the receiver after channel/BW config — the kernel does this after * every switch_channel; without it the RX engine never runs. */ bb_reset_toggle(); + /* phydm_igi_toggle_8822{c,e} (register-identical on both variants): force + * the BB to send the 3-wire command so the RF hardware re-enters RX mode — + * the BB does NOT do this automatically after path/channel/BW + * reconfiguration, and without the toggle the RF can be left in a stale + * mode after a full switch (intermittent RX deafness / early TX stall). + * The kernel runs it after every switch_channel on both dies. */ + { + const uint32_t igi = _device.rtw_read32(0x1d70); + _device.rtw_write32(0x1d70, igi - 0x202); + _device.rtw_write32(0x1d70, igi); + } + /* halrf_ex_dac_fifo_rst — the vendor runs this after EVERY switch_bandwidth * ("fix dac fifo error after TXCK setting"): 40/80 MHz change the TX clock, * so reset the DAC FIFO for vendor parity. 8822e only (its AFE DACK-bank soft @@ -236,6 +263,168 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, DumpCanary(); } +/* phydm_cck_tx_shaping_filter_8822e: per-channel CCK TX shaping coefficients + * + CCK/OFDM TX backoff + TX scaling. ch14 uses the tight Japan-band-edge + * set; every other 2G channel the standard one. */ +void RadioManagementJaguar3::cck_tx_shaping_8822e(uint8_t central) { + if (central == 14) { + _device.phy_set_bb_reg(0x1a20, 0xffff0000, 0x3da0); + _device.phy_set_bb_reg(0x1a24, 0xffffffff, 0x4962c931); + _device.phy_set_bb_reg(0x1a28, 0x0000ffff, 0x6aa3); + _device.phy_set_bb_reg(0x1a98, 0xffff0000, 0xaa7b); + _device.phy_set_bb_reg(0x1a9c, 0x0000ffff, 0xf3d7); + _device.phy_set_bb_reg(0x1aa0, 0xffffffff, 0x00000000); + _device.phy_set_bb_reg(0x1aac, 0xffffffff, 0xfe012577); + _device.phy_set_bb_reg(0x1ab0, 0xffffffff, 0x0000ffff); + _device.phy_set_bb_reg(0x818, 0xf8000000, 0x1e); /* Tx backoff CCK */ + _device.phy_set_bb_reg(0x818, 0x07c00000, 0x7); /* Tx backoff OFDM */ + _device.phy_set_bb_reg(0x81c, 0x001fc000, 0x8); /* Tx scaling */ + } else { + _device.phy_set_bb_reg(0x1a20, 0xffff0000, 0x5284); + _device.phy_set_bb_reg(0x1a24, 0xffffffff, 0x3e18fec8); + _device.phy_set_bb_reg(0x1a28, 0x0000ffff, 0x0a88); + _device.phy_set_bb_reg(0x1a98, 0xffff0000, 0xacc4); + _device.phy_set_bb_reg(0x1a9c, 0x0000ffff, 0xc8b2); + _device.phy_set_bb_reg(0x1aa0, 0xffffffff, 0x00faf0de); + _device.phy_set_bb_reg(0x1aac, 0xffffffff, 0x00122344); + _device.phy_set_bb_reg(0x1ab0, 0xffffffff, 0x0fffffff); + _device.phy_set_bb_reg(0x818, 0xf8000000, 0x1a); /* Tx backoff CCK */ + _device.phy_set_bb_reg(0x818, 0x07c00000, 0xc); /* Tx backoff OFDM */ + _device.phy_set_bb_reg(0x81c, 0x001fc000, 0x4); /* Tx scaling */ + } +} + +/* --- phydm spur-elimination helpers (8822E), straight ports ------------- */ + +/* phydm_set_manual_nbi_8822e */ +void RadioManagementJaguar3::set_manual_nbi_8822e(bool en, uint32_t tone_idx) { + _device.phy_set_bb_reg(0x1944, 0x001ff000, en ? tone_idx : 0x0); + _device.phy_set_bb_reg(0x4044, 0x001ff000, en ? tone_idx : 0x0); + _device.phy_set_bb_reg(0x1940, 1u << 31, en ? 0x1 : 0x0); + _device.phy_set_bb_reg(0x4040, 1u << 31, en ? 0x1 : 0x0); + _device.phy_set_bb_reg(0x818, 1u << 11, en ? 0x1 : 0x0); + _device.phy_set_bb_reg(0x1d3c, 0x78000000, en ? 0xf : 0x0); +} + +/* phydm_set_nbi_wa_para_8822e */ +void RadioManagementJaguar3::set_nbi_wa_para_8822e(bool en, + ChannelWidth_t bw) { + if (en) { + _device.phy_set_bb_reg(0x810, 0xf, 0x7); + _device.phy_set_bb_reg(0x810, 0xf0000, 0x7); + _device.phy_set_bb_reg(0x88c, 0x30000, 0x3); + const uint32_t v = (bw == CHANNEL_WIDTH_40) ? 0x0 : 0x3; + _device.phy_set_bb_reg(0x1944, 0x300, v); + _device.phy_set_bb_reg(0x4044, 0x300, v); + } else { + _device.phy_set_bb_reg(0x810, 0xf, 0x0); + _device.phy_set_bb_reg(0x810, 0xf0000, 0x0); + _device.phy_set_bb_reg(0x88c, 0x30000, 0x2); + _device.phy_set_bb_reg(0x1944, 0x300, 0x3); + _device.phy_set_bb_reg(0x4044, 0x300, 0x3); + } +} + +/* phydm_set_csi_mask_8822e — one tone's CSI weighting into the mask table. */ +void RadioManagementJaguar3::set_csi_mask_8822e(uint32_t tone_idx, + uint8_t weight) { + _device.phy_set_bb_reg(0x1ee8, 0x3, 0x3); /* clk on */ + _device.phy_set_bb_reg(0x1d94, (1u << 31) | (1u << 30), 0x1); /* wr en */ + _device.phy_set_bb_reg(0x1d94, 0x00ff0000, (tone_idx >> 1) & 0xff); + if (tone_idx & 1) + _device.phy_set_bb_reg(0x1d94, 0xf0, weight); + else + _device.phy_set_bb_reg(0x1d94, 0xf, weight); + _device.phy_set_bb_reg(0x1ee8, 0x3, 0x0); /* clk off */ +} + +/* phydm_clean_specific_csi_mask_8822e — zero the 8 tones the spur cases + * touch (the kernel's cheap "clean" used on every spur path). */ +void RadioManagementJaguar3::clean_csi_mask_8822e() { + static const uint8_t tones[8] = {7, 8, 16, 55, 56, 103, 104, 112}; + _device.phy_set_bb_reg(0x1ee8, 0x3, 0x3); + _device.phy_set_bb_reg(0x1d94, (1u << 31) | (1u << 30), 0x1); + for (uint8_t t : tones) { + _device.phy_set_bb_reg(0x1d94, 0x00ff0000, t); + _device.phy_set_bb_reg(0x1d94, 0x000000ff, 0x0); + } + _device.phy_set_bb_reg(0x1ee8, 0x3, 0x0); +} + +/* Central-channel/BW combos phydm_spur_eliminate_8822e special-cases — + * shared by the spur apply below and the fast_retune decline gate. */ +bool RadioManagementJaguar3::is_spur_combo_8822e(uint8_t ch, + ChannelWidth_t bw) { + switch (bw) { + case CHANNEL_WIDTH_20: + return ch == 153 || ch == 161 || ch == 169; + case CHANNEL_WIDTH_40: + return ch == 151 || ch == 159 || ch == 167 || ch == 54 || ch == 102 || + ch == 118; + case CHANNEL_WIDTH_80: + return ch == 155 || ch == 171 || ch == 58 || ch == 106 || ch == 122; + default: + return false; + } +} + +/* phydm_spur_eliminate_8822e: per-channel/BW NBI notch + CSI mask + packet- + * detection tweak for the channels whose synthesizer harmonics (5760 / + * 5280 / 5600 MHz) land in-band; explicit spur-free default otherwise. */ +void RadioManagementJaguar3::spur_eliminate_8822e(uint8_t ch, + ChannelWidth_t bw) { + /* phydm_set_auto_nbi(false) */ + _device.phy_set_bb_reg(0x818, 1u << 3, 0x0); + _device.phy_set_bb_reg(0x1d3c, 0x78000000, 0x0); + /* phydm_csi_mask_enable(true) */ + _device.phy_set_bb_reg(0xc0c, 1u << 3, 0x1); + + struct Case { + uint8_t ch; + ChannelWidth_t bw; + bool nbi; /* manual NBI on (5760 MHz spur family) */ + uint16_t nbi_tone; /* manual NBI tone index */ + uint16_t csi_tones[3]; /* CSI tones to weight (0 = unused slot) */ + uint8_t csi_n; + uint8_t weight; /* CSI weighting value */ + uint16_t pkt_det; /* 0xc24[15:0] packet-detection value */ + }; + static const Case cases[] = { + {153, CHANNEL_WIDTH_20, true, 112, {111, 112, 113}, 3, 0xa, 0x60e0}, + {161, CHANNEL_WIDTH_20, true, 112, {112, 0, 0}, 1, 0xa, 0x60e0}, + {169, CHANNEL_WIDTH_20, true, 112, {112, 0, 0}, 1, 0xa, 0x60e0}, + {151, CHANNEL_WIDTH_40, true, 16, {15, 16, 17}, 3, 0xa, 0x00e0}, + {159, CHANNEL_WIDTH_40, true, 16, {16, 0, 0}, 1, 0xa, 0x00e0}, + {167, CHANNEL_WIDTH_40, true, 16, {16, 0, 0}, 1, 0xa, 0x00e0}, + {155, CHANNEL_WIDTH_80, true, 208, {207, 208, 209}, 3, 0xa, 0xc0e0}, + {171, CHANNEL_WIDTH_80, true, 208, {208, 0, 0}, 1, 0xa, 0xc0e0}, + {54, CHANNEL_WIDTH_40, false, 0, {32, 0, 0}, 1, 0xc, 0x00ff}, + {102, CHANNEL_WIDTH_40, false, 0, {32, 0, 0}, 1, 0xc, 0x00ff}, + {58, CHANNEL_WIDTH_80, false, 0, {224, 0, 0}, 1, 0xa, 0x00ff}, + {106, CHANNEL_WIDTH_80, false, 0, {224, 0, 0}, 1, 0xa, 0x00ff}, + {118, CHANNEL_WIDTH_40, false, 0, {32, 0, 0}, 1, 0xc, 0x00ff}, + {122, CHANNEL_WIDTH_80, false, 0, {224, 0, 0}, 1, 0xa, 0x00ff}, + }; + for (const Case &c : cases) { + if (c.ch != ch || c.bw != bw) + continue; + clean_csi_mask_8822e(); + set_manual_nbi_8822e(c.nbi, c.nbi_tone); + set_nbi_wa_para_8822e(c.nbi, bw); + for (uint8_t i = 0; i < c.csi_n; ++i) + set_csi_mask_8822e(c.csi_tones[i], c.weight); + _device.phy_set_bb_reg(0xc24, 0xffff, c.pkt_det); + return; + } + /* Default (spur-free channel): everything off, mask cleaned, packet + * detection at its default. */ + set_manual_nbi_8822e(false, 0); + set_nbi_wa_para_8822e(false, bw); + clean_csi_mask_8822e(); + _device.phy_set_bb_reg(0xc0c, 1u << 3, 0x0); /* csi_mask_enable(false) */ + _device.phy_set_bb_reg(0xc24, 0xffff, 0x00ff); +} + void RadioManagementJaguar3::DumpCanary() { /* Channel/BW-relevant set: TX DFIR + block enables (0x808), RX DFIR (0x810), * AGC bound/tables (0x828/0x18ac/0x41ac), subtune (0x88c), small-BW/RF-BW/ @@ -251,9 +440,7 @@ void RadioManagementJaguar3::DumpCanary() { 0x1a00, 0x1a14, 0x1a80, 0x1a9c, 0x1abc, 0x1ae8, 0x1aec, 0x1c80, 0x18ac, 0x41ac, 0x1944, 0x4044, /* TXAGC refs (runtime TX-power API): OFDM 0x18e8/0x41e8, CCK - * 0x18a0/0x41a0, + the first per-rate diff dword. The regcheck's - * TX+RX cell asserts 0x41e8 stays at its table default (the 8822E - * RX-desense quirk) while path A moves with the offset. */ + * 0x18a0/0x41a0, + the first per-rate diff dword. */ 0x18e8, 0x41e8, 0x18a0, 0x41a0, 0x3a00}; static const uint16_t mac_canary[] = {0x0, 0x24, 0x454, 0x483, 0x55c, 0x638, 0x668}; @@ -463,6 +650,17 @@ bool RadioManagementJaguar3::fast_retune(uint8_t channel, return false; /* band change needs RFE/AGC/CCK-RxIQ — full path only */ if (channel == _last_channel) return true; /* no-op hop */ + /* Spur channels (phydm_spur_eliminate_8822e) carry per-channel NBI/CSI-mask + * state that only the full path programs — a lean hop into OR out of one + * would leave a stale notch/mask. Decline; the caller falls back to the + * full SetMonitorChannel. 8822E only (the spur port is 8822E-gated). */ + if (_variant == ChipVariant::C8822E) { + uint8_t cc, cp, tc, tp; + central_and_pri(_last_channel, channel_offset, bwmode, cc, cp); + central_and_pri(channel, channel_offset, bwmode, tc, tp); + if (is_spur_combo_8822e(cc, bwmode) || is_spur_combo_8822e(tc, bwmode)) + return false; + } devourer::HopProf prof(_logger->events(), _cfg.debug.hop_prof, "j3", channel); @@ -608,8 +806,7 @@ void RadioManagementJaguar3::set_mac_bw_txsc(ChannelWidth_t bw, uint8_t pri) { _device.rtw_write8(0x483, sc); } -void RadioManagementJaguar3::set_tx_power_ref(uint8_t idx, bool zero_diffs, - bool skip_path_b_ofdm_ref) { +void RadioManagementJaguar3::set_tx_power_ref(uint8_t idx, bool zero_diffs) { /* Writes the 0x1c90 TXAGC gate below — the fast path's composed 0x1c90 * cache would go stale. */ invalidate_fast_caches(); @@ -628,8 +825,7 @@ void RadioManagementJaguar3::set_tx_power_ref(uint8_t idx, bool zero_diffs, _device.phy_set_bb_reg(off, mask, v); }; wr(0x18e8, 0x1fc00, idx); - if (!skip_path_b_ofdm_ref) - wr(0x41e8, 0x1fc00, idx); /* path B — 8822E TX+RX RX-desense hazard */ + wr(0x41e8, 0x1fc00, idx); wr(0x18a0, 0x7f0000, idx); wr(0x41a0, 0x7f0000, idx); if (zero_diffs) @@ -637,10 +833,7 @@ void RadioManagementJaguar3::set_tx_power_ref(uint8_t idx, bool zero_diffs, wr(off, 0xffffffff, 0x0); _logger->info("Jaguar3: TX power reference set to 0x{:02x} ({}) " "(per-rate diffs {})", - idx, - skip_path_b_ofdm_ref ? "path A + CCK B; 0x41e8 kept" - : "both paths", - zero_diffs ? "zeroed" : "kept"); + idx, "both paths", zero_diffs ? "zeroed" : "kept"); } /* Map a phy_reg_pg pseudo-address to the four MGN_* rates it encodes (byte i of @@ -684,8 +877,8 @@ static bool pg_addr_to_rates(uint32_t addr, std::array &rates) { } #endif /* DEVOURER_HAVE_JAGUAR3_8822E */ -void RadioManagementJaguar3::apply_tx_power_refs_8822e( - uint8_t ref_a, uint8_t ref_b, bool skip_path_b_ofdm_ref) { +void RadioManagementJaguar3::apply_tx_power_refs_8822e(uint8_t ref_a, + uint8_t ref_b) { invalidate_fast_caches(); /* writes the 0x1c90 TXAGC gate below */ /* Clamp to the 7-bit ref field — the masked BB write truncates mod 128. */ if (ref_a > 0x7f) @@ -697,14 +890,14 @@ void RadioManagementJaguar3::apply_tx_power_refs_8822e( _device.phy_set_bb_reg(off, mask, v); }; wr(0x18e8, 0x1fc00, ref_a); /* path A OFDM/HT/VHT ref */ - if (!skip_path_b_ofdm_ref) - wr(0x41e8, 0x1fc00, ref_b); /* path B — RX-desense hazard, see header */ + wr(0x41e8, 0x1fc00, ref_b); /* path B */ wr(0x18a0, 0x7f0000, ref_a); /* CCK ref */ wr(0x41a0, 0x7f0000, ref_b); } -void RadioManagementJaguar3::apply_power_by_rate_8822e( - uint8_t channel, uint8_t ref_a, uint8_t ref_b, bool skip_path_b_ofdm_ref) { +void RadioManagementJaguar3::apply_power_by_rate_8822e(uint8_t channel, + uint8_t ref_a, + uint8_t ref_b) { invalidate_fast_caches(); /* writes the 0x1c90 TXAGC gate below */ /* Clamp to the 7-bit ref fields (masked BB writes truncate mod 128). */ if (ref_a > 0x7f) @@ -751,8 +944,7 @@ void RadioManagementJaguar3::apply_power_by_rate_8822e( _device.phy_set_bb_reg(off, mask, v); }; wr(0x18e8, 0x1fc00, ref_a); /* path A OFDM/HT/VHT ref */ - if (!skip_path_b_ofdm_ref) - wr(0x41e8, 0x1fc00, ref_b); /* path B — RX-desense hazard, see header */ + wr(0x41e8, 0x1fc00, ref_b); /* path B */ wr(0x18a0, 0x7f0000, ref_a); /* CCK ref (2.4G) */ wr(0x41a0, 0x7f0000, ref_b); diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index 229f2db..14edd48 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -86,43 +86,39 @@ class RadioManagementJaguar3 { * (the DEVOURER_TX_PWR debug knob) the per-rate diff table is flattened so * every rate emits at `idx`. With zero_diffs=false (the default bring-up path) * the diff table applied by the BB tables is preserved, so per-rate spread is - * kept and only the reference base is programmed. skip_path_b_ofdm_ref leaves - * 0x41e8 at its table default — the 8822E TX+RX RX-desense quirk (see - * apply_power_by_rate_8822e); previously a flat override on a TX+RX session - * wrote it unconditionally and deafened the EU's RX. */ - void set_tx_power_ref(uint8_t idx, bool zero_diffs = true, - bool skip_path_b_ofdm_ref = false); + * kept and only the reference base is programmed. Both TXAGC paths are + * written in every mode (TX+RX included). */ + void set_tx_power_ref(uint8_t idx, bool zero_diffs = true); /* Apply the 8822e phy_reg_pg power-by-rate table for `channel`'s band: sets * the OFDM/CCK reference PER PATH (ref_a -> 0x18e8/0x18a0, ref_b -> 0x41e8/ * 0x41a0) and writes the per-rate diff table (0x3a00) so robust low rates get * the kernel's by-rate boost instead of a flat reference. The per-path refs * come from the efuse per-channel base (the kernel programs a distinct path-A/ - * path-B base, e.g. 0x4b/0x54 at ch36). - * - * skip_path_b_ofdm_ref: leave 0x41e8 (path-B OFDM ref) at its table default. - * Hardware-bisected: ANY nonzero value in that one field desenses the EU's RX - * to near-deaf (value-independent; path-A ref, CCK refs, the diff table and - * the DPK bypass are all RX-safe) — root cause open, suspected path-B - * TSSI/gain-stage asymmetry in devourer's bring-up vs the kernel's. TX+RX - * callers set this so RX works at the cost of path-B OFDM TX running at the - * table-default reference. */ - void apply_power_by_rate_8822e(uint8_t channel, uint8_t ref_a, uint8_t ref_b, - bool skip_path_b_ofdm_ref = false); + * path-B base, e.g. 0x4b/0x54 at ch36). */ + void apply_power_by_rate_8822e(uint8_t channel, uint8_t ref_a, uint8_t ref_b); /* Light TX-power step (8822e): just the gated reference writes of - * apply_power_by_rate_8822e — 0x18e8[16:10] (path-A OFDM), optionally - * 0x41e8[16:10] (path B, same RX-desense hazard/flag as above), + * apply_power_by_rate_8822e — 0x18e8/0x41e8[16:10] (OFDM per path), * 0x18a0/0x41a0[22:16] (CCK) — WITHOUT the 0x3a00 per-rate diff walk. The * diff table is offset-invariant (an offset shifts the reference anchor; * the calibrated per-rate shape rides on top), so a runtime offset step is * ~8 register ops instead of the full by-rate apply. Refs are clamped to * the 7-bit field here — the BB masked write truncates mod 128, so an * unclamped over-range ref would wrap to near-zero TX silently. */ - void apply_tx_power_refs_8822e(uint8_t ref_a, uint8_t ref_b, - bool skip_path_b_ofdm_ref); + void apply_tx_power_refs_8822e(uint8_t ref_a, uint8_t ref_b); private: + /* 8822E channel-switch helpers (straight phydm ports, 8822E-gated): + * CCK TX shaping filter + spur elimination (manual NBI / CSI mask). */ + void cck_tx_shaping_8822e(uint8_t central); + void spur_eliminate_8822e(uint8_t ch, ChannelWidth_t bw); + static bool is_spur_combo_8822e(uint8_t ch, ChannelWidth_t bw); + void set_manual_nbi_8822e(bool en, uint32_t tone_idx); + void set_nbi_wa_para_8822e(bool en, ChannelWidth_t bw); + void set_csi_mask_8822e(uint32_t tone_idx, uint8_t weight); + void clean_csi_mask_8822e(); + /* Jaguar3 baseband bandwidth/clock registers (from * config_phydm_switch_bandwidth_8822c). These do NOT exist on Jaguar1. */ static constexpr uint16_t R_RX_DFIR_8822C = 0x810; /* [13:4] RX DFIR coeff */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 1681b76..b0153a8 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -41,7 +41,7 @@ RtlJaguar3Device::RtlJaguar3Device(RtlAdapter device, Logger_t logger, devourer::DeviceConfig cfg) : _device{device}, _cfg{std::move(cfg)}, _logger{logger}, _variant{variant}, _hal{device, logger, variant, _cfg}, - _radioManagement{device, logger, variant, _cfg} { + _radioManagement{device, logger, variant, _cfg}, _phydm{device, logger} { _logger->info("RtlJaguar3Device constructed ({})", variant == jaguar3::ChipVariant::C8822E ? "8822E/EU" : "8822C/CU"); } @@ -49,7 +49,7 @@ RtlJaguar3Device::RtlJaguar3Device(RtlAdapter device, Logger_t logger, void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, SelectedChannel channel) { _channel = channel; - _rx_wanted = true; /* RX-side bring-up: no TXAGC apply may touch 0x41e8 */ + _rx_wanted = true; _hal.rtw_hal_init(channel); /* full vendor-source bring-up */ /* Tune the channel/bandwidth (5/10 MHz ChannelWidth re-clocks to narrowband), * then run IQK calibration (it reads RF18 for the tuned channel). */ @@ -105,6 +105,8 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, if (_cfg.rx.ack_responder) SetAckResponder(*_cfg.rx.ack_responder); /* DEVOURER_ACK_RESPONDER */ + apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ — end of both bring-ups, + * like Jaguar2's. */ StartRxLoop(std::move(packetProcessor)); } @@ -181,6 +183,30 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { _logger->info("Jaguar3 cfo.track: 0x1040 base read failed — disabled"); } } + /* phydm dynamic mechanisms on RX-only sessions: the vendor watchdog's ~2 s + * cadence on a dedicated thread (register I/O CANNOT run on this thread — + * a sync control transfer from the bulk-IN event thread starves its own + * event loop; a separate thread under _reg_mu is the proven pattern, same + * as the TX-side coex thread, which owns the tick in TX+RX mode). */ + std::atomic phydm_stop{false}; + std::thread phydm_thread; + if (!_coex_thread.joinable()) { + phydm_thread = std::thread([this, &phydm_stop] { + auto next = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!phydm_stop && !g_devourer_should_stop) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (std::chrono::steady_clock::now() < next) + continue; + next += std::chrono::seconds(2); + try { + std::lock_guard lk(_reg_mu); + _phydm.tick(_channel.Channel, !_cca_disabled); + } catch (...) { + break; /* chip gone — the RX loop will wind down too */ + } + } + }); + } auto cfo_next = std::chrono::steady_clock::now() + std::chrono::seconds(2); auto cfo_tick = [&]() { if (!_cfg.tuning.cfo_track || !reg1040_ok) @@ -297,6 +323,9 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { return _rx_stop || g_devourer_should_stop; }); _rx_loop_active = false; + phydm_stop = true; + if (phydm_thread.joinable()) + phydm_thread.join(); } /* Coex runtime (port of the rtw88 watchdog's coex path): drain the firmware's @@ -365,6 +394,10 @@ void RtlJaguar3Device::coex_runtime_loop() { std::lock_guard lk(_reg_mu); _hal.coex_run_5g(); _hal.pwr_track(); /* thermal TX-power compensation (sustains upper 5 GHz) */ + /* phydm dynamic mechanisms (vendor watchdog parity): FA/CCA window + * statistics -> DIG -> CCK-PD -> EDCCA. EDCCA tracking is owned by + * SetCcaMode when the EDCCA-disable knob is active. */ + _phydm.tick(_channel.Channel, !_cca_disabled); _hal.fw_update_wl_phy_info(); _hal.fw_set_pwr_mode_active(); _hal.fw_coex_query_bt_info(); @@ -375,6 +408,37 @@ void RtlJaguar3Device::coex_runtime_loop() { } } +/* Golden-init replay — same file format as Jaguar2's ("%x %u %llx" = addr + * width value per line, tests/decode_wseq.py output). */ +void RtlJaguar3Device::apply_replay_wseq() { + if (_cfg.debug.replay_wseq.empty()) + return; + FILE *fp = fopen(_cfg.debug.replay_wseq.c_str(), "r"); + if (!fp) { + _logger->error("replay_wseq: cannot open {}", _cfg.debug.replay_wseq); + return; + } + unsigned addr, width; + unsigned long long val; + size_t n = 0; + while (fscanf(fp, "%x %u %llx", &addr, &width, &val) == 3) { + if (width == 1) + _device.rtw_write8(static_cast(addr), + static_cast(val)); + else if (width == 2) + _device.rtw_write16(static_cast(addr), + static_cast(val)); + else + _device.rtw_write32(static_cast(addr), + static_cast(val)); + if (++n % 1000 == 0) + _logger->info("replay_wseq: {} writes applied", n); + } + fclose(fp); + _logger->info("replay_wseq: DONE — {} writes from {}", n, + _cfg.debug.replay_wseq); +} + /* Clean shutdown — see IRtlDevice::Stop. Best-effort: a chip that already * dropped off the bus will make the de-init writes fail, which is fine. */ void RtlJaguar3Device::Stop() { @@ -397,7 +461,7 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { * after the coex/FW steps leaves the RX path dead, and the register RMWs * race the running TX). */ const bool want_rx = _cfg.rx.enable_with_tx; - _rx_wanted = want_rx; /* consumed by every TXAGC ref write (0x41e8 quirk) */ + _rx_wanted = want_rx; _hal.rtw_hal_init(channel); /* full vendor-source bring-up */ _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); @@ -448,7 +512,10 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { /* TXAGC from the current runtime-knob state (flat override / offset folded * onto the efuse-calibrated refs) — see apply_tx_power_current. Pre-coex, - * so no _reg_mu needed here. */ + * so no _reg_mu needed here. NOTE: the FW coex/power-mode H2Cs below rewrite + * the OFDM refs wholesale — the authoritative apply is the post-coex + * re-apply at the end of this function; this early one just keeps the + * intermediate bring-up steps on sane references. */ apply_tx_power_current(/*full=*/true); _brought_up = true; /* WiFi-only coex bring-up: disable the BT/LTE antenna arbitration and lock the @@ -545,6 +612,45 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { * so the AFE write doesn't contend with the periodic coex re-apply. */ if (_cfg.tuning.xtal_cap) SetXtalCap(*_cfg.tuning.xtal_cap); + /* Re-apply TXAGC as the LAST bring-up register step: the FW power-mode / + * coex H2Cs above reprogram the OFDM TXAGC references (0x18e8/0x41e8) + * wholesale in firmware, clobbering the pre-coex apply — construction-time + * TX-power state (flat override / offset) only sticks when applied after + * them. The coex thread's ~2 s ticks do not rewrite the refs. */ + apply_tx_power_current(/*full=*/true); + if (_variant == jaguar3::ChipVariant::C8822E) { + /* halmac "Config PIN Mux" (halmac_gpio_8822e.c HALMAC_WL_DPDT_SEL — + * REG_LED_CFG+3 |= BIT(0)): route the DPDT antenna transfer switch to WL + * control — 0x4c[24] = DPDT_WLBT_SEL set, [22] = GPIO13_14_WL_CTRL_EN + * clear. With the switch mis-routed the chip still transmits at full + * duty, but everything faster than ~26 Mbps PHY (HT MCS4+, legacy + * 48M/54M) airs a PPDU no receiver can sync to, and the surviving low + * rates run ~20 dB above the kernel's TX EVM. Must be written after the + * FW H2C steps above; interacts with the single-path 1SS TX mapping + * (HalJaguar3::config_channel_8822e) — with 1SS duplicated onto both + * chains this write wedges MCS0 TX, so the two ship together. */ + const uint32_t v4c = _device.rtw_read(0x4c); + _device.rtw_write(0x4c, (v4c & ~0x00400000u) | 0x01000000u); + /* Same pin-mux family: PAD_CTRL1[29:28] route the WL PAPE/antenna pads. + * MacInit sets both (halmac pre-init), but the FW/coex bring-up steps + * clear bit29 — re-assert post-coex. */ + const uint32_t v64 = _device.rtw_read(0x0064); + if ((v64 & 0x30000000u) != 0x30000000u) + _device.rtw_write(0x0064, v64 | 0x30000000u); + _logger->info("Jaguar3(8822e): DPDT/pad pin-mux applied (0x4c[24], " + "0x64[29:28])"); + } + apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */ + if (_cfg.debug.bb_dump) { + /* Full MAC+BB dump (0x000..0x4ffc — MAC plane, then BB incl. the RF + * direct-read windows at 0x3c00/0x4c00) in the same "BBDUMP" format as + * Jaguar2's, for end-state diffing against the vendor kernel's rtw_proc + * read_reg. */ + for (uint32_t a = 0x000; a <= 0x4ffc; a += 0x10) + _logger->info("BBDUMP 0x{:04x} 0x{:08x} 0x{:08x} 0x{:08x} 0x{:08x}", a, + _device.rtw_read32(a), _device.rtw_read32(a + 4), + _device.rtw_read32(a + 8), _device.rtw_read32(a + 12)); + } _coex_thread = std::thread([this] { coex_runtime_loop(); }); if (_cfg.rx.ack_responder) SetAckResponder(*_cfg.rx.ack_responder); /* DEVOURER_ACK_RESPONDER */ @@ -808,6 +914,7 @@ void RtlJaguar3Device::SetCcaMode(bool disabled) { } void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { + _phydm.on_channel_change(); /* Serialize against the coex thread's housekeeping tick (and any concurrent * FastRetune) — channel config is register RMW. Init/InitWrite call the * radio-management core directly (no lock needed: the coex thread isn't @@ -868,14 +975,13 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { _channel.ChannelWidth = bw; } -/* Re-program TXAGC from the current knob state (see header). The 0x41e8 - * TX+RX quirk is 8822E-specific, so the skip flag is derived here — once — - * from _rx_wanted AND the variant; the 8822C keeps its path-B ref writes. */ +/* Re-program TXAGC from the current knob state (see header). Both TXAGC + * paths are written in every mode, TX+RX included — with the DPDT/pad + * pin-mux applied at bring-up, a live 0x41e8 write does not disturb the + * EU's RX (tests/eu_41e8_desense_recheck.sh). */ void RtlJaguar3Device::apply_tx_power_current(bool full) { const int off = _tx_pwr_offset_steps; const int flat = _tx_pwr_override; - const bool skip_b = - _rx_wanted && _variant == jaguar3::ChipVariant::C8822E; _txpwr_sat_low = false; _txpwr_sat_high = false; auto clamp127 = [&](int v) -> uint8_t { @@ -894,7 +1000,7 @@ void RtlJaguar3Device::apply_tx_power_current(bool full) { /* Flat semantics: reference = flat + offset, per-rate diffs zeroed (once — * offset-only steps skip the 32-dword re-zero). */ _radioManagement.set_tx_power_ref(clamp127(flat + off), - /*zero_diffs=*/!_diffs_zeroed, skip_b); + /*zero_diffs=*/!_diffs_zeroed); _diffs_zeroed = true; return; } @@ -922,14 +1028,10 @@ void RtlJaguar3Device::apply_tx_power_current(bool full) { if (full || _diffs_zeroed) { /* Full apply: refs + the per-rate diff walk (also the path back from * flat semantics, which zeroed the diffs). */ - _radioManagement.apply_power_by_rate_8822e(_channel.Channel, ra, rb, - skip_b); + _radioManagement.apply_power_by_rate_8822e(_channel.Channel, ra, rb); _diffs_zeroed = false; - if (skip_b) - _logger->info("Jaguar3(8822e): TX+RX mode — path-B OFDM TXAGC ref left " - "at table default (keeps RX alive)"); } else { - _radioManagement.apply_tx_power_refs_8822e(ra, rb, skip_b); + _radioManagement.apply_tx_power_refs_8822e(ra, rb); } return; } @@ -939,7 +1041,7 @@ void RtlJaguar3Device::apply_tx_power_current(bool full) { * per-rate default is a follow-up; the offset shifts this reference. */ _radioManagement.set_tx_power_ref( clamp127(static_cast(JAGUAR3_TXPWR_REF_BASE_8822C) + off), - /*zero_diffs=*/!_diffs_zeroed, skip_b); + /*zero_diffs=*/!_diffs_zeroed); _diffs_zeroed = true; } diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index b968f0c..00323e4 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -16,6 +16,7 @@ #include "HalJaguar3.h" #include "LaCapture.h" #include "RadioManagementJaguar3.h" +#include "PhydmRuntimeJaguar3.h" /* RtlJaguar3Device is the orchestrator for the Realtek "Jaguar3" 802.11ac family * — RTL8822CU, RTL8812EU, RTL8822EU. It is the Jaguar3 sibling of @@ -99,8 +100,7 @@ class RtlJaguar3Device : public IRtlDevice { * set_tx_power_ref) under _reg_mu, serialized against the coex tick's * pwr_track (which RMWs the [7:0] thermal field of the SAME 0x18a0/0x41a0 * dwords — field-disjoint, so thermal compensation and the offset compose). - * The 8822E TX+RX 0x41e8 quirk is enforced structurally: every ref write - * takes skip_path_b_ofdm_ref from _rx_wanted. A full SetMonitorChannel + * A full SetMonitorChannel * re-folds the knobs against the new channel group's efuse refs (gated on a * knob being active); FastRetune never touches TXAGC. GetThermalStatus * reads RF 0x42[6:1] via the calibration impl (efuse baseline on the E, @@ -216,6 +216,9 @@ class RtlJaguar3Device : public IRtlDevice { /* Lazy LA-mode capture helper (la_capture). */ std::unique_ptr _la; jaguar3::RadioManagementJaguar3 _radioManagement; + /* phydm dynamic mechanisms (FA/DIG/CCK-PD/EDCCA), ticked from the coex + * thread every ~2 s like the vendor watchdog. */ + jaguar3::PhydmRuntimeJaguar3 _phydm; SelectedChannel _channel{}; Action_ParsedRadioPacket _packetProcessor = nullptr; /* Runtime TX-power knobs (atomic so GetTxPowerState's cached snapshot is @@ -255,9 +258,7 @@ class RtlJaguar3Device : public IRtlDevice { bool _cca_disabled = false; void apply_cca_mode_locked(bool disabled); /* TX+RX intent (DEVOURER_TX_WITH_RX at InitWrite / an RX-side Init): - * consumed as skip_path_b_ofdm_ref by EVERY TXAGC ref write, so no offset - * churn can ever touch 0x41e8 while RX is alive (the 8822E RX-desense - * quirk is enforced structurally, not by call-site discipline). */ + * keeps the RX filters open across the TX bring-up. */ bool _rx_wanted = false; /* Cached 8822E per-channel-group efuse base refs (the values InitWrite * derived, incl. the 0x4b fallback) so an offset-only step recomputes @@ -272,6 +273,10 @@ class RtlJaguar3Device : public IRtlDevice { * change / flat<->efuse transitions); full=false is the light offset step. * Caller holds _reg_mu when the coex thread may be running. */ void apply_tx_power_current(bool full); + /* Golden-init replay (DEVOURER_REPLAY_WSEQ, debug.replay_wseq): apply a + * captured kernel register-write stream verbatim at the end of InitWrite — + * the hardware-diff lever (same as Jaguar2's; found the 8822B RF18 bug). */ + void apply_replay_wseq(); /* Runtime TX-mode default (SetTxMode/ClearTxMode). */ std::optional _tx_mode_default; diff --git a/tests/cu_2g_ground_liveness.sh b/tests/cu_2g_ground_liveness.sh new file mode 100755 index 0000000..2ab4da2 --- /dev/null +++ b/tests/cu_2g_ground_liveness.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# 8822CU 2.4 GHz RX liveness series (issue #238 loop): N fresh rxdemo +# bring-ups on ch6, each judged by an 8812AU MCS0 burst (ambient traffic is +# an unreliable liveness signal on quiet channels). Historically the CU +# ground was sporadically deaf on ch6 across bring-ups; the IGI-toggle port +# (now on both Jaguar3 variants) targets that stale-RF-mode mechanism. +# sudo tests/cu_2g_ground_liveness.sh [reps] +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT=/tmp/cu-2g-liveness +CH="${CH:-6}" +REPS="${1:-5}" +mkdir -p "$OUT" +cleanup(){ sudo -n pkill -x rxdemo 2>/dev/null; sudo -n pkill -x txdemo 2>/dev/null; true; } +trap cleanup EXIT INT TERM +cleanup; sleep 1 + +alive=0 +for i in $(seq 1 "$REPS"); do + : >"$OUT/g$i.log" + sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + timeout 30 "$ROOT/build/rxdemo" >"$OUT/g$i.log" 2>"$OUT/g$i.err" & + GJ=$! + sleep 12 + sudo -n env DEVOURER_PID=0x8812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS0 DEVOURER_TX_GAP_US=1500 \ + timeout 10 "$ROOT/build/txdemo" >/dev/null 2>&1 || true + sleep 1 + sudo -n pkill -x rxdemo 2>/dev/null; wait "$GJ" 2>/dev/null + n=$(grep -c '"ev":"rx.frame"' "$OUT/g$i.log" || true); n=${n:-0} + ok=$([ "$n" -gt 500 ] && echo ALIVE || echo DEAF) + [ "$ok" = ALIVE ] && alive=$((alive+1)) + echo "bring-up $i: rx.frame=$n $ok" + sleep 2 +done +echo "== $alive/$REPS ground bring-ups alive on ch$CH ==" +[ "$alive" -eq "$REPS" ] && echo "RESULT PASS" || { echo "RESULT FAIL"; exit 1; } diff --git a/tests/eu_41e8_desense_recheck.sh b/tests/eu_41e8_desense_recheck.sh new file mode 100755 index 0000000..b99c42c --- /dev/null +++ b/tests/eu_41e8_desense_recheck.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Issue #238 quirk #1 re-test on the FIXED driver (post DPDT-0x4c / pinmux / +# 1SS-path fixes): does a nonzero path-B OFDM TXAGC ref write (0x41e8) still +# desense the 8812EU's RX to near-deaf? +# +# Cells (EU = RX under test on ch36, 8812AU = MCS0 traffic source): +# base EU rxdemo, 0x41e8 untouched (structural skip active) +# poke EU rxdemo + DEVOURER_REPLAY_WSEQ writes 0x41e8 = 0x00009e00 +# (the kernel's own working value) at end of bring-up +# Desense gone => poke count ~ base count (quirk was a front-end artifact; +# skip_path_b_ofdm_ref can be retired, recovering path-B +# OFDM TX power in TX+RX mode). +# Desense stays => quirk #1 is real independent of the front-end fixes. +# sudo tests/eu_41e8_desense_recheck.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT=/tmp/eu-41e8-recheck +CH="${CH:-36}" +SECS="${SECS:-14}" +mkdir -p "$OUT" +cleanup(){ sudo -n pkill -x rxdemo 2>/dev/null; sudo -n pkill -x txdemo 2>/dev/null; true; } +trap cleanup EXIT INT TERM +cleanup; sleep 1 + +printf '41e8 4 9e00\n' > /tmp/eu_41e8_poke.wseq + +cell() { # $1=label $2=extra-env + : >"$OUT/rx-$1.log" + sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 $2 \ + stdbuf -oL timeout $((SECS+16)) "$ROOT/build/rxdemo" \ + >"$OUT/rx-$1.log" 2>"$OUT/rx-$1.err" & + RJ=$! + sleep 12 # EU bring-up + sudo -n env DEVOURER_PID=0x8812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS0 DEVOURER_TX_GAP_US=1500 \ + timeout "$SECS" "$ROOT/build/txdemo" >/dev/null 2>&1 || true + sleep 1 + sudo -n pkill -x rxdemo 2>/dev/null; wait "$RJ" 2>/dev/null + n=$(grep -c '"ev":"rx.frame"' "$OUT/rx-$1.log" || true) + echo "$1: rx.frame n=${n:-0}" + sleep 2 +} + +echo "== EU RX ch$CH, AU MCS0 source ==" +cell base "" +cell poke "DEVOURER_REPLAY_WSEQ=/tmp/eu_41e8_poke.wseq" +grep -m1 "replay_wseq: DONE" "$OUT/rx-poke.err" || echo "WARN: poke replay did not run" diff --git a/tests/eu_bb_endstate_diff.sh b/tests/eu_bb_endstate_diff.sh new file mode 100755 index 0000000..ac140c2 --- /dev/null +++ b/tests/eu_bb_endstate_diff.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# BB end-state diff: vendor kernel (monitor ch36 @ 5 dBm, pristine MCS7) vs +# devourer (same channel, MCS4+ garbled) on the 8812EU — issue #238. +# Kernel side reads via /proc read_reg while the driver is live; devourer side +# via DEVOURER_BB_DUMP at the end of InitWrite. Diff of 0x800..0x4ffc BB space +# = the candidate register set for the MCS4+ whole-PPDU corruption. +# sudo tests/eu_bb_endstate_diff.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KMOD=rtl88x2eu_ohd +CH="${CH:-36}" +OUT=/tmp/devourer-eu-endstate +mkdir -p "$OUT" + +cleanup(){ sudo -n pkill -x txdemo 2>/dev/null || true; } +trap cleanup EXIT INT TERM + +eu_sys(){ + SYS="" + for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } + done + [ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } +} +vbus_cold(){ + eu_sys + sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || \ + echo "WARN: uhubctl cycle failed" + sleep 4 +} + +echo "== kernel side: cold bind + monitor ch$CH @5dBm ==" +sudo -n rmmod "$KMOD" 2>/dev/null +vbus_cold +sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" 2>/dev/null || true +sleep 5 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +sudo -n iw dev "$IF" set txpower fixed 500 +sleep 5 # settle: one watchdog/TSSI pass + +PROC="/proc/net/$KMOD/$IF" +[ -e "$PROC/read_reg" ] || PROC="/proc/net/rtl88x2eu/$IF" +[ -e "$PROC/read_reg" ] || { echo "ERROR: no proc read_reg"; exit 1; } +echo "== dumping kernel MAC+BB 0x000..0x4ffc via $PROC/read_reg ==" +sudo -n python3 - "$PROC/read_reg" >"$OUT/kernel.dump" <<'PYEOF' +import re, sys +proc = sys.argv[1] +for a in range(0x000, 0x5000, 4): + with open(proc, "w") as f: + f.write(f"{a:x} 4") + with open(proc) as f: + m = re.search(r"=0x(\w+)", f.read()) + print(f"0x{a:04x} 0x{int(m.group(1),16):08x}" if m else f"0x{a:04x} ERR") +PYEOF +echo "kernel dump: $(wc -l <"$OUT/kernel.dump") regs" +sudo -n rmmod "$KMOD" 2>/dev/null + +echo "== devourer side: cold bring-up + BB dump ==" +vbus_cold +sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS7 DEVOURER_BB_DUMP=1 DEVOURER_TX_GAP_US=5000 \ + timeout 30 "$ROOT/build/txdemo" >/dev/null 2>"$OUT/devourer.err" || true +grep -oE "BBDUMP 0x[0-9a-f]{4} 0x[0-9a-f]{8} 0x[0-9a-f]{8} 0x[0-9a-f]{8} 0x[0-9a-f]{8}" \ + "$OUT/devourer.err" | while read -r _ base v0 v1 v2 v3; do + b=$((base)) + printf "0x%04x %s\n0x%04x %s\n0x%04x %s\n0x%04x %s\n" \ + $b "$v0" $((b+4)) "$v1" $((b+8)) "$v2" $((b+12)) "$v3" +done >"$OUT/devourer.dump" +echo "devourer dump: $(wc -l <"$OUT/devourer.dump") regs" + +echo "== diff (kernel vs devourer) ==" +python3 - "$OUT/kernel.dump" "$OUT/devourer.dump" <<'PYEOF' +import sys +def load(p): + d = {} + for line in open(p): + parts = line.split() + if len(parts) == 2 and parts[1].startswith("0x"): + d[int(parts[0], 16)] = int(parts[1], 16) + return d +k, u = load(sys.argv[1]), load(sys.argv[2]) +# Noisy/live registers to ignore: counters, AGC/IGI, IQK/DACK results, RF +# read windows change with 3-wire state; keep them but tag. +diffs = [(a, k[a], u[a]) for a in sorted(set(k) & set(u)) if k[a] != u[a]] +print(f"{len(diffs)} differing registers of {len(set(k) & set(u))}") +for a, kv, uv in diffs: + tag = "" + if 0x3c00 <= a < 0x4000: tag = " (RF-A win)" + elif 0x4c00 <= a < 0x5000: tag = " (RF-B win)" + print(f"0x{a:04x}: kernel=0x{kv:08x} devourer=0x{uv:08x}{tag}") +PYEOF diff --git a/tests/eu_burst_probe.sh b/tests/eu_burst_probe.sh new file mode 100755 index 0000000..0cb674d --- /dev/null +++ b/tests/eu_burst_probe.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Burst-length fingerprint of the 8812EU TX at a pinned MCS (issue #238): +# run txdemo at the given rate with a fixed gap and measure the on-air burst +# duration histogram with the B210. One SDR read per invocation. +# sudo tests/eu_burst_probe.sh MCS4 +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RATE="${1:-MCS4}" +CH="${CH:-36}"; FREQ="${FREQ:-5180e6}" +cleanup(){ sudo -n pkill -x txdemo 2>/dev/null || true; } +trap cleanup EXIT INT TERM + +sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE="$RATE" DEVOURER_TX_GAP_US=2000 \ + stdbuf -oL timeout 25 "$ROOT/build/txdemo" \ + >/tmp/eu_burst_tx.log 2>&1 & +sleep 6 +echo "--- burst fingerprint during $RATE TX (ch$CH) ---" +sudo -n python3 "$ROOT/tests/sdr_burst_len.py" --freq "$FREQ" --secs 4 +cleanup diff --git a/tests/eu_fullduplex_pathb_check.sh b/tests/eu_fullduplex_pathb_check.sh new file mode 100755 index 0000000..9f0ec83 --- /dev/null +++ b/tests/eu_fullduplex_pathb_check.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Full-duplex validation of the retired 0x41e8 skip (issue #238 quirk #1): +# EU in TX+RX=thread mode with path-B OFDM power now APPLIED — +# TX half: EU injects MCS7, counted by the 8822CU ground (rate=19). +# RX half: 8812AU floods MCS0, counted by the EU's own RX (rx.count). +# PASS = both halves alive simultaneously (TX+RX with full TXAGC). +# sudo tests/eu_fullduplex_pathb_check.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT=/tmp/eu-fullduplex-pathb +CH="${CH:-36}" +mkdir -p "$OUT" +cleanup(){ sudo -n pkill -x rxdemo 2>/dev/null; sudo -n pkill -x txdemo 2>/dev/null; true; } +trap cleanup EXIT INT TERM +cleanup; sleep 1 + +echo "== CU ground up (ch$CH) ==" +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 70 "$ROOT/build/rxdemo" >"$OUT/ground.log" 2>"$OUT/ground.err" & +GJ=$! +sleep 12 + +echo "== EU TX+RX up (MCS7 inject + RX thread) ==" +sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_WITH_RX=thread DEVOURER_TX_RATE=MCS7 DEVOURER_TX_GAP_US=2000 \ + stdbuf -oL timeout 40 "$ROOT/build/txdemo" >"$OUT/eu.log" 2>"$OUT/eu.err" & +EJ=$! +sleep 14 + +echo "== AU MCS0 flood (EU RX traffic) ==" +sudo -n env DEVOURER_PID=0x8812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS0 DEVOURER_TX_GAP_US=1500 \ + timeout 20 "$ROOT/build/txdemo" >/dev/null 2>&1 || true +sleep 2 +cleanup; wait "$GJ" "$EJ" 2>/dev/null + +eu_rx=$(grep -o '"total":[0-9]*' "$OUT/eu.log" | tail -1 | cut -d: -f2) +cu_m7=$(python3 - "$OUT/ground.log" <<'PYEOF' +import json,sys +n=0 +for line in open(sys.argv[1],errors="replace"): + if '"ev":"rx.frame"' not in line: continue + try: ev=json.loads(line) + except ValueError: continue + if ev.get("rate")==19: n+=1 +print(n) +PYEOF +) +pathb=$(grep -c "both paths" "$OUT/eu.err" || true) +echo +echo "EU RX total (rx.count): ${eu_rx:-0}" +echo "CU ground EU-MCS7 (rate19): ${cu_m7:-0}" +echo "EU TXAGC 'both paths' logs: ${pathb:-0}" +if [ "${eu_rx:-0}" -gt 1000 ] && [ "${cu_m7:-0}" -gt 1000 ]; then + echo "RESULT PASS: full duplex with path-B OFDM power applied" +else + echo "RESULT FAIL: one half dead (RX=${eu_rx:-0}, TX->ground=${cu_m7:-0})" + exit 1 +fi diff --git a/tests/eu_golden_capture.sh b/tests/eu_golden_capture.sh new file mode 100755 index 0000000..c64f39c --- /dev/null +++ b/tests/eu_golden_capture.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Golden-init capture for the 8812EU (issue #238): record the vendor kernel's +# ENTIRE cold-init register-write stream (VBUS cold -> insmod -> monitor ch36 +# -> txpower fixed 500) via usbmon, and emit DEVOURER_REPLAY_WSEQ files: +# /tmp/eu_golden_full.wseq - every control write (addr width value) +# /tmp/eu_golden_bb.wseq - BB/RF plane only (addr >= 0x800) — the safe +# replay set (no MAC power/DMA hazards) +# The kernel FW download rides bulk-out, so control-write capture naturally +# excludes it. This is the lever that found the 8822B RF18 bug. +# sudo tests/eu_golden_capture.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KMOD=rtl88x2eu_ohd +CH="${CH:-36}" +RAW=/tmp/eu_golden_usbmon.txt + +CATPID="" +cleanup(){ [ -n "$CATPID" ] && sudo -n kill "$CATPID" 2>/dev/null || true; } +trap cleanup EXIT INT TERM +sudo -n modprobe usbmon 2>/dev/null || true + +# Locate the EU + VBUS cold-cycle it with the driver unloaded. +sudo -n rmmod "$KMOD" 2>/dev/null +SYS="" +for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } +done +[ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } +BUS=$(cat /sys/bus/usb/devices/$SYS/busnum) +sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || { + echo "WARN: uhubctl cycle failed (continuing warm)"; } +sleep 4 + +MON=/sys/kernel/debug/usb/usbmon/${BUS}u +sudo -n sh -c ": > $RAW" +sudo -n sh -c "cat $MON > $RAW" & +CATPID=$! +sleep 1 + +echo "== insmod + monitor ch$CH + txpower 500 (capturing) ==" +sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" 2>/dev/null || true +sleep 5 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +sudo -n iw dev "$IF" set txpower fixed 500 +sleep 5 # let one watchdog/TSSI pass land in the capture +cleanup; CATPID="" +sleep 0.5 + +# The device number can change across the VBUS cycle — parse every device on +# the bus and let the write count sanity-check it. +python3 - "$RAW" <<'PYEOF' +import re, sys +pat = re.compile(r"S Co:\d+:0*(\d+):0 s 40 05 (\w+) (\w+) (\w+) \d+(?: = (\w+))?") +full = open("/tmp/eu_golden_full.wseq", "w") +bb = open("/tmp/eu_golden_bb.wseq", "w") +n = nb = 0 +for line in open(sys.argv[1], errors="replace"): + m = pat.search(line) + if not m: + continue + addr = int(m.group(2), 16) + wlen = int(m.group(4), 16) + data = m.group(5) or "" + if not data or wlen not in (1, 2, 4) or len(data) < 2 * wlen: + continue + # usbmon data is the raw wire byte stream (LE) — convert to a host value. + val = int.from_bytes(bytes.fromhex(data[:2 * wlen]), "little") + print(f"{addr:x} {wlen} {val:x}", file=full) + n += 1 + if addr >= 0x800: + print(f"{addr:x} {wlen} {val:x}", file=bb) + nb += 1 +sys.stderr.write(f"{n} writes -> eu_golden_full.wseq; {nb} BB/RF -> eu_golden_bb.wseq\n") +PYEOF +sudo -n rmmod "$KMOD" 2>/dev/null +echo "done (driver unloaded; VBUS-cycle the EU before a devourer replay run)" diff --git a/tests/eu_heat_soak_lck.sh b/tests/eu_heat_soak_lck.sh new file mode 100755 index 0000000..9f3ce9b --- /dev/null +++ b/tests/eu_heat_soak_lck.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Heat-soak the 8812EU at max TX duty and observe the thermal/LCK runtime +# (issue #238 loop: validates the LCK synthesizer re-lock port live). +# - EU: MCS7, GAP_US=0 (max duty) for SOAK_SECS. +# - Ground (8822CU): samples EVM continuously; we compare the first vs last +# 2 minutes (EVM drift without LCK would grow with chip heating). +# - EU stderr: pwr_track thermal/swing logs + "LCK re-lock" events. +# sudo tests/eu_heat_soak_lck.sh [soak_secs] +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT=/tmp/eu-heat-soak +CH="${CH:-36}" +SOAK="${1:-1320}" +mkdir -p "$OUT" +cleanup(){ sudo -n pkill -x rxdemo 2>/dev/null; sudo -n pkill -x txdemo 2>/dev/null; true; } +trap cleanup EXIT INT TERM +cleanup; sleep 1 + +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout $((SOAK+40)) "$ROOT/build/rxdemo" 2>"$OUT/ground.err" \ + | while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done \ + >>"$OUT/ground.log" & +GJ=$! +sleep 12 + +echo "== EU MCS7 max-duty soak for ${SOAK}s ==" +sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS7 DEVOURER_TX_GAP_US=0 \ + timeout "$SOAK" "$ROOT/build/txdemo" >/dev/null 2>"$OUT/eu.err" || true +sleep 1 +cleanup; wait "$GJ" 2>/dev/null + +echo "== thermal / LCK events ==" +grep -E "pwr_track|LCK" "$OUT/eu.err" | tail -20 +python3 - "$OUT/ground.log" "$SOAK" <<'PYEOF' +import json, statistics, sys +soak = float(sys.argv[2]) +rows = [] +for line in open(sys.argv[1], errors="replace"): + if '"ev":"rx.frame"' not in line: continue + ts,_,js = line.partition(" ") + try: ev = json.loads(js) + except ValueError: continue + if ev.get("rate") != 19: continue + rows.append((float(ts), ev["evm"][0])) +if not rows: + print("RESULT FAIL: ground caught no EU MCS7 frames"); sys.exit(1) +t0 = rows[0][0] +head = [e for t,e in rows if t - t0 < 120] +tail = [e for t,e in rows if t - t0 > soak - 120] +mh = statistics.median(head) if head else 0 +mt = statistics.median(tail) if tail else 0 +print(f"frames={len(rows)} EVM first2min={mh} last2min={mt} drift={mt-mh:+}") +print("RESULT PASS" if tail and abs(mt-mh) <= 4 else + "RESULT WARN: EVM drifted >4 dB across the soak (LCK/thermal check)") +PYEOF diff --git a/tests/eu_kernel_2g_verified.sh b/tests/eu_kernel_2g_verified.sh new file mode 100755 index 0000000..cc0a357 --- /dev/null +++ b/tests/eu_kernel_2g_verified.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Kernel ch6 TX ground truth with an IN-SESSION ground control (issue #238 +# quirk #3): one CU ground bring-up hears (1) an 8812AU control burst, then +# (2) the vendor-kernel 8812EU injecting MCS0 — both on ch6. If (1) decodes +# and (2) doesn't, the kernel's own 2.4 GHz TX on this module is undecodable +# too (module property, not a devourer gap). +# sudo tests/eu_kernel_2g_verified.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KMOD=rtl88x2eu_ohd +CH=6 +OUT=/tmp/eu-k2g-verified +mkdir -p "$OUT" +cleanup(){ + sudo -n pkill -x rxdemo 2>/dev/null || true + sudo -n pkill -x txdemo 2>/dev/null || true + sudo -n pkill -9 -f kernel_tx_inject 2>/dev/null || true + sudo -n rmmod "$KMOD" 2>/dev/null || true +} +trap cleanup EXIT INT TERM +sudo -n pkill -x rxdemo 2>/dev/null; sudo -n pkill -x txdemo 2>/dev/null; sleep 1 + +# Kernel EU up FIRST (VBUS cold; the CU ground stays untouched after). +SYS="" +for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } +done +[ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } +sudo -n rmmod "$KMOD" 2>/dev/null +sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || true +sleep 4 +sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" 2>/dev/null || true +sleep 6 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +sudo -n iw dev "$IF" set txpower fixed 500 2>/dev/null || true +sleep 2 + +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 120 "$ROOT/build/rxdemo" 2>"$OUT/ground.err" \ + | while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done \ + >>"$OUT/ground.log" & +GJ=$! +sleep 12 + +echo "== phase 1: AU control burst (MCS0, ch$CH) ==" +t0=$(date +%s.%N) +sudo -n env DEVOURER_PID=0x8812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS0 DEVOURER_TX_GAP_US=1500 \ + timeout 10 "$ROOT/build/txdemo" >/dev/null 2>&1 || true +t1=$(date +%s.%N) +sleep 2 +echo "== phase 2: kernel EU inject (MCS0, ch$CH) ==" +t2=$(date +%s.%N) +sudo -n python3 "$ROOT/tests/kernel_tx_inject.py" "$IF" 0 148 10 \ + >"$OUT/ktx.log" 2>&1 || true +t3=$(date +%s.%N) +sleep 1 +sudo -n pkill -x rxdemo 2>/dev/null; wait "$GJ" 2>/dev/null + +python3 - "$OUT/ground.log" "$t0" "$t1" "$t2" "$t3" <<'PYEOF' +import json, statistics, sys +t0,t1,t2,t3 = map(float, sys.argv[2:6]) +au=[]; eu=[] +for line in open(sys.argv[1], errors="replace"): + if '"ev":"rx.frame"' not in line: continue + ts,_,js=line.partition(" ") + try: ev=json.loads(js) + except ValueError: continue + t=float(ts) + if ev.get("rate")!=12: continue + e=ev.get("evm",[0])[0] + if t0<=t<=t1: au.append(e) + elif t2<=t<=t3: eu.append(e) +def s(v): return f"n={len(v)} medEVM={int(statistics.median(v))}" if v else "n=0" +print(f"AU control : {s(au)}") +print(f"kernel EU : {s(eu)}") +PYEOF +tail -1 "$OUT/ktx.log" diff --git a/tests/eu_kernel_mcs_probe.sh b/tests/eu_kernel_mcs_probe.sh new file mode 100755 index 0000000..bbaac90 --- /dev/null +++ b/tests/eu_kernel_mcs_probe.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Ground truth for issue #238 MCS4+ on THIS 8812EU unit: does the VENDOR +# kernel driver (rtl88x2eu_ohd) inject decodable MCS4/MCS7, measured by the +# same devourer 8822CU ground that decodes devourer MCS0-3 and 8812AU MCS7? +# snokvist verified kernel-MCS7-clean on their unit; this checks ours. +# sudo tests/eu_kernel_mcs_probe.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${EU_KMCS_OUT:-/tmp/devourer-eu-kernel-mcs}" +CH="${CH:-36}" +KMOD=rtl88x2eu_ohd +mkdir -p "$OUT" + +cleanup() { + sudo -n pkill -9 -f kernel_tx_inject 2>/dev/null || true + sudo -n pkill -x rxdemo 2>/dev/null || true + sudo -n rmmod "$KMOD" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +sudo -n pkill -x txdemo 2>/dev/null; sudo -n pkill -x rxdemo 2>/dev/null +sleep 1 + +echo "== ground RX (0xc812, devourer) up on ch$CH ==" +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 300 "$ROOT/build/rxdemo" 2>"$OUT/ground.err" \ + | while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done \ + >>"$OUT/ground.log" & +GJ=$! +sleep 12 + +echo "== VBUS cold-cycling the EU (chip retains devourer state; kernel FW-DL + fails on a warm chip) ==" +SYS="" +for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } +done +[ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } +HUBLOC=${SYS%.*}; HUBPORT=${SYS##*.} +sudo -n uhubctl -l "$HUBLOC" -p "$HUBPORT" -a cycle -d 3 >/dev/null 2>&1 || { + echo "WARN: uhubctl cycle failed (continuing warm)"; } +sleep 4 + +echo "== binding $KMOD on the EU ==" +lsmod | grep -q "^$KMOD" || sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" +sleep 6 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +echo "EU kernel netdev = $IF" +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +# snokvist's clean-MCS7 kernel baseline ran at 5 dBm; the driver's monitor +# default on this unit measures ~0 duty, so pin the power explicitly. +sudo -n iw dev "$IF" set txpower fixed 500 2>/dev/null || \ + echo "WARN: set txpower failed (driver may not support it in monitor)" +sleep 1 + +: >"$OUT/cells.txt" +for mcs in 0 4 7; do + echo "== kernel inject MCS$mcs ==" + t0="$(date +%s.%N)" + sudo -n python3 "$ROOT/tests/kernel_tx_inject.py" "$IF" "$mcs" 148 10 \ + >"$OUT/ktx-mcs$mcs.log" 2>&1 || true + t1="$(date +%s.%N)" + echo "mcs$mcs $mcs $t0 $t1" >>"$OUT/cells.txt" + sleep 2 +done +sudo -n pkill -x rxdemo 2>/dev/null +wait "$GJ" 2>/dev/null + +python3 - "$OUT/ground.log" "$OUT/cells.txt" <<'PYEOF' +import json, statistics, sys +RATE = {0: 12, 4: 16, 7: 19} +frames = [] +for line in open(sys.argv[1], errors="replace"): + if '"ev":"rx.frame"' not in line: + continue + ts, _, js = line.partition(" ") + try: + ev = json.loads(js) + except ValueError: + continue + evm = ev.get("evm") + frames.append((float(ts), ev.get("rate", -1), + evm[0] if isinstance(evm, list) and evm else 0)) +print("\ncell rate-matched other medEVM(dB)") +for line in open(sys.argv[2]): + label, mcs, t0, t1 = line.split() + t0, t1 = float(t0), float(t1) + want = RATE[int(mcs)] + win = [(r, e) for (t, r, e) in frames if t0 + 1 <= t <= t1 - 0.2] + hit = [e for (r, e) in win if r == want] + other = sum(1 for (r, _) in win if r != want) + med = int(statistics.median(hit)) if hit else 0 + print(f"{label:<11} {len(hit):>12} {other:>5} {med:>10}") +PYEOF diff --git a/tests/eu_kernel_sdr_check.sh b/tests/eu_kernel_sdr_check.sh new file mode 100755 index 0000000..0f56dff --- /dev/null +++ b/tests/eu_kernel_sdr_check.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Is the vendor rtl88x2eu_ohd actually RADIATING during monitor injection on +# this unit? (issue #238 ground-truth harness debugging: the CU ground caught +# 0 of 33k kernel-injected MCS0 frames — silent-drop vs deaf-ground bisect.) +# One SDR read per invocation. +# sudo tests/eu_kernel_sdr_check.sh [mcs] [payload] +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MCS="${1:-0}"; PAYLOAD="${2:-148}" +CH="${CH:-36}"; FREQ="${FREQ:-5180e6}" +KMOD=rtl88x2eu_ohd +cleanup(){ sudo -n pkill -9 -f kernel_tx_inject 2>/dev/null || true; } +trap cleanup EXIT INT TERM + +# Bind the vendor driver if not already up (VBUS cold first — kernel FW-DL +# fails on a chip warm from devourer). +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +if [ -z "$IF" ]; then + SYS="" + for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } + done + [ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } + sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || true + sleep 4 + lsmod | grep -q "^$KMOD" || sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" + sleep 6 + for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } + done +fi +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +echo "EU kernel netdev = $IF" +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +sleep 1 + +sudo -n python3 "$ROOT/tests/kernel_tx_inject.py" "$IF" "$MCS" "$PAYLOAD" 20 \ + >/tmp/eu_ktx_sdr.log 2>&1 & +sleep 4 +echo "--- SDR duty during kernel MCS$MCS inject (ch$CH) ---" +sudo -n python3 "$ROOT/tests/sdr_duty.py" --freq "$FREQ" --rate 25e6 --secs 5 \ + --gain 60 --bw 20 2>&1 | grep -iE "duty=|error" | head -3 +cleanup +tail -1 /tmp/eu_ktx_sdr.log diff --git a/tests/eu_matched_power_evm.sh b/tests/eu_matched_power_evm.sh new file mode 100755 index 0000000..4fb719c --- /dev/null +++ b/tests/eu_matched_power_evm.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Matched-power TX EVM comparison, devourer vs vendor kernel, on the SAME +# 8812EU unit measured by the SAME 8822CU ground session (ch36/20M): +# - devourer: MCS7 + MCS0 at DEVOURER_TX_PWR_OFFSET_QDB=-44 — lands the +# OFDM TXAGC refs at A=31/B=40, bit-identical to the kernel's end state +# at `iw set txpower fixed 500` (5 dBm) on this unit. +# - kernel: rtl88x2eu_ohd monitor ch36 @ fixed 500, injecting MCS7+MCS0. +# Output: per-cell clean-frame count + median/percentile ground EVM. +# sudo tests/eu_matched_power_evm.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT=/tmp/eu-matched-evm +CH=36 +KMOD=rtl88x2eu_ohd +mkdir -p "$OUT" +cleanup(){ + sudo -n pkill -x rxdemo 2>/dev/null || true + sudo -n pkill -x txdemo 2>/dev/null || true + sudo -n pkill -9 -f kernel_tx_inject 2>/dev/null || true + sudo -n rmmod "$KMOD" 2>/dev/null || true +} +trap cleanup EXIT INT TERM +cleanup; sleep 1 + +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 220 "$ROOT/build/rxdemo" 2>"$OUT/ground.err" \ + | while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done \ + >>"$OUT/ground.log" & +GJ=$! +sleep 12 + +: >"$OUT/cells.txt" +dev_cell() { # $1=label $2=rate + t0=$(date +%s.%N) + sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE="$2" DEVOURER_TX_GAP_US=1500 \ + DEVOURER_TX_PWR_OFFSET_QDB=-44 \ + timeout 14 "$ROOT/build/txdemo" >/dev/null 2>"$OUT/tx-$1.err" || true + t1=$(date +%s.%N) + echo "$1 $2 $t0 $t1" >>"$OUT/cells.txt" + sleep 2 +} +echo "== devourer cells (offset -44 = kernel 5 dBm refs) ==" +dev_cell dev-mcs7 MCS7 +dev_cell dev-mcs0 MCS0 + +echo "== VBUS cold + kernel bind (monitor ch$CH @ fixed 500) ==" +SYS="" +for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "a81a" ] && { SYS=$(basename "$(dirname "$d")"); break; } +done +[ -n "$SYS" ] || { echo "ERROR: a81a not on USB"; exit 1; } +sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || true +sleep 4 +sudo -n insmod "$ROOT/reference/rtl88x2eu/${KMOD}.ko" 2>/dev/null || true +sleep 6 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + [ "$drv" = "$KMOD" ] && { IF=$(basename "$d"); break; } +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev"; exit 1; } +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set channel "$CH" +sudo -n iw dev "$IF" set txpower fixed 500 +sleep 2 + +k_cell() { # $1=label $2=mcs-index + t0=$(date +%s.%N) + sudo -n python3 "$ROOT/tests/kernel_tx_inject.py" "$IF" "$2" 148 12 \ + >"$OUT/ktx-$1.log" 2>&1 || true + t1=$(date +%s.%N) + echo "$1 mcs$2 $t0 $t1" >>"$OUT/cells.txt" + sleep 2 +} +echo "== kernel cells ==" +k_cell ker-mcs7 7 +k_cell ker-mcs0 0 +sudo -n pkill -x rxdemo 2>/dev/null; wait "$GJ" 2>/dev/null + +python3 - "$OUT/ground.log" "$OUT/cells.txt" <<'PYEOF' +import json, statistics, sys +RATE = {"MCS7": 19, "MCS0": 12, "mcs7": 19, "mcs0": 12} +frames = [] +for line in open(sys.argv[1], errors="replace"): + if '"ev":"rx.frame"' not in line: continue + ts,_,js = line.partition(" ") + try: ev = json.loads(js) + except ValueError: continue + frames.append((float(ts), ev.get("rate"), ev["evm"][0], ev["rssi"][0])) +print(f"\n{'cell':<12} {'n':>6} {'medEVM':>7} {'p10':>5} {'p90':>5} {'medRSSI':>8}") +for line in open(sys.argv[2]): + label, rate, t0, t1 = line.split() + t0, t1 = float(t0), float(t1) + want = RATE[rate] + e = sorted(x[2] for x in frames if t0+2 <= x[0] <= t1-0.3 and x[1] == want) + r = sorted(x[3] for x in frames if t0+2 <= x[0] <= t1-0.3 and x[1] == want) + if not e: + print(f"{label:<12} {0:>6}") + continue + med = statistics.median(e) + p10 = e[len(e)//10]; p90 = e[(len(e)*9)//10] + print(f"{label:<12} {len(e):>6} {med:>7} {p10:>5} {p90:>5} {statistics.median(r):>8}") +PYEOF diff --git a/tests/eu_mcs7_sdr_probe.sh b/tests/eu_mcs7_sdr_probe.sh new file mode 100755 index 0000000..e7b8f82 --- /dev/null +++ b/tests/eu_mcs7_sdr_probe.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Is the 8812EU actually RADIATING during an MCS7 TX session, or are the +# frames dropped before the antenna? (issue #238 MCS4+ bisect: garbled +# constellation would still show ~normal SDR duty; a MAC-level drop shows ~0.) +# One SDR read per invocation — a second back-to-back B210 read can fail to +# reacquire and report ~0 (session lesson), so run once per rate. +# sudo tests/eu_mcs7_sdr_probe.sh [MCS7] +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RATE="${1:-MCS7}" +CH="${CH:-36}"; FREQ="${FREQ:-5180e6}" +cleanup(){ sudo -n pkill -x txdemo 2>/dev/null || true; } +trap cleanup EXIT INT TERM + +sudo -n env DEVOURER_PID=0xa81a DEVOURER_VID=0x0bda DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE="$RATE" DEVOURER_TX_GAP_US=0 \ + stdbuf -oL timeout 25 "$ROOT/build/txdemo" \ + >/tmp/eu_sdr_probe_tx.log 2>&1 & +sleep 6 +echo "--- SDR duty during $RATE TX (ch$CH) ---" +sudo -n python3 "$ROOT/tests/sdr_duty.py" --freq "$FREQ" --rate 25e6 --secs 5 \ + --gain 60 --bw 20 2>&1 | grep -iE "duty=|error" | head -3 +cleanup diff --git a/tests/eu_mcs7_txagc_fix.sh b/tests/eu_mcs7_txagc_fix.sh new file mode 100755 index 0000000..7ffb30e --- /dev/null +++ b/tests/eu_mcs7_txagc_fix.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# Issue #238 follow-up validation: 8822E (8812EU) MCS4+/64-QAM TX garbling. +# +# Root cause (bench-proven by snokvist, ordering confirmed in source): the FW +# power-mode / coex H2Cs at the tail of InitWrite reprogram the OFDM TXAGC +# references (0x18e8/0x41e8) wholesale, clobbering the pre-coex TXAGC apply — +# so ALL construction-time TX-power state (DEVOURER_TX_PWR et al.) was inert +# on the EU and the chip always transmitted at the FW's efuse-derived refs +# (~idx 72/83 on this board), driving the PA into compression and shredding +# 16/64-QAM while BPSK/QPSK ride through. Fix = re-apply TXAGC as the LAST +# bring-up register step (RtlJaguar3Device::InitWrite). +# +# This script proves the fix end-to-end on-air: +# TX = 8812EU (0bda:a81a), RX ground = 8822CU (0bda:c812), ch36 / 20 MHz. +# Cells: {MCS0, MCS7} x {default power, TX_PWR=39, TX_PWR=28}. +# Expected: +# - MCS0 delivers in every cell (control). +# - MCS7 @ default: ~0 clean frames (PA compression — unchanged physics). +# - MCS7 @ TX_PWR=39/28: substantial clean delivery (rate=19) with sane EVM. +# Pre-fix these cells read ~0 because the construction-time TX_PWR never +# reached the hardware. +# - txdemo stderr shows "TXAGC refs re-applied post-coex (FW had rewritten +# them)" — direct evidence of the FW clobber. +# +# Usage: sudo -v && tests/eu_mcs7_txagc_fix.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${EU_MCS7_OUT:-/tmp/devourer-eu-mcs7-fix}" +CH="${CH:-36}" +TX_PID="${TX_PID:-0xa81a}" TX_VID="${TX_VID:-0x0bda}" +GROUND_PID="${GROUND_PID:-0xc812}" GROUND_VID="${GROUND_VID:-0x0bda}" +CELL_SECS="${CELL_SECS:-10}" +mkdir -p "$OUT" + +cleanup() { + sudo -n pkill -x txdemo 2>/dev/null || true + sudo -n pkill -x rxdemo 2>/dev/null || true + true +} +trap cleanup EXIT INT TERM +plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; } +plugged "$TX_PID" "$TX_VID" || { echo "SKIP: TX $TX_PID not plugged"; exit 0; } +plugged "$GROUND_PID" "$GROUND_VID" || { echo "SKIP: ground $GROUND_PID not plugged"; exit 0; } + +echo "== building ==" +cmake --build "$ROOT/build" -j --target txdemo rxdemo >/dev/null || exit 1 + +echo "== ground RX ($GROUND_PID) up on ch$CH ==" +# The 8822CU ground is intermittently deaf at 2.4 GHz across bring-ups +# (issue #238 quirk-#2-adjacent): gate every session on ambient traffic — +# a ground that hears nothing in GROUND_PROBE_SECS on a busy channel is +# restarted (up to GROUND_TRIES bring-ups) before any TX cell is judged. +GROUND_TRIES="${GROUND_TRIES:-4}" +GROUND_PROBE_SECS="${GROUND_PROBE_SECS:-14}" +GJ="" +start_ground() { + : >"$OUT/ground.log" + sudo -n env DEVOURER_PID="$GROUND_PID" DEVOURER_VID="$GROUND_VID" \ + DEVOURER_CHANNEL="$CH" DEVOURER_STREAM_OUT=1 ${GROUND_EXTRA_ENV:-} \ + stdbuf -oL timeout 600 "$ROOT/build/rxdemo" 2>"$OUT/ground.err" \ + | while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done \ + >>"$OUT/ground.log" & + GJ=$! +} +try=1 +while :; do + start_ground + sleep "$GROUND_PROBE_SECS" + n=$(grep -c '"ev":"rx.frame"' "$OUT/ground.log" 2>/dev/null || true) + n=${n:-0} + [ "$n" -gt 0 ] && { echo "ground alive (ambient n=$n, try $try)"; break; } + echo "ground deaf (try $try/$GROUND_TRIES) — restarting rxdemo" + sudo -n pkill -x rxdemo 2>/dev/null; wait "$GJ" 2>/dev/null; sleep 3 + try=$((try+1)) + if [ "$try" -gt "$GROUND_TRIES" ]; then + # Ambient absence is NOT proof of a deaf ground (quiet channels / + # hours give false negatives — bench-observed on ch6): by default + # warn and continue; GROUND_GATE=strict makes it fatal. Verify a + # suspicious ground with a known-good TX (e.g. the 8812AU control). + if [ "${GROUND_GATE:-warn}" = "strict" ]; then + echo "RESULT SKIP: ground never heard ambient traffic on ch$CH — cells unjudgeable" + exit 2 + fi + echo "WARN: no ambient traffic on ch$CH after $GROUND_TRIES ground bring-ups — continuing (channel may just be quiet; verify with a control TX)" + start_ground + sleep 3 + break + fi +done + +# cells: "