Skip to content

jaguar3(8822e): port kernel eFEM GPIO pinmux — RX path B was antenna-less under the static DPDT route#289

Merged
josephnef merged 3 commits into
OpenIPC:masterfrom
gilankpam:fix/8822e-efem-pinmux
Jul 15, 2026
Merged

jaguar3(8822e): port kernel eFEM GPIO pinmux — RX path B was antenna-less under the static DPDT route#289
josephnef merged 3 commits into
OpenIPC:masterfrom
gilankpam:fix/8822e-efem-pinmux

Conversation

@gilankpam

@gilankpam gilankpam commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

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) → halmac pinmux_set_func_8822e for RFE_CTRL_3/5/7/8/9/11, 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.

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:

DPDT route RX chain A RX chain B TX @ MCS7 (10M offered)
static 0x4c[24]+[22]-clear (current) −68 dBm / 27 dB −99 dBm / dead 9.9M goodput, clean
0x4c[24] only (halmac WL_DPDT_SEL bit alone) −68 / 27 −77 / 19 airs nothing
eFEM pinmux (this PR) −70 / 26 −77 / 19 9.7M goodput, clean
  • The bit-level bisect that found it: with the full merge in place, skipping only the DPDT block brings chain B back; skipping only the 1SS TX mapping does not. Within the block, keeping [22] untouched revives chain B (and breaks TX — hence neither bit combination is sufficient; the pin needs its function claim).
  • Read-back before the write shows 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.
  • Validated with both link ends running this port simultaneously: TX MCS7 9.7M / MCS5 6.0M at 0.0% loss / MCS0 clean, while RX reports both chains live (−70/−77 dBm, snr 26/19) — the combination that was previously impossible.
  • 0x41e8 history (quirks doc updated): the register is exonerated by direct A/B; the "RX desense" it was blamed for was this antenna disconnection all along, and both prior rechecks were blind to it because they counted total frames.

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).
  • Runs post-coex in InitWrite — coex GPIO_MUXCFG writes would mask it; same ordering lesson as the existing RFE pad block. The PAD_CTRL1[29:28] post-coex re-assert is kept.
  • The old routes remain selectable for board A/B: DEVOURER_DPDT_MODE=legacy|bit24|skip (default efem).
  • docs/8822e-quirks.md updated: 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

…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 josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 first switch_func match per pinmux_switch_8822e (GPIO2/14 stop at the _4_5 variant, GPIO4/13 at WL_RFE_CTRL_8/9); walk semantics (byte RMW, ~val & msk disable, 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.

Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.cpp Outdated
Comment thread src/jaguar3/RtlJaguar3Device.h

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
gilankpam added a commit to gilankpam/devourer that referenced this pull request Jul 15, 2026
…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 josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, InitWrite path (tests/regress.py --tx-pid a81a --rx-pid 8812, devourer:devourer): 1100/8000 hits / 15 s — TX alive, no regression.

  • The new RX-only Init path, per-chain RSSI (DEVOURER_RX_ALLPATHS=1), VBUS cold-cycle per trial, ~5.5k rx.path frames/mode:

    mode chain A med chain B med
    efem 63 67
    legacy 9 22
    efem (repeat) 64 68

    Independently reproduces the fix on the Init path. 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 the 0x4c pre-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.

@josephnef josephnef merged commit 7e83fbb into OpenIPC:master Jul 15, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants