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.
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.
…RX-only Init Review response (josephnef, PR OpenIPC#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) <noreply@anthropic.com>
…ux shared into RX-only Init Lands the review-response commit (5d901cb) on top of the earlier eFEM pin-mux merge: DEVOURER_DPDT_MODE via DeviceConfig, rfe_type 21-24 gate, warn->info, and apply_dpdt_route_8822e() shared between Init and InitWrite. Bench-validated on an 8812EU (RX chain-B alive under eFEM, dead under the legacy static route). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
josephnef
left a comment
There was a problem hiding this comment.
Approving — both blocking items from the review are fixed in 5d901cb, and I re-validated on hardware at the new head (205dac0).
Code: DEVOURER_DPDT_MODE now routes through DeviceConfig (tuning.dpdt_8822e) with the env mapping in env_config.cpp and the undocumented DEVOURER_SKIP_DPDT folded into mode=skip; the eFEM pin-mux is gated on rfe_type 21–24 via the new HalJaguar3::rfe_type() (ordering safe — the efuse value resolves before either call site). The deferred Init/InitWrite asymmetry got a proper fix too: apply_dpdt_route_8822e() runs in both bring-ups post-coex_wlan_only_init, matching rtl8822e_init_misc. CI green including the 8822e-only config.
Bench (second independent board — slot BL-M8812EU2, rfe 21, ch36, 8812AU peer):
-
TX regression,
InitWritepath (tests/regress.py --tx-pid a81a --rx-pid 8812, devourer:devourer): 1100/8000 hits / 15 s — TX alive, no regression. -
The new RX-only
Initpath, per-chain RSSI (DEVOURER_RX_ALLPATHS=1), VBUS cold-cycle per trial, ~5.5krx.pathframes/mode:mode chain A med chain B med efem 63 67 legacy 9 22 efem (repeat) 64 68 Independently reproduces the fix on the
Initpath. Interesting datapoint: on this board the legacy route degrades both chains, not just B (your board showed A alive / B dead) — same direction, board-dependent damage. The efem repeat matches the first run exactly, the gate passed ([I] eFEM pin-mux applied), and the legacy branch warns with the0x4cpre-value as designed.
Thanks for the thorough response — the extraction into apply_dpdt_route_8822e() plus your own cold-cycled A/B table made this easy to verify.
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