Skip to content

RTL8821AU: partial bring-up — chip init OK, RX silent — WIP#30

Merged
josephnef merged 1 commit into
masterfrom
feat/rtl8821au-support
May 23, 2026
Merged

RTL8821AU: partial bring-up — chip init OK, RX silent — WIP#30
josephnef merged 1 commit into
masterfrom
feat/rtl8821au-support

Conversation

@josephnef
Copy link
Copy Markdown
Collaborator

Status: WIP — chip init succeeds, RX data flow still silent

Brings up the RTL8821AU end-to-end except for RX bulk-IN data flow. Posting so @RomanLut and others can continue the investigation from a clean checkpoint that builds against current master instead of resurrecting #22 (which deletes the 8814AU work and has a parallel dispatch enum that doesn't compose with the ICType-based convention #23-#29 established).

What works on T2U Plus 2357:0120 (verified on macOS)

  • Chip detection: CHIP_8821_Normal_Chip_TSMC_D_CUT_1T1R
  • 8821 power-on flow (rtl8821A_card_enable_flow)
  • Firmware download — 8821 blob, signature 0x2101, FW ready in ~30ms
  • MAC/BB/AGC/RadioA register tables via existing PhyTableLoader (same phydm conditional encoding as 8814AU)
  • RFE pinmux (phy_SetRFEReg8821), band switch 2.4G/5G, channel + TX power table on ch6/36/100
  • USB endpoint discovery: bulk IN 0x84, OUTs 0x05/0x06/0x08/0x09 (vs 8812/8814's 0x81 and 0x02-0x05)
  • libusb_clear_halt on IN + REG_USB_HRPWM=0x84 LPS wake

What's NOT working

  • RX bulk-IN reads succeed at the USB layer but the chip never pushes data. 0 RX packets across 15s on ch100 even with the host Mac actively associated to a busy 5GHz AP. The chip-internal RX-DMA → bulk-IN-EP binding isn't engaging despite all known init steps.
  • TX path is wired (correct OUT EP, no LIBUSB_ERROR_NOT_FOUND) but unvalidated end-to-end on 8821AU — no peer sniffer in this session.

Regression matrix (Linux trainer-arch, master vs feat/rtl8821au-support)

Adapter Test Result vs master
8812AU (0bda:8812) RX 41 pkts / 15s, 0 errors, CHIP_8812 detected ✓ no regression
8812AU TX 15 prints all rc=1, 0 failures (2 runs) ✓ no regression
8814AU (0bda:8813) RX 0 pkts ✓ matches master baseline (pre-existing)
8814AU TX rc=1, ~270-320 async failures (timing-variant) ✓ matches master baseline

The new CHIP_8821 dispatch correctly routes 8812 → 8812 path and 8814 → 8814 path. No misrouting.

Suggested next steps for whoever picks this up

  1. usbmon trace of aircrack-ng/rtl8812au's RX bring-up against an 8821AU on Linux; diff post-fwdl register writes vs ours (REG_TRXDMA_CTRL, REG_USB_AGG_TH/TO, REG_RXDMA_AGG_PG_TH, REG_USB_SPECIAL_OPTION).
  2. Compare register state post-init (kernel-driver readback vs our post-init pyusb dump). Same technique that unblocked the 8814AU TX work.
  3. Re-read @RomanLut's rtl8811au support #22 HalModule.cpp for any 8821-specific init steps I didn't carry over when rewiring through ICType.

What this preserves

Attribution

8821a HAL data (Hal8821APwrSeq, Hal8821PhyReg, hal8821a_fw) ported verbatim from @RomanLut's #22 (svpcom/rtl8812au v5.2.20). Wiring re-done to follow master's existing convention.

Refs #20 (BadPotato1007's underlying request) and #22 (RomanLut's original port).

🤖 Generated with Claude Code

Ports the RTL8821AU (Jaguar 1T1R AC+BT combo, CHIP_8821) HAL onto current
master following the ICType-based dispatch convention established by the
8814AU work in #23-#29. Bring-up reaches end-of-init on the TP-Link Archer
T2U Plus (2357:0120) but the chip remains silent on bulk IN.

HAL assets (verbatim from svpcom/rtl8812au v5.2.20 via #22):
- hal/Hal8821APwrSeq.{c,h}  — power on/off/disable/suspend/lps sequences
- hal/Hal8821PhyReg.h        — MAC / PHY / AGC / RadioA register tables
- hal/hal8821a_fw.{c,h}      — 8821a NIC firmware blob (signature 0x2101)

Dispatch wiring (mirrors 8814AU pattern):
- EepromManager::read_chip_version_8812a — VID:PID table for genuine 8821
  SKUs (T2U Plus + canonical Realtek-numbered + common OEM rebadges).
  Sets ICType=CHIP_8821, RFType=RF_TYPE_1T1R.
- HalModule::InitPowerOn / PHY_MACConfig8812 / PHY_BBConfig8812 /
  PHY_RF6052_Config_8812 — three-way branches (8814 / 8821 / 8812).
- Four new HalModule helpers wrap the 8821 tables through PhyTableLoader.
- HalModule::rtl8812au_hal_init — REG_USB_HRPWM=0x84 wake for CHIP_8821.
- Firmware.h::PickFirmwareForChip returns 8821 blob for CHIP_8821;
  jaguar_fw_header_present accepts the 0x2100 signature.
  CONFIG_RTL8821A scoped tightly around hal8821a_fw.h include.
- RadioManagementModule::phy_SetRFEReg8821 ported from #22, branched in
  PHY_SwitchWirelessBand8812.

USB endpoint discovery (RtlUsbAdapter::InitDvObj):
- Dynamic bulk IN endpoint selection instead of hardcoded 0x81. 8821AU
  exposes 0x84 (8812/8814 expose 0x81).
- Bulk OUT endpoints stored in a vector; send_packet picks index 0 instead
  of hardcoded 0x02. 8821AU exposes 0x05/0x06/0x08/0x09 (8812 uses
  0x02-0x05). DEVOURER_TX_EP env override still wins.
- libusb_clear_halt on the IN endpoint after enumeration.
- infinite_read rate-limits the error log + sleeps 50ms on rc<0. Caught a
  runaway-loop bug that previously generated 4.6 GB of logs in 15s when
  the device dropped off USB.

Demos:
- DEVOURER_VID env override (mirrors existing DEVOURER_PID). Needed for
  non-Realtek-VID dongles (T2U Plus is 2357:xxxx).
- DEVOURER_USB_QUIET=1 downgrades libusb log to WARNING (was hardcoded
  DEBUG = ~7 MB / 15s, repeatedly filled /tmp on test machines).
- Both demos fall back to direct libusb_open_device_with_vid_pid with
  env-provided VID:PID if not in kRealtekProductIds[].

What works on T2U Plus (verified on macOS, ch6/36/100):
- Chip detection: CHIP_8821_Normal_Chip_TSMC_D_CUT_1T1R
- 8821 power-on flow, firmware download (~30ms to FW ready)
- MAC/BB/AGC/RadioA register tables via PhyTableLoader
- RFE pinmux, band switch (2.4G + 5G), channel + TX power
- USB endpoint discovery (IN 0x84, OUTs 0x05/0x06/0x08/0x09)
- libusb_clear_halt + REG_USB_HRPWM=0x84 wake

What's NOT working — the gap for follow-up:
- Bulk-IN reads succeed at the USB layer but the chip never pushes
  data. 0 RX packets across 15s on ch100 even with the host Mac
  actively associated to a busy 5GHz AP.
- TX path is wired (correct OUT EP) but unvalidated end-to-end — no
  peer sniffer in this session.

Regression matrix (Linux trainer-arch, master vs feat/rtl8821au-support):

  Adapter            RX                              TX
  -----              --                              --
  8812AU (0bda:8812) 41 pkts/15s, 0 errors           15 prints rc=1, 0 fails
  8814AU (0bda:8813) 0 pkts (matches master)         rc=1, ~270-320 async
                                                     fails (matches master)

8812AU + 8814AU code paths preserved end-to-end.

Suggested next investigation:
1. usbmon trace of aircrack-ng/rtl8812au's RX bring-up on Linux against
   an 8821AU; diff post-fwdl register writes vs ours (REG_TRXDMA_CTRL,
   REG_USB_AGG_TH/TO, REG_RXDMA_AGG_PG_TH, REG_USB_SPECIAL_OPTION).
2. Compare register state post-init (kernel-driver readback vs our
   post-init pyusb dump). Same technique used for 8814AU.
3. Re-read #22's HalModule.cpp for any 8821-specific init steps that I
   didn't carry over when rewiring through ICType.

Co-Authored-By: RomanLut <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@josephnef josephnef merged commit 22d1cd0 into master May 23, 2026
5 checks passed
@josephnef josephnef deleted the feat/rtl8821au-support branch May 23, 2026 08:21
josephnef added a commit that referenced this pull request May 23, 2026
## What this is

A manual-run Python orchestrator that compares devourer's userspace
stack against the kernel driver (mainline `rtw88` / out-of-tree
`aircrack-ng/rtl8812au`) on a host with two plugged-in USB Wi-Fi
adapters. Emits a markdown table — designed to paste into PR review
comments.

```
                  TX = devourer        TX = kernel
RX = devourer     [end-to-end dvr]     [does dvr RX a kernel-TX frame?]
RX = kernel       [does dvr emit       [baseline / rig sanity]
                   valid frames?]
```

Each cell injects/receives the canonical beacon (SA `57:42:75:05:d6:00`,
matching `txdemo/main.cpp`) for `--duration` seconds and counts hits.

## Why now

PRs like #30 (RTL8821AU partial bring-up) need cross-driver validation:
\"does devourer's TX really emit valid frames?\" and \"can devourer RX a
frame the kernel driver knows works?\". Running these checks manually is
fiddly (modprobe / unbind / iw / tcpdump dance per cell); this script
does it in one command and prints a structured result.

This is **not** a 24x7 CI runner — too few PRs to justify the
infrastructure. It's a script the reviewer runs on demand on a test rig.

## Usage

```bash
cd /path/to/devourer && cmake --build build -j
sudo python3 tests/regress.py --channel 100
```

See [`tests/README.md`](tests/README.md) for full options + prereqs.

## First-run validation on trainer-arch

Arch Linux, kernel 6.x, USB hub with 0bda:8812 (8812AU) + 0bda:8813
(8814AU):

```
## Regression matrix — channel 100
- TX adapter: 0bda:8813 (RTL8814AU)
- RX adapter: 0bda:8812 (RTL8812AU)

|   | TX = devourer | TX = kernel |
|---|---|---|
| RX = devourer | 0 hits / 10 TX (437 fail) / 10s ✗ | 0 hits / 0 TX / 0s ✗ |
| RX = kernel | 1 hits / 10 TX (351 fail) / 10s ✓ | 0 hits / 0 TX / 0s ✗ |
```

The **devourer-TX(8814) → kernel-RX(8812) cell passed** — independent
confirmation that #29's 8814AU TX bring-up really does land frames on
the air. The remaining cells correctly identified the rig's known
limitations: mainline `rtw88_8814au` can't probe this 8814AU dongle on
this kernel (`failed to download firmware`, probe error -22), and 8814AU
RX is a pre-existing TODO.

## Portability

- Tool paths resolved via `which` (no `/usr/bin/X` hardcoding)
- Wlan iface names discovered via `iw dev` (works for systemd `wlp*` and
classic `wlan*`)
- Kernel driver claiming each DUT read from sysfs (no hardcoded module
names)
- Preflight check prints distro-agnostic install hints if anything's
missing
- Tested on Arch; should work on any modern Linux with `iw`, `tcpdump`,
`python3-scapy`, `aircrack-ng`

## VM-readiness

The kernel-cell shell-outs all go through one function
(`run_kernel_cmd`). Today: local exec. To migrate the kernel driver into
a pinned-kernel VM (recommended once host kernel upgrades start breaking
the out-of-tree aircrack-ng driver), wrap that function with `ssh
trainer-vm sudo` and arrange USB hot-plug passthrough via libvirt. The
matrix orchestrator doesn't need to change.

## Known limitations (documented in README)

- Tests \"signal of life\", not throughput — air noise makes absolute
counts unreliable; default pass-threshold is 1 hit with guidance to bump
for higher-confidence runs.
- Sequential matrix takes ~100s for 4 cells (devourer fwdl warmup + 4 ×
~25s).
- Two-adapter scope today. Extending to >2 is a pairing loop in
`main()`.
- One known bug: `<devourer-tx>TX #N` prints are rate-limited so when
the chip is failing every send, the parser undercounts attempts.
Mitigated by surfacing failure count separately in the output.

## Test plan

- [x] Builds + runs on trainer-arch (Arch + kernel 6.x)
- [x] Markdown table emitted correctly
- [x] At least one cell passes against real hardware (8814 dvr-TX → 8812
kernel-RX)
- [ ] Validate on a different distro (Ubuntu / Fedora) — anyone with a
2-adapter rig
- [ ] Validate against the out-of-tree `aircrack-ng/rtl8812au` driver
instead of mainline rtw88

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

Usage:
    sudo python3 tests/regress.py --full-matrix --channel 100 \\
        --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>

For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.

Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
  of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
  placeholders in example commands

Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):

  ## Kernel-only (rig sanity)
  All 6 cross-chipset cells pass — 88XXau handles all three chipsets
  cleanly in the pinned-kernel VM (88-271 hits per cell).

  ## devourer-TX → kernel-RX
  devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
  reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
  (chip-state degradation across cell sequencing — known sensitive).

  ## kernel-TX → devourer-RX
  Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
  contradicting PR #30's "RX silent" finding. devourer-RX 8812
  confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
  confirmed broken (0 hits all directions — known TODO).

  ## devourer ↔ devourer
  All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
  degraded mid-run).

Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
  peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
  #30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
  investigation, may want a chip reset between cells

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

Usage:
    sudo python3 tests/regress.py --full-matrix --channel 100 \\
        --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>

For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.

Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
  of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
  placeholders in example commands

Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):

  ## Kernel-only (rig sanity)
  All 6 cross-chipset cells pass — 88XXau handles all three chipsets
  cleanly in the pinned-kernel VM (88-271 hits per cell).

  ## devourer-TX → kernel-RX
  devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
  reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
  (chip-state degradation across cell sequencing — known sensitive).

  ## kernel-TX → devourer-RX
  Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
  contradicting PR #30's "RX silent" finding. devourer-RX 8812
  confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
  confirmed broken (0 hits all directions — known TODO).

  ## devourer ↔ devourer
  All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
  degraded mid-run).

Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
  peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
  #30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
  investigation, may want a chip reset between cells

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef added a commit that referenced this pull request May 23, 2026
## What this is

Extends `tests/regress.py` with `--full-matrix`: iterates every ordered
(TX, RX) pair of plugged-in DUTs across all 4 driver-side combinations
(kernel-only / dvr-TX×kernel-RX / kernel-TX×dvr-RX / devourer-only) and
emits one NxN table per mode. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.

```bash
sudo python3 tests/regress.py --full-matrix --channel 100 \
    --vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
```

For N adapters → N×(N-1)×4 cells. ~16 min for N=3 in VM mode. Diagonal
blanked (same physical adapter can't simultaneously TX and RX with one
driver).

Also scrubs placeholder usernames from earlier docs/scripts
(`tests/setup_vm.sh` now reads `VM_USER` from `$SUDO_USER`/`$USER`
instead of hardcoding; READMEs + docstrings use `<user>@<VM-IP>`).

## Validation on a 3-adapter rig

Ubuntu 22.04 VM with aircrack-ng/88XXau, DUTs `0bda:8812 + 0bda:8813 +
2357:0120`, channel 100, 10s/cell:

### Kernel-only (rig sanity / cross-chipset kernel interop)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 99 hits ✓ | 271 hits ✓ |
| RTL8812AU | 243 hits ✓ | — | 270 hits ✓ |
| RTL8821AU | 260 hits ✓ | 88 hits ✓ | — |

**6/6 cells pass.** `88XXau` in the pinned-kernel VM gives full
cross-chipset kernel-side interop. Rig is sound.

### devourer-TX → kernel-RX (does devourer emit valid frames?)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 0 ✗ (2272 fail) | 0 ✗ (2322 fail) |
| RTL8812AU | **4114 ✓** | — | **4693 ✓** |
| RTL8821AU | **4341 ✓** | 0 ✗ | — |

- **devourer-TX 8812 validated** end-to-end at the wire — 4000+ hits to
each kernel RX peer
- **devourer-TX 8821 validated end-to-end** — 4341 hits reaching
kernel-RX 8814. New result: PR #30 had no peer sniffer to confirm; the
VM gives us one. The TX path that #30 wired works.
- 8814 TX flaky after the chip cycles through host↔VM passthroughs
(chip-state degradation across cell sequencing — single-cell tests in PR
#33 worked, this is a multi-cell wear issue)

### kernel-TX → devourer-RX (does devourer RX a known-good frame?)

| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | **100 ✓** | **200 ✓** |
| RTL8812AU | 0 ✗ | — | 0 ✗ |
| RTL8821AU | 0 ✗ | **100 ✓** | — |

- **Surprise: devourer-RX 8821 caught 200 frames** from kernel-TX 8814 —
contradicts PR #30's "RX silent" finding. Reopen-worthy: 8821 RX may
actually be working under certain conditions and #30's session-local
conclusion was wrong.
- devourer-RX 8812 confirmed working (100 hits from each of 8814 and
8821 TX)
- devourer-RX 8814 confirmed broken — pre-existing TODO

### devourer ↔ devourer (end-to-end devourer)

All 0. Every cell hits at least one broken side (8814 RX broken or 8814
TX degraded mid-run).

## Net new product signal

1. **devourer-TX 8821 is working** — closes the gap PR #30 documented as
"wired but unvalidated"
2. **devourer-RX 8821 may actually be working** — at least one cell got
200 hits. PR #30's negative conclusion needs reopening.
3. **Chip state degrades through repeated passthrough** — devourer-TX
8814 is reliable in single-cell tests but flakes after several virsh
attach/detach cycles. Suggests we should consider a chip-reset /
power-cycle between cells in the orchestrator, or accept this as a known
limitation of the VM rig.

## What this PR doesn't touch

- The `--full-matrix` extension doesn't change any existing code path;
the single-pair mode (`--tx-pid`/`--rx-pid`) still works exactly as
before. Pure addition.
- Doesn't address the 8814 chip-state degradation (that's separate work
— possibly a `usbreset` between cells)
- Doesn't open the PR #30 reopen (that's a follow-up: rerun the 8821 RX
investigation with this rig)

## Test plan

- [x] `--full-matrix` runs end-to-end against 3 adapters in VM mode
- [x] All 4 NxN tables render correctly
- [x] Cell counts and per-pair timing match expected (~30-40s per cell)
- [x] Kernel-only baseline (6 cells) all pass
- [x] At least one cell in each non-trivial mode passes (validates
harness logic)
- [ ] Run on a different rig with different chipset combinations
- [ ] Validate on 4+ adapters (script supports it; not exercised here)

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@josephnef
Copy link
Copy Markdown
Collaborator Author

Update: the "RX silent" conclusion in this PR appears to be wrong — RX path actually works under at least some conditions.

The full-matrix regression rig (#34) was just run against a 3-adapter setup (0bda:8812 + 0bda:8813 + 2357:0120) with aircrack-ng/88XXau as the kernel-side reference. Two cells directly contradict the assumptions this PR was merged under:

  • devourer-RX 8821 caught 200 of 281 frames sent by kernel-TX 8814 (~71% delivery rate). The PR concluded RX was silent based on a single test session against an actively-associated 5GHz AP; the regression rig instead injects a known-SA beacon from a peer adapter and confirmed devourer's 8821 RX path moves frames.

  • devourer-TX 8821 hit 4341 frames to kernel-RX 8814 (~96% delivery). The PR documented TX as "wired but unvalidated" because no peer sniffer was available that session; that's also now confirmed working.

So both halves of the 8821 path that this PR labelled as gaps are actually functional. Two follow-up questions worth pinning down:

  1. Why was 8821 RX silent in the original PR session? Possible candidates: the chip was in a degraded post-fwdl state from earlier test runs, ambient 5GHz channel was too quiet, or the SA-match filter logic missed something. Rerunning the original test conditions with the regression rig would isolate this.

  2. The 8821→8812 direction at the wire showed 0 hits in mode 2 of the full matrix (devourer-TX 8821 → kernel-RX 8812), even though devourer-TX 8821 → kernel-RX 8814 worked fine in the same session. That's a different signal — possibly chip-state degradation by that cell, possibly 8812 kernel-RX in VM was stale. Worth a focused single-pair retest.

Tracking the deeper investigation in #35.

cc @RomanLut — this updates the situation since your #22 port. The dispatch is in master, and the RX path that we initially thought was broken seems to actually work — the chip just needs the right conditions / state.

josephnef added a commit that referenced this pull request May 23, 2026
… RX asymmetry) (#37)

## Status: partial — register-state parity with kernel, but doesn't fix
#30's 8812-peer asymmetry

Brings devourer's `CHIP_8821` init register-write set into parity with
`aircrack-ng/88XXau`'s post-fwdl monitor-mode bring-up, based on a
usbmon-trace diff. Adds 13 missing post-fwdl writes, 17 BB/AGC value
overrides, MAC-address programming, and corrects an earlier wrong guess
at REG_USB_HRPWM.

**This does NOT fix the asymmetry** (`devourer-RX 8821` catches
kernel-TX 8814 frames but drops kernel-TX 8812 frames at 0/280) that #34
surfaced. The cheap "what's different at the register level" patches
don't cross the threshold. Filing anyway because it's honest progress +
brings the chip's baseline state into kernel parity, which the next fix
attempt can build on.

## What this changes

| Where | Change |
|---|---|
| `HalModule.cpp::rtl8812au_hal_init` | REG_USB_HRPWM for CHIP_8821:
0x84 → 0x00 (kernel writes 0; my earlier "leave LPS wake" guess was
wrong) |
| `HalModule.cpp::rtl8812au_hal_init` | CHIP_8821: program MAC address
to REG_MACID (0x0610-0x0615). Kernel always does this even in monitor;
some chip RX-path logic gates on MACID being non-zero. |
| `HalModule.cpp::rtl8812au_hal_init` | CHIP_8821: 13 trace-derived
post-fwdl writes (REG_AUTO_LLT region, REG_TX_PTCL_CTRL, NAV-related,
8821 BB high-addr block 0x1874-0x187f) |
| `HalModule.cpp::rtl8812au_hal_init` | CHIP_8821: 17 BB/AGC value
overrides (0x0830 PWED_TH, 0x0c20-0x0c44 AGC table, 0x0e90 TX power
region) forced to kernel's observed runtime values |
| `tests/inject_beacon.py` | `--rate` CLI arg (added during rate-decode
hypothesis testing; useful diagnostic knob) |

8812AU + 8814AU paths untouched.

## Why the RX gap probably remains

Aircrack-ng runs **phydm** — a runtime feedback loop that continually
adjusts AGC / RX-gain based on observed signal quality. This PR sets the
chip's *static* values to match kernel's post-init snapshot, but the
chip needs the active loop to *maintain* them during RX. Without phydm
running, the chip drifts back toward defaults that work for higher-SNR
peers (8814) but not for 8812.

Three follow-up paths to actually fix the asymmetry:

1. **Port phydm runtime** for 8821 (substantial — most of
`aircrack-ng/hal/phydm/rtl8821a/`). Days of work, cleanest fix.
2. **Loop-replay**: capture kernel's full register sequence over a
5-second RX window (not just init) and replay periodically from
devourer. Hacky but cheap.
3. **Accept devourer-RX 8821 as "works for some peers, not others"**
until phydm is in. Document the limit in the adapter inventory.

## usbmon methodology (for future trace-diff work)

Capture kernel side (in VM with `aircrack-ng/88XXau`):

```bash
# On VM:
sudo modprobe usbmon
sudo bash -c "cat /sys/kernel/debug/usb/usbmon/0u > /tmp/trace_kernel.txt &"
# Then attach DUT via virsh, bring up monitor mode, stop usbmon
```

Capture devourer side (on host):

```bash
sudo bash -c "timeout 20 cat /sys/kernel/debug/usb/usbmon/Nu > /tmp/trace_dvr.txt" &
sudo ./build/WiFiDriverDemo ...
```

Filter Realtek control writes:

```bash
grep -E "S Co:.* 40 05 " trace.txt | awk '{print $8, $13}'
```

**Important encoding gotcha:** usbmon shows wire bytes in transmission
order (= LE u32's byte sequence). To write the same value via
`rtw_write32` on a LE host, the u32 value is read-as-LE — so the trace
text `82824001` represents u32 `0x01408282` (NOT `0x82824001`). The
first attempt at the post-fwdl writes in this PR used wrong-endian
values and naturally had no effect; the fix was to byte-pair-reverse
them.

## Validation

Single-pair test on the trainer rig (Arch host, Ubuntu 22.04 VM with
aircrack-ng/88XXau, channel 100, 10s):

| Cell | Hits | Note |
|---|---|---|
| kernel-TX 8812 → kernel-RX 8821 (baseline) | 283 ✓ | rig sane |
| devourer-TX 8821 → kernel-RX 8814 | 4663 ✓ | dvr-TX 8821 fine |
| kernel-TX 8812 → devourer-RX 8821 | **0 ✗** | the asymmetry —
unresolved |
| devourer-TX 8821 → devourer-RX 8812 | **0 ✗** | same |

Other chips untouched.

## Test plan

- [x] Builds clean
- [x] CHIP_8812 + CHIP_8814A paths unchanged
- [x] CHIP_8821 init now writes the kernel-equivalent register set
- [x] devourer-TX 8821 still works → kernel-RX 8814 (4663 hits)
- [ ] dvr-RX 8821 ↔ 8812 — known *not* fixed by this PR
- [ ] phydm runtime ported (follow-up)

Refs #30 (initial 8821 port), #34 (full matrix that surfaced the
asymmetry).

🤖 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