diff --git a/docs/8822e-quirks.md b/docs/8822e-quirks.md index 719c300..967ded6 100644 --- a/docs/8822e-quirks.md +++ b/docs/8822e-quirks.md @@ -22,14 +22,28 @@ 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). 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 ONE chain (2SS gets both). The old 1ss-on-both mapping (`0x33`) interacts @@ -49,11 +63,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/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 1972576..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). */ @@ -434,6 +439,195 @@ 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->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() { @@ -655,28 +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) { - /* 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_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 53cd596..cc60c4a 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -281,6 +281,20 @@ 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. 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;