From 126fb4e3c204a35c4a5516e15c9e53d36fa97203 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:47:08 +0300 Subject: [PATCH 01/31] 8814: clamp CCK TX rate to OFDM on 5GHz channels (fixes 5GHz TX) 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) --- src/RtlJaguarDevice.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/RtlJaguarDevice.cpp b/src/RtlJaguarDevice.cpp index f10afca..b794afb 100644 --- a/src/RtlJaguarDevice.cpp +++ b/src/RtlJaguarDevice.cpp @@ -151,6 +151,19 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { } } + /* 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 but + * nothing goes on-air (verified on hardware: default MGN_1M beacon = 0 + * frames at ch36/ch100, but ~14k on-air once the rate is OFDM). 2.4GHz + * CCK is fine. So on a 5GHz channel, clamp a CCK rate to the lowest OFDM + * rate. (The 8812 chip happens to auto-fall-back CCK->OFDM at 5G; the + * 8814 does not, so we must do it in software.) */ + if (_channel.Channel > 14 && + (fixed_rate == MGN_1M || fixed_rate == MGN_2M || + fixed_rate == MGN_5_5M || fixed_rate == MGN_11M)) { + fixed_rate = MGN_6M; + } + usb_frame = new uint8_t[usb_frame_length](); ptxdesc = (struct tx_desc *)usb_frame; From ed6baa9337e94bb7123e6fa10c00a6f92a614ae4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:29:21 +0300 Subject: [PATCH 02/31] 8814: env-gated deinit-before-init scaffold (DEVOURER_8814_DEINIT_BEFORE_INIT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/HalModule.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index a59f219..c9e00af 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -133,6 +133,27 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { * functions that do diverge dispatch internally on ICType. */ const bool is_8814a = _eepromManager->version_id.ICType == CHIP_8814A; const bool is_8821 = _eepromManager->version_id.ICType == CHIP_8821; + + /* Experimental, env-gated (default OFF): deinit-before-init for the 8814. + * The 8814 path below skips BOTH rtl8812au_hw_reset() (the + * "DEINIT_BEFORE_INIT" double-init guard) and InitPowerOn() — it relies on + * the 242-op rtw88-mimic inside FirmwareDownload_8814A instead. So a + * WARM/dirty chip (a re-run without a cold USB Vbus power-cycle, or after + * rtw88_8814au auto-bound it) is never reset: RX bulk-IN wedges (~10 frames + * then LIBUSB_ERROR_TIMEOUT) and TX goes 0 on-air, until a Vbus drop. The + * kernel avoids this by card_disable-on-unbind (CardDisableRTL8814AU runs + * Rtl8814A_NIC_DISABLE_FLOW); devourer never powers the chip off. Mirror it + * as a deinit-before-init here. GATED: the 8814 PWR_SEQ has a known + * cut-mask-filter interaction with fwdl, so this needs clean-rig validation + * (set the env, run devourer twice with no power-cycle, confirm the 2nd run + * does not wedge AND cold init is unaffected) before it can become default. */ + if (is_8814a && std::getenv("DEVOURER_8814_DEINIT_BEFORE_INIT") != nullptr) { + _logger->info("8814 deinit-before-init: running card_disable_flow"); + if (!HalPwrSeqCmdParsing(rtl8814A_card_disable_flow)) { + _logger->warn("8814 deinit-before-init: card_disable_flow failed"); + } + } + if (!is_8814a) { if (!is_8821) { _device.rtw_write8(REG_RF_CTRL, 5); From 83be051cf6bfc63dcb6460f364e95a8f355c3f0d Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:57:56 +0300 Subject: [PATCH 03/31] 8814: add missing RFE GPIO pin-select at init (kernel PHY_SetRFEReg8814A bInit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/HalModule.cpp | 6 ++++++ src/RadioManagementModule.cpp | 21 +++++++++++++++++++++ src/RadioManagementModule.h | 5 +++++ 3 files changed, 32 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index c9e00af..2fbd9e7 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -393,6 +393,12 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { * band-set runs the correct per-chip sequence (issue #51, confirmed via * kernel-vs-devourer usbmon register diff). */ _radioManagementModule->PHY_SwitchWirelessBand8814A(init_band); + /* Kernel PHY_SetRFEReg8814A(bInit=TRUE) (usb_halinit.c:1279): select the + * GPIO pins that physically drive the external RFE (PA + T/R switch). The + * per-band band-switch above only sets the RFE pin *functions*; without + * this one-time pin-select the pins never output, so TX never reaches the + * antenna (submits OK, 0 on-air) while RX still works. */ + _radioManagementModule->InitRFEGpio8814A(); } else { _radioManagementModule->PHY_SwitchWirelessBand8812(init_band); } diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 8f8f58a..6869944 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -914,6 +914,27 @@ void RadioManagementModule::phy_SetRFEReg8814A(BandType Band) { } } +void RadioManagementModule::InitRFEGpio8814A() { + /* Mirror of the kernel PHY_SetRFEReg8814A(bInit=TRUE) branch + * (rtl8814a_phycfg.c:1026-1039, called once from usb_halinit.c:1279). + * Enables the GPIO pins that physically drive the external RFE (PA + T/R + * antenna switch). devourer's per-band phy_SetRFEReg8814A programs only the + * RFE pinmux *functions* (0xCB0/0xEB0/0x18B4/0x1AB4); without this one-time + * pin-select the pins are never enabled as RFE outputs, so the external + * PA/T-R switch never engages on TX — TX submits succeed (err:0) but nothing + * reaches the air, while RX still works (issue surfaced after the chip + * stopped inheriting a prior kernel-set GPIO state). */ + const auto rfe_type = _eepromManager->rfe_type; + constexpr uint16_t REG_GPIO_IO_SEL_8814A = 0x0042; /* byte 2 of the 0x40 dword */ + _device.phy_set_bb_reg(0x1994, 0xf, 0xf); /* 0x1994[3:0] = 0xf */ + const uint8_t sel = _device.rtw_read8(REG_GPIO_IO_SEL_8814A); + /* rfe_type 1/2 -> 0x40[23:20]=0xf (0x42 |= 0xf0); type 0 -> [23:22]=11b (0xc0) */ + const uint8_t orv = (rfe_type == 0) ? 0xc0 : 0xf0; + _device.rtw_write8(REG_GPIO_IO_SEL_8814A, (uint8_t)(sel | orv)); + _logger->info("8814A RFE GPIO pin-select (rfe_type={}, 0x42 |= 0x{:02x})", + (int)rfe_type, orv); +} + /* Port of upstream `phy_SetBwRegAdc_8814A` * (rtl8814a_phycfg.c:1454). Programs rRFMOD_Jaguar (0x8AC) bits [1:0] * per bandwidth; both bands write the same value here. */ diff --git a/src/RadioManagementModule.h b/src/RadioManagementModule.h index 38859b9..b61b0c1 100644 --- a/src/RadioManagementModule.h +++ b/src/RadioManagementModule.h @@ -193,6 +193,11 @@ class RadioManagementModule { uint8_t Offset40, uint8_t Offset80); void PHY_SwitchWirelessBand8812(BandType Band); void PHY_SwitchWirelessBand8814A(BandType Band); + /* One-time RFE GPIO pin-select for the 8814 (mirror of the kernel's + * PHY_SetRFEReg8814A bInit=TRUE branch). Must run once at init, after the + * first band-set. Without it the external PA/T-R switch never engages on + * TX (submits OK, 0 on-air) even though RX works. */ + void InitRFEGpio8814A(); void SetTxPower(uint8_t p); private: From c5bb4ad3f783addcdb2a7c93822d342048ba0516 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:00:30 +0300 Subject: [PATCH 04/31] 8814: port 8814 _InitBurstPktLen; 8812 body clobbered BCNQ1 boundary (kernel usb_halinit.c:_InitBurstPktLen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/HalModule.cpp | 62 ++++++++++++++++++++++++++++++++++++++++++++- src/HalModule.h | 1 + src/RtlUsbAdapter.h | 1 + 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 2fbd9e7..c75b26a 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -28,6 +28,11 @@ constexpr uint16_t REG_TXPKTBUF_BCNQ_BDNY_8814A = 0x0424; constexpr uint16_t REG_TXPKTBUF_BCNQ1_BDNY_8814A = 0x0456; /* per rtl8814a_spec.h:262 */ constexpr uint16_t REG_MGQ_PGBNDY_8814A = 0x047A; constexpr uint16_t REG_RXFF_PTR_8814A = 0x011C; +constexpr uint16_t REG_FAST_EDCA_VOVI_SETTING_8814A = 0x1448; /* per rtl8814a_spec.h:526 */ +constexpr uint16_t REG_FAST_EDCA_BEBK_SETTING_8814A = 0x144C; /* per rtl8814a_spec.h:527 */ +constexpr uint16_t REG_RXDMA_AGG_PG_TH_8814A = 0x0280; /* per rtl8814a_spec.h:156 */ +constexpr uint16_t REG_RXDMA_MODE_8814A = 0x0290; /* per rtl8814a_spec.h:160 */ +constexpr uint16_t REG_SW_AMPDU_BURST_MODE_CTRL_8814A = 0x04BC; /* per rtl8814a_spec.h:295 */ constexpr uint32_t HPQ_PGNUM_8814A = 0x20; /* 32 pages per queue (USB) */ constexpr uint32_t LPQ_PGNUM_8814A = 0x20; @@ -276,7 +281,11 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { _InitBeaconParameters_8812A(); _InitBeaconMaxError_8812A(); - _InitBurstPktLen(); // added by page. 20110919 + if (is_8814a) { + _InitBurstPktLen_8814A(); + } else { + _InitBurstPktLen(); // added by page. 20110919 + } // Init CR MACTXEN, MACRXEN after setting RxFF boundary REG_TRXFF_BNDY to // patch Hw bug which Hw initials RxFF boundry size to a value which is larger @@ -1961,6 +1970,57 @@ void HalModule::_InitBurstPktLen() { _device.rtw_write32(REG_ARFR3_8812 + 4, 0xffcff000); } +/* Port of upstream 8814 _InitBurstPktLen (usb_halinit.c). The 8812 body + * above must NOT run on 8814A: its 0x456 write (REG_AMPDU_MAX_TIME_8812, + * "suggested by Zhilin") lands on REG_TXPKTBUF_BCNQ1_BDNY_8814A and + * corrupts the beacon-queue TX-buffer boundary programmed by + * _InitQueueReservedPage_8814AUsb; the USTIME_TSF/EDCA = 0x50 writes are + * 8812's 80MHz-clock value where the 8814 MAC table keeps 0x64; and the + * PIFS/MAX_AGGR/ARFR/RSV_CTRL pokes have no counterpart in the kernel's + * 8814 init. */ +void HalModule::_InitBurstPktLen_8814A() { + using namespace rtl8814a; + + /* yx_qi 131128 move to 0x1448, 144c */ + _device.rtw_write32(REG_FAST_EDCA_VOVI_SETTING_8814A, 0x08070807); + _device.rtw_write32(REG_FAST_EDCA_BEBK_SETTING_8814A, 0x08070807); + + /* check device operation speed: SS 0xff bit7 */ + const bool supportUsb3 = (_device.rtw_read8(0xff) & BIT7) == 0; + if (!supportUsb3) { /* USB2/1.1 Mode */ + /* Kernel keys this off UsbBulkOutSize (512 on high-speed, 64 on + * full-speed). */ + if (_device.speed() == LIBUSB_SPEED_HIGH) { + /* set burst pkt len=512B */ + _device.rtw_write8(REG_RXDMA_MODE_8814A, 0x1e); + } else { + /* set burst pkt len=64B */ + _device.rtw_write8(REG_RXDMA_MODE_8814A, 0x2e); + } + _device.rtw_write16(REG_RXDMA_AGG_PG_TH_8814A, 0x2005); /* dmc agg th 20K */ + } else { /* USB3 Mode */ + /* set burst pkt len=1k */ + _device.rtw_write8(REG_RXDMA_MODE_8814A, 0x0e); + _device.rtw_write16(REG_RXDMA_AGG_PG_TH_8814A, 0x0a05); /* dmc agg th 20K */ + + /* set Reg 0xf008[3:4] to 2'00 to disable U1/U2 Mode to avoid 2.5G spur + * in USB3.0. added by page, 20120712 */ + _device.rtw_write8(0xf008, (uint8_t)(_device.rtw_read8(0xf008) & 0xE7)); + /* to avoid usb 3.0 H2C fail */ + _device.rtw_write16(0xf002, 0); + + /* turn off the LDPC pre-TX */ + _device.rtw_write8( + REG_SW_AMPDU_BURST_MODE_CTRL_8814A, + (uint8_t)(_device.rtw_read8(REG_SW_AMPDU_BURST_MODE_CTRL_8814A) & + ~BIT6)); + } + + /* Upstream tail: `if (pHalData->AMPDUBurstMode) write8(0x4BC, 0x5F)` — + * AMPDUBurstMode is never assigned anywhere in the kernel tree + * (zero-initialised false), so that write never runs there either. */ +} + bool HalModule::PHY_BBConfig8812() { /* tangw check start 20120412 */ /* . APLL_EN,,APLL_320_GATEB,APLL_320BIAS, auto config by hw fsm after diff --git a/src/HalModule.h b/src/HalModule.h index ec699bc..acdcbd1 100644 --- a/src/HalModule.h +++ b/src/HalModule.h @@ -120,6 +120,7 @@ class HalModule { void _InitBeaconParameters_8812A(); void _InitBeaconMaxError_8812A(); void _InitBurstPktLen(); + void _InitBurstPktLen_8814A(); bool PHY_BBConfig8812(); bool phy_BB8812_Config_ParaFile(); diff --git a/src/RtlUsbAdapter.h b/src/RtlUsbAdapter.h index 1691614..624d7ce 100644 --- a/src/RtlUsbAdapter.h +++ b/src/RtlUsbAdapter.h @@ -57,6 +57,7 @@ class RtlUsbAdapter { uint16_t idVendor() const { return _idVendor; } uint16_t idProduct() const { return _idProduct; } + enum libusb_speed speed() const { return usbSpeed; } bool AutoloadFailFlag = false; bool EepromOrEfuse = false; From 993e0d96c055e7a9c12deb0f58a66676bafce966 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:00:48 +0300 Subject: [PATCH 05/31] 8814: keep USTIME_TSF/EDCA at the 100MHz MAC-table value (kernel usb_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 --- src/HalModule.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index c75b26a..0f1a435 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -1764,9 +1764,13 @@ void HalModule::_InitEDCA_8812AUsb() { _device.rtw_write32(REG_EDCA_VI_PARAM, 0x005EA324); _device.rtw_write32(REG_EDCA_VO_PARAM, 0x002FA226); - /* 0x50 for 80MHz clock */ - _device.rtw_write8(REG_USTIME_TSF, 0x50); - _device.rtw_write8(REG_USTIME_EDCA, 0x50); + if (_eepromManager->version_id.ICType != CHIP_8814A) { + /* 0x50 for 80MHz clock */ + _device.rtw_write8(REG_USTIME_TSF, 0x50); + _device.rtw_write8(REG_USTIME_EDCA, 0x50); + } + /* 8814A keeps the MAC-table value 0x64 (100MHz tick): the kernel's + * _InitEDCA_8814AUsb has the 0x50 writes commented out. */ } void HalModule::_InitRetryFunction_8812A() { From 8b45e23696955f56d0257c2a71c5fdb62bce5102 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:01:05 +0300 Subject: [PATCH 06/31] 8814: arm USB TX-agg block-descriptor config (kernel usb_halinit.c:usb_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 --- src/HalModule.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 0f1a435..13ebce2 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -1795,6 +1795,20 @@ void HalModule::init_UsbAggregationSetting_8812A() { } void HalModule::usb_AggSettingTxUpdate_8812A() { + if (_eepromManager->version_id.ICType == CHIP_8814A) { + /* Kernel usb_AggSettingTxUpdate_8814A runs with UsbTxAggMode=1 and + * UsbTxAggDescNum=3 (wifi_spec=0 default): program the block-descriptor + * count into REG_TDECTRL[7:4] and REG_TDECTRL+3 (0x20B) = DescNum<<1. + * Devourer still submits one frame per bulk URB, but this is part of + * the reference TXDMA state the kernel chip runs with. */ + constexpr uint8_t kUsbTxAggDescNum = 3; + uint32_t value32 = _device.rtw_read32(REG_TDECTRL); + value32 &= ~(BLK_DESC_NUM_MASK << BLK_DESC_NUM_SHIFT); + value32 |= (kUsbTxAggDescNum & BLK_DESC_NUM_MASK) << BLK_DESC_NUM_SHIFT; + _device.rtw_write32(REG_TDECTRL, value32); + _device.rtw_write8(REG_TDECTRL + 3, kUsbTxAggDescNum << 1); + return; + } if (_usbTxAggMode) { uint32_t value32 = _device.rtw_read32(REG_TDECTRL); value32 = value32 & ~(BLK_DESC_NUM_MASK << BLK_DESC_NUM_SHIFT); From 0cbeea54df8f19690dc30e3609b3f0db368d2472 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:01:25 +0300 Subject: [PATCH 07/31] 8814: program per-byte aggregation limits 0x4CA/0x4CB=0x36 (kernel usb_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 --- src/HalModule.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 13ebce2..3e26a77 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -33,6 +33,8 @@ constexpr uint16_t REG_FAST_EDCA_BEBK_SETTING_8814A = 0x144C; /* per rtl8814a_sp constexpr uint16_t REG_RXDMA_AGG_PG_TH_8814A = 0x0280; /* per rtl8814a_spec.h:156 */ constexpr uint16_t REG_RXDMA_MODE_8814A = 0x0290; /* per rtl8814a_spec.h:160 */ constexpr uint16_t REG_SW_AMPDU_BURST_MODE_CTRL_8814A = 0x04BC; /* per rtl8814a_spec.h:295 */ +constexpr uint16_t REG_MAX_AGGR_NUM_8814A = 0x04CA; /* per rtl8814a_spec.h:303 */ +constexpr uint16_t REG_RTS_MAX_AGGR_NUM_8814A = 0x04CB; /* per rtl8814a_spec.h:304 */ constexpr uint32_t HPQ_PGNUM_8814A = 0x20; /* 32 pages per queue (USB) */ constexpr uint32_t LPQ_PGNUM_8814A = 0x20; @@ -273,6 +275,14 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { _InitNetworkType_8812A(); /* set msr */ _InitWMACSetting_8812A(); _InitAdaptiveCtrl_8812AUsb(); + if (is_8814a) { + /* Tail of kernel _InitMacConfigure_8814A (usb_halinit.c:551-552): 8814 + * splits the aggregation limits into per-byte registers. The 8812-body + * 16-bit 0x4CA=0x1f1f write no longer runs on 8814 (see + * _InitBurstPktLen_8814A), so program the kernel values here. */ + _device.rtw_write8(rtl8814a::REG_MAX_AGGR_NUM_8814A, 0x36); + _device.rtw_write8(rtl8814a::REG_RTS_MAX_AGGR_NUM_8814A, 0x36); + } _InitEDCA_8812AUsb(); _InitRetryFunction_8812A(); From cffeba5a4f00e57a4c017126c54d6a35031ddc20 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:01:39 +0300 Subject: [PATCH 08/31] 8814: restore NAV_UPPER=0xEB after the init zero-write (kernel usb_halinit.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 --- src/HalModule.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 3e26a77..a618859 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -453,6 +453,14 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { // Nav limit , suggest by scott _device.rtw_write8(0x652, 0x0); + if (is_8814a) { + /* Kernel restores NAV_UPPER at the end of hal init via + * HW_VAR_NAV_UPPER (usb_halinit.c:1286 -> rtl8814a_hal_init.c:3794): + * ceil(WiFiNavUpperUs=30000 / 128us-unit) = 0xEB. Without it the + * zero-write above leaves the MAC honouring arbitrarily long NAV. */ + _device.rtw_write8(REG_NAV_UPPER, 0xEB); + } + /* 0x4c6[3] 1: RTS BW = Data BW */ /* 0: RTS BW depends on CCA / secondary CCA result. */ _device.rtw_write8(REG_QUEUE_CTRL, From e20afaae85c93235697cc96818eca3a1bf93aed0 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:01:55 +0300 Subject: [PATCH 09/31] 8814: skip REG_USB_HRPWM init write (kernel usb_halinit.c:1354 has it 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 --- src/HalModule.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index a618859..7a11a9c 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -500,12 +500,17 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { _device.rtw_write8(REG_SDIO_CTRL_8812, 0x0); _device.rtw_write8(REG_ACLK_MON, 0x0); - /* USB Host Read PWM. All chip families: write 0. Earlier hypothesis + /* USB Host Read PWM. 8812/8821: write 0. Earlier hypothesis * (8821 needing 0x84 "leave LPS" wake) was wrong — usbmon trace of * aircrack-ng/88XXau on the same T2U Plus shows kernel writes 0x00 * here, not 0x84. The LPS-leave flow in Hal8821APwrSeq.h is only - * traversed when actually leaving LPS, not during init. */ - _device.rtw_write8(REG_USB_HRPWM, 0); + * traversed when actually leaving LPS, not during init. + * 8814: the kernel has this init write commented out + * (usb_halinit.c:1354); its only live REG_USB_HRPWM writes are on the + * LPS path, which monitor mode never enters. Skip to match. */ + if (!is_8814a) { + _device.rtw_write8(REG_USB_HRPWM, 0); + } // TODO: ///* ack for xmit mgmt frames. */ From 334d04f720d3346e6cd0c7a4bddd23e2c80ea0a2 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:03:16 +0300 Subject: [PATCH 10/31] docs: drop stale DEVOURER_FORCE_TXPWR; TX power runs by default 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 --- CLAUDE.md | 6 +++--- README.md | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a40f139..a86946d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,9 +94,9 @@ Both `WiFiDriverDemo` and `WiFiDriverTxDemo` honour: - `DEVOURER_CHANNEL=N` — override monitor channel. - `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim; useful when picking up a chip whose firmware is already running. -- `DEVOURER_FORCE_TXPWR=1` — force the per-rate TX-power loop during channel - switch. Skipped by default in 8814 monitor mode (~300 vendor ctrl - transfers, indices are unused for RX-only). +- `DEVOURER_SKIP_TXPWR=1` — skip the per-rate TX-power loop during channel + switch (runs by default on every chip; escape hatch for RX-only + experiments). - `DEVOURER_USB_QUIET=1` — downgrade libusb log level from DEBUG to WARNING (DEBUG produces ~7 MB per 15 s and has filled `/tmp` mid-capture). diff --git a/README.md b/README.md index 6c5b3b8..fd424bf 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,10 @@ Common to both demos: - `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim. Useful when picking up a chip whose firmware is already running (e.g. after unbinding a kernel driver that left fw state intact). -- `DEVOURER_FORCE_TXPWR=1` — force the per-rate TX-power loop to run during - channel switch. Skipped by default in 8814 monitor mode: the loop issues - ~300 vendor control transfers and the resulting per-rate indices are - unused for RX-only operation. - `DEVOURER_SKIP_TXPWR=1` — skip the per-rate TX-power loop entirely on - every chip. Useful for fast iteration during BB/RF debugging when the - per-rate indices aren't relevant to what you're measuring. + every chip (it runs by default, including 8814). Useful for fast + iteration during BB/RF debugging when the per-rate indices aren't + relevant to what you're measuring. - `DEVOURER_FORCE_IQK=1` — run phydm I/Q calibration on every channel-set, not just band transitions. For 8814, IQK is otherwise off by default — the kernel doesn't run it on `iw set channel` either, and devourer From be22303d8694443b43b71a7b6118c56665bc0a88 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:13:53 +0300 Subject: [PATCH 11/31] 8814: make deinit-before-init scaffold faithful to kernel card-disable (kernel usb_halinit.c:hal_carddisable_8814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/HalModule.cpp | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 7a11a9c..4ef35cc 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -115,7 +115,8 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { auto regCr = _device.rtw_read8(REG_CR); _logger->info("power-on :REG_SYS_CLKR 0x09=0x{:X}. REG_CR 0x100=0x{:X}", (int)value8, (int)regCr); - if ((value8 & BIT3) != 0 && (regCr != 0 && regCr != 0xEA)) { + const bool macAlreadyOn = (value8 & BIT3) != 0 && (regCr != 0 && regCr != 0xEA); + if (macAlreadyOn) { /* pHalData.bMACFuncEnable = TRUE; */ _logger->info("MAC has already power on"); } else { @@ -155,9 +156,21 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { * (set the env, run devourer twice with no power-cycle, confirm the 2nd run * does not wedge AND cold init is unaffected) before it can become default. */ if (is_8814a && std::getenv("DEVOURER_8814_DEINIT_BEFORE_INIT") != nullptr) { - _logger->info("8814 deinit-before-init: running card_disable_flow"); - if (!HalPwrSeqCmdParsing(rtl8814A_card_disable_flow)) { - _logger->warn("8814 deinit-before-init: card_disable_flow failed"); + if (macAlreadyOn) { + /* Kernel card-disable prologue (hal_carddisable_8814, + * usb_halinit.c:1394-1398): quiesce MAC DMA first ("stop rx"), then + * run the disable flow. The kernel also only card-disables a chip + * whose HW init completed (usb_halinit.c:1453) — mirror that with + * the warm-boot detect above rather than feeding ACT_TO_CARDEMU to + * a cold chip, a path the kernel never exercises. */ + _logger->info("8814 deinit-before-init: running card_disable_flow"); + _device.rtw_write8(REG_CR, 0x0); /* stop rx */ + if (!HalPwrSeqCmdParsing(rtl8814A_card_disable_flow)) { + _logger->warn("8814 deinit-before-init: card_disable_flow failed"); + } + } else { + _logger->info( + "8814 deinit-before-init: chip is cold, skipping card_disable_flow"); } } @@ -892,6 +905,16 @@ bool HalModule::HalPwrSeqCmdParsing(WLAN_PWR_CFG *PwrSeqCmd) { cutMask = _eepromManager->version_id.ChipType == NORMAL_CHIP ? PWR_CUT_A_MSK : PWR_CUT_TESTCHIP_MSK; + } else if (_eepromManager->version_id.ICType == CHIP_8814A) { + /* The kernel passes the chip-independent CONSTANT + * (u8)~PWR_CUT_TESTCHIP_MSK for both the 8814 enable and disable + * flows (usb_halinit.c:217, :1398) — it never derives this mask + * from the chip's real cut, so the "cut extraction from SYS_CFG is + * untrustworthy" concern above doesn't apply here. Effect: the + * test-chip-only entries (e.g. card-disable's 0x0002[0]=0 + 2us + * delay, and five analog writes in the enable flow) are skipped, + * exactly as on the kernel. */ + cutMask = (uint8_t)~PWR_CUT_TESTCHIP_MSK; } if (!(GET_PWR_CFG_CUT_MASK(PwrCfgCmd) & cutMask)) { AryIdx++; From 217fb76f189e87b1868cd57de740472ad6ba6d35 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:14:14 +0300 Subject: [PATCH 12/31] pwrseq: poll retry delay is 10us upstream, not 10ms (kernel HalPwrSeqCmd.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 --- src/HalModule.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 4ef35cc..365267e 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -960,7 +960,10 @@ bool HalModule::HalPwrSeqCmdParsing(WLAN_PWR_CFG *PwrSeqCmd) { bPollingBit = true; } else { using namespace std::chrono_literals; - std::this_thread::sleep_for(10ms); + /* Kernel retries with rtw_udelay_os(10) — 10us, not 10ms + * (HalPwrSeqCmd.c:134). With maxPollingCnt=5000 the worst-case + * failing-poll budget is ~50ms upstream; 10ms here made it ~50s. */ + std::this_thread::sleep_for(10us); } if (pollingCount++ > maxPollingCnt) { From 2931ad606be1cc832739a117a35286911c632cb4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:20:11 +0300 Subject: [PATCH 13/31] 8814: FW-boot poll must require chip-set CPU_DL_READY (kernel rtl8814a_hal_init.c:649-656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/FirmwareManager.cpp | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/FirmwareManager.cpp b/src/FirmwareManager.cpp index d28d3da..9743102 100644 --- a/src/FirmwareManager.cpp +++ b/src/FirmwareManager.cpp @@ -550,9 +550,14 @@ void FirmwareManager::FirmwareDownload_8814A() { _device.rtw_write8(0x001d, 0x09); /* REG_AFE_OSC_CTRL2+1 */ _device.rtw_write8(0x0003, 0xfe); /* REG_RSV_CTRL+1, enable 8051 */ - /* Poll for CPU_DL_READY (BIT15 of REG_MCUFWDL). The 8051 sets this bit - * once it's running and has finished its on-chip init. */ + /* Poll for CPU_DL_READY (BIT15 of REG_MCUFWDL). The chip sets this bit + * once the 3081 is running and has finished its on-chip init. */ if (!_FWFreeToGo8812(10, 5000, CHIP_8814A)) { + _logger->error( + "8814A firmware boot NOT confirmed: CPU_DL_READY (REG_MCUFWDL bit15) " + "never asserted within 5s. Final REG_MCUFWDL=0x{:08X}. The 3081 MCU " + "is likely not running — expect dead TX (and no TX reports).", + _device.rtw_read32(REG_MCUFWDL)); return; } @@ -888,11 +893,15 @@ bool FirmwareManager::_FWFreeToGo8812(uint32_t min_cnt, uint32_t timeout_ms, * upstream renames it REG_8051FW_CTRL_8814A. The "FW is alive" indicator * differs by chip: * - 8812 uses WINTINI_RDY (BIT6) as the single-bit ready flag - * - 8814 has the chip set BIT15 (FW_INIT_RDY) briefly during fw init, - * then settles to byte 0 = 0x78 (IMEM_DL_RDY|IMEM_CHKSUM_OK| - * DMEM_DL_RDY|DMEM_CHKSUM_OK, FWDL_EN cleared). Polling BIT15 in - * userspace misses the transient window; the stable post-boot state - * is byte 0 == 0x78 with FW_DW_RDY (BIT14) also set. */ + * - 8814: the chip sets CPU_DL_READY (BIT15) once the 3081 has booted, + * and the kernel polls exactly that as its TERMINAL success condition + * (FirmwareDownload8814A -> rtl8814a_hal_init.c:649-656, 50ms x 100) + * — which only works because the bit is stable once set. An earlier + * devourer revision additionally accepted byte0==0x78 as a "stable + * post-boot state", but byte0=0x78 is written BY US in the 0x6078 + * kick just before this poll, so that arm self-satisfied on the + * first read and made the check vacuous: a never-booted 8051 was + * indistinguishable from success. Kernel-parity: BIT15 only. */ if (ic_type != CHIP_8814A) { value32 = _device.rtw_read32(REG_MCUFWDL); value32 |= MCUFWDL_RDY; @@ -911,10 +920,10 @@ bool FirmwareManager::_FWFreeToGo8812(uint32_t min_cnt, uint32_t timeout_ms, cnt++; value32 = _device.rtw_read32(REG_MCUFWDL); if (ic_type == CHIP_8814A) { - /* Match either the transient FW_INIT_RDY (BIT15) OR the stable - * post-boot pattern (byte 0 == 0x78 = all DL_RDY + CHKSUM_OK, - * FWDL_EN cleared). */ - if ((value32 & (1u << 15)) || ((value32 & 0xFF) == 0x78)) { + /* CPU_DL_READY (BIT15), chip-set on 3081 boot. We poll much faster + * than the kernel's 50ms cadence, so a short-lived assertion cannot + * be missed either. */ + if ((value32 & (1u << 15)) != 0) { break; } } else if ((value32 & ready_bit) != 0) { @@ -924,7 +933,7 @@ bool FirmwareManager::_FWFreeToGo8812(uint32_t min_cnt, uint32_t timeout_ms, } while (since(start).count() < timeout_ms || cnt < min_cnt); if (ic_type == CHIP_8814A) { - if (!((value32 & (1u << 15)) || ((value32 & 0xFF) == 0x78))) { + if ((value32 & (1u << 15)) == 0) { goto exit; } } else if (!((value32 & ready_bit) != 0)) { From 6021880e36beb579ebe33e5650342f738761b34a Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:20:24 +0300 Subject: [PATCH 14/31] 8814: log the real fw version; header macro already offsets +4 (kernel 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 --- src/FirmwareManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FirmwareManager.cpp b/src/FirmwareManager.cpp index 9743102..9e8f8ee 100644 --- a/src/FirmwareManager.cpp +++ b/src/FirmwareManager.cpp @@ -187,7 +187,7 @@ void FirmwareManager::FirmwareDownload_8814A() { uint32_t fw_len = static_cast(blob.len); const uint16_t firmwareVersion = - static_cast(GET_FIRMWARE_HDR_VERSION_8812(fw + 4)); + static_cast(GET_FIRMWARE_HDR_VERSION_8812(fw)); const uint16_t firmwareSignature = static_cast(GET_FIRMWARE_HDR_SIGNATURE_8812(fw)); _logger->info("FirmwareDownload_8814A: fw_ver={} sig=0x{:X} blob={} bytes", From 3c958cee40d0b559a106165b56c971f5a23660c2 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:20:59 +0300 Subject: [PATCH 15/31] 8814: correct register-name labels in fwdl comments (rtl8814a_spec.h) 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 --- src/FirmwareManager.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/FirmwareManager.cpp b/src/FirmwareManager.cpp index 9e8f8ee..5d02254 100644 --- a/src/FirmwareManager.cpp +++ b/src/FirmwareManager.cpp @@ -530,7 +530,7 @@ void FirmwareManager::FirmwareDownload_8814A() { /* Post-fwdl CPU kick sequence — mirrors rtw88_8814au's usbmon trace * byte-for-byte after the last fwdl IDDMA program. */ _device.rtw_write8(REG_MCUFWDL, 0x79); /* declare init ready */ - _device.rtw_write8(0x010d, 0x00); /* REG_RD_CTRL+1 */ + _device.rtw_write8(0x010d, 0x00); /* REG_TRXDMA_CTRL+1 */ /* DO NOT write 0x0100 (REG_CR) = 0 here. Bisect 2026-05-26 of #36 wedge: * zeroing REG_CR disables byte 0's DMA-enable bits (HCI_TXDMA_EN/ * HCI_RXDMA_EN/TXDMA_EN/RXDMA_EN/PROTOCOL_EN/SCHEDULE_EN). The later @@ -542,13 +542,16 @@ void FirmwareManager::FirmwareDownload_8814A() { * never writes this address with this value. With this single write * removed, devourer-TX on 8814AU goes from 0.4% completion to 100%. */ _device.rtw_write32(0x1330, 0x80000000); /* REG_3081_DCDC_CTRL */ - _device.rtw_write16(0x0230, 0x0000); /* REG_PCIE_CTRL_REG word */ - _device.rtw_write32(0x022c, 0x80000000); /* REG_BIST_CTRL */ + _device.rtw_write16(0x0230, 0x0000); /* REG_FIFOPAGE_INFO_1_8814A — + * zeroes the HPQ page count; + * restored later by + * _InitQueueReservedPage_8814AUsb */ + _device.rtw_write32(0x022c, 0x80000000); /* REG_RQPN_CTRL_2_8814A (LD_RQPN) */ _device.rtw_write8(REG_BCN_CTRL, 0x14); /* REG_BCN_CTRL */ - _device.rtw_write32(0x0210, 0x00000004); /* REG_RXFLTMAP */ + _device.rtw_write32(0x0210, 0x00000004); /* REG_TXDMA_STATUS_8814A (W1C) */ _device.rtw_write16(REG_MCUFWDL, 0x6078); /* clear FWDL_EN; kick */ - _device.rtw_write8(0x001d, 0x09); /* REG_AFE_OSC_CTRL2+1 */ - _device.rtw_write8(0x0003, 0xfe); /* REG_RSV_CTRL+1, enable 8051 */ + _device.rtw_write8(0x001d, 0x09); /* REG_RSV_CTRL+1 */ + _device.rtw_write8(0x0003, 0xfe); /* REG_SYS_FUNC_EN+1, enable 8051 */ /* Poll for CPU_DL_READY (BIT15 of REG_MCUFWDL). The chip sets this bit * once the 3081 is running and has finished its on-chip init. */ From f3188eac6a116c8cdba8885ac07b99c9c45977b4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:25:22 +0300 Subject: [PATCH 16/31] 8814: 5GHz CCK clamp read an uninitialized channel - store it (kernel 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 --- src/RtlJaguarDevice.cpp | 5 +++++ src/RtlJaguarDevice.h | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/RtlJaguarDevice.cpp b/src/RtlJaguarDevice.cpp index b794afb..ebfbf42 100644 --- a/src/RtlJaguarDevice.cpp +++ b/src/RtlJaguarDevice.cpp @@ -325,6 +325,11 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, } void RtlJaguarDevice::SetMonitorChannel(SelectedChannel channel) { + /* Keep the device-level channel state current: send_packet's 5GHz + * CCK->OFDM clamp keys off _channel.Channel. Before this assignment + * existed, _channel was never written anywhere — the clamp read an + * uninitialised member and fired nondeterministically. */ + _channel = channel; _radioManagement->set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); } diff --git a/src/RtlJaguarDevice.h b/src/RtlJaguarDevice.h index 09fb92a..a9049c0 100644 --- a/src/RtlJaguarDevice.h +++ b/src/RtlJaguarDevice.h @@ -32,7 +32,10 @@ using Action_ParsedRadioPacket = std::function; class RtlJaguarDevice { std::shared_ptr _eepromManager; std::shared_ptr _radioManagement; - SelectedChannel _channel; + /* Last channel handed to SetMonitorChannel. Value-initialised so the + * 5GHz CCK clamp in send_packet reads Channel=0 (clamp off) rather than + * indeterminate garbage before the first channel set. */ + SelectedChannel _channel{}; RtlUsbAdapter _device; HalModule _halModule; Logger_t _logger; From 9c38f80998b7fa1a722961fef817a89d721abfe2 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:25:36 +0300 Subject: [PATCH 17/31] tx: VHT radiotap bandwidth never reached the descriptor (MHz literals 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 --- src/RtlJaguarDevice.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/RtlJaguarDevice.cpp b/src/RtlJaguarDevice.cpp index ebfbf42..2abdc64 100644 --- a/src/RtlJaguarDevice.cpp +++ b/src/RtlJaguarDevice.cpp @@ -125,12 +125,17 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { stbc = 1; if (known & 0x40) { auto bw = iterator.this_arg[3] & 0x1f; + /* Map radiotap VHT bandwidth codes to CHANNEL_WIDTH enums — the + * descriptor BW switch below compares against the enums (as the + * HT path above does). The previous MHz literals (40/80) never + * matched CHANNEL_WIDTH_40(1)/CHANNEL_WIDTH_80(2), so VHT 40/80 + * silently transmitted as 20MHz. */ if (bw >= 1 && bw <= 3) - bwidth = 40; + bwidth = CHANNEL_WIDTH_40; else if (bw >= 4 && bw <= 10) - bwidth = 80; + bwidth = CHANNEL_WIDTH_80; else - bwidth = 20; + bwidth = CHANNEL_WIDTH_20; } if (iterator.this_arg[8] & 1) From 1a8ea834a11e102c0c8f02716ba3df3a0e521efc Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:33:50 +0300 Subject: [PATCH 18/31] 8814: fix crystal-cap trim bit positions + port RCK1 path sync (kernel 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 --- src/HalModule.cpp | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 365267e..88622e2 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -2201,10 +2201,28 @@ bool HalModule::odm_config_bb_with_header_file(odm_bb_config_type config_type) { void HalModule::hal_set_crystal_cap(uint8_t crystal_cap) { crystal_cap = (uint8_t)(crystal_cap & 0x3F); - - /* write 0x2C[30:25] = 0x2C[24:19] = CrystalCap */ - _device.phy_set_bb_reg(REG_MAC_PHY_CTRL, 0x7FF80000u, - (uint8_t)(crystal_cap | (crystal_cap << 6))); + const uint32_t reg_val = (uint32_t)(crystal_cap | (crystal_cap << 6)); + + /* The XTAL-trim field of 0x2C sits at different bit positions per chip + * (upstream phydm_cfotracking.c::odm_set_crystal_cap): + * 8812A: 0x2C[30:25] = 0x2C[24:19] -> mask 0x7FF80000 + * 8821A: 0x2C[23:18] = 0x2C[17:12] -> mask 0x00FFF000 + * 8814A: 0x2C[26:21] = 0x2C[20:15] -> mask 0x07FF8000 + * The 8812 mask was applied to every chip — on 8814 the cap landed 4 + * bits high (real trim field untouched, bits [30:27] clobbered), i.e. + * a carrier-frequency offset on TX and RX. */ + switch (_eepromManager->version_id.ICType) { + case CHIP_8814A: + _device.phy_set_bb_reg(REG_MAC_PHY_CTRL, 0x07FF8000u, reg_val); + break; + case CHIP_8821: + _device.phy_set_bb_reg(REG_MAC_PHY_CTRL, 0x00FFF000u, reg_val); + break; + default: + /* write 0x2C[30:25] = 0x2C[24:19] = CrystalCap */ + _device.phy_set_bb_reg(REG_MAC_PHY_CTRL, 0x7FF80000u, reg_val); + break; + } } static uint32_t array_mp_8812a_phy_reg_mp[] = { @@ -2668,6 +2686,25 @@ void HalModule::phy_RF6052_Config_ParaFile_8814() { break; } } + + /* Kernel PHY_RFConfig8814A tail (rtl8814a_rf6052.c:143-146): copy path + * A's RC-calibration word (RF_RCK1_Jaguar = RF 0x1C) to paths B/C/D so + * all chains run the same RC filter trim. Path A is readable; B/C/D + * writes take effect even though they can't be read back (see note + * below). */ + { + /* RF_RCK1_Jaguar (0x1c) comes from Hal8812PhyReg.h. */ + const uint32_t rck1 = _radioManagementModule->phy_query_rf_reg( + RfPath::RF_PATH_A, RF_RCK1_Jaguar, 0xfffff); + _radioManagementModule->phy_set_rf_reg(RfPath::RF_PATH_B, RF_RCK1_Jaguar, + 0xfffff, rck1); + _radioManagementModule->phy_set_rf_reg(RfPath::RF_PATH_C, RF_RCK1_Jaguar, + 0xfffff, rck1); + _radioManagementModule->phy_set_rf_reg(RfPath::RF_PATH_D, RF_RCK1_Jaguar, + 0xfffff, rck1); + _logger->info("8814A RCK1 sync: RF-A[0x1c]=0x{:05x} copied to B/C/D", rck1); + } + /* Verify path A/B RF reads return sensible values. NOTE: paths C/D do * not support RF read-back via the standard 3-wire SI/PI mechanism on * 8814 — rtw88's rtw88xxa_phy_read_rf only indexes paths A/B (rf_phy_num From 5dde6cc6a7bdb2cc8c8691dee5017953de75696d Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:34:44 +0300 Subject: [PATCH 19/31] 8814: port secondary-CCA control write 0x577=0x03 (kernel usb_halinit.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 --- src/HalModule.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 88622e2..a9a9236 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -459,6 +459,13 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { // 2010.04.09 add by hpfan _device.rtw_write32(REG_BAR_MODE_CTRL, 0x0201ffff); + if (is_8814a) { + /* Kernel usb_halinit.c:1250: REG_SECONDARY_CCA_CTRL_8814A. Gates TX + * deferral on the secondary channel; one of the few unported MAC + * writes in the TX-relevant 0x5xx block. */ + _device.rtw_write8(0x577, 0x03); + } + if (registry_priv::wifi_spec) { _device.rtw_write16(REG_FAST_EDCA_CTRL, 0); } From c9394a465c32d25816d4f8c2ae623fe559e1a6d8 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:34:44 +0300 Subject: [PATCH 20/31] 8814: stop running 8812 TX-power-training writes (kernel PHY_SetTxPowerLevel8814 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 --- src/RadioManagementModule.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 6869944..2b6debd 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -1774,10 +1774,20 @@ uint8_t RadioManagementModule::phy_GetSecondaryChnl_8812() { } void RadioManagementModule::PHY_SetTxPowerLevel8812(uint8_t Channel) { + const bool is_8814a = _eepromManager->version_id.ICType == CHIP_8814A; for (uint8_t path = 0; (uint8_t)path < _eepromManager->numTotalRfPath; path++) { phy_set_tx_power_level_by_path(Channel, (RfPath)path); - PHY_TxPowerTrainingByPath_8812((RfPath)path); + /* TX power training is an 8812 mechanism: kernel + * PHY_SetTxPowerLevel8814 (rtl8814a_phycfg.c:636) only loops + * phy_set_tx_power_level_by_path — no training write exists anywhere + * in the 8814 tree, and its BB table inits 0xC54/0xE54 to 0. Running + * the 8812 trainee on 8814 wrote a non-zero word into 0xC54 and + * collapsed paths B/C/D onto 0xE54 (last-writer-wins) on every + * channel set. */ + if (!is_8814a) { + PHY_TxPowerTrainingByPath_8812((RfPath)path); + } } } From 95d1b9765176544833fdbc4f8ca3fa91b9fd43e7 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:37:24 +0300 Subject: [PATCH 21/31] 8814: port the kernel RFE-type decision tree (kernel rtl8814a_hal_init.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 --- src/EepromManager.cpp | 111 ++++++++++++++++++++++++++++++++++++------ src/EepromManager.h | 2 + 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index 5b8d436..794b261 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -99,8 +99,11 @@ void EepromManager::LateInitFor8814A() { Hal_EfuseParseBTCoexistInfo8812A(); Hal_EfuseParseXtal_8812A(); Hal_ReadThermalMeter_8812A(); - Hal_ReadAmplifierType_8812A(); - Hal_ReadRFEType_8812A(); + /* 8814 derives amplifier state from rfe_type (kernel + * hal_ReadRFEType_8814A -> hal_ReadAmplifierType_8814A) — the 8812 + * pair (EFUSE-parsed amplifier + 8812 RFE heuristic) resolved the + * wrong fallback (0 instead of 1) on unburnt 0xCA boards. */ + Hal_ReadRFEType_8814A(); _logger->info("8814A LateInit: rfe_type={} crystal_cap=0x{:X} " "PA_2G/5G=0x{:X}/0x{:X} LNA_2G/5G=0x{:X}/0x{:X}", rfe_type, crystal_cap, PAType_2G, PAType_5G, @@ -948,18 +951,12 @@ JaguarPhyContext EepromManager::GetPhyContext() const { constexpr uint8_t kOdmItrfUsb = 0x02; constexpr uint8_t kOdmCe = 0x04; - /* Every conditional block in array_mp_8814a_phy_reg + _agc_tab requires - * a non-zero rfe_type (122 + 52 blocks, low-byte values 1..11). If - * LateInitFor8814A hasn't run yet — or the chip's EFUSE doesn't carry - * a board RFE — fall back to rfe_type=1 so at least the BB/AGC tables - * apply. The board's RFE pinmux may then be slightly off, but the chip - * will still receive: verified on CF-938AC, even with the fallback - * value the chip lands on the same RFE_PIN_0824 = 0x00033E40 that - * rtw88's working trace shows. */ - const uint8_t rfe_for_ctx = - (version_id.ICType == CHIP_8814A && rfe_type == 0) - ? 1 - : static_cast(rfe_type); + /* Pass the resolved rfe_type straight through. Hal_ReadRFEType_8814A now + * mirrors the kernel decision tree (unburnt/BIT7 0xCA -> 1 on 8814AU, + * incl. the autoload-fail branch), so the old 0->1 ctx patch-up is + * redundant — and would mis-map a board whose EFUSE legitimately burns + * rfe_type=0. */ + const uint8_t rfe_for_ctx = static_cast(rfe_type); return JaguarPhyContext{ .cut_version = static_cast(version_id.CUTVersion), @@ -1731,6 +1728,92 @@ void EepromManager::Hal_ReadRFEType_8812A() { _logger->info("RFE Type: 0x{:X}", rfe_type); } +/* Kernel hal_ReadAmplifierType_8814A (rtl8814a_hal_init.c:1474-1525): on + * 8814 the PA/LNA state is DERIVED from rfe_type, not parsed from the + * 0xBC-0xC0 EFUSE bytes (the byte-parsing hal_ReadPAType_8814A is dead + * code upstream — no caller). Note: neither driver's 8814 table + * check_positive consumes the Type* words (cond2 ignored both sides), so + * these mostly matter for logging/diagnostics parity. */ +void EepromManager::hal_ReadAmplifierType_8814A() { + switch (rfe_type) { + case 1: /* 8814AU */ + external_pa_5g = external_lna_5g = 1; + TypeAPA = TypeALNA = 0; + break; + case 2: /* socket board 8814AR and 8194AR */ + ExternalPA_2G = true; + ExternalLNA_2G = true; + external_pa_5g = external_lna_5g = 1; + TypeAPA = TypeALNA = 0x55; + TypeGPA = TypeGLNA = 0x55; + break; + case 3: /* high power on-board 8814AR and 8194AR */ + ExternalPA_2G = true; + ExternalLNA_2G = true; + external_pa_5g = external_lna_5g = 1; + TypeAPA = TypeALNA = 0xaa; + TypeGPA = TypeGLNA = 0xaa; + break; + case 4: /* on-board 8814AR and 8194AR */ + ExternalPA_2G = true; + ExternalLNA_2G = true; + external_pa_5g = external_lna_5g = 1; + TypeAPA = 0x55; + TypeALNA = 0xff; + TypeGPA = TypeGLNA = 0x55; + break; + case 5: + ExternalPA_2G = true; + ExternalLNA_2G = true; + external_pa_5g = external_lna_5g = 1; + TypeAPA = 0xaa; + TypeALNA = 0x5500; + TypeGPA = TypeGLNA = 0xaa; + break; + case 6: + external_lna_5g = 1; + TypeALNA = 0; + break; + case 0: + default: /* 8814AE */ + break; + } +} + +/* Kernel hal_ReadRFEType_8814A (rtl8814a_hal_init.c:1527-1568). Differs + * from the 8812 tree in three load-bearing ways: the fallback for an + * unprogrammed/BIT7 EFUSE 0xCA is rfe_type=1 on 8814AU (8812: 0 or the + * PA/LNA heuristic), the programmed-value mask is 0x7F (8812: 0x3F + the + * rfe==4 customer workaround), and the amplifier state is derived from + * the resolved rfe_type afterwards. EEPROM_RFE_OPTION is 0xCA on both + * chips. (CF-938AC ground truth: 0xCA = 0x01 -> rfe_type 1 either way; + * the fallback difference bites on unburnt boards.) */ +void EepromManager::Hal_ReadRFEType_8814A() { + if (!_device.AutoloadFailFlag) { + if (registry_priv::RFE_Type != 64 || + 0xFF == efuse_eeprom_data[EEPROM_RFE_OPTION_8812] || + (efuse_eeprom_data[EEPROM_RFE_OPTION_8812] & BIT7) != 0) { + if (registry_priv::RFE_Type != 64) { + rfe_type = registry_priv::RFE_Type; + } else { + /* IS_HARDWARE_TYPE_8814AU -> 1 (the AE/PCIe case is 0). */ + rfe_type = 1; + } + } else { + /* bit7==0 means RFE type defined by 0xCA[6:0] */ + rfe_type = + (uint16_t)(efuse_eeprom_data[EEPROM_RFE_OPTION_8812] & 0x7F); + } + } else { + rfe_type = (registry_priv::RFE_Type != 64) + ? registry_priv::RFE_Type + : 1; /* 8814AU autoload-fail default */ + } + hal_ReadAmplifierType_8814A(); + _logger->info("8814A RFE Type: 0x{:X} (ext PA_5G={} LNA_5G={})", rfe_type, + (int)external_pa_5g, (int)external_lna_5g); +} + #define EEPROM_USB_MODE_8812 0x08 void EepromManager::hal_ReadUsbType_8812AU() { diff --git a/src/EepromManager.h b/src/EepromManager.h index 240599b..04430f5 100644 --- a/src/EepromManager.h +++ b/src/EepromManager.h @@ -187,6 +187,8 @@ class EepromManager { void Hal_ReadAmplifierType_8812A(); void hal_ReadPAType_8812A(); void Hal_ReadRFEType_8812A(); + void Hal_ReadRFEType_8814A(); + void hal_ReadAmplifierType_8814A(); void hal_ReadUsbType_8812AU(); }; From f3b8c8c05c23fbcd7c9ff0b9699726a6b010e85c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:39:18 +0300 Subject: [PATCH 22/31] 8814: fix 5G EFUSE PG diff-block layout (kernel hal_com_phycfg.c:hal_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 --- src/EepromManager.cpp | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index 794b261..f979d63 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -484,30 +484,32 @@ void EepromManager::LoadTxPowerInfo() { BW20_5G_Diff[path][0] = pg_msb_diff(v); OFDM_5G_Diff[path][0] = pg_lsb_diff(v); } - /* Ntx=2..4: 2 bytes each (BW40|BW20, OFDM|-) */ + /* Ntx=2..4: ONE byte each (MSB=BW40, LSB=BW20). Unlike the 2.4G + * block, 5G packs the OFDM diffs separately below — the previous + * two-bytes-per-Ntx parse reused the 2.4G shape and shifted every + * field from byte 16 onward (kernel hal_load_pg_txpwr_info_path_5g, + * hal_com_phycfg.c:848-953). */ for (int t = 1; t < 4; t++) { uint8_t v = efuse_eeprom_data[off++]; BW40_5G_Diff[path][t] = pg_msb_diff(v); BW20_5G_Diff[path][t] = pg_lsb_diff(v); - v = efuse_eeprom_data[off++]; - OFDM_5G_Diff[path][t] = pg_msb_diff(v); - /* LSB nibble of this byte is unused for 5G (no CCK on 5G). */ } - /* 3 bytes BW80 diffs, Ntx=1..3 stored as nibble pairs: - * byte 0: MSB=Ntx2-BW80, LSB=Ntx1-BW80 - * byte 1: MSB=Ntx4-BW80, LSB=Ntx3-BW80 - * byte 2: reserved - * Upstream uses a different layout per IC; the 8812 path packs as - * above per `hal_load_pg_txpwr_info_path_5g`. */ + /* OFDM diff 2T~3T: one byte (MSB=2T, LSB=3T). */ { uint8_t v = efuse_eeprom_data[off++]; - BW80_5G_Diff[path][1] = pg_msb_diff(v); - BW80_5G_Diff[path][0] = pg_lsb_diff(v); - v = efuse_eeprom_data[off++]; - BW80_5G_Diff[path][3] = pg_msb_diff(v); - BW80_5G_Diff[path][2] = pg_lsb_diff(v); - /* third byte ignored */ - off++; + OFDM_5G_Diff[path][1] = pg_msb_diff(v); + OFDM_5G_Diff[path][2] = pg_lsb_diff(v); + } + /* OFDM diff 4T: one byte, LSB nibble only. */ + { + uint8_t v = efuse_eeprom_data[off++]; + OFDM_5G_Diff[path][3] = pg_lsb_diff(v); + } + /* BW80|BW160 diffs: four bytes, tx 0..3 (MSB=BW80, LSB=BW160 — no + * 160MHz support here, the LSB nibble is consumed for layout only). */ + for (int t = 0; t < 4; t++) { + uint8_t v = efuse_eeprom_data[off++]; + BW80_5G_Diff[path][t] = pg_msb_diff(v); } } From b3929ef8222ab9b9cf198c26cc3a9ed7d1f5c67c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:40:15 +0300 Subject: [PATCH 23/31] 8814: per-Ntx TX-power diffs are cumulative over rate ranges (kernel 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 --- src/EepromManager.cpp | 83 +++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index f979d63..8137a0b 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -783,25 +783,29 @@ uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, goto clamp_and_return; } /* MCS / VHT — pick BW20 / BW40 (BW80 falls through to BW40 per upstream - * comment "Willis suggest adopt BW 40M power index while in BW 80 mode"). */ - if (bandwidth == 0) { /* BW20 */ - if (is_mcs0_7 (rate) || is_vht1ss(rate) || is_vht2ss(rate) || - is_vht3ss (rate) || is_vht4ss(rate)) txPower += BW20_24G_Diff[path][0]; - if (is_mcs8_15 (rate) || (ntx_idx >= 1 && (is_vht2ss(rate) || is_vht3ss(rate) || is_vht4ss(rate)))) - txPower += BW20_24G_Diff[path][1]; - if (is_mcs16_23(rate) || (ntx_idx >= 2 && (is_vht3ss(rate) || is_vht4ss(rate)))) - txPower += BW20_24G_Diff[path][2]; - if (is_mcs24_31(rate) || (ntx_idx >= 3 && is_vht4ss(rate))) - txPower += BW20_24G_Diff[path][3]; - } else { /* BW40 or BW80 */ - if (is_mcs0_7 (rate) || is_vht1ss(rate) || is_vht2ss(rate) || - is_vht3ss (rate) || is_vht4ss(rate)) txPower += BW40_24G_Diff[path][0]; - if (is_mcs8_15 (rate) || (ntx_idx >= 1 && (is_vht2ss(rate) || is_vht3ss(rate) || is_vht4ss(rate)))) - txPower += BW40_24G_Diff[path][1]; - if (is_mcs16_23(rate) || (ntx_idx >= 2 && (is_vht3ss(rate) || is_vht4ss(rate)))) - txPower += BW40_24G_Diff[path][2]; - if (is_mcs24_31(rate) || (ntx_idx >= 3 && is_vht4ss(rate))) - txPower += BW40_24G_Diff[path][3]; + * comment "Willis suggest adopt BW 40M power index while in BW 80 mode"). + * + * Kernel accumulation is CUMULATIVE over rate ranges + * (hal_com_phycfg.c:2490-2496): MCS8-31 adds [0]+[1], MCS16-31 adds + * [0]+[1]+[2], VHT2SS+ adds [1], VHT3SS+ adds [2], etc. The previous + * exclusive windows gave e.g. MCS16-23 only [2]. */ + { + const bool ge_1s = is_mcs0_7(rate) || is_mcs8_15(rate) || + is_mcs16_23(rate) || is_mcs24_31(rate) || + is_vht1ss(rate) || is_vht2ss(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_2s = is_mcs8_15(rate) || is_mcs16_23(rate) || + is_mcs24_31(rate) || is_vht2ss(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_3s = is_mcs16_23(rate) || is_mcs24_31(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_4s = is_mcs24_31(rate) || is_vht4ss(rate); + const int8_t *diff = + (bandwidth == 0) ? BW20_24G_Diff[path] : BW40_24G_Diff[path]; + if (ge_1s) txPower += diff[0]; + if (ge_2s) txPower += diff[1]; + if (ge_3s) txPower += diff[2]; + if (ge_4s) txPower += diff[3]; } } else { /* 5G — no CCK */ @@ -815,25 +819,28 @@ uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, if (ntx_idx >= 3) txPower += OFDM_5G_Diff[path][3]; goto clamp_and_return; } - /* MCS / VHT BW20 / BW40 / BW80. */ - if (bandwidth == 0) { - if (is_mcs0_7 (rate) || is_vht1ss(rate) || is_vht2ss(rate) || - is_vht3ss (rate) || is_vht4ss(rate)) txPower += BW20_5G_Diff[path][0]; - if (is_mcs8_15 (rate)) txPower += BW20_5G_Diff[path][1]; - if (is_mcs16_23(rate)) txPower += BW20_5G_Diff[path][2]; - if (is_mcs24_31(rate)) txPower += BW20_5G_Diff[path][3]; - } else if (bandwidth == 1) { - if (is_mcs0_7 (rate) || is_vht1ss(rate) || is_vht2ss(rate) || - is_vht3ss (rate) || is_vht4ss(rate)) txPower += BW40_5G_Diff[path][0]; - if (is_mcs8_15 (rate)) txPower += BW40_5G_Diff[path][1]; - if (is_mcs16_23(rate)) txPower += BW40_5G_Diff[path][2]; - if (is_mcs24_31(rate)) txPower += BW40_5G_Diff[path][3]; - } else { /* BW80 */ - if (is_mcs0_7 (rate) || is_vht1ss(rate) || is_vht2ss(rate) || - is_vht3ss (rate) || is_vht4ss(rate)) txPower += BW80_5G_Diff[path][0]; - if (is_mcs8_15 (rate)) txPower += BW80_5G_Diff[path][1]; - if (is_mcs16_23(rate)) txPower += BW80_5G_Diff[path][2]; - if (is_mcs24_31(rate)) txPower += BW80_5G_Diff[path][3]; + /* MCS / VHT BW20 / BW40 / BW80 — cumulative over rate ranges, same + * scheme as 2.4G (kernel hal_com_phycfg.c:2550-2601). The previous + * code had no VHT clauses beyond [0] at all on 5G, so VHT2SS missed + * [1] and VHT3SS missed [1]+[2]. */ + { + const bool ge_1s = is_mcs0_7(rate) || is_mcs8_15(rate) || + is_mcs16_23(rate) || is_mcs24_31(rate) || + is_vht1ss(rate) || is_vht2ss(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_2s = is_mcs8_15(rate) || is_mcs16_23(rate) || + is_mcs24_31(rate) || is_vht2ss(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_3s = is_mcs16_23(rate) || is_mcs24_31(rate) || + is_vht3ss(rate) || is_vht4ss(rate); + const bool ge_4s = is_mcs24_31(rate) || is_vht4ss(rate); + const int8_t *diff = (bandwidth == 0) ? BW20_5G_Diff[path] + : (bandwidth == 1) ? BW40_5G_Diff[path] + : BW80_5G_Diff[path]; + if (ge_1s) txPower += diff[0]; + if (ge_2s) txPower += diff[1]; + if (ge_3s) txPower += diff[2]; + if (ge_4s) txPower += diff[3]; } } From 3597aefd23b6337396832803ad27962fbefcc73c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:41:33 +0300 Subject: [PATCH 24/31] 8814: hygiene - label 0x670 as REG_CAMCMD, remove dead BIT0 LLT port 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 --- src/HalModule.cpp | 35 ++++++++++++++--------------------- src/HalModule.h | 1 - 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index a9a9236..f65eaa1 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -638,7 +638,10 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { * (kept value; no usbmon * trace for 0x0524 in * current capture set) - * 0x0670 00 00 00 c0 0xc0000000 NAV-related + * 0x0670 00 00 00 c0 0xc0000000 REG_CAMCMD: BIT31|BIT30 = + * security-CAM clear-all + * (= kernel invalidate_cam_all + * at usb_halinit.c:1236) * 0x0990 00 00 10 27 0x27100000 RA-table base * 0x0994 00 01 48 4c 0x4c480100 * 0x0998 24 28 2c 30 0x302c2824 @@ -651,7 +654,7 @@ bool HalModule::rtl8812au_hal_init(uint8_t init_channel) { _device.rtw_write8(0x04c6, 0x04); /* REG_QUEUE_CTRL */ _device.rtw_write32(0x0520, 0x00002f0fu); /* REG_TX_PTCL_CTRL */ _device.rtw_write32(0x0524, 0x0f4fff00u); /* REG_RD_CTRL — kept */ - _device.rtw_write32(0x0670, 0xc0000000u); + _device.rtw_write32(0x0670, 0xc0000000u); /* REG_CAMCMD clear-all */ /* Rate-adaptation table init (first-write values from cold-init * trace; kernel emits 3+ runtime updates from IQK that devourer * cannot reproduce — settle for the first/initial value). */ @@ -795,25 +798,15 @@ bool HalModule::InitPowerOn() { return true; } -/* 8814AU's LLT (linked-list table) for TX FIFO pages is initialized by chip - * hardware: set BIT0 of REG_AUTO_LLT (0x0208), then poll for the bit to - * clear, meaning init is done. Mirrors upstream InitLLTTable8814A in - * hal/rtl8814a/rtl8814a_hal_init.c. */ -bool HalModule::InitLLTTable8814A() { - constexpr uint16_t REG_AUTO_LLT_8814A = 0x0208; - uint8_t v = _device.rtw_read8(REG_AUTO_LLT_8814A); - _device.rtw_write8(REG_AUTO_LLT_8814A, (uint8_t)(v | BIT0)); - for (int i = 0; i < 100; ++i) { - v = _device.rtw_read8(REG_AUTO_LLT_8814A); - if (!(v & BIT0)) { - _logger->info("InitLLTTable8814A: auto-init OK after {} iters", i); - return true; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - _logger->error("InitLLTTable8814A: timeout waiting for BIT0 to clear"); - return false; -} +/* NOTE: there is deliberately no BIT0-style InitLLTTable8814A here. The + * vendor function (rtl8814a_hal_init.c:71-92) writes BIT0 of an 8-bit + * access at 0x208 and its poll loop tests a stale pre-write variable, so + * it never verifies anything. The only structured in-tree definition of + * 0x208's fields on this generation says BIT_AUTO_INIT_LLT = BIT(16) + * (hal_com_reg.h "2 AUTO_LLT" block), and the BIT16 trigger was verified + * on hardware to self-clear within 2 ms. The live trigger is the 32-bit + * BIT16 RMW in rtl8812au_hal_init above; keep FIFOPAGE_INFO/RQPN + * programming immediately before it. */ bool HalModule::InitLLTTable8812A(uint8_t txpktbuf_bndy) { bool status; diff --git a/src/HalModule.h b/src/HalModule.h index acdcbd1..d1e9311 100644 --- a/src/HalModule.h +++ b/src/HalModule.h @@ -66,7 +66,6 @@ class HalModule { bool rtl8812au_hal_init(uint8_t init_channel); bool InitPowerOn(); bool InitLLTTable8812A(uint8_t txpktbuf_bndy); - bool InitLLTTable8814A(); bool _LLTWrite_8812A(uint32_t address, uint32_t data); void _InitHardwareDropIncorrectBulkOut_8812A(); bool HalPwrSeqCmdParsing(WLAN_PWR_CFG *PwrSeqCmd); From e834ce5cd7b714086b36b70e1ecc04a04db8258c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:45:07 +0300 Subject: [PATCH 25/31] rx: bulk-IN buffer must cover the 20K RX aggregate (kernel MAX_RECVBUF_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 --- src/RtlUsbAdapter.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/RtlUsbAdapter.cpp b/src/RtlUsbAdapter.cpp index b9243d6..b5cd1b7 100644 --- a/src/RtlUsbAdapter.cpp +++ b/src/RtlUsbAdapter.cpp @@ -65,7 +65,14 @@ RtlUsbAdapter::RtlUsbAdapter(libusb_device_handle *dev_handle, Logger_t logger) */ std::vector RtlUsbAdapter::infinite_read() { - static constexpr int BUF_SIZE = 16 * 1024; + /* Must cover one full chip-side RX aggregate: the 8814 init programs + * REG_RXDMA_AGG_PG_TH = 0x05 ("dmc agg th 20K"), and the kernel pairs + * that with MAX_RECVBUF_SZ = 32768 (rtl8814a_recv.h:25) — the threshold + * plus the in-flight frame must fit the host read. A 16 KB buffer (an + * exact multiple of wMaxPacketSize, so no short-packet terminates the + * transfer) split >16 KB aggregates and the tail bytes were then parsed + * as an RX descriptor at the head of the next transfer. */ + static constexpr int BUF_SIZE = 32 * 1024; uint8_t buffer[BUF_SIZE] = {}; int actual_length = 0; int rc; From abe49882591f647276cd189d7854765dfa8194ae Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:45:07 +0300 Subject: [PATCH 26/31] 8814: clear USB_AGG_EN (0x283 bit7) like the kernel RX-agg setup (kernel 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 --- src/HalModule.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index f65eaa1..ce1a270 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -1898,6 +1898,17 @@ void HalModule::usb_AggSettingRxUpdate_8812A() { (uint16_t)(_device.rxagg_usb_size | (_device.rxagg_usb_timeout << 8)); _device.rtw_write16(REG_RXDMA_AGG_PG_TH, temp); } + if (_eepromManager->version_id.ICType == CHIP_8814A) { + /* Kernel usb_AggSettingRxUpdate_8814A explicitly RMW-clears + * USB_AGG_EN_8814A (BIT7 of REG_RXDMA_AGG_PG_TH+3, 0x283) in its + * default RX_AGG_DMA mode (usb_halinit.c:705-727). Devourer's + * taken path never wrote that byte, leaving USB-mode aggregation + * at whatever the reset/firmware value is — mixed DMA+USB agg + * framing would break the RND8 parse walk. */ + uint8_t valueUSB = _device.rtw_read8(REG_RXDMA_AGG_PG_TH + 3); + _device.rtw_write8(REG_RXDMA_AGG_PG_TH + 3, + (uint8_t)(valueUSB & ~BIT7)); + } break; case RX_AGG_MIX: case RX_AGG_DISABLE: From 10927e6ea69db39ee44d944eaeadbf3b1b474cd7 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:45:48 +0300 Subject: [PATCH 27/31] rx: parse hardening - physt gating, tail-fragment guard, 8814 DWORD4 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 --- src/FrameParser.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/FrameParser.cpp b/src/FrameParser.cpp index a549753..512068c 100644 --- a/src/FrameParser.cpp +++ b/src/FrameParser.cpp @@ -107,7 +107,13 @@ static rx_pkt_attrib rtl8812_query_rx_desc_status(uint8_t *pdesc) { /* Offset 12 */ pattrib.data_rate = GET_RX_STATUS_DESC_RX_RATE_8812(pdesc); - /* Offset 16 */ + /* Offset 16 — 8812/8821 ONLY. On 8814A this DWORD holds + * PATTERN_IDX[7:0] / RX_EOF[8] / RX_SCRAMBLER[15:9] + * (rtl8814a_recv.h:148-150) and the kernel's 8814 rx-desc query never + * reads SGI/LDPC/STBC/BW from the descriptor at all (it sets + * bw = CHANNEL_WIDTH_MAX). These four attribs are therefore garbage + * when the RX chip is an 8814 — no current consumer reads them, but + * don't trust them there without chip-gating first. */ pattrib.sgi = GET_RX_STATUS_DESC_SPLCP_8812(pdesc); pattrib.ldpc = GET_RX_STATUS_DESC_LDPC_8812(pdesc); pattrib.stbc = GET_RX_STATUS_DESC_STBC_8812(pdesc); @@ -151,6 +157,12 @@ std::vector FrameParser::recvbuf2recvframe(std::span ptr) { auto ret = std::vector{}; do { + /* Never parse a descriptor out of a tail fragment shorter than the + * descriptor itself (kernel rejects transfers < RXDESC_SIZE before + * parsing, os_dep usb_ops_linux.c). */ + if (pbuf.size() < RXDESC_SIZE) { + break; + } auto pattrib = rtl8812_query_rx_desc_status(pbuf.data()); auto pkt_offset = RXDESC_SIZE + pattrib.drvinfo_sz + pattrib.shift_sz + @@ -204,8 +216,19 @@ std::vector FrameParser::recvbuf2recvframe(std::span ptr) { pattrib.drvinfo_sz + RXDESC_SIZE, pattrib.pkt_len)}); - struct _phy_status_rpt_8812 driver_data; - memcpy(static_cast(&driver_data), pbuf.data() + RXDESC_SIZE, sizeof(driver_data)); + struct _phy_status_rpt_8812 driver_data = {}; + /* Only read the PHY-status report when the descriptor says one is + * present and it fits the remaining buffer. The kernel gates this + * on pattrib->physt (usb_ops_linux.c:179); drvinfo_sz >= the report + * size is the equivalent condition with the fields we carry — + * without it, frames with drvinfo_sz==0 had payload bytes decoded + * as RSSI/EVM/SNR, and a frame ending near the buffer tail + * over-read the transfer buffer. */ + if (pattrib.drvinfo_sz >= sizeof(driver_data) && + pbuf.size() >= RXDESC_SIZE + sizeof(driver_data)) { + memcpy(static_cast(&driver_data), pbuf.data() + RXDESC_SIZE, + sizeof(driver_data)); + } ret.back().RxAtrib.rssi[0] = driver_data.gain_trsw[0]; ret.back().RxAtrib.rssi[1] = driver_data.gain_trsw[1]; /* 8814AU path C/D RSSI lives in gain_trsw_cd; on 8812/8811 these bytes @@ -259,8 +282,12 @@ std::vector FrameParser::recvbuf2recvframe(std::span ptr) { pbuf = pbuf.subspan(pkt_offset, pbuf.size() - pkt_offset); } while (pbuf.size() > 0); + /* pkt_cnt (DMA_AGG_NUM from the first descriptor) is informational only: + * neither the kernel nor devourer uses it for loop control, and devourer + * never decremented it — so a non-zero value here is the norm for every + * aggregated transfer, not an error. */ if (pkt_cnt != 0) { - _logger->info("Unprocessed packets: {}", pkt_cnt); + _logger->debug("RX aggregate carried {} packets (DMA_AGG_NUM)", pkt_cnt); } //_logger->info("{} received in frame", ret.size()); From dbeb720bad71276ad54644f1eb71c42b918825e5 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:56:55 +0300 Subject: [PATCH 28/31] 8814: RF reads must use the direct shadow blocks, not 8812 serial readback (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 --- src/RadioManagementModule.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 2b6debd..b07a76c 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -528,6 +528,22 @@ uint32_t RadioManagementModule::phy_query_rf_reg(RfPath eRFPath, uint32_t RadioManagementModule::phy_RFSerialRead(RfPath eRFPath, uint32_t Offset) { + if (_eepromManager->version_id.ICType == CHIP_8814A) { + /* Kernel phy_RFRead_8814A (rtl8814a_phycfg.c:86-122): 8814 RF + * registers are read back through per-path direct BB shadow blocks, + * NOT the 8812 HSSI/LSSI serial mechanism below. This is also the + * only read path that works for paths C/D — the SI readback returns + * garbage there, which corrupted every masked (read-modify-write) RF + * write on those paths: the channel and bandwidth RMWs of RF 0x18 + * destroyed all bits outside their masks on C/D at every channel + * set. */ + static constexpr uint16_t kDirectBase[4] = {0x2800, 0x2c00, 0x3800, + 0x3c00}; + const uint16_t direct_addr = (uint16_t)( + kDirectBase[static_cast(eRFPath) & 3] + (Offset & 0xff) * 4); + return phy_query_bb_reg(direct_addr, 0xfffff /* bRFRegOffsetMask */); + } + uint32_t retValue; BbRegisterDefinition pPhyReg = PhyRegDef[eRFPath]; From e280b9231f3994c5752fde345ea6f971fcfa8ff6 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:57:16 +0300 Subject: [PATCH 29/31] 8814: rfe_type 2 5G pinmux nibble is 0x37173717 (kernel PHY_SetRFEReg8814A) 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 --- src/RadioManagementModule.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index b07a76c..c0c2a35 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -905,9 +905,13 @@ void RadioManagementModule::phy_SetRFEReg8814A(BandType Band) { } else { switch (rfe_type) { case 2: - _device.phy_set_bb_reg(rA_RFE_Pinmux_Jaguar, bMaskDWord, 0x33173717); - _device.phy_set_bb_reg(rB_RFE_Pinmux_Jaguar, bMaskDWord, 0x33173717); - _device.phy_set_bb_reg(0x18B4, bMaskDWord, 0x33173717); + /* Kernel PHY_SetRFEReg8814A 5G case 2: 0x37173717 on A/B/C — the + * previous 0x33173717 carried rfe-1's [27:24] nibble (copy slip + * between adjacent cases; flagged independently by two audit + * passes against the rtl8814au reference). */ + _device.phy_set_bb_reg(rA_RFE_Pinmux_Jaguar, bMaskDWord, 0x37173717); + _device.phy_set_bb_reg(rB_RFE_Pinmux_Jaguar, bMaskDWord, 0x37173717); + _device.phy_set_bb_reg(0x18B4, bMaskDWord, 0x37173717); _device.phy_set_bb_reg(0x1AB4, bMaskDWord, 0x77177717); _device.phy_set_bb_reg(0x1ABC, 0x0FF00000, 0x37); break; From 559f39a7030b36557613d78de369d930b30f9884 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:58:10 +0300 Subject: [PATCH 30/31] 8814: IGI floor must cover paths C/D too (kernel phydm_write_dig_reg, 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 --- src/HalModule.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index ce1a270..52e56c1 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -3259,6 +3259,16 @@ void HalModule::phydm_SetIgiFloor_Jaguar() { * kernel driver. Match kernel by writing the floor once here. */ _device.phy_set_bb_reg(rA_IGI_Jaguar, bMaskByte0, 0x1c); _device.phy_set_bb_reg(rB_IGI_Jaguar, bMaskByte0, 0x1c); + if (_eepromManager->version_id.ICType == CHIP_8814A) { + /* 4-path chip: kernel DIG always writes the same IGI to all four + * paths (phydm_write_dig_reg covers 0xC50/0xE50/0x1850/0x1A50 for + * ODM_IC_AC_4SS). Flooring only A/B left C/D at the 0x20 BB-table + * seed — a 4 dB per-path gain imbalance the kernel never has, which + * skews MRC combining and per-path RSSI. (0x1850/0x1A50 = + * rC_IGI_Jaguar2 / rD_IGI_Jaguar2.) */ + _device.phy_set_bb_reg(0x1850, bMaskByte0, 0x1c); + _device.phy_set_bb_reg(0x1A50, bMaskByte0, 0x1c); + } } void HalModule::PHY_BB8812_Config_1T() { From 45e6a75ba0780a0bd9dcec23cf2d2f8fba30fdc0 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:00:35 +0300 Subject: [PATCH 31/31] docs: 8814 port-audit report - ranked findings, coverage map, validation 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 --- docs/8814-port-audit.md | 152 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/8814-port-audit.md diff --git a/docs/8814-port-audit.md b/docs/8814-port-audit.md new file mode 100644 index 0000000..d3f12cd --- /dev/null +++ b/docs/8814-port-audit.md @@ -0,0 +1,152 @@ +# RTL8814AU port audit — devourer vs aircrack-ng/rtl8814au + +Systematic source-level comparison of devourer against its kernel reference +(`aircrack-ng/rtl8814au` @ `8926414`, `CONFIG_RTL8814A=y`, USB, BT/WoWLAN off, +module-param defaults), motivated by 8814 TX being silent on-air and by the +suspicion that the port carries mistakes. Ten work packages covered every +devourer 8814 code path; each finding below was verified against both sources +at the cited lines before classification. Several were found independently by +two blind audit passes. + +Classes: **C1** unambiguous porting error (fixed) · **C2** intentional +adaptation (kept, documented) · **C3** kernel feature not ported (risk-assessed) +· **C4** ambiguous (documented experiment, no speculative fix). + +Method: static function-pair diff with kernel `#ifdef`s resolved per the real +build config, plus table-parity regeneration. usbmon/canary lenses and on-air +validation are listed under *Hardware validation* — **none of the fixes below +have been hardware-validated yet**. + +## C1 fixes (27 commits, each independently buildable) + +Ranked by suspected relevance to the motivating "USB accepts, 0 frames on air" +symptom. + +| # | Commit | Finding | Kernel ref | +|---|---|---|---| +| 1 | `c5bb4ad` | 8812 `_InitBurstPktLen` body ran on 8814: its `0x456=0x70` (AMPDU_MAX_TIME on 8812) clobbered `REG_TXPKTBUF_BCNQ1_BDNY_8814A` — the TX-buffer boundary programmed to 0x7F6 moments earlier. Ported the 8814 body (FAST_EDCA, RXDMA burst mode, `0xf002=0` "avoid usb 3.0 H2C fail", LDPC-pre-TX off); removed PIFS-zeroing, USTIME, MAX_AGGR 0x1f1f, RSV_CTRL/ARFR/0xf050/0x288/0x289 8812-isms | `usb_halinit.c:122-166` | +| 2 | `f3188ea` | The 5GHz CCK→OFDM TX clamp gated on `_channel.Channel` — a member **never assigned anywhere** (read of indeterminate memory; the 5G-TX fix fired nondeterministically per build/run). Channel now stored in `SetMonitorChannel`; member value-initialised | `core/rtw_mlme_ext.c:1058` (kernel keys off maintained `cur_channel`) | +| 3 | `2931ad6` | FW-boot poll was vacuous: accepted `byte0==0x78` of REG_MCUFWDL — which devourer itself writes in the 0x6078 kick — so a never-booted 3081 looked like success. Now requires chip-set `CPU_DL_READY` (BIT15) and errors loudly on timeout. **A failure here on a virgin chip is the smoking gun for the TX silence** | `rtl8814a_hal_init.c:649-656` | +| 4 | `dbeb720` | 8814 RF reads used the 8812 3-wire serial mechanism; the kernel reads via per-path direct BB shadow blocks (`0x2800/0x2C00/0x3800/0x3C00 + reg*4`). Paths C/D returned garbage from the serial path, so every masked RF RMW (channel + BW writes of RF 0x18) corrupted C/D tuning on **every channel set**. Overturns the old "C/D write-only by design" note | `rtl8814a_phycfg.c:86-122` | +| 5 | `1a8ea83` | Crystal-cap trim wrote 8812 bit positions on every chip — on 8814 the XTAL trim landed 4 bits high (real field untouched, `0x2C[30:27]` clobbered) ⇒ uncorrected carrier-frequency offset on TX+RX. Also fixed: 8821's distinct mask, the `(uint8_t)` truncation of the 12-bit pattern (hurt 8812 too), and the missing RCK1 A→B/C/D RC-trim sync | `phydm_cfotracking.c:230-249`, `rtl8814a_rf6052.c:143-146` | +| 6 | `993e0d9` | `USTIME_TSF/EDCA` forced to 0x50 (8812's 80MHz tick); 8814 MAC runs 100MHz, table value 0x64 — all µs-derived TX timing ran ~25% fast | `usb_halinit.c:577-579` (writes commented out) | +| 7 | `cffeba5` | `NAV_UPPER` left 0 after the init zero-write (kernel restores `ceil(30000/128)=0xEB`) — MAC honoured arbitrarily long NAV; can defer TX indefinitely on busy air | `rtl8814a_hal_init.c:3794-3807` | +| 8 | `8b45e23` | USB TX-agg block-descriptor config never armed (`TDECTRL[7:4]=3`, `0x20B=0x06` — kernel always does) | `usb_halinit.c:654-676` | +| 9 | `0cbeea5` | Per-byte aggregation limits `0x4CA/0x4CB=0x36` from `_InitMacConfigure_8814A` tail (devourer ported the old 8812 pair the kernel folded away) | `usb_halinit.c:487-554` | +| 10 | `e834ce5` | Bulk-IN host buffer 16KB vs the 20KB chip-side RX-aggregation threshold (kernel: 32KB × 8 async URBs). Oversize aggregates split with no short packet → tail parsed as a descriptor, remainder dropped. *Companion to #1, which introduced the kernel 20K threshold* | `rtl8814a_recv.h:25` | +| 11 | `f3b8c8c` | 5G EFUSE PG diff block parsed with the 2.4G two-bytes-per-Ntx shape — every field from relative byte 16 wrong-sourced (BW20/40 3S/4S ↔ OFDM bytes; BW80 diffs read BW160 nibbles). Found independently by two passes | `hal_com_phycfg.c:848-953` | +| 12 | `b3929ef` | Per-Ntx power diffs are **cumulative** upstream (MCS16-31 = `[0]+[1]+[2]`; VHT2SS+ adds `[1]`); devourer used exclusive windows, and 5G had no VHT clauses at all | `hal_com_phycfg.c:2490-2601` | +| 13 | `95d1b97` | Kernel 8814 RFE decision tree ported: unburnt/BIT7 `0xCA` → rfe_type **1** on AU (was 0 via 8812 parse), `&0x7F`, amplifier state derived from rfe_type. Removed the now-wrong `GetPhyContext` 0→1 patch. *Runtime-inert on the CF-938AC (0xCA=0x01 → 1 either way)* | `rtl8814a_hal_init.c:1474-1568` | +| 14 | `c9394a4` | 8812 TX-power-training writes ran on 8814 each channel set (kernel 8814 has none; paths B/C/D collapsed last-writer-wins onto 0xE54) | `rtl8814a_phycfg.c:636-673` | +| 15 | `559f39a` | IGI floor wrote paths A/B only — 4dB initial-gain imbalance vs C/D on the 4-path chip (RX/MRC skew) | phydm `ODM_IC_AC_4SS` DIG | +| 16 | `217fb76` | Power-seq poll retry 10ms vs kernel `udelay(10)` — failing poll cost ~50s instead of ~50ms | `HalPwrSeqCmd.c:134` | +| 17 | `be22303` | Deinit-before-init scaffold made faithful to `hal_carddisable_8814`: `REG_CR=0` stop-rx prologue, gated on the warm-boot detect (kernel never feeds `ACT_TO_CARDEMU` to a cold chip), kernel's constant `0xFE` cut mask | `usb_halinit.c:1386-1455` | +| 18 | `5dde6cc` | `0x577=0x03` secondary-CCA control port | `usb_halinit.c:1250` | +| 19 | `abe4988` | `USB_AGG_EN` (0x283 bit7) now explicitly cleared like the kernel RX-agg setup | `usb_halinit.c:705-727` | +| 20 | `9c38f80` | VHT radiotap bandwidth never reached the descriptor (MHz literals compared against `CHANNEL_WIDTH` enums) — every VHT frame TXed at 20MHz | `rtl8814a_xmit.c:443` | +| 21 | `e280b92` | rfe_type-2 5G pinmux constant `0x37173717` (copy slip; latent — rfe-2 boards only) | `PHY_SetRFEReg8814A` | +| 22 | `e20afaa` | `REG_USB_HRPWM` init write skipped on 8814 (kernel has it commented out) | `usb_halinit.c:1354` | +| 23 | `6021880` | fw version logged from double-offset header read (always 0; blob is v33, md5-identical to kernel's) | `rtl8814a_hal.h:76` | +| 24 | `10927e6` | RX parse hardening: PHY-status memcpy gated (payload bytes were decoded as RSSI/EVM/SNR on physt-less frames + tail over-read), short-fragment guard, 8814 DWORD4 trap documented, spurious log demoted | `usb_ops_linux.c:179` | +| 25 | `3c958ce` | fwdl comment register labels corrected (0x0230 is FIFOPAGE_INFO_1/HPQ count, 0x0210 TXDMA_STATUS, …) — debug-risk only | `rtl8814a_spec.h` | +| 26 | `3597aef` | Hygiene: `0x670` labeled REG_CAMCMD clear-all (was "NAV-related"); dead BIT0 LLT port removed with the adjudication recorded | — | +| 27 | `334d04f` | Docs: stale `DEVOURER_FORCE_TXPWR` removed (only `DEVOURER_SKIP_TXPWR` exists) | — | + +## Key negative results (settled — don't re-chase) + +- **The kernel sends ZERO H2C commands in monitor bring-up/channel-set/inject** + under this config (exhaustive caller-traced inventory; `RegFWOffload` is + never assigned). Devourer's zero-H2C is *not* a divergence; missing H2C + cannot explain "kernel TX works, devourer silent". The surviving FW axes are + boot verification (#3, now instrumented) and the rtw88-trace download + protocol itself. +- **IQK-off in monitor is parity**: the kernel's init IQK block is commented + out, the channel-set trigger requires `bNeedIQK` (join/AP/DFS only), and the + watchdog IQK is `#if 0`. +- **PHY tables are byte-identical** (vendored inputs match the kernel tree; + generated arrays in parity) and the table walker + `check_positive` are + semantically exact — including both drivers ignoring cond2/3/4 (GLNA/GPA/ + ALNA/APA never select 8814 table branches in either tree). +- **PWR_SEQ arrays + parser semantics** are byte-identical/faithful. The + historical "cut-mask filter broke fwdl" mystery has a mechanism: the kernel + passes the chip-independent **constant** `~PWR_CUT_TESTCHIP_MSK` (0xFE) — + deriving the mask from EEPROM cut_version (cut ≥ H overflows past BIT7 → + mask 0) silently no-ops the whole flow. +- **Auto-LLT trigger is BIT16** of 0x208 (the only structured in-tree + definition; HW-verified 2ms self-clear). The vendor BIT0 function polls a + stale variable and verifies nothing. Invariant: FIFOPAGE_INFO/RQPN latch + immediately precedes the BIT16 trigger, both after fwdl (devourer's + rsvd-page fwdl transport requires post-fwdl LLT, unlike the kernel's DDMA + position). +- **TX descriptor field map**: every field devourer sets has identical + (dword, bit, width) in the `_8812` and `_8814A` macro sets; checksum domain + (16 LE16 words, dwords 8-9 excluded) exact; QSEL/MACID/EP mapping exact. +- Kernel `ip link down` does **not** HW-deinit (call commented out, IPS off); + virsh surprise-removal also skips card-disable — only rmmod/clean-unbind + powers the chip down. Devourer has no teardown at all (C3): the env-gated + deinit-before-init scaffold is the mitigation. + +## Residual C3 risks (not ported; assessed) + +| Area | Risk | +|---|---| +| Thermal power-tracking + LCK (8814) — kernel watchdog compensates TX AGC/BB-swing per thermal delta with **no link gating** | TX power drifts as the chip heats during sustained injection (long-range video duty cycles); LCK never re-runs. Highest-value C3 to port next | +| DIG / CCK-PD / EDCCA / FA-reset watchdog slices | IGI frozen at floor; CCK PD fixed; EDCCA thresholds stay at table defaults — RX adaptation in noisy environments (watchdog exists but is env-gated off for measured USB-contention reasons; ports only FA+DIG) | +| `phy_SpurCalibration_8814A` (skip premise was wrong) | ch153 NBI/CSI (any rfe) and ch140 swap (rfe 0) are live at 20MHz — RX spur masking missing on those channels | +| Vendor-request single-shot (kernel: 10× retry + io-error escalation) | One EP0 hiccup aborts a session (read) or silently skips a register write — credible #36 (passthrough-cycle) contributor | +| Sync 1×32KB polling read (kernel: 8×32KB async URBs) | IN-token gaps while parsing + 50ms error sleep → RX loss under load only | +| `Index5G_BW80_Base` (kernel averages adjacent BW40 groups) | 80MHz TXAGC base off by up to half the inter-group delta — 80MHz only | +| 0xC9 TRX-antenna option / USB2→2T4R policy | TX nss policy on USB2 ports / non-0xFF 0xC9 boards | +| TX-power validity check (wrong offset, throws on blank maps; kernel falls back to default tables) | Crash on blank-TX-power EFUSE boards | +| Skip-fwdl-if-running (`MCUFWDL==0x78`) has no kernel counterpart | Warm runs skip ALL fw arming. Entangled with the deinit experiment (scaffold makes the chip cold → skip not taken) | +| Bulk-boundary padding (kernel pads 8B via PKT_OFFSET when `(40+sz)%512==0`; devourer sends exact-multiple + ZLP) | Deterministic per frame size; a 472/984-byte payload sweep settles chip ZLP tolerance | +| EFUSE physical walk 512 vs 1024; EFUSE mask unapplied; 0x8129 ID-check (kernel dropped it) | Odd/heavily-reprogrammed boards only | + +## C4 experiments (no speculative fixes made) + +1. **HWSEQ for injected frames**: this kernel does `HWSEQ_EN=0` + copies the + frame seqnum (`monitor_overwrite_seqnum=0`); devourer does `HWSEQ_EN=1` + (matches the 88XXau byte-match instead — two GPL trees disagree, same story + for `DATA_RETRY_LIMIT` 12-vs-0). HW currently overwrites injected seqnums — + matters for wfb-ng-style consumers. Experiment: flip to kernel behaviour, + verify on-air seq == injected seq, watch for TX regressions. +2. **HWSEQ_CTRL byte3**: devourer forces 0xFF via 32-bit RMW (source intent); + the working kernel chip ends at 0x03 (its 8-bit write no-ops on silicon). + Experiment: drop the force, byte-match the kernel chip state. +3. **0x283 bit7 reset value** — one vendor-read on live HW settles whether the + new explicit clear ever mattered. +4. **RX-wedge falsification**: re-run 8814 RX under dense traffic with the + 32KB buffer (and optionally the old ≤12K threshold) to see whether the + split-aggregate mechanism was the "~10 frames then bulk-IN timeout" wedge. +5. **rtw88-mimic ops with no vendor counterpart** (`0x0064/0x004C/0x00EC/ + 0x1103/0x1330/0x01A0/0x0009` + `0x10C2` BIT5): resolvable only against the + rtw88 source; candidates for byte-state diffing if the FW-boot check (#3) + fails on a virgin chip. + +## Coverage map + +| Devourer 8814 path | Audited in | +|---|---| +| Init flow order + warm-boot detect (`HalModule::rtl8812au_hal_init`) | WP3 (45-item ordered checklist) | +| Power-on / fwdl mimic / FW vars / H2C (`FirmwareManager`) | WP2, WP7 | +| Queue/page/EP/boundary, MAC config, EDCA/retry/agg/beacon/burst | WP1 | +| Deinit scaffold + PWR_SEQ arrays + parser | WP4 | +| TX descriptor + radiotap mapping + USB xmit rules (`RtlJaguarDevice`, `FrameParser` TX) | WP5 | +| Channel/BW/band/RFE/TXAGC/power chain (`RadioManagementModule`) | WP6 | +| EFUSE machinery + derived config (`EepromManager`) | WP7 | +| PHY tables + walker (`PhyTableLoader`, `hal/phydm/rtl8814a/*`) | Lens D + WP8 | +| DM defaults, watchdog slices, IQK (`PhydmWatchdog`, `Iqk8814a`) | WP9 | +| RX descriptor/aggregation/filters + USB primitives (`FrameParser` RX, `RtlUsbAdapter`) | WP10 | + +## Hardware validation (pending — user-assisted) + +1. `cmake --build build -j` — done for every commit. +2. **Virgin-chip ch6 on-air check** (AR9271 sniffer, fresh Vbus power-cycle): + watch the new log lines — `8814A firmware boot NOT confirmed` = FW-arming + axis confirmed; frames on air = the BCNQ1/USTIME/NAV cluster was the gate. +3. Canary re-diff at ch6 + ch36 (extend set with 0x2C, 0x456, 0x55C/0x638, + 0x652, 0x577, 0x4CA/B, 0x208/0x20B, 0x283, RF 0x18 on C/D via the new + direct read). +4. `sudo python3 tests/regress.py --vm-name devourer-testrig --vm-ssh …` + default matrix; `--channel 36` spot-check; full matrix + encoding-matrix if + 8814 TX goes green.