Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions docs/8822e-quirks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand Down
196 changes: 176 additions & 20 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>(~e.msk);
if (i + 1 == f.n_ents)
v |= static_cast<uint8_t>(e.val & e.msk); /* enable the target */
else
v |= static_cast<uint8_t>(~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<uint8_t>(v | x.bit)
: static_cast<uint8_t>(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() {
Expand Down Expand Up @@ -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<uint32_t>(0x4c);
_device.rtw_write<uint32_t>(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<uint32_t>(0x0064);
if ((v64 & 0x30000000u) != 0x30000000u)
_device.rtw_write<uint32_t>(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<uint32_t>(0x4c);
if (dpdt_mode == "bit24")
_device.rtw_write<uint32_t>(0x4c, v4c | 0x01000000u);
else
_device.rtw_write<uint32_t>(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<uint32_t>(0x0064);
if ((v64 & 0x30000000u) != 0x30000000u)
_device.rtw_write<uint32_t>(0x0064, v64 | 0x30000000u);
}
}
apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */
if (_cfg.debug.bb_dump) {
Expand Down
6 changes: 6 additions & 0 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<devourer::TxMode> _tx_mode_default;

Expand Down
Loading