Skip to content
Merged
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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**):
Sustained 5 GHz TX needs the **coex runtime thread**
(`RtlJaguar3Device::coex_runtime_loop`, started in `InitWrite`) — without its
~2 s WiFi-only coex re-apply + FW heartbeats, the combo chip's coex firmware
silences the antenna.
silences the antenna. The rtl8822e's hardware-bisected constraints (DPDT/
pin-mux front end, single-path 1SS TX, spur channels, LCK, the 2.4 GHz TX
kernel-parity limitation) live in `docs/8822e-quirks.md`.

Naming traps: **RTL8821AU is Jaguar1** (not Jaguar2, despite the Jaguar2
RTL8821C's similar name); RTL8822**B**U (Jaguar2) ≠ RTL8822**C**U (Jaguar3).
Expand Down Expand Up @@ -224,9 +226,7 @@ Knob-specific facts that aren't obvious from the field docs:
- `DEVOURER_TX_WITH_RX=thread` (concurrent TX+RX on one claimed handle:
`InitWrite` once, then `StartRxLoop` on a thread) must be set **before**
`InitWrite` on Jaguar3 — the bring-up keeps the RX filters open; retrofitting
RX later is unreliable. On the 8822E, TX+RX mode leaves the path-B OFDM TXAGC
reference (0x41e8) at table default — any nonzero value there desenses the
EU's RX to near-deaf (hardware-bisected, value-independent). This is the
RX later is unreliable. This is the
single-radio beamforming self-sounding station: pair with
`DEVOURER_BF_ARM_SOUNDER` / `DEVOURER_TX_NDPA` / `DEVOURER_BF_DETECT_REPORT`
(`docs/beamforming-self-sounding.md`). Non-`thread` values select a
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ if(DEVOURER_JAGUAR3)
src/jaguar3/HalmacJaguar3MacInit.cpp src/jaguar3/HalmacJaguar3MacInit.h
src/jaguar3/HalmacJaguar3Regs.h
src/jaguar3/RadioManagementJaguar3.cpp src/jaguar3/RadioManagementJaguar3.h
src/jaguar3/PhydmRuntimeJaguar3.cpp src/jaguar3/PhydmRuntimeJaguar3.h
src/jaguar3/FrameParserJaguar3.h
src/jaguar3/PhyTableLoaderJaguar3.cpp src/jaguar3/PhyTableLoaderJaguar3.h
src/jaguar3/Phy8822cTables.cpp
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Bandwidth cells are devourer's measured on-air TX throughput (Mbps, HT MCS7,
| **RTL8821CU** | 1T1R + BT | — | — | — | rides the 8811CU (8821C) code path. 5/10 MHz capable |
| **RTL8812CU** | 2T2R | 65 | 60 | 60 | LB-LINK WDN1300H (`0bda:c812`). 5/10 MHz capable |
| **RTL8822CU** | 2T2R + BT | — | — | — | not benchmarked (`0bda:c82c`). 5/10 MHz capable |
| **RTL8812EU** | 2T2R | 8 | 51 | 47 | LB-LINK BL-M8812EU2 (`0bda:a81a`); bare 5 GHz FPV module. 5/10 MHz capable |
| **RTL8812EU** | 2T2R | | 51 | 47 | LB-LINK BL-M8812EU2 (`0bda:a81a`); bare 5 GHz FPV module. 5/10 MHz capable. ‡ 2.4 GHz TX airs energy but no receiver decodes it — the vendor kernel driver behaves identically on this module ([quirks](docs/8822e-quirks.md)) |
| **RTL8822EU** | 2T2R + BT | — | — | — | not benchmarked. 5/10 MHz capable |
| **RTL8821CE** (PCIe) | 1T1R + BT | — | — | — | Radxa X4 onboard Wi-Fi (`10ec:c821`); not benchmarked |

Expand Down
166 changes: 166 additions & 0 deletions docs/8822e-quirks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# RTL8822E (RTL8812EU / RTL8822EU) — definitive quirks

The rtl8822e family (WiFi-only **RTL8812EU** `0bda:a81a`, BT-combo **RTL8822EU**
`0bda:e822`, chip-id `0x17`, Jaguar3) as devourer drives it today. Every entry
states what the chip needs, what devourer does about it, the residual cost if
any, and the reproducer that proves it. Reference hardware: LB-LINK
BL-M8812EU2 (`0bda:a81a`, rfe-type 21); the BT-combo part and a second board
for variance remain untested (see **Untested**).

The methodology that closed most of these: vendor-kernel ground truth on the
same unit (`tests/eu_kernel_mcs_probe.sh`), full MAC+BB end-state diff
kernel-vs-devourer (`tests/eu_bb_endstate_diff.sh` — kernel via `/proc`
`read_reg` live, devourer via `DEVOURER_BB_DUMP`), then subset bisection with
the Jaguar3 golden-replay hook (`DEVOURER_REPLAY_WSEQ`).

## Front-end pin-mux is load-bearing (DPDT / PAPE / pads)

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.
- **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
with the DPDT fix — MCS0 TX stalls (bulk NAK wedge) and MCS7 delivery
collapses. Path B is selected at 5 GHz on measured merit (MCS7 ~2.5× the
delivery of 1ss-A at ~3 dB better EVM on this module); the 2.4 GHz choice is
the kernel default, unvalidated (see **2.4 GHz TX**).
- **`PAD_CTRL1 0x64[29:28]`**: halmac pre-init sets both; the bring-up's
FW/coex steps clear bit 29 — re-asserted post-coex.

Reproducers: `tests/eu_mcs7_txagc_fix.sh` (parametrized TX-rate/power cell
bench, EU→CU), `tests/eu_bb_endstate_diff.sh`. Bench state: full ladder clean
at ch36 — MCS0/MCS4/MCS7/54M ≈ 5k frames / 12 s at EVM −26/−32/−48/−45.

## TX+RX mode and the path-B TXAGC reference (0x41e8)

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.

`DEVOURER_TX_WITH_RX=thread` must still be set **before** `InitWrite`
(bring-up keeps the RX filters open; retrofitting RX later is unreliable).

## RF register 0x0 needs the legacy FON write port

RF reg `0x0` (mode register) writes through the direct `0x3c00`/`0x4c00` BB
window silently no-op — the kernel special-cases it through the FON port
`0x1808`/`0x4108` (`addr<<20 | data`). Devourer does the same in the halrf
code and the radio-table loader (the vendor tables carry reg-0 entries).
Reads stay direct-window for every register. Same rule on the 8822C.

## Channel-switch obligations (run at EVERY switch)

Ported straight from the kernel's `switch_channel` tail; skipping any of these
was a real failure on the bench:

- **IGI toggle** (`0x1d70` −0x202/restore): forces the BB to send the 3-wire
command so the RF re-enters RX mode — the BB does not do this on its own
after path/channel/BW changes. Symptoms when missing: intermittent
post-switch RX deafness and a first-cell TX wedge (~50 frames then bulk
NAK). Applies to both Jaguar3 variants. Liveness reproducer:
`tests/cu_2g_ground_liveness.sh`.
- **Spur elimination**: per-channel NBI notch + CSI mask + packet-detection
for the 14 channel/BW combos whose synthesizer harmonics land in-band
(153/161/169@20, 151/159/167@40, 155/171@80, 54/102/118@40, 58/106/122@80),
explicit spur-free default elsewhere. `FastRetune` declines hops into or
out of these combos (the lean hop can't reprogram the notch state).
- **CCK TX shaping filter** (2.4 GHz, per channel; ch14 special set) + per-band
TX backoff/scaling.
- 2.4 GHz RX needs the RF-write force-update brackets (`0x1830[29]`/
`0x4130[29]`) around the RF18 tune — the BB-window write path does not push
the analog front-end shadow on its own.

## Runtime obligations (the ~2 s watchdog cadence)

devourer runs the vendor watchdog's monitor-mode dynamic mechanisms
(`PhydmRuntimeJaguar3`, both Jaguar3 variants) — from the coex thread in
TX/TX+RX sessions, from a dedicated housekeeping thread in RX-only sessions
(register I/O must not run on the bulk-IN event thread):

- **FA/CCA window statistics** (read + reset per tick) feeding
- **DIG**: unlinked IGI stepping by false-alarm level (thresholds
2000/4000/5000 per window; DFS channels pin `0x20`), written to `0x1d70`
per path. The coverage window floors at `0x1e`, NOT phydm's generic
`DIG_MIN_COVERAGE 0x1c`: at IGI `0x1c` the 8822CU's MCS4+/dense-QAM RX
decodes nothing (0 of 65k kernel-injected MCS7 frames) while `0x1e` is
transparent (65.9k/65k) — hardware-bisected, value-specific, the same
class of per-IC "For HW setting" floor exception the kernel carries for
other ICs.
- **CCK packet detection** (type4, 2.4 GHz): CCK-FA moving average drives the
PD/CS level ladder in `0x1ac8/0x1acc/0x1ad0`.
- **EDCCA tracking**: `th_l2h = max(IGI+8, 48)`, `th_h2l = th_l2h − 8` into
`0x84c` (matches the kernel end state). `SetCcaMode`'s EDCCA-disable knob
suppresses this tracking.

The kernel's remaining watchdog mechanisms self-disable without a link and
are intentionally absent: CFO tracking (returns unless associated), rate
adaptation and the beamforming watchdog (no station entries), antenna
diversity (off on 2T2R).

## Coex / thermal (the same tick)

- **Coex re-apply + FW heartbeats**: without the WiFi-only coex re-apply the
combo firmware silences the antenna during sustained 5 GHz TX (shared
Jaguar3 behavior).
- **Thermal swing tracking**: RF `0x42[6:1]` meter → delta-swing table →
`0x18a0/0x41a0[7:0]`. The chip runs THERMAL mode (the kernel's TSSI
machinery is compiled in but its efuse mode-select is forced to thermal;
`0x1e7c[30]`=0 on both drivers — the 2600-line TSSI subsystem is *not* a
devourer gap).
- **LCK synthesizer re-lock**: when the averaged thermal drifts ≥4 units from
the LCK baseline, re-run AACK+RTK (RF-A `0xca[0]` / `0xcc[18]` pulse+poll —
the SYN lives on path A). Validated live: 22-minute max-duty soak
(`tests/eu_heat_soak_lck.sh`) — 1.10 M clean MCS7 frames, re-lock fired at
drift=4, EVM drift +3 dB.
- The FW power-mode/coex H2Cs **rewrite the OFDM TXAGC refs wholesale** during
bring-up — devourer's authoritative TXAGC apply runs after them (the coex
runtime ticks do not re-clobber; watchable via the coex-tick register trace
under `DEVOURER_LOG_LEVEL=debug`).

## 2.4 GHz TX: undecodable on this module — kernel parity

The module airs 2.4 GHz TX energy (SDR duty ≈ expected) but **no receiver
decodes any of it** — CCK and OFDM alike, at any power, under **both**
devourer and the vendor kernel driver (`tests/eu_kernel_2g_verified.sh`: the
same verified ground decodes an 8812AU control at −63 EVM while 51k
kernel-injected EU frames yield zero). Treat 2.4 GHz TX as unusable on the
BL-M8812EU2 until a second board proves it module-specific vs family-wide.
2.4 GHz **RX** works. (The old README "8 Mbps @ ch6" figure was SDR
duty×rate — energy, not decodable throughput.)

## EFUSE / bring-up constraints

- **OTP/efuse is only reliably readable at init** (burst mode + sw-power-cut,
one-shot); post-bring-up probes are refused by design.
- Efuse supplies: per-channel-group per-path TXAGC base refs (`0x22`/`0x4C`
logical), rfe-type (`0xca`), thermal baselines (`0xd0`/`0xd1`).
- **DPK is force-bypassed** on rfe 21/22 — by the kernel too; both drivers run
DPK-bypassed.
- **IQK clears the 40 MHz `TX_CCK_IND` RF bit** — calibration-order fix-up in
the channel path.
- The TXAGC ref→power transfer is **not a constant 0.25 dB/step** on the E
(accelerating ~0.3→0.9 dB/idx up the range; TSSI/kfree trims reshape it) —
`GetTxPowerCaps().step_measured` stays false; controllers should calibrate
their own dB-per-step or use ground RSSI.

## Untested

- **RTL8822EU (`0bda:e822`, BT combo)**: entirely untested — including whether
it needs more than the WiFi-only coex handling the CU-style combo parts get.
- **Board variance**: all of the above is one LB-LINK BL-M8812EU2 (rfe 21).
rfe 22–24 boards differ in RFE pin mapping and DPK policy; the 2.4 GHz TX
verdict especially needs a second board.
11 changes: 11 additions & 0 deletions examples/tx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,17 @@ int main(int argc, char **argv) {
if (const char *p = std::getenv("DEVOURER_TX_PWR"))
rtlDevice->SetTxPower(static_cast<uint8_t>(std::strtol(p, nullptr, 0)));

/* DEVOURER_TX_PWR_OFFSET_QDB=N — quarter-dB offset relative to the
* efuse-calibrated per-rate/per-path default (SetTxPowerOffsetQdb).
* Unlike DEVOURER_TX_PWR (flat: both paths forced to ONE index, per-rate
* diffs zeroed) this PRESERVES the per-path trim and by-rate shape —
* e.g. -44 on an 8822e shifts both OFDM refs down 11 dB while keeping
* the path-A/path-B calibration spread. Recorded now, applied at
* bring-up. */
if (const char *p = std::getenv("DEVOURER_TX_PWR_OFFSET_QDB"))
rtlDevice->SetTxPowerOffsetQdb(
static_cast<int>(std::strtol(p, nullptr, 0)));

/* DEVOURER_TX_PKT_OFSET=N — (Jaguar2 8822B/8821C) default per-packet
* TXPWR_OFSET LUT step written into every TX descriptor: 0=none, 1=-3dB,
* 2=-7dB, 3=-11dB, 4=+3dB, 5=+6dB. The measurement driver for the descriptor
Expand Down
31 changes: 23 additions & 8 deletions src/jaguar3/HalJaguar3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,21 @@ void HalJaguar3::config_channel_8822e(uint8_t channel) {
_device.phy_set_bb_reg(0x81c, 0x001fc000, 0x4); /* Tx scaling */
}

/* phydm_config_tx_path_8822e (BB_PATH_AB): OFDM + CCK TX antenna routing. The
* phy_reg table leaves 0x820 at its default (0x11111111 = path-A-only on [7:0]);
* the vendor runtime overrides it to the AB mapping. Without this the OFDM TX
* path is not routed to both antennas — on this module the 5 GHz output collapses
* (the antenna for 5 GHz is not on the default path) while 2 GHz still works.
* OFDM 0x820[7:0]=0x33 (AB 2ss/1ss) + 0x1e2c[15:0]=0x0404
/* phydm_config_tx_path_8822e: OFDM + CCK TX antenna routing. The phy_reg
* table leaves 0x820 at its default (0x11111111 = 2ss/1ss both path-A on
* [7:0]); the vendor runtime overrides it. Kernel semantics
* (rtw_hal_runtime_trx_path_decision): 2SS rates drive both chains, 1SS
* rates (every HT MCS / legacy OFDM frame) drive ONE selected path — a
* 1ss-on-both mapping (0x33/0x0404) sums two chains and interacts with
* the DPDT pin-mux write in RtlJaguar3Device::InitWrite (MCS0 TX stalls,
* MCS7 delivery collapses). At 5 GHz 1SS selects path B: bench
* head-to-head on the BL-M8812EU2 it delivers ~2.5x the MCS7 frames of
* 1ss-A at ~3 dB better EVM. At 2.4 GHz 0x31 is the kernel's default
* (unvalidated on-air here — see docs/8822e-quirks.md, 2.4 GHz TX).
* OFDM 0x820[7:0]=0x31/0x32 (2ss AB, 1ss A@2G / B@5G) + 0x1e2c=0x0400
* CCK 0x1a04[31:28]=0xc (AB) */
_device.phy_set_bb_reg(0x820, 0xff, 0x33);
_device.phy_set_bb_reg(0x1e2c, 0xffff, 0x0404);
_device.phy_set_bb_reg(0x820, 0xff, is_2g ? 0x31 : 0x32);
_device.phy_set_bb_reg(0x1e2c, 0xffff, 0x0400);
_device.phy_set_bb_reg(0x1a04, 0xf0000000, 0xc);

/* phydm_tx_triangular_shap_cfg_8822e: CFR + triangular TX shaping (both bands). */
Expand Down Expand Up @@ -903,6 +909,15 @@ void HalJaguar3::apply_bb_rf_agc_tables() {
case 0xffe: std::this_thread::sleep_for(std::chrono::milliseconds(50)); return;
case 0xfe: std::this_thread::sleep_for(std::chrono::microseconds(100)); return;
case 0xffff: std::this_thread::sleep_for(std::chrono::microseconds(1)); return;
case 0x0:
/* RF reg 0x0 (mode register) can't be written through the direct
* window — it silently no-ops (hardware-observed on the 8822e). The
* kernel's table load routes it through config_phydm_write_rf_reg,
* i.e. the legacy FON write port 0x1808 (A) / 0x4108 (B), addr in
* [27:20] (0 here), data in [19:0]. The radio tables DO carry reg-0
* entries (0x00010000 / 0x0003001F), so this branch is load-bearing. */
_device.rtw_write32(base == 0x4c00 ? 0x4108 : 0x1808, data & RFREG_MASK);
return;
default:
_device.phy_set_bb_reg(static_cast<uint16_t>(base + ((addr & 0xff) << 2)),
RFREG_MASK, data);
Expand Down
21 changes: 19 additions & 2 deletions src/jaguar3/Halrf8822c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,28 @@ uint32_t Halrf8822c::rf_read(uint8_t path, uint16_t addr, uint32_t mask) {
return bb_get(direct, mask & RFREG_MASK);
}

/* config_phydm_direct_write_rf_reg_8822c: RF write via the same window. */
/* config_phydm_write_rf_reg_8822c: RF reg 0x0 (the mode register) canNOT be
* written through the direct 0x3c00/0x4c00 window — the kernel routes it
* through the legacy FON write port 0x1808 (A) / 0x4108 (B), addr in [27:20],
* data in [19:0] (same split as the 8822e, where the direct-window write was
* hardware-observed to silently no-op). Every other register goes through the
* direct window (config_phydm_direct_write_rf_reg_8822c). */
void Halrf8822c::rf_write(uint8_t path, uint16_t addr, uint32_t mask,
uint32_t val) {
mask &= RFREG_MASK;
if ((addr & 0xff) == 0x0) {
uint32_t data = val;
if (mask != RFREG_MASK) {
uint32_t orig = rf_read(path, addr, RFREG_MASK);
data = (orig & ~mask) | ((val << mask_shift(mask)) & mask);
}
uint32_t data_and_addr =
(((addr & 0xffu) << 20) | (data & 0x000fffffu)) & 0x0fffffffu;
_device.rtw_write32(path & 1 ? 0x4108 : 0x1808, data_and_addr);
return;
}
uint16_t direct = static_cast<uint16_t>(RF_WIN[path & 1] + ((addr & 0xff) << 2));
bb_set(direct, mask & RFREG_MASK, val);
bb_set(direct, mask, val);
}

/* _iqk_nctl_8822c — load the IQK calibration-engine microcode (generated table
Expand Down
Loading
Loading