Skip to content

T1: port 8812AU phydm I/Q calibration#66

Merged
josephnef merged 3 commits into
masterfrom
t1-8812au-iqk-port
Jun 2, 2026
Merged

T1: port 8812AU phydm I/Q calibration#66
josephnef merged 3 commits into
masterfrom
t1-8812au-iqk-port

Conversation

@josephnef
Copy link
Copy Markdown
Collaborator

@josephnef josephnef commented Jun 2, 2026

Summary

Port of upstream phydm's IQK (phy_iq_calibrate_8812a_iqk_tx_8812a) for RTL8812AU. Closes the IQK side of T1 canary divergence vs aircrack-ng/88XXau:

  • BB 0x8b0: 0x00000642 matches kernel byte-for-byte (was 0x00000618 pre-port — IQK side effect we'd been mis-attributing as static init drift).
  • IQK output regs (0xc10, 0xccc, 0xcd4, 0xe10, 0xecc, 0xed4) now get calibrated values; previously held BB-init seeds.
  • Convergence: TX A_done=1 B_done=1, RX A_done=1 B_done=1 with 0-1 retries per path.

Wiring

  • HalModule::rtl8812au_hal_init arms _needIQK = true post-RF-config via new RadioManagementModule::ArmIQKOnNextChannelSet.
  • phy_SwBand8812 arms _needIQK = true on band transitions (2.4G ↔ 5G).
  • phy_SwChnlAndSetBwMode8812 calls _iqk.Calibrate() when _needIQK is set.
  • Env knobs:
    • DEVOURER_FORCE_IQK=1 — run IQK on every channel-set (canary-diff workflow).
    • DEVOURER_DISABLE_IQK=1 — emergency escape hatch.

Scope

In: _phy_iq_calibrate_8812a end-to-end (backup → run → restore), _iqk_tx_8812a TX-tone + RX-tone calibration loops with ±4 averaging convergence over up to 10 cal iterations per path, _iqk_backup_* / _iqk_restore_* for MAC/BB/RF/AFE state, _iqk_configure_mac_8812a, _iqk_tx_fill_iqc_8812a / _iqk_rx_fill_iqc_8812a.

Out (not reachable from devourer's monitor-mode init): FW-offload IQK (no H2C mailbox), per-channel iqk_matrix_reg_setting[] cache, VDF/VHT-160, LC calibration, DPK (_phy_dp_calibrate_8812a, separate ~700 LOC; that's where BB 0xc90 and residual RF[A/B] 0x00 divergence come from).

Bug found during validation: 5GHz TX regression

First matrix run with IQK enabled surfaced a regression — 8812AU TX at ch36 dropped from 6500/6500 ✓ to 0/6500 ✗. Root cause was a subtle porting bug in commit b4b1038:

Upstream uses odm_get_bb_reg(dm, 0xd00, 0x07ff0000) << 21 to read the 11-bit IQK output. odm_get_bb_reg right-shifts by the mask offset (16) before returning — devourer's phy_query_bb_reg matches this contract, but my naive port used (rtw_read32(0xd00) & 0x07ff0000) << 21 which leaves the value at bits 16:26, then shifts to positions 37:47 — UB in 32-bit int, in practice yields 0. All 8 TX/RX read sites had the bug → every IQK sample was 0 → convergence "succeeded" with both samples == 0 → FillIQC wrote X=0 instead of either calibrated or default X=0x200 → 5GHz TX broken.

Fix: insert the missing >> 16 to match odm_get_bb_reg's shift-down semantics. The lesson is now captured in kaeru (Realtek phydm porting trap — odm_get_bb_reg returns shifted-down value).

Test plan

Full matrix in VM mode across 3 channels with IQK default-on:

Commits

  1. 6027fe9 Initial IQK port (env-gated due to regression).
  2. b4b1038 Fix mask-extract bug — root cause of 5GHz TX regression.
  3. af97973 Flip IQK trigger back to default-on after fix verified.

🤖 Generated with Claude Code

josephnef and others added 3 commits June 2, 2026 13:25
Closes the IQK side of the T1 canary divergence — upstream
`phy_iq_calibrate_8812a` runs at init + on band switch; devourer just
held `_needIQK = false` and never called IQK. Without it, BB 0x8b0 /
RF[A/B] 0x00 / 0xc10 / 0xccc / 0xcd4 stay at their BB-init seed.

Port covers:
  - `_phy_iq_calibrate_8812a` end-to-end — backup MAC/BB/RF/AFE state,
    run `_iqk_tx_8812a`, restore state, replay 0xcb8/0xeb8.
  - `_iqk_tx_8812a` (~480 LOC) — TX-tone calibration loop, RX-tone
    calibration loop, fill IQC, with ±4 averaging convergence over
    up to 10 cal iterations per path. Drops the VDF (>80MHz) branch.
  - `_iqk_backup_*` / `_iqk_restore_*` helpers (MAC/BB, RF, AFE).
  - `_iqk_configure_mac_8812a` — MAC scratch state for IQK duration.
  - `_iqk_tx_fill_iqc_8812a` / `_iqk_rx_fill_iqc_8812a` — final
    coefficient write to 0xc10/0xe10 (RX) and 0xccc/0xcd4/0xecc/0xed4
    (TX), or default 0x200/0x0 for paths that didn't converge.

Helpers intentionally omitted (out-of-scope or not reachable from
devourer's monitor-mode init path): FW-offload IQK, per-channel
`iqk_matrix_reg_setting[]` cache, VDF/VHT-160, LC calibration, DPK.

**Opt-in via env, not default-on.** Full matrix at ch36 surfaced a
regression: with IQK enabled at init, 8812AU TX at 5GHz stops
reaching kernel receivers (8812-dev → 8821/8814-ker drops from
6500/6500 to 0/6500). Devourer-vs-kernel canary at ch6 IS clean
(BB 0x8b0 = 0x642 matches kernel byte-for-byte) and on-air TX still
works at 2.4GHz, but the 5GHz regression blocks default-on. Root
cause not yet isolated — likely a register that survives the
IQK MAC/BB/RF/AFE backup-restore cycle but disturbs the 5G TX
chain in a way that doesn't surface in kernel-side use. Code lands
behind env flags so the canary-parity work is preserved + the IQK
plumbing is available for root-cause investigation; defaults match
master (no behaviour change):

  - DEVOURER_ENABLE_IQK=1: enable the existing `_needIQK` trigger
    surface (init + band switch).
  - DEVOURER_FORCE_IQK=1:  run IQK on every channel-set (implies
    enable; for canary-diff workflow against kernel).

Wiring (gated by `iqk_enabled` env check in
`phy_SwChnlAndSetBwMode8812`):
  - `HalModule::rtl8812au_hal_init` arms `_needIQK = true` post
    RF-config via new `RadioManagementModule::ArmIQKOnNextChannelSet`.
  - `phy_SwBand8812` arms `_needIQK = true` whenever band transitions.
  - Channel-set callback runs `_iqk.Calibrate()` only when the env
    is set.

Verification:
  - DEVOURER_ENABLE_IQK=1 at ch6: BB 0x8b0 = 0x00000642 matches
    kernel canary byte-for-byte (was 0x00000618 pre-port).
  - Default (no env): identical canary + matrix to PR #65 baseline.
  - Full matrix ch6 + ch100 unchanged from PR #65.
  - IQK convergence log: TX A_done=1 B_done=1, RX A_done=1 B_done=1
    with 0-1 retries observed on RTL8812AU.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the 5GHz TX regression flagged in the parent commit.
Upstream's `_iqk_tx_8812a` reads BB 0xd00/0xd40 IQK output via:

    TX_IQC_temp[i][0] = odm_get_bb_reg(dm, R_0xd00, 0x07ff0000) << 21;

`odm_get_bb_reg(dm, addr, mask)` **right-shifts by the mask offset**
before returning — so the 11-bit IQK value at bits 16:26 of 0xd00
comes back at bits 0:10 (range 0..0x7FF). The `<< 21` then encodes
it as a signed 11-bit value in the top 11 bits of an int (bits 21:31),
which the dx/dy convergence test later sign-extends back via `>> 21`.

My port skipped the shift-down step:

    TX_IQC_temp[i][0] = (rtw_read32(0xd00) & 0x07ff0000) << 21;

`& 0x07ff0000` leaves the value at bits 16:26, then `<< 21` tries
to shift to bits 37:47 — UB in 32-bit int, in practice yields 0.
All eight TX/RX read sites had the same bug. Result: every IQK
measurement was silently zero; the ±4 convergence test trivially
"succeeded" with both samples == 0; the IQK fill wrote X=0 (instead
of either the calibrated value or the default X=0x200) to
0xccc/0xcd4/0xecc/0xed4 + 0xc10/0xe10. On 2.4GHz this happened to
be tolerable but at 5GHz the invalid IQ correction killed devourer's
TX path on the 2T2R 8812AU — matrix at ch36 went from 6500/6500 to
0/6500 on 8812-dev → kernel-RX cells (parent commit's regression
note).

Fix: insert the missing `>> 16` so the mask extract matches
`odm_get_bb_reg`'s contract. The net shift is now `<< 5`, equivalent
to upstream's `>> 16` then `<< 21`.

The env-gating from the parent commit is left in place pending
hardware confirmation that the fix closes the regression. Once
verified, the trigger can flip back to default-on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mask-extract bug fixed in the previous commit (b4b1038) was the
root cause of the 5GHz TX regression that had forced IQK behind
`DEVOURER_ENABLE_IQK=1` env opt-in. Re-verified post-fix on RTL8812AU
hardware:

  ch36 8812-dev → 8821-ker (IQK on)  = 6534/6500 ✓
  ch36 8821-ker → 8812-dev (IQK on)  =  100/444  ✓

(Pre-fix had been 0/6500 ✗ for the dev→ker direction.) The dev-dev
cell at ch36 remains 0/6500 — pre-existing on master, bisect against
5b7fc12 produced identical numbers; not introduced by this PR.

Flips the trigger condition back to plain `_needIQK` so init + band
switches run IQK by default. Kept `DEVOURER_DISABLE_IQK=1` as an
emergency escape hatch and `DEVOURER_FORCE_IQK=1` for canary-diff
workflow (runs IQK on every channel-set, not just band changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@josephnef josephnef changed the title 8812AU phydm I/Q calibration port (opt-in, default-off) T1: port 8812AU phydm I/Q calibration Jun 2, 2026
@josephnef josephnef merged commit df42b60 into master Jun 2, 2026
5 checks passed
@josephnef josephnef deleted the t1-8812au-iqk-port branch June 2, 2026 12:38
josephnef added a commit that referenced this pull request Jun 2, 2026
…DPK outputs) (#67)

## Summary

Two related fixes uncovered by expanding the T1 canary set to 5G
channels and the path-B TX-AGC mirror. Brings devourer's 5GHz TX power
output byte-for-byte in line with the upstream aircrack-ng/88XXau USB
build.

## Findings

Pre-fix, at ch36 + ch100, devourer wrote ~6 power-index steps higher
than kernel across the full TX-AGC table:

| Reg @ ch36 | Pre-fix | Post-fix / Kernel |
|---|---|---|
| `BB 0xc24` (OFDM 6/9/12/18) | `0x1E1E1E1E` | `0x16161616` |
| `BB 0xc28` (OFDM 24/36/48/54) | `0x181A1E1E` | `0x16161616` |
| `BB 0xc3c` (VHT1SS 0..3) | `0x2E303232` | `0x16161616` |
| path-B mirror 0xe20-0xe40 | same divergence | match path-A |

Two root causes:

### 1. `CONFIG_TXPWR_BY_RATE_EN=n` on upstream USB build

Upstream aircrack-ng/88XXau ships with `CONFIG_TXPWR_BY_RATE_EN = n`
(Makefile line 48). This sets `RegEnableTxPowerByRate = 0` →
`phy_is_tx_power_by_rate_needed()` returns FALSE →
`PHY_GetTxPowerByRate()` short-circuits to **always return 0**.
Upstream's USB driver never applies the PG-table per-rate offsets; all
TX power = `base + boost (=2)`.

PR #64's `LoadTxPowerByRate` + headroom-cap formula was effectively a
no-op at 2.4G (because the cap zeroed by_rate when `base + boost >
limit` for FCC's 2.4G OFDM caps), so the canary matched — masking the
bug. At 5G the headroom is positive, by_rate gets applied as +6 →
devourer overshoots uniformly.

**Fix:** default-off the by-rate layer to match upstream's USB build.
The EFUSE PG-table parse + headroom-cap formula are preserved behind
`DEVOURER_ENABLE_TXPWR_BY_RATE=1` for deployments that mirror upstream's
`CONFIG_TXPWR_BY_RATE_EN=y` (Windows / some Android variants).

### 2. `PowerTracking8812a` init-ordering bug

`PowerTracking8812a::Init()` captures `default_ofdm_index` BEFORE
`phy_SetBBSwingByBand_8812A` runs the per-band BB-swing write. For 5G
dongles with EFUSE-driven swing-down (our 8812AU writes `0x16A = -3 dB`
per `EEPROM_TX_BBSWING_5G_8812`), this leaves `default_ofdm_index = 24`
(matching the post-BB-init `0x200`) while the actual base after band
switch is index 18. The first pwrtrk tick then computes `final = 24 +
abs_swing_idx` instead of `18 + abs_swing_idx` — six steps too high.

**Fix:** refresh `default_ofdm_index = LookupSwingIndexFromBb()` from
inside `ClearState()`, which `phy_SetBBSwingByBand_8812A` already calls
post-write. Init()-time snapshot remains as the cold-init seed; every
band switch reseeds.

## Canary expansion

Added to `DEVOURER_DUMP_CANARY` + `tools/canary_kernel_dump.sh`:
- Path-B TX-AGC mirror: `0xe20-0xe40` (catches 2T2R-specific drifts the
previous canary couldn't surface).
- IQK output regs: `0xc10`, `0xc14`, `0xe10`, `0xe14`.
- DPK output regs: `0xc94`, `0xe94` (`0xc90` was already there).

Plus a new env-gated diagnostic `DEVOURER_LOG_TXPWR=1` — traces
`base/by_rate/limit/headroom/final` per (channel, path, rate, ntx, bw)
for future canary-divergence investigations on the per-rate calc.

## Test plan

- [x] Canary diff at ch6/ch36/ch100: TX-AGC table now matches kernel
byte-for-byte (full diff in commit message). Remaining canary
divergences are runtime dynamic state (thermal meter, beacon counters,
IQK/DPK fire-on-init differences) — not init bugs.
- [x] Single-cell at ch36: `8812-dev → 8821-ker = 6528/6500 ✓` (was
`0/6500 ✗` pre-fix).
- [x] Partial matrix at ch36: `8812-dev → 8814-ker = 6182/6500 ✓` (was
`0/6500 ✗` pre-fix).
- [x] Full matrix at ch6: `22/24 ✓` identical to PR #66 baseline.
- [ ] Full matrix at ch36 + ch100 with all 3 adapters — deferred; the
8821 dongle was temporarily detached + the 8812 dropped off USB
mid-matrix during this PR's test pass (recurring rig flake, unrelated to
the code change).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant