From 609b0c7c6e93cc5676a17918f6ce35e36157561c Mon Sep 17 00:00:00 2001 From: Gilang Date: Tue, 14 Jul 2026 01:01:54 +0700 Subject: [PATCH] =?UTF-8?q?jaguar3(8822e):=20port=20kernel=20eFEM=20GPIO?= =?UTF-8?q?=20pinmux=20=E2=80=94=20RX=20path=20B=20was=20antenna-less=20un?= =?UTF-8?q?der=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;