jaguar3(8822e): port kernel eFEM GPIO pinmux — RX path B was antenna-less under the static DPDT route#289
Conversation
…less under the static DPDT route The 8822E front-end fix (OpenIPC#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 OpenIPC#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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbMmK39gvTZUeWuAx8ujCH
josephnef
left a comment
There was a problem hiding this comment.
Reviewed with the vendor tree open and the local bench running — details per finding inline.
Verified:
- Vendor-source parity is exact. Diffed every table against
reference/rtl88x2eu/.../halmac_gpio_8822e.c: all 6 entry lists match entry-for-entry, correctly truncated at the firstswitch_funcmatch perpinmux_switch_8822e(GPIO2/14 stop at the_4_5variant, GPIO4/13 atWL_RFE_CTRL_8/9); walk semantics (byte RMW,~val & mskdisable, enable target, stop) and all post-switch fixups match, address arithmetic checked (0x43=REG_GPIO_MUXCFG+3,0x4F=REG_LED_CFG+3— bit0 ≙0x4c[24], so the eFEM path still sets the old TX bit via the proper claim;0x67=REG_PAD_CTRL1+3,0x3F=REG_GPIO_MUXCFG_2). Call order 28..33 → RFE_CTRL_3,5,7,8,9,11 matches_efem_pinmux_config/_gpio_to_func_for_rfe_ctrl. - Build clean, ctest 20/20.
- On-air TX A/B on this rig (
regress.py --modes devourer:devourer, 8812EU TX → 8812AU RX, ch36, 10 s cells, two RX cells per mode): legacy 270/200 hits, efem 283/200, bit24 90/10 — efem at TX parity with legacy, bit24 collapsed. Independently confirms the TX side of your matrix. The RX chain-B half isn't locally verifiable here (single 8812EU on the rig), but the mechanism is vendor-exact and the bench evidence is coherent.
Asks before merge: the env-read convention violation (inline) and preferably the one-line rfe_type 21–24 gate (inline). The Init-vs-InitWrite asymmetry can be a follow-up bench check alongside your suggested GetActiveRxPaths CI gating.
Good work — the bisect narrative is convincing and the legacy A/B escape hatch is the right safety net.
| 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"; |
There was a problem hiding this comment.
Convention: the library reads no environment (CLAUDE.md Configuration). Every existing knob — including apply_replay_wseq just below, which reads _cfg.debug.replay_wseq — routes through DeviceConfig with the env mapping in examples/common/env_config.cpp. Please move this to a debug (or tuning) field with the usual env: doc tag and map DEVOURER_DPDT_MODE there.
Also DEVOURER_SKIP_DPDT is an undocumented second spelling of mode=skip (it appears nowhere in the docs or DeviceConfig) — drop it or fold it into the documented knob.
| if (std::getenv("DEVOURER_SKIP_DPDT") != nullptr) | ||
| dpdt_mode = "skip"; | ||
| if (dpdt_mode == "efem") { | ||
| efem_pinmux_8822e(); |
There was a problem hiding this comment.
The kernel gates _efem_pinmux_config on rfe_type 21–24 (rtl8822e_halinit.c); this applies unconditionally on every 8822E. The gate is cheap kernel parity: _phy_ctx.rfe_type is already read from efuse, and the existing RFE pad block (HalJaguar3.cpp, config_rfe) already gates on 21..24. All benched boards are rfe 21 (and devourer defaults efuse 0/0xff → 21), so low practical risk today — but a non-eFEM 8822E board would get GPIOs claimed for an RFE engine its front end doesn't wire.
| _logger->warn("Jaguar3(8822e): eFEM pin-mux applied (RFE_CTRL 3/5/7/8/9/11 " | ||
| "-> RFE engine; DPDT under hardware TX/RX control)"); |
There was a problem hiding this comment.
warn for the default/successful path — info is the house style for applied-config lines (the warns on the legacy/skip branches are right, those should be loud).
| * 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 |
There was a problem hiding this comment.
Nit: the hardcoded bench date will rot; the fact stands without it (git is the changelog).
| * 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(); |
There was a problem hiding this comment.
The kernel runs this in rtl8822e_init_misc during general HAL init (both directions); this port only touches InitWrite, so RX-only Init sessions keep the reset-default pins (GPIO13 parked as BT_WAKE per your own read-back). Worth one bench check: is chain B alive in an RX-only session? If not, the pinmux belongs in the shared bring-up; if it is, a one-line comment on the asymmetry would prevent the next reader from "fixing" it. Fine as a follow-up alongside your suggested per-chain CI gating.
josephnef
left a comment
There was a problem hiding this comment.
Marking changes requested per the inline review (#289 (review)): (1) move DEVOURER_DPDT_MODE off std::getenv into DeviceConfig + env_config.cpp (the library reads no environment) and drop/fold the undocumented DEVOURER_SKIP_DPDT; (2) gate the eFEM pinmux on rfe_type 21–24 like the kernel. The Init-vs-InitWrite asymmetry and the nits can be follow-ups.
Summary
The 8822E front-end fix (#268 / b5a6df7) routes the DPDT antenna transfer switch with a static two-bit write:
REG_LED_CFG 0x4c[24]set,[22]clear. That made MCS4+ TX work — but it parks 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) no matter which antenna is fitted.Nobody saw it because every RX validation to date — including #268's full-duplex proof and the 0x41e8 desense recheck — counts total frames, and chain A alone carries the stream. The loss is real, though: a dead chain B forfeits MRC diversity (3–5 dB, more in NLOS multipath).
This PR replaces the static write with a port of the kernel's actual mechanism:
_efem_pinmux_config(rtl8822e_halinit.c, rfe 21–24) → halmacpinmux_set_func_8822eforRFE_CTRL_3/5/7/8/9/11, transcribed entry-for-entry fromhalmac_gpio_8822e.c. GPIO13 becomesWL_DPDT_SELdriven 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.Evidence
Two-adapter bench (BL-M8812EU2 ↔ 8812EU, ch149, 20 MHz, MCS5 at 6 Mbps offered unless noted, txagc 30 near-field, per-chain rssi/snr from the phystatus via a per-chain-reporting RX tool). Same card, back-to-back bring-ups, only the DPDT route changing:
0x4c[24]+[22]-clear (current)0x4c[24]only (halmacWL_DPDT_SELbit alone)[22]untouched revives chain B (and breaks TX — hence neither bit combination is sufficient; the pin needs its function claim).0x4c[22]SET from reset (0x0062e282), i.e. GPIO13 parked as BT_WAKE — which is also why[24]-only TX airs nothing: no one has claimed the pin for the RFE engine. The missing piece is the pin-function claim (0x42[1]on GPIO13 +0x43[3]/[4]fixups), which only the full pinmux walk performs.Implementation notes
RtlJaguar3Device::efem_pinmux_8822e(): per-pin priority-list walk (halmac semantics — disable every claim above the target:field := ~val & msk; enable the target:field := val & msk; stop) + the WL/BT ownership fixups, as data tables. Kernel call order preserved (RFE_CTRL_3, 5, 7, 8, 9, 11).InitWrite— coexGPIO_MUXCFGwrites would mask it; same ordering lesson as the existing RFE pad block. ThePAD_CTRL1[29:28]post-coex re-assert is kept.DEVOURER_DPDT_MODE=legacy|bit24|skip(defaultefem).docs/8822e-quirks.mdupdated: the pin-mux section now describes the hardware-controlled route and its A/B matrix, and adds the honest moral — per-chain RSSI is the only trustworthy RX-health metric on a 2T2R part.Suggested follow-up (not in this PR)
GetActiveRxPaths/per-chain health could gate CI-style validation so a single-chain regression can't hide behind total-frame counts again.🤖 Generated with Claude Code