Skip to content

8814 port-audit: fix 8812-on-8814 mis-ports + CCK/FW-boot bugs (no regressions)#96

Merged
josephnef merged 31 commits into
masterfrom
8814-port-audit
Jun 11, 2026
Merged

8814 port-audit: fix 8812-on-8814 mis-ports + CCK/FW-boot bugs (no regressions)#96
josephnef merged 31 commits into
masterfrom
8814-port-audit

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

A full audit of devourer's RTL8814AU code path against the kernel reference (aircrack-ng/rtl8814au), fixing structural mis-ports plus two pure C++ bugs. 31 commits, all build-verified; ranked findings + coverage map in docs/8814-port-audit.md.

Root pattern: most findings are 8812-suffixed code running unconditionally on the 8814, where the 8814 needs a different value/register/sequence — e.g. the burst-len body (clobbered the BCNQ1 TX-buffer boundary via a moved register), USTIME clock constants, crystal-cap trim bit positions, RF serial readback (paths C/D are readable via direct shadow blocks, not 8812 serial readback), the RFE-type parser, TX-power-training, the 5G EFUSE PG-block layout, and the IGI floor (must cover paths C/D). Plus two C++ bugs no wire-diff would catch: the 5 GHz CCK clamp read an uninitialized channel member, and the FW-boot poll was satisfied by devourer's own register write.

Regression validation (on hardware, this branch vs working baseline)

Tested the full devourer TX/RX matrix on the rig — no functional regressions:

path ch6 (2.4G) ch36 (5G UNII-1)
8812 devourer TX + RX TX ✓
8821 devourer TX + RX
8814 devourer RX ✓ (12.8k hits) ✓ (13.8k hits)
8812 VHT-TX (20 MHz) ✓ (472)

The shared-code changes (FrameParser RX parse hardening, RtlUsbAdapter 20 K bulk-IN buffer, the VHT-TX bandwidth fix) do not regress the working chips. The VHT 80 MHz frame is now genuinely 80 MHz wide (the bandwidth field finally reaches the descriptor) — that's the 9c38f80 fix working, not a break.

Important: this does NOT fix 8814 TX

8814 TX remains 0 on-air — root-caused (and validated this branch) to the firmware never booting (CPU_DL_READY/bit15 never asserts; the 3081 MCU stays halted). That is the now-real FW-boot check (2931ad6) exposing what the old self-satisfied poll hid. Tracked separately in #95 (root cause of #50). This PR lands the correctness fixes + the diagnostics that pinpointed it; the FW-download/boot port is follow-up work.

Side effect to note: the real FW-boot poll adds ~5 s to every 8814 init (it waits for a CPU_DL_READY that never comes today). RX still works; init is just slower until #95 is fixed.

Refs

🤖 Generated with Claude Code

josephnef and others added 30 commits June 10, 2026 15:47
send_packet defaults fixed_rate to MGN_1M (1M CCK) and only overrides it
from the radiotap RATE/VHT fields (or HT-MCS when DEVOURER_TX_HT_MCS is
set). CCK rates (1/2/5.5/11M) do not exist at 5GHz: the RTL8814AU
silently drops a CCK-rated frame on a 5GHz channel -- the bulk-OUT
completes (rc ok, 100% URB completion) but nothing goes on-air. 2.4GHz
CCK transmits fine.

On a 5GHz channel (Channel > 14), clamp a CCK fixed_rate to the lowest
OFDM rate (MGN_6M). The 8812 chip happens to auto-fall-back CCK->OFDM at
5G; the 8814 does not, so devourer must do it in software.

Verified on hardware (RTL8814AU 0bda:8813, 8821 kernel-monitor witness):
8814 TX at ch100 with the default rate goes 0 -> ~13770 on-air frames
(6M OFDM, 11a); ch6 (2.4G CCK) unaffected. Addresses the 5GHz half of the
8814 devourer-TX on-air gate (#50).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ORE_INIT)

The 8814 init path skips BOTH rtl8812au_hw_reset() (the DEINIT_BEFORE_INIT
double-init guard) and InitPowerOn(), relying on the 242-op rtw88-mimic in
FirmwareDownload_8814A. So a warm/dirty 8814 (a re-run without a cold USB
Vbus power-cycle, or after rtw88_8814au auto-bound it) is never reset: the
RX bulk-IN wedges (~10 frames then LIBUSB_ERROR_TIMEOUT) and TX goes 0
on-air, recoverable only by dropping Vbus. The kernel avoids this by
card_disable-on-unbind (CardDisableRTL8814AU runs Rtl8814A_NIC_DISABLE_FLOW);
devourer never powers the chip off.

Add a deinit-before-init that runs rtl8814A_card_disable_flow for a warm
8814, mirroring the kernel's teardown deinit. Gated behind
DEVOURER_8814_DEINIT_BEFORE_INIT (default OFF) because the 8814 PWR_SEQ has
a known cut-mask-filter interaction with fwdl and this needs clean-rig
validation (run twice with no power-cycle: 2nd run must not wedge; cold init
must be unaffected) before becoming the default.

NOT YET HARDWARE-VALIDATED — rig RF is degraded; staged for validation.
…14A bInit)

devourer's phy_SetRFEReg8814A programs only the per-band RFE pinmux *functions*
(0xCB0/0xEB0/0x18B4/0x1AB4) but never runs the kernel's one-time bInit branch
(rtl8814a_phycfg.c:1026-1039, called from usb_halinit.c:1279) that selects the
GPIO pins which physically drive the external RFE (PA + T/R antenna switch):
0x1994[3:0]=0xf and REG_GPIO_IO_SEL_8814A(0x42)[23:20]=0xf (rfe_type 1/2) or
0xc0 (type 0). Add it as InitRFEGpio8814A(), called once after the initial
band-set.

This is a genuine kernel divergence (verified: devourer had zero writes to
0x1994 / 0x42). NECESSARY but NOT SUFFICIENT on its own: with this applied,
8814 TX still emits 0 on-air at ch6 (submits err:0). Remaining divergence is in
the BB/RF TX-chain config at channel-set time — to be found via a usbmon
register diff against the proven-working kernel IBSS reference (kernel TXes 228
beacons @ -34dBm on this same chip, so the PA is healthy; this is purely a
devourer software bug). Not yet on-air-validated as a fix.
…(kernel usb_halinit.c:_InitBurstPktLen)

The 8812 _InitBurstPktLen body ran unconditionally on 8814A. Its
0x456=0x70 write (REG_AMPDU_MAX_TIME_8812, "suggested by Zhilin") lands
on REG_TXPKTBUF_BCNQ1_BDNY_8814A, corrupting the beacon-queue TX-buffer
boundary that _InitQueueReservedPage_8814AUsb programmed to 0x7F6 moments
earlier — the same register class (TX FIFO page layout) whose mis-set is
already known to make 8814 bulk-OUT frames land nowhere.

Also wrong for 8814 in the 8812 body: USTIME_TSF/EDCA forced to 0x50
(8812's 80MHz-clock tick; the 8814 MAC table value is 0x64 and the
kernel's _InitEDCA_8814AUsb keeps the 0x50 writes commented out), PIFS
zeroed (kernel table value 0x1C stands), MAX_AGGR_NUM 16-bit 0x1f1f
(kernel: per-byte 0x36/0x36 — restored in a follow-up commit), plus
RSV_CTRL/0xf050/0x288/0x289/ARFR0-3/RX_PKT_LIMIT/HT_SINGLE_AMPDU pokes
that have no counterpart in the kernel's 8814 init.

The new _InitBurstPktLen_8814A ports the kernel body: FAST_EDCA
VOVI/BEBK = 0x08070807, RXDMA burst mode by USB speed (0x1e/0x2e/0x0e),
RXDMA_AGG_PG_TH = 0x2005 (USB2) / 0x0a05 (USB3), and on USB3 the
0xf008[3:4] U1/U2 disable, 0xf002=0 ("to avoid usb 3.0 H2C fail") and
LDPC pre-TX off (0x4BC &= ~BIT6). Kernel's trailing AMPDUBurstMode=0x5F
write never runs there (flag is never assigned) so it is not ported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…halinit.c:_InitEDCA_8814AUsb)

_InitEDCA_8812AUsb forced USTIME_TSF(0x55C)/USTIME_EDCA(0x638) to 0x50 on
every chip. 0x50 is 8812's 80MHz-clock microsecond tick; the 8814 MAC
table programs 0x64 (100MHz) and the kernel's _InitEDCA_8814AUsb keeps
the 0x50 writes commented out ("0x50 for 80MHz clock"). Running the MAC
~25% fast skews every microsecond-derived TX timing (SIFS/slot/timeout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…b_AggSettingTxUpdate_8814A)

The kernel always runs usb_AggSettingTxUpdate_8814A with UsbTxAggMode=1,
UsbTxAggDescNum=3: REG_TDECTRL[7:4]=3 plus REG_TDECTRL+3 (0x20B)=0x06.
Devourer's _usbTxAggMode=false meant neither write happened (and the 8812
path omits the 0x20B write even when enabled), leaving the TXDMA
block-descriptor state different from every working kernel chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…b_halinit.c:_InitMacConfigure_8814A)

The kernel folded the old _InitWMACSetting_8812A + _InitAdaptiveCtrl into
_InitMacConfigure_8814A, whose tail writes REG_MAX_AGGR_NUM_8814A(0x4CA)
and REG_RTS_MAX_AGGR_NUM_8814A(0x4CB) to 0x36 each. Devourer ported the
old 8812 pair, whose only aggregation-limit write was the 8812-body
16-bit 0x4CA=0x1f1f in _InitBurstPktLen (removed for 8814 in c5bb4ad).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…linit.c HW_VAR_NAV_UPPER)

Both drivers zero 0x652 mid-init ("Nav limit, suggest by scott"), but the
kernel restores it at the end of hal init: rtw_hal_set_hwreg(
HW_VAR_NAV_UPPER, 30000us) -> ceil(30000/128) = 0xEB
(rtl8814a_hal_init.c:3794-3807). Devourer left it at 0, i.e. the MAC
honours arbitrarily long NAV reservations - on a busy channel that can
defer TX indefinitely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… commented out)

The kernel 8814 init never writes REG_USB_HRPWM (0xFE58); its only live
writes are on the LPS enter/leave path, unreachable in monitor mode.
Keep the write for 8812/8821 whose kernels do it at init.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TX-power loop default flipped when the 8814 packed-0x1998 TXAGC path
landed: it now runs on every chip and DEVOURER_SKIP_TXPWR is the only
knob (RadioManagementModule.cpp). DEVOURER_FORCE_TXPWR no longer exists
in code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e (kernel usb_halinit.c:hal_carddisable_8814)

Three deltas vs the kernel teardown the scaffold mirrors:

- Quiesce MAC first: hal_carddisable_8814 writes REG_CR=0 ("stop rx")
  before running NIC_DISABLE_FLOW (usb_halinit.c:1394-1398); the scaffold
  went straight to the power-seq while RX/TX DMA enables were live.
  (Mid-lifecycle REG_CR=0 is a known #36 foot-gun, but here the full init
  that follows rebuilds CR — same position as the kernel's write.)
- Gate on the warm-boot detect: the kernel only card-disables when HW
  init completed (usb_halinit.c:1453); feeding ACT_TO_CARDEMU to a cold
  chip is a path the kernel never exercises. Reuse the REG_SYS_CLKR/REG_CR
  warm detect computed just above instead of discarding it.
- Cut mask: the kernel passes the chip-independent CONSTANT
  (u8)~PWR_CUT_TESTCHIP_MSK for both 8814 flows (usb_halinit.c:217,:1398).
  Devourer's relaxed PWR_CUT_ALL_MSK additionally executed the
  test-chip-only entries (0x0002[0]=0 + 2us delay in the disable flow).
  Match the constant for 8814; 8821 logic unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Cmd.c:134)

HalPwrSeqCmdParsing retried failing PWR_CMD_POLLING reads every 10ms; the
kernel uses rtw_udelay_os(10). With maxPollingCnt=5000 the worst-case
failing-poll budget is ~50ms upstream - devourer's was ~50s, which is
what a wedged chip would cost each regress cell. Success path (first
read matches) is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a_hal_init.c:649-656)

The 8814 branch of _FWFreeToGo8812 accepted byte0==0x78 of REG_MCUFWDL as
"fw booted" — but devourer itself writes 0x6078 there in the kick
immediately before polling, so the check self-satisfied on the first
read. A never-booted 3081 was indistinguishable from success, which is
exactly the blind spot the silent-TX investigation has been circling:
every "fw is fine" assumption downstream was unvalidated.

The kernel's terminal success condition is CPU_DL_READY (BIT15), set by
the chip when the 3081 boots, polled at 50ms x 100 — only sound because
the bit is stable once set (the old "transient BIT15" rationale here
contradicted the kernel's own polling structure). Poll BIT15 only, keep
the fast cadence, and log loudly on timeout: a failure here on a virgin
chip is the smoking gun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l GET_FIRMWARE_HDR_VERSION_3081)

GET_FIRMWARE_HDR_VERSION_8812 reads LE16 at __FwHdr+4; passing fw+4
double-offset it to header bytes 8-9 (= 0) so every boot logged
fw_ver=0. The blob actually carries v33 (byte-identical to the kernel's
array_mp_8814a_fw_nic, md5 42b8d181d0aaa7946ebec6d0bd5f068d).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several post-download writes carried wrong register names (8812-era or
guessed): 0x010d is REG_TRXDMA_CTRL+1 (not REG_RD_CTRL+1), 0x0230 is
REG_FIFOPAGE_INFO_1_8814A i.e. the HPQ page count - zeroed here and only
restored later by _InitQueueReservedPage_8814AUsb (not REG_PCIE_CTRL),
0x022c is REG_RQPN_CTRL_2 / LD_RQPN (not REG_BIST_CTRL), 0x0210 is
REG_TXDMA_STATUS W1C (not REG_RXFLTMAP), 0x001d is REG_RSV_CTRL+1, and
0x0003 is REG_SYS_FUNC_EN+1. Comment-only change, but these labels were
actively misleading while debugging the TX gate (e.g. "fixing" RXFLTMAP
via 0x0210 would poke TXDMA error-status).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… keys off maintained cur_channel)

send_packet's CCK->OFDM clamp (126fb4e, the 5GHz TX fix) gated on
_channel.Channel, but _channel was never assigned anywhere: not in the
constructor init-list, and SetMonitorChannel/InitWrite passed the channel
through to RadioManagementModule without storing it. The only band-aware
TX-rate decision therefore read indeterminate memory - if the garbage
byte was <=14 the clamp never fired on 5GHz (CCK beacon, 0 frames on
air); if >14 it silently clamped on 2.4GHz too. The kernel keys the same
decision off pmlmeext->cur_channel, which every channel-set maintains
(core/rtw_mlme_ext.c:1058).

Store the channel in SetMonitorChannel (both Init and InitWrite route
through it) and value-initialise the member so the clamp is off until
the first channel set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… vs CHANNEL_WIDTH enums)

The VHT info-field parse stored 40/80 (MHz) into bwidth, but the
DATA_BW switch compares against CHANNEL_WIDTH_40(1)/CHANNEL_WIDTH_80(2)
- 40!=1, 80!=2 - so every VHT frame transmitted at 20MHz regardless of
the requested bandwidth (kernel reference: BWMapping_8814,
rtl8814a_xmit.c:443). The HT path already used the enums; align the VHT
path. Affects DEVOURER_TX_VHT=1 + DEVOURER_TX_BW=40/80 and the
encoding-matrix VHT-BW cells.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l phydm_cfotracking.c, rtl8814a_rf6052.c:143)

Two RF/BB trim fixes from the init-order audit:

1. hal_set_crystal_cap applied the 8812A mask 0x7FF80000
   (0x2C[30:25]=[24:19]) to every chip. Upstream phydm places the
   XTAL-trim field per chip: 8814A at 0x2C[26:21]=[20:15] (0x07FF8000),
   8821A at 0x2C[23:18]=[17:12] (0x00FFF000). On 8814 the EFUSE cap
   landed 4 bits high - real trim field untouched, bits [30:27]
   clobbered - i.e. an uncorrected carrier-frequency offset on TX and RX
   on every init. (8821's 5GHz gates are still open; wrong XTAL trim is
   a plausible contributor there too.)

2. PHY_RFConfig8814A's tail copies path A's RC-calibration word
   (RF 0x1C, RF_RCK1_Jaguar) to paths B/C/D after the radio tables load
   (rtl8814a_rf6052.c:143-146); devourer never did, leaving B/C/D at
   table-default RC trim - RC filter bandwidth skew across chains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….c:1250)

REG_SECONDARY_CCA_CTRL_8814A was one of the few unported MAC writes left
in the TX-relevant 0x5xx block; the kernel writes it unconditionally
right after the BAR-mode disable. Gates TX deferral behaviour on the
secondary channel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erLevel8814 has none)

PHY_SetTxPowerLevel8814 (rtl8814a_phycfg.c:636-673) only loops
phy_set_tx_power_level_by_path; no TxPowerTraining exists anywhere in
the 8814 kernel tree and the BB table initialises 0xC54/0xE54 to 0.
Devourer ran the 8812 trainee for all 4 paths on every channel set:
paths B, C and D collapsed onto 0xE54 (the 8812 function only knows two
write offsets, last-writer-wins) and 0xC54 got a non-zero training word
the kernel never writes - an uncontrolled write into page-C/E AGC space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t.c:hal_ReadRFEType_8814A)

The 8812 RFE parser ran for 8814: unprogrammed/BIT7 EFUSE 0xCA resolved
to rfe_type=0 (kernel 8814AU: 1), the programmed-value mask was 0x3F +
the rfe==4 customer workaround (kernel 8814: plain 0x7F), and amplifier
state came from the 8812 0xBC-0xC0 byte parse (kernel 8814 derives it
from the resolved rfe_type; the byte-parsing hal_ReadPAType_8814A is
dead code upstream). On unburnt boards the wrong fallback selected the
rfe-0 pinmux/GPIO branches (5G pinmux 0x54775477 vs 0x33173317, GPIO
0x42|=0xc0 vs 0xf0) - the register class that drives the external
PA/T-R switch.

The CF-938AC burns 0xCA=0x01, so both parsers agreed there (rfe=1);
ground truth from the 2026-05-29 EFUSE readout. With the kernel tree in
place the GetPhyContext rfe 0->1 patch-up is redundant (and would
mis-map a legitimately-burnt rfe=0 board), so it is removed; the
constructor's autoload-fail parse now also lands on 1 per the kernel's
autoload-fail branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…load_pg_txpwr_info_path_5g)

The 5G section was parsed with the 2.4G shape (two bytes per Ntx). The
kernel's 5G layout after the 14 base bytes + tx0 byte is: one byte per
tx 1..3 (MSB=BW40,LSB=BW20), one OFDM-2T~3T byte (MSB|LSB), one OFDM-4T
byte (LSB), then four BW80|BW160 bytes for tx 0..3. Total stride is 24
both ways, so path alignment and all 2.4G fields survived - but every
5G field from relative byte 16 onward was wrong-sourced: BW20/40 diffs
for 3S/4S read OFDM bytes, OFDM 2T-4T read BW40/BW80 bytes, and the
BW80 diffs read BW160 nibbles. Affects 5G TX-power index for >=2SS
rates and all 80MHz rates on PG-programmed boards.

Found independently by two audit passes with matching byte maps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hal_com_phycfg.c:2490-2601)

The kernel adds the per-Ntx diff for every rate range that *includes*
the rate: MCS8-31 gets [0]+[1], MCS16-31 gets [0]+[1]+[2], VHT2SS+ adds
[1], VHT3SS+ adds [2]. Devourer used exclusive windows (MCS16-23 -> [2]
only) on 2.4G, and the 5G branch had no VHT clauses beyond [0] at all -
so 5G VHT2SS missed [1] and VHT3SS missed [1]+[2]. Wrong TXAGC indexes
for every >=2SS rate on PG-programmed boards (0.5dB per diff step).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0x670=0xc0000000 end-of-init write was commented "NAV-related"; it
is REG_CAMCMD BIT31|BIT30 = security-CAM clear-all, i.e. the kernel's
invalidate_cam_all (usb_halinit.c:1236) relocated to end-of-init.

Remove the uncalled BIT0-style InitLLTTable8814A(): the init-order audit
adjudicated the auto-LLT trigger as BIT16 (the only structured in-tree
field definition, hal_com_reg.h "2 AUTO_LLT", hardware-verified
self-clear) and the vendor BIT0 function's poll tests a stale pre-write
variable - keeping a dead "corrected port" of it around only invites
someone to wire it back up. A comment now records the adjudication and
the pages-before-trigger invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…F_SZ=32768)

The 8814 init programs REG_RXDMA_AGG_PG_TH=0x05 ("dmc agg th 20K") - as
of c5bb4ad via the kernel-parity burst-len port - but infinite_read()
posted a 16 KB host buffer. Realtek sized the contract together: 20 KB
threshold + in-flight frame <= 32 KB URB (rtl8814a_recv.h:25, 8 async
URBs). A 16 KB read is an exact wMaxPacketSize multiple, so an oversize
aggregate is split with no short packet: its tail lands at the head of
the next transfer where it gets parsed as an RX descriptor and the
remainder is discarded. 8812/8821 final thresholds (<=12-16 KB) fit the
old buffer, consistent with only 8814 RX misbehaving under load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nel usb_AggSettingRxUpdate_8814A)

The kernel RMW-clears USB_AGG_EN_8814A in REG_RXDMA_AGG_PG_TH+3 during
RX-aggregation setup (usb_halinit.c:705-727); devourer's executed path
never wrote that byte, relying on the reset value. If the bit powers up
(or a previous driver leaves it) set, the chip produces mixed DMA+USB
aggregation framing that the RND8 parse walk does not expect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…caveat

Three RX-parse robustness items vs the kernel walk:
- gate the PHY-status memcpy on drvinfo_sz >= report size + buffer bound
  (kernel gates on pattrib->physt, usb_ops_linux.c:179); frames without a
  PHY status had payload bytes decoded as RSSI/EVM/SNR, and frames ending
  near the buffer tail over-read the transfer buffer.
- never parse a descriptor out of a tail fragment < RXDESC_SIZE (kernel
  rejects short transfers before parsing).
- document that SGI/LDPC/STBC/BW descriptor fields are 8812/8821-only:
  8814 DWORD4 holds PATTERN_IDX/RX_EOF/RX_SCRAMBLER and the kernel's 8814
  rx-desc query never reads them (no current consumer, but a trap).
- demote the "Unprocessed packets" log: DMA_AGG_NUM is informational and
  never decremented, so it fired on every aggregated transfer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dback (kernel rtl8814a_phycfg.c:phy_RFRead_8814A)

The kernel reads 8814 RF registers through per-path direct BB addresses
(0x2800/0x2C00/0x3800/0x3C00 + reg*4); devourer used the 8812 HSSI/LSSI
serial mechanism for every chip. On paths C/D the serial readback
returns garbage, so every masked (read-modify-write) RF write corrupted
all bits outside its mask there - the channel write (RF 0x18,
BIT18|17|16|9|8|byte0) and the bandwidth write (RF 0x18, BIT11|10)
destroyed path C/D tuning state on every channel set. This also
overturns the standing "paths C/D are write-only by HW design" note:
they are readable, just not via the 3-wire SI path.

Route phy_RFSerialRead through the direct block on 8814; the RMW in
phy_set_rf_reg, phy_query_rf_reg, the RCK1 sync and IQK backup/restore
all inherit the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…8814A)

The 5G case-2 branch wrote 0x33173717 to the A/B/C RFE pinmux regs -
nibble [27:24] carried rfe-1's value, a copy slip between adjacent
cases. Kernel writes 0x37173717 on all three. Latent on the CF-938AC
(rfe_type 1) but wrong antenna-switch function codes on rfe-2 boards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… ODM_IC_AC_4SS)

phydm_SetIgiFloor_Jaguar floored only 0xC50/0xE50 to 0x1c; on the 4-path
8814 that left C/D (0x1850/0x1A50) at the BB-table seed 0x20 - a 4 dB
per-path initial-gain imbalance the kernel never has (its DIG writes all
four paths the same value), skewing MRC combining and per-path RSSI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion plan

Full source-level audit of the 8814 code paths against
aircrack-ng/rtl8814au: 27 fix commits ranked by TX-relevance, the
settled negative results (zero H2C in monitor mode, IQK-off parity,
table/walker/pwr-seq parity, BIT16 LLT verdict), residual unported-
feature risks, the C4 experiment list, and the pending hardware
validation checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit f580900 into master Jun 11, 2026
5 checks passed
@josephnef josephnef deleted the 8814-port-audit branch June 11, 2026 11:14
josephnef added a commit that referenced this pull request Jun 11, 2026
## Summary

The RTL8814AU's 3081 MCU firmware never booted under devourer —
`CPU_DL_READY` (REG_MCUFWDL bit15) never asserted — which was the root
cause of dead host-pushed TX (#50, #95) while RX worked. (The port-audit
groundwork merged separately as #96; this PR is the firmware-boot fix on
top.)

### Firmware-boot fix

The fwdl bracket is now a verbatim port of the vendor kernel's
`FirmwareDownload8814A` + `HalROMDownloadFWRSVDPage8814A`, wired through
helpers that already existed in-tree but were never called:
`_FWDownloadEnable_8814A` → `_3081Disable` → DDMA BIT16 toggle →
RSVD-page download (kernel chunking, beacon-queue state set once,
per-section checksum-driven `DL_RDY|CHKSUM_OK` RMW) → conditional
`FW_DW_RDY` → `_3081Enable` → poll `CPU_DL_READY`.

The rtw88-mimic power-on prefix is retained (devourer's only 8814
power-on path). The legacy fwdl sequence is preserved bit-for-bit behind
`DEVOURER_8814_FWDL=rtw88` and still reproduces the failure — clean
causal A/B. `DEVOURER_8814_FWDL_CHUNK` overrides chunk size;
`_DumpFwdlState8814A` traces each bracket step.

Boot trajectory on a virgin chip: `REG_MCUFWDL 0x...2079 → 0x...6079 →
0x...E078` (`CPU_DL_READY` asserts instantly on `_3081Enable`).

### Also in this PR

- Logger: byte-sized integers stream as numbers, not raw chars (register
values like `0xF0` were landing in logs as invalid UTF-8 and crashing
`tests/regress.py` parsing); regress parsers made byte-tolerant
- Docs: hardware table flips 8814 to TX + RX on every band; stale
gotchas removed

## Validation (real hardware)

Regress matrix, TX=RTL8814AU / RX=RTL8812AU, VM-mode kernel cells:

| channel | devourer→kernel | devourer→devourer |
|---|---|---|
| 6 | ✓ 4281/4500 | ✓ 4500/4500 |
| 36 | ✓ 2315/4500 | ✓ 2200/4500 |
| 100 | ✓ 1378/4500 | ✓ 1400/4500 |

(kernel-TX cells read 0 at every channel — `88XXau` host-push beacon
injection doesn't emit on that driver; its probe-request injection does.
Pre-existing, documented in CLAUDE.md.)

- 8814 RX unaffected; 8812 TX/RX unaffected (8463 frames on-air)
- #36 passthrough-cycle wedge no longer reproduces: 9 virsh
attach/detach cycles, then 14,500 submits with 0 `LIBUSB_ERROR_IO`,
10,688 frames on-air, FW boots from the dirty post-passthrough state

Fixes #95. Fixes #50. Fixes #36.

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

---------

Co-authored-by: Claude Opus 4.8 <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