From 609b0c7c6e93cc5676a17918f6ce35e36157561c Mon Sep 17 00:00:00 2001 From: Gilang Date: Tue, 14 Jul 2026 01:01:54 +0700 Subject: [PATCH 1/2] =?UTF-8?q?jaguar3(8822e):=20port=20kernel=20eFEM=20GP?= =?UTF-8?q?IO=20pinmux=20=E2=80=94=20RX=20path=20B=20was=20antenna-less=20?= =?UTF-8?q?under=20the=20static=20DPDT=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8822E front-end fix (#268/b5a6df7) routed the DPDT antenna transfer switch with a static two-bit write (REG_LED_CFG 0x4c[24] set, [22] clear). That fixed MCS4+ TX — but parked the switch in a TX-favoring position that disconnects RX path B's antenna on every 8812EU: chain-B pwdb pins at the noise floor (~10 raw / -99 dBm) regardless of antenna. Invisible to every total-frame-count validation (incl. the #268 full-duplex proof and the 0x41e8 desense recheck) because chain A carries the stream; found with per-chain RSSI on a two-adapter bench (2026-07-14). Live A/B on the same card, seconds apart (8812EU <-> 8812EU, ch149, MCS5 6 Mbps offered, per-chain rssi/snr from the phystatus): DPDT route RX chain A RX chain B TX @ MCS7 static (b5a6df7) -68 / 27dB -99 / dead 9.9M clean 0x4c[24] only -68 / 27dB -77 / 19dB airs nothing eFEM pinmux (this) -70 / 26dB -77 / 19dB 9.7M clean Fix: port the kernel's _efem_pinmux_config (rtl8822e_halinit.c, rfe 21-24) -> halmac pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11: per-pin priority-list walk (disable the claims above the target, enable the target) + WL/BT ownership fixups, transcribed entry-for-entry from halmac_gpio_8822e.c. GPIO13 becomes WL_DPDT_SEL driven by the RFE engine's RFE_CTRL_9, so the transfer switch follows TX/RX in hardware: PA on TX, BOTH LNAs on RX — kernel parity. Runs post-coex in InitWrite (coex GPIO_MUXCFG writes would mask it; same ordering lesson as the existing RFE pad block). Validated with both ends on this port simultaneously: TX MCS7 9.7M goodput / MCS5 6.0M 0.0% loss / MCS0 clean, while RX reports both chains live (A/B -70/-77 dBm, snr 26/19). The old routes stay selectable for board A/B via DEVOURER_DPDT_MODE=legacy|bit24|skip (default: efem). docs/8822e-quirks.md updated — including the 0x41e8 history: the register is exonerated, the desense it was blamed for was this antenna disconnection, and per-chain RSSI is the only honest RX health metric on a 2T2R part. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QbMmK39gvTZUeWuAx8ujCH --- docs/8822e-quirks.md | 41 +++++-- src/jaguar3/RtlJaguar3Device.cpp | 196 +++++++++++++++++++++++++++---- src/jaguar3/RtlJaguar3Device.h | 6 + 3 files changed, 210 insertions(+), 33 deletions(-) diff --git a/docs/8822e-quirks.md b/docs/8822e-quirks.md index 719c300..bb7ec90 100644 --- a/docs/8822e-quirks.md +++ b/docs/8822e-quirks.md @@ -22,14 +22,25 @@ 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. +- **The DPDT transfer switch must be under HARDWARE TX/RX control — via the + full eFEM GPIO pin-mux, not a static route.** The kernel's + `_efem_pinmux_config` (rfe 21–24) claims GPIO2/3/4/13/14/EECS for the RFE + engine (`RFE_CTRL_3/5/7/8/9/11`, halmac `pinmux_set_func_8822e`); GPIO13 + becomes `WL_DPDT_SEL` driven by RFE_CTRL_9, so the antenna transfer switch + follows the TX/RX state — PA on TX, both LNAs on RX. Devourer ports this as + `RtlJaguar3Device::efem_pinmux_8822e()` (post-coex; coex GPIO_MUXCFG writes + would mask it). An earlier static approximation — set `0x4c[24]` + (`BIT_DPDT_WLBT_SEL`), clear `[22]` — fixed TX (without WL DPDT ownership, + every PPDU faster than ~26 Mbps PHY airs as garbage, and MCS0–3 leak + through at a ~−30 dB EVM floor) but parked the switch TX-favoring: **RX + path B lost its antenna on every 8812EU** — chain-B pwdb pinned at the + noise floor (~10 raw / −99 dBm) regardless of antenna, invisible to + total-frame-count validation because chain A carries the stream (bench + 2026-07-14, per-chain RSSI A/B: static write = TX 9.9M MCS7 / RX-B dead; + `[24]`-only = RX-B alive / TX airs nothing; eFEM pin-mux = TX 9.7M MCS7 + AND RX A/B −70/−77 dBm concurrently). Legacy behavior remains selectable + via `DEVOURER_DPDT_MODE=legacy|bit24|skip` for board A/B. 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 @@ -49,11 +60,15 @@ at ch36 — MCS0/MCS4/MCS7/54M ≈ 5k frames / 12 s at EVM −26/−32/−48/− 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. +appeared to near-deafen the EU's RX. The register is exonerated — a direct A/B +with per-chain RSSI shows no RX effect from the write — but the historical +observation was pointing at something real: the static DPDT route disconnects +RX path B's antenna outright (see the pin-mux section above), and every +recheck counted TOTAL frames, which chain A dominates — +`tests/eu_41e8_desense_recheck.sh`'s −2% "noise" verdict and the 24k-frame +full-duplex proof (`tests/eu_fullduplex_pathb_check.sh`) were both blind to a +dead chain B. Per-chain RSSI is the only honest RX-health metric on a 2T2R +part.) Full-duplex holds under the eFEM pin-mux with path-B power applied. `DEVOURER_TX_WITH_RX=thread` must still be set **before** `InitWrite` (bring-up keeps the RX filters open; retrofitting RX later is unreliable). diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 1972576..4615658 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -434,6 +434,142 @@ void RtlJaguar3Device::coex_runtime_loop() { } } +namespace { + +/* --- 8822E eFEM GPIO pin-function tables -------------------------------- + * Transcribed from halmac_gpio_8822e.c PINMUX_LIST_GPIO{2,3,4,13,14}_8822E / + * PINMUX_LIST_EECS_8822E, truncated at the target function. halmac's + * pinmux_switch_8822e walks a pin's priority list top-down: every claim + * ABOVE the target is disabled (field := ~val & msk), the target itself is + * enabled (field := val & msk), and the walk stops — entries below the + * target are never touched. Byte-register RMW throughout. */ +struct EfemEnt { + uint8_t off; /* MAC byte register */ + uint8_t msk; + uint8_t val; /* the claim's ENABLE value; disable writes ~val & msk */ +}; +struct EfemFix { uint8_t off; uint8_t bit; bool set; }; +struct EfemFunc { + const char *name; + const EfemEnt *ents; + size_t n_ents; /* last entry is the target */ + const EfemFix *fixes; /* post-switch WL/BT ownership fixups */ + size_t n_fixes; +}; + +constexpr uint8_t B0 = 0x01, B1 = 0x02, B2 = 0x04, B3 = 0x08, B4 = 0x10, + B5 = 0x20, B6 = 0x40, B7 = 0x80; + +/* RFE_CTRL_3 = GPIO3 -> WL_LNAON_SEL */ +constexpr EfemEnt kEntsRfe3[] = { + {0x66, B2, B2}, /* BT_GPIO3 */ + {0x4F, B6, B6}, /* BT_ANT_SW_3 */ + {0x41, B1, 0}, /* WL_PRI (BT_PTA) */ + {0x41, B2, B2}, /* BT_PRI (WL_PTA) */ + {0x40, B1 | B0, B0}, /* WLMAC_DBG */ + {0x40, B1 | B0, B1}, /* WLPHY_DBG */ + {0x40, B1 | B0, B1 | B0}, /* BT_DBG */ + {0x4F, B2, B2}, /* SW LNAON_SEL */ + {0x42, B0, B0}, /* BT_RFE_CTRL_5 (BT_LNAON_SEL) */ + {0x42, B0, B0}, /* RFE_CTRL_3 (WL_LNAON_SEL) — target */ +}; +constexpr EfemFix kFixRfe3[] = {{0x43, B7, false}, {0x67, B4, true}}; + +/* RFE_CTRL_5 = GPIO14 -> WLPHY_RFE_CTRL2GPIO */ +constexpr EfemEnt kEntsRfe5[] = { + {0x4E, B6, B6}, /* UART_WAKE (GPIO13_14_WL_CTRL_EN) */ + {0x4F, B6, B6}, /* BT_ANT_SW_1 */ + {0x40, B2, B2}, /* RFE_CTRL_5_4_5 — target */ +}; +constexpr EfemFix kFixRfe5[] = {{0x3F, B2, true}}; + +/* RFE_CTRL_7 = EECS pad -> WLPHY_RFE_CTRL2GPIO_2 */ +constexpr EfemEnt kEntsRfe7[] = { + {0x67, B1, B1}, /* BT_GPIO17 */ + {0x40, B3, B3}, /* RFE_CTRL_7 — target */ +}; + +/* RFE_CTRL_8 = GPIO4 -> WL_DPDT_SEL */ +constexpr EfemEnt kEntsRfe8[] = { + {0x66, B4, B4}, /* BT_SPI_D0 */ + {0x42, B3, B3}, /* WL_SPI_D0 */ + {0x67, B0, B0}, /* BT_JTAG_TRST */ + {0x65, B7, B7}, /* WL_JTAG_TRST */ + {0x73, B3, B3}, /* DBG_GNT_WL */ + {0x40, B1 | B0, B0}, /* WLMAC_DBG */ + {0x40, B1 | B0, B1}, /* WLPHY_DBG */ + {0x40, B1 | B0, B1 | B0}, /* BT_DBG */ + {0x4E, B7, B7}, /* ANT_SWB (SW_DPDT_SEL) */ + {0x42, B1, B1}, /* BT_RFE_CTRL_0 (BT_DPDT_SEL) */ + {0x42, B1, B1}, /* WL_RFE_CTRL_8 (WL_DPDT_SEL) — target */ +}; +constexpr EfemFix kFixRfe8[] = {{0x43, B2, true}, {0x4F, B0, true}}; + +/* RFE_CTRL_9 = GPIO13 -> WL_DPDT_SEL — the DPDT transfer switch itself. */ +constexpr EfemEnt kEntsRfe9[] = { + {0x4E, B6, B6}, /* BT_WAKE (GPIO13_14_WL_CTRL_EN — 0x4c[22]) */ + {0x4F, B6, B6}, /* BT_ANT_SW_0 */ + {0x4E, B7, B7}, /* ANT_SW_GPIO13 (SW_DPDT_SEL) */ + {0x42, B1, B1}, /* BT_RFE_CTRL_1 (BT_DPDT_SEL) */ + {0x42, B1, B1}, /* WL_RFE_CTRL_9 (WL_DPDT_SEL) — target */ +}; +constexpr EfemFix kFixRfe9[] = { + {0x43, B3, false}, {0x43, B4, true}, {0x4F, B0, true}}; + +/* RFE_CTRL_11 = GPIO2 -> WLPHY_RFE_CTRL2GPIO */ +constexpr EfemEnt kEntsRfe11[] = { + {0x66, B2, B2}, /* BT_GPIO2 */ + {0x4F, B6, B6}, /* BT_ANT_SW_2 */ + {0x41, B1, 0}, /* WL_STATE (BT_PTA) */ + {0x41, B2, B2}, /* BT_STATE (WL_PTA) */ + {0x40, B1 | B0, B0}, /* WLMAC_DBG */ + {0x40, B1 | B0, B1}, /* WLPHY_DBG */ + {0x40, B1 | B0, B1 | B0}, /* BT_DBG */ + {0x40, B2, B2}, /* RFE_CTRL_11_4_5 — target */ +}; +constexpr EfemFix kFixRfe11[] = {{0x3F, B0, false}}; + +/* Kernel order: rtw_halmac_rfe_ctrl_cfg(28..33) = RFE_CTRL_3,5,7,8,9,11. */ +constexpr EfemFunc kEfemFuncs[] = { + {"RFE_CTRL_3/GPIO3", kEntsRfe3, std::size(kEntsRfe3), kFixRfe3, + std::size(kFixRfe3)}, + {"RFE_CTRL_5/GPIO14", kEntsRfe5, std::size(kEntsRfe5), kFixRfe5, + std::size(kFixRfe5)}, + {"RFE_CTRL_7/EECS", kEntsRfe7, std::size(kEntsRfe7), nullptr, 0}, + {"RFE_CTRL_8/GPIO4", kEntsRfe8, std::size(kEntsRfe8), kFixRfe8, + std::size(kFixRfe8)}, + {"RFE_CTRL_9/GPIO13", kEntsRfe9, std::size(kEntsRfe9), kFixRfe9, + std::size(kFixRfe9)}, + {"RFE_CTRL_11/GPIO2", kEntsRfe11, std::size(kEntsRfe11), kFixRfe11, + std::size(kFixRfe11)}, +}; + +} /* namespace */ + +void RtlJaguar3Device::efem_pinmux_8822e() { + for (const EfemFunc &f : kEfemFuncs) { + for (size_t i = 0; i < f.n_ents; ++i) { + const EfemEnt &e = f.ents[i]; + uint8_t v = _device.rtw_read8(e.off); + v &= static_cast(~e.msk); + if (i + 1 == f.n_ents) + v |= static_cast(e.val & e.msk); /* enable the target */ + else + v |= static_cast(~e.val & e.msk); /* disable the claim */ + _device.rtw_write8(e.off, v); + } + for (size_t i = 0; i < f.n_fixes; ++i) { + const EfemFix &x = f.fixes[i]; + uint8_t v = _device.rtw_read8(x.off); + v = x.set ? static_cast(v | x.bit) + : static_cast(v & ~x.bit); + _device.rtw_write8(x.off, v); + } + } + _logger->warn("Jaguar3(8822e): eFEM pin-mux applied (RFE_CTRL 3/5/7/8/9/11 " + "-> RFE engine; DPDT under hardware TX/RX control)"); +} + /* 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() { @@ -656,26 +792,46 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { * 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])"); + /* eFEM GPIO pin-function routing (rfe 21-24) — port of the kernel's + * _efem_pinmux_config (rtl8822e_halinit.c) → halmac + * pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11. Replaces the + * improvised b5a6df7 DPDT write, which took two 0x4c bits in isolation + * (set [24], clear [22]) and thereby parked the DPDT antenna transfer + * switch in a static TX-favoring position: TX MCS4+ worked, but RX + * path B lost its antenna on every 8812EU (bench 2026-07-14: chain-B + * pwdb pinned ~10 / -99 dBm — invisible to total-frame validation, + * chain A masks it). The full pinmux routes GPIO13 to the RFE engine's + * RFE_CTRL_9 (WL_DPDT_SEL) so the switch follows TX/RX like the + * kernel: PA on TX, BOTH LNAs on RX. Runs post-coex (coex GPIO_MUXCFG + * writes would mask it — same ordering lesson as the pre-coex RFE + * block above). + * DEVOURER_DPDT_MODE knob for A/B: efem (default) | legacy (b5a6df7 + * write) | bit24 ([24] only) | skip. */ + const char *dpdt_env = std::getenv("DEVOURER_DPDT_MODE"); + std::string dpdt_mode = dpdt_env ? dpdt_env : "efem"; + if (std::getenv("DEVOURER_SKIP_DPDT") != nullptr) + dpdt_mode = "skip"; + if (dpdt_mode == "efem") { + efem_pinmux_8822e(); + } else if (dpdt_mode != "skip") { + const uint32_t v4c = _device.rtw_read(0x4c); + if (dpdt_mode == "bit24") + _device.rtw_write(0x4c, v4c | 0x01000000u); + else + _device.rtw_write(0x4c, (v4c & ~0x00400000u) | 0x01000000u); + _logger->warn("Jaguar3(8822e): DPDT legacy write mode={} " + "(0x4c was 0x{:08x})", dpdt_mode, v4c); + } else { + _logger->warn("Jaguar3(8822e): DPDT/eFEM pin-mux SKIPPED"); + } + if (dpdt_mode != "skip") { + /* 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 (bench-validated cold-TX fix). */ + const uint32_t v64 = _device.rtw_read(0x0064); + if ((v64 & 0x30000000u) != 0x30000000u) + _device.rtw_write(0x0064, v64 | 0x30000000u); + } } apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */ if (_cfg.debug.bb_dump) { diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 53cd596..7ad8784 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -281,6 +281,12 @@ class RtlJaguar3Device : public IRtlDevice { * 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(); + /* 8822E eFEM (rfe 21-24) GPIO pin-function routing — kernel-parity port of + * _efem_pinmux_config/pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11. + * Routes the DPDT antenna transfer switch to the RFE engine (hardware + * TX/RX switching) instead of the b5a6df7 static write that deafened RX + * path B. Called from InitWrite post-coex; see the call site. */ + void efem_pinmux_8822e(); /* Runtime TX-mode default (SetTxMode/ClearTxMode). */ std::optional _tx_mode_default; From 5d901cb6e4543c11753138184a1d9237a5292374 Mon Sep 17 00:00:00 2001 From: Gilang Date: Wed, 15 Jul 2026 23:41:00 +0700 Subject: [PATCH 2/2] jaguar3(8822e): address #289 review + share eFEM pin-mux into RX-only Init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review response (josephnef, PR #289): - DEVOURER_DPDT_MODE moves off std::getenv into DeviceConfig (tuning.dpdt_8822e, new Dpdt8822eMode enum) with the env mapping in examples/common/env_config.cpp — the library reads no environment. The undocumented DEVOURER_SKIP_DPDT second spelling is dropped (folded into mode=skip). - Gate the eFEM pin-mux on rfe_type 21..24, matching the kernel's _efem_pinmux_config (rtl8822e_halinit.c) and the existing config_rfe block; non-eFEM boards keep the reset-default pins. New HalJaguar3::rfe_type() exposes the resolved efuse value. - eFEM "applied" log demoted warn -> info (house style for the successful/default path); the legacy/bit24/skip branches stay warn. - Drop the rotting bench date from the code comment (git is the changelog). Init/InitWrite asymmetry (the review's deferred item): the vendor runs _efem_pinmux_config from rtl8822e_init_misc in BOTH bring-up directions, but the port only touched InitWrite — so a pure-RX Init session (rxdemo, spectrum sensing) kept the reset-default pins. Extract the DPDT routing (mode dispatch + PAD_CTRL1 re-assert) into apply_dpdt_route_8822e() and call it from both Init and InitWrite, each post-coex_wlan_only_init. InitWrite behavior is byte-identical (same code, same call site). Bench-validated on a second 8812EU (aarch64 GS, ch149, RX-only Init path, per-chain RSSI, every trial VBUS cold-cycled): DPDT mode chainA_med chainB_med n legacy 41 255 (DEAD) 15894 skip 42 46 14354 efem 46 47 15078 Independently reproduces the PR's core claim (the b5a6df7 static route kills RX chain B; the eFEM pin-mux keeps it live) on a board the reviewer could not verify locally, and confirms the shared Init path applies the pin-mux without breaking RX (both chains live, ~15k clean frames). The rfe_type gate passes on this board (no gate-fail warning), so efem_pinmux_8822e() executed on the real chip. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/8822e-quirks.md | 7 ++- examples/common/env_config.cpp | 10 +++ src/DeviceConfig.h | 15 +++++ src/jaguar3/HalJaguar3.h | 5 ++ src/jaguar3/RtlJaguar3Device.cpp | 103 ++++++++++++++++++------------- src/jaguar3/RtlJaguar3Device.h | 10 ++- 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/docs/8822e-quirks.md b/docs/8822e-quirks.md index bb7ec90..967ded6 100644 --- a/docs/8822e-quirks.md +++ b/docs/8822e-quirks.md @@ -38,8 +38,11 @@ family programs. Three of these must hold, or TX breaks in distinctive ways: total-frame-count validation because chain A carries the stream (bench 2026-07-14, per-chain RSSI A/B: static write = TX 9.9M MCS7 / RX-B dead; `[24]`-only = RX-B alive / TX airs nothing; eFEM pin-mux = TX 9.7M MCS7 - AND RX A/B −70/−77 dBm concurrently). Legacy behavior remains selectable - via `DEVOURER_DPDT_MODE=legacy|bit24|skip` for board A/B. NB `0x204c` is a + AND RX A/B −70/−77 dBm concurrently). The eFEM pin-mux is gated on + `rfe_type` 21–24 (kernel parity with `_efem_pinmux_config` / `config_rfe`); + other boards keep the reset-default pins. Legacy behavior remains selectable + via `DEVOURER_DPDT_MODE=efem|legacy|bit24|skip` (default `efem`) for board + A/B. 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 diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 5e02db9..10ffe10 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -139,6 +139,16 @@ devourer::DeviceConfig devourer_config_from_env() { cfg.tuning.fwdl_8814 = devourer::Fwdl8814Path::Rtw88; if (env_long("DEVOURER_8814_FWDL_CHUNK", &v)) cfg.tuning.fwdl_8814_chunk = static_cast(v); + if (const char *e = env_str("DEVOURER_DPDT_MODE")) { + if (str_ieq(e, "legacy")) + cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Legacy; + else if (str_ieq(e, "bit24")) + cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Bit24; + else if (str_ieq(e, "skip")) + cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Skip; + else + cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::EfemPinmux; + } /* ---- debug ---- */ cfg.debug.dump_canary = env_flag("DEVOURER_DUMP_CANARY"); diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index fdabcb9..fde3fc3 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -55,6 +55,16 @@ enum class Fwdl8814Path : uint8_t { Rtw88, /* legacy rtw88-mimic sequence (A/B fallback) */ }; +/* 8822E DPDT antenna-transfer-switch routing (board A/B). */ +enum class Dpdt8822eMode : uint8_t { + EfemPinmux, /* kernel _efem_pinmux_config port — DPDT under HW TX/RX + control, both RX chains live (default) */ + Legacy, /* improvised b5a6df7 static write: 0x4c[24] set, [22] clear + (TX-favoring, RX chain B antenna-less) */ + Bit24, /* 0x4c[24] only (halmac WL_DPDT_SEL bit alone) */ + Skip, /* leave the DPDT/pin-mux untouched */ +}; + struct DeviceConfig { /* ---- RX ------------------------------------------------------------- */ struct Rx { @@ -227,6 +237,11 @@ struct DeviceConfig { * site; out-of-range values are ignored). */ Fwdl8814Path fwdl_8814 = Fwdl8814Path::KernelBracket; std::optional fwdl_8814_chunk; + /* env: DEVOURER_DPDT_MODE=efem|legacy|bit24|skip — 8822E DPDT + * antenna-transfer-switch routing for board A/B. Default (efem) ports the + * kernel eFEM pin-mux so the DPDT follows TX/RX in hardware (both RX chains + * live); the others are the pre-fix static routes. See docs/8822e-quirks.md. */ + Dpdt8822eMode dpdt_8822e = Dpdt8822eMode::EfemPinmux; } tuning; /* ---- Diagnostics output --------------------------------------------- */ diff --git a/src/jaguar3/HalJaguar3.h b/src/jaguar3/HalJaguar3.h index 1fdd958..840624e 100644 --- a/src/jaguar3/HalJaguar3.h +++ b/src/jaguar3/HalJaguar3.h @@ -120,6 +120,11 @@ class HalJaguar3 { * RX works. No-op for 8822C. Call after the channel is tuned. */ void config_rfe(uint8_t channel); + /* Resolved efuse RFE type (8822E; 0/0xff already folded to the rfe-21 + * default in rtw_hal_init). The eFEM pin-mux gate keys on this, like the + * kernel's _efem_pinmux_config (rfe 21..24). */ + uint8_t rfe_type() const { return _phy_ctx.rfe_type; } + /* 8822E channel-finalize TX writes the shared (8822c-derived) set_channel_bwmode * omits: the band-specific OFDM Tx backoff / Tx scaling (5 GHz 0x818/0x81c) and * the TX triangular-shaping / CFR (0xa74/0x80c/0x81c/0x8a0) from diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 4615658..66a373f 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -78,6 +78,11 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ _hal.coex_wlan_only_init(); /* lock antenna to WLAN (disable BT/LTE coex) */ + /* 8822E DPDT/eFEM pin-mux — post-coex, so an RX-only session also gets both + * receive chains (GPIO13 -> RFE engine). Kernel parity: _efem_pinmux_config + * runs in rtl8822e_init_misc in both directions. See InitWrite for the TX + * bring-up's copy of this call. No-op on non-8822E. */ + apply_dpdt_route_8822e(); _brought_up = true; /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ @@ -566,10 +571,63 @@ void RtlJaguar3Device::efem_pinmux_8822e() { _device.rtw_write8(x.off, v); } } - _logger->warn("Jaguar3(8822e): eFEM pin-mux applied (RFE_CTRL 3/5/7/8/9/11 " + _logger->info("Jaguar3(8822e): eFEM pin-mux applied (RFE_CTRL 3/5/7/8/9/11 " "-> RFE engine; DPDT under hardware TX/RX control)"); } +/* 8822E DPDT antenna-transfer-switch routing — mode dispatch + PAD_CTRL1 + * post-coex re-assert. Called from BOTH Init (RX) and InitWrite (TX), each + * time right after coex_wlan_only_init: coex sets GPIO_MUXCFG BT-PTA bits that + * would mask the RFE routing, so this MUST follow it. Kernel parity — the + * vendor runs _efem_pinmux_config from rtl8822e_init_misc in both directions. + * + * The eFEM (default) path replaces the improvised b5a6df7 DPDT write, which + * took two 0x4c bits in isolation (set [24], clear [22]) and parked the DPDT + * transfer switch in a static TX-favoring position: TX MCS4+ worked, but RX + * path B lost its antenna on every 8812EU (chain-B pwdb pinned ~10 / -99 dBm + * — invisible to total-frame validation, chain A masks it). The full pinmux + * routes GPIO13 to the RFE engine's RFE_CTRL_9 (WL_DPDT_SEL) so the switch + * follows TX/RX in hardware: PA on TX, BOTH LNAs on RX. Gated on rfe_type + * 21..24 like the kernel (as config_rfe already does); non-eFEM boards keep + * the reset-default pins. + * DEVOURER_DPDT_MODE knob for A/B: efem (default) | legacy (b5a6df7 write) | + * bit24 ([24] only) | skip. */ +void RtlJaguar3Device::apply_dpdt_route_8822e() { + if (_variant != jaguar3::ChipVariant::C8822E) + return; + const devourer::Dpdt8822eMode dpdt_mode = _cfg.tuning.dpdt_8822e; + const uint8_t rfe = _hal.rfe_type(); + if (dpdt_mode == devourer::Dpdt8822eMode::EfemPinmux) { + if (rfe >= 21 && rfe <= 24) + efem_pinmux_8822e(); + else + _logger->warn("Jaguar3(8822e): eFEM pin-mux SKIPPED — rfe_type={} " + "not in 21..24", + rfe); + } else if (dpdt_mode != devourer::Dpdt8822eMode::Skip) { + const uint32_t v4c = _device.rtw_read(0x4c); + if (dpdt_mode == devourer::Dpdt8822eMode::Bit24) + _device.rtw_write(0x4c, v4c | 0x01000000u); + else + _device.rtw_write(0x4c, (v4c & ~0x00400000u) | 0x01000000u); + _logger->warn("Jaguar3(8822e): DPDT legacy write mode={} " + "(0x4c was 0x{:08x})", + dpdt_mode == devourer::Dpdt8822eMode::Bit24 ? "bit24" + : "legacy", + v4c); + } else { + _logger->warn("Jaguar3(8822e): DPDT/eFEM pin-mux SKIPPED"); + } + if (dpdt_mode != devourer::Dpdt8822eMode::Skip) { + /* 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 (bench-validated cold-TX fix). */ + const uint32_t v64 = _device.rtw_read(0x0064); + if ((v64 & 0x30000000u) != 0x30000000u) + _device.rtw_write(0x0064, v64 | 0x30000000u); + } +} + /* 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() { @@ -791,48 +849,7 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { * 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) { - /* eFEM GPIO pin-function routing (rfe 21-24) — port of the kernel's - * _efem_pinmux_config (rtl8822e_halinit.c) → halmac - * pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11. Replaces the - * improvised b5a6df7 DPDT write, which took two 0x4c bits in isolation - * (set [24], clear [22]) and thereby parked the DPDT antenna transfer - * switch in a static TX-favoring position: TX MCS4+ worked, but RX - * path B lost its antenna on every 8812EU (bench 2026-07-14: chain-B - * pwdb pinned ~10 / -99 dBm — invisible to total-frame validation, - * chain A masks it). The full pinmux routes GPIO13 to the RFE engine's - * RFE_CTRL_9 (WL_DPDT_SEL) so the switch follows TX/RX like the - * kernel: PA on TX, BOTH LNAs on RX. Runs post-coex (coex GPIO_MUXCFG - * writes would mask it — same ordering lesson as the pre-coex RFE - * block above). - * DEVOURER_DPDT_MODE knob for A/B: efem (default) | legacy (b5a6df7 - * write) | bit24 ([24] only) | skip. */ - const char *dpdt_env = std::getenv("DEVOURER_DPDT_MODE"); - std::string dpdt_mode = dpdt_env ? dpdt_env : "efem"; - if (std::getenv("DEVOURER_SKIP_DPDT") != nullptr) - dpdt_mode = "skip"; - if (dpdt_mode == "efem") { - efem_pinmux_8822e(); - } else if (dpdt_mode != "skip") { - const uint32_t v4c = _device.rtw_read(0x4c); - if (dpdt_mode == "bit24") - _device.rtw_write(0x4c, v4c | 0x01000000u); - else - _device.rtw_write(0x4c, (v4c & ~0x00400000u) | 0x01000000u); - _logger->warn("Jaguar3(8822e): DPDT legacy write mode={} " - "(0x4c was 0x{:08x})", dpdt_mode, v4c); - } else { - _logger->warn("Jaguar3(8822e): DPDT/eFEM pin-mux SKIPPED"); - } - if (dpdt_mode != "skip") { - /* 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 (bench-validated cold-TX fix). */ - const uint32_t v64 = _device.rtw_read(0x0064); - if ((v64 & 0x30000000u) != 0x30000000u) - _device.rtw_write(0x0064, v64 | 0x30000000u); - } - } + apply_dpdt_route_8822e(); /* 8822E DPDT/eFEM pin-mux (post-coex) */ 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 diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 7ad8784..cc60c4a 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -285,8 +285,16 @@ class RtlJaguar3Device : public IRtlDevice { * _efem_pinmux_config/pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11. * Routes the DPDT antenna transfer switch to the RFE engine (hardware * TX/RX switching) instead of the b5a6df7 static write that deafened RX - * path B. Called from InitWrite post-coex; see the call site. */ + * path B. Applied by apply_dpdt_route_8822e(). */ void efem_pinmux_8822e(); + /* 8822E DPDT antenna-transfer-switch routing — mode dispatch + * (_cfg.tuning.dpdt_8822e: efem/legacy/bit24/skip) + the PAD_CTRL1[29:28] + * post-coex re-assert. No-op on non-8822E. MUST run after + * coex_wlan_only_init (coex GPIO_MUXCFG writes would mask the RFE routing). + * Called from BOTH Init (RX bring-up) and InitWrite (TX bring-up) so RX-only + * sessions get chain B too — the kernel routes it in rtl8822e_init_misc, + * which runs in both directions. */ + void apply_dpdt_route_8822e(); /* Runtime TX-mode default (SetTxMode/ClearTxMode). */ std::optional _tx_mode_default;