From f5de7b1e3132d01b24c366ef2e05e3c42eb65718 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:53:42 +0300 Subject: [PATCH 1/4] =?UTF-8?q?8814:=20boot=20the=203081=20MCU=20=E2=80=94?= =?UTF-8?q?=20kernel-faithful=20fwdl=20bracket=20(fixes=20the=20#95=20/=20?= =?UTF-8?q?#50=20TX-silence=20root=20cause)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rtw88-mimic fwdl sequence left CPU_DL_READY (REG_MCUFWDL bit15) permanently deasserted: FW bytes landed (DDMA checksum OK) but the 3081 never booted, so host-pushed TX was 0 on-air while RX worked. Replace the fwdl bracket with a verbatim port of the vendor kernel's FirmwareDownload8814A + HalROMDownloadFWRSVDPage8814A (hal/rtl8814a/rtl8814a_hal_init.c), wired through the pre-existing kernel-faithful helpers that were never called: _FWDownloadEnable_8814A(true) RMW 0x0080 bit13|bit0 _3081Disable8814A RMW, hold MCU in reset _DDMAReset8814A 0x1080 BIT16 clear->set toggle HalROM RSVD-page download 1488-byte chunks; beacon-queue state set once (not per-chunk); 0x0205 BIT7 ack poll/clear via RMW; per-section checksum-driven DL_RDY|CHKSUM_OK RMW FW_DW_RDY (0x0081 |= BIT6) only if both section checksums OK _3081Enable8814A release MCU -> fw boots _FWDownloadEnable_8814A(false) RMW clear bit0 poll CPU_DL_READY The rtw88-mimic power-on prefix is retained (it is devourer's only 8814 power-on path), as are the pre-fwdl staging ops whose kernel equivalents live in _InitPowerOn_8814AU/_InitQueueReservedPage. The legacy sequence is preserved bit-for-bit behind DEVOURER_8814_FWDL=rtw88; DEVOURER_8814_FWDL_CHUNK overrides the kernel chunk size. Per-step register-state dumps (_DumpFwdlState8814A) trace the bracket. Hardware-validated on a virgin chip (Vbus power-cycled): - REG_MCUFWDL trajectory 0x...2079 -> 0x...6079 -> 0x...E078 (CPU_DL_READY asserts instantly on _3081Enable) - TX on-air at the kernel-monitor witness: ch6 12844, ch36 10752, ch100 10817 canonical-SA frames (previously 0 at every channel) - 8814 RX at ch6 unaffected; 8812 TX unaffected (8463 frames) - DEVOURER_8814_FWDL=rtw88 still reproduces the failure (0x00606078), a clean causal A/B Co-Authored-By: Claude Opus 4.8 --- docs/8814-port-audit.md | 16 ++ src/FirmwareManager.cpp | 368 ++++++++++++++++++++++++++++++++++++---- src/FirmwareManager.h | 11 +- 3 files changed, 364 insertions(+), 31 deletions(-) diff --git a/docs/8814-port-audit.md b/docs/8814-port-audit.md index d3f12cd..7afdaea 100644 --- a/docs/8814-port-audit.md +++ b/docs/8814-port-audit.md @@ -150,3 +150,19 @@ symptom. 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. + +## Resolution: FW-boot / TX-silence axis (2026-06-11) + +The smoking gun fired exactly as predicted by finding #3: on a virgin chip the +`CPU_DL_READY` poll failed (`REG_MCUFWDL` stuck at `0x00606078`) and TX was +0 on-air. Replacing the rtw88-mimic fwdl *bracket* with a verbatim port of the +vendor kernel's `FirmwareDownload8814A` + `HalROMDownloadFWRSVDPage8814A` +(keeping the mimic's power-on prefix, which remains devourer's only 8814 +power-on) makes the 3081 boot: trajectory `0x...2079` (per-section +DL_RDY/CHKSUM_OK RMW) → `0x...6079` (FW_DW_RDY) → `0x...E078` +(`CPU_DL_READY`, asserted instantly on `_3081Enable`). With the FW running, +TX emits on-air: ch6 12.8k / ch36 10.7k / ch100 10.8k canonical-SA frames at +the kernel-monitor witness; RX unaffected. The legacy sequence is preserved +bit-for-bit behind `DEVOURER_8814_FWDL=rtw88` and still reproduces the +failure — a clean causal A/B. Residual-risk items #1 (blanket `0x79/0x6078` +kick) and the FW-boot arm of #5 are thereby resolved. diff --git a/src/FirmwareManager.cpp b/src/FirmwareManager.cpp index 5d02254..3cbee23 100644 --- a/src/FirmwareManager.cpp +++ b/src/FirmwareManager.cpp @@ -5,6 +5,7 @@ #include "rtl8812a_hal.h" #include +#include #include #include #include @@ -40,6 +41,10 @@ constexpr uint32_t FW_HEADER_SIZE_8814A = 64; constexpr uint16_t TX_PAGE_BOUNDARY_8814A_USB = 0xF8; constexpr uint32_t TX_PAGE_SIZE = 128; constexpr uint32_t MAX_RSVD_PAGE_CHUNK_SZ = 4096u; /* rtw88 sends 4096-byte fw chunks */ +/* Kernel chunk size: MAX_XMIT_EXTBUF_SZ (1536) - TXDESC_OFFSET (40 + 8 byte + * packet offset = 48) as used by HalROMDownloadFWRSVDPage8814A's + * MaxRsvdPageBufSize. Override per-run via DEVOURER_8814_FWDL_CHUNK. */ +constexpr uint32_t MAX_RSVD_PAGE_BUF_SZ_8814A = 1536u - 48u; } // namespace template set toggle + * HalROM RSVD-page download -- per-chunk bulk-OUT + IDDMA with + * checksum-driven DL_RDY/CHKSUM_OK RMW + * FW_DW_RDY (0x0081 |= BIT6) -- only if both section checksums OK + * _3081Enable8814A -- release MCU -> fw boots + * _FWDownloadEnable_8814A(false) -- RMW: clear bit0 + * poll CPU_DL_READY (bit15) -- 3081 reports it's running + * + * ("rtw88") _Fwdl8814_Rtw88Path — the legacy open-loop rtw88-usbmon-mimic + * staging + blanket REG_MCUFWDL=0x79/0x6078 kick. Kept bit-identical on the + * wire as an A/B fallback; under it the FW bytes land (DDMA checksum OK) + * but the 3081 never reaches CPU_DL_READY (REG_MCUFWDL stuck at + * 0x00606078). + * + * The 8814 firmware header is 64 bytes (vs 8812's 32). */ void FirmwareManager::FirmwareDownload_8814A() { /* If fw is already running on the chip (e.g. another driver loaded it and * we're picking up the device mid-flight), skip fwdl entirely. @@ -200,8 +215,17 @@ void FirmwareManager::FirmwareDownload_8814A() { * the chip see the header.) */ (void)FW_HEADER_SIZE_8814A; - if ((_device.rtw_read8(REG_MCUFWDL) & BIT7) != 0) { - /* 8051 already running from previous session — reset it. */ + /* Path select: kernel-faithful bracket by default; DEVOURER_8814_FWDL=rtw88 + * restores the legacy rtw88-mimic sequence bit-for-bit (A/B fallback). */ + const char *fwdl_mode = std::getenv("DEVOURER_8814_FWDL"); + const bool use_rtw88_path = + (fwdl_mode != nullptr && std::strcmp(fwdl_mode, "rtw88") == 0); + + if (use_rtw88_path && (_device.rtw_read8(REG_MCUFWDL) & BIT7) != 0) { + /* 8051 already running from previous session — reset it. (rtw88 path + * only: the kernel's FirmwareDownload8814A has no such prologue, and the + * absolute REG_MCUFWDL=0x00 write destroys the bit12-13 state that + * _FWDownloadEnable_8814A is specified to preserve.) */ _device.rtw_write8(REG_MCUFWDL, 0x00); _device._8051Reset8812(); } @@ -364,6 +388,61 @@ void FirmwareManager::FirmwareDownload_8814A() { break; } } + _logger->info("8814A: rtw88-mimic power-on prefix applied"); + + if (use_rtw88_path) { + _Fwdl8814_Rtw88Path(fw, dmem_size, iram_size); + return; + } + + /* Kernel path. First: pre-fwdl staging ops with no counterpart inside the + * kernel's fwdl bracket — rtw88 issues them between power-on and fwdl; the + * kernel does their equivalents in _InitPowerOn_8814AU / + * _InitQueueReservedPage_8814AUsb, which both run BEFORE fwdl in the + * kernel but which devourer runs post-fwdl. 0x0100=0x05 in particular + * gates bulk-OUT DMA drain (see the #36 bisect note in the rtw88 path). + * Kept in rtw88 trace order with their paired reads (read-side-effects). + * Deliberately dropped vs the rtw88 path — the kernel bracket performs + * these RMW-style at the kernel position instead: 0x1080=0xF7816D20, + * 0x0003=0xFA, 0x0550=0x14, 0x0080=0x2001, 0x1208=0x02000000, + * 0x0204=0x8000, 0x0101=0x01. */ + (void)_device.rtw_read8(0x0003); + _device.rtw_write8(0x0003, 0xFE); + (void)_device.rtw_read8(0x1103); + _device.rtw_write8(0x1103, 0x0C); + (void)_device.rtw_read32(0x0080); + _device.rtw_write8(0x01A0, 0xFD); + (void)_device.rtw_read8(0x001D); + _device.rtw_write8(0x001D, 0x08); + (void)_device.rtw_read8(0x010D); + _device.rtw_write8(0x010D, 0xC0); + (void)_device.rtw_read8(0x0100); + _device.rtw_write8(0x0100, 0x05); + _device.rtw_write32(0x1330, 0x80000000); + (void)_device.rtw_read16(0x0230); + (void)_device.rtw_read32(0x022C); + _device.rtw_write16(0x0230, 0x0200); + _device.rtw_write32(0x022C, 0x80000000); + (void)_device.rtw_read8(0x1082); + _device.rtw_write8(0x1082, 0x80); + (void)_device.rtw_read8(0x0009); + _device.rtw_write8(0x0009, 0xBC); + (void)_device.rtw_read8(0x1082); + _device.rtw_write8(0x1082, 0x81); + (void)_device.rtw_read8(0x0009); + _device.rtw_write8(0x0009, 0xFC); + + _Fwdl8814_KernelPath(fw, dmem_size, iram_size); +} + +/* Legacy rtw88-mimic fwdl bracket — open-loop staging ops + chunk loop + + * blanket CPU kick, bit-identical on the wire to the pre-#95 single-path + * sequence. Known-bad outcome on real hardware: FW bytes land (DDMA checksum + * passes) but CPU_DL_READY never asserts (issue #95). Retained for A/B + * iteration via DEVOURER_8814_FWDL=rtw88. */ +void FirmwareManager::_Fwdl8814_Rtw88Path(const uint8_t *fw, + uint32_t dmem_size, + uint32_t iram_size) { (void)_device.rtw_read32(0x1080); _device.rtw_write32(0x1080, 0xF7816D20); (void)_device.rtw_read8(0x0003); @@ -567,6 +646,189 @@ void FirmwareManager::FirmwareDownload_8814A() { InitializeFirmwareVars8812(); } +/* One-line register-state snapshot of every fwdl-bracket-relevant register, + * tagged per step, so each hardware iteration yields a decisive REG_MCUFWDL + * trajectory. All listed registers are status/config reads the kernel also + * performs during fwdl (no read side-effects; 0x0205 BIT7 is W1C-on-write + * only). */ +void FirmwareManager::_DumpFwdlState8814A(const char *tag) { + _logger->info( + "8814A fwdl[{}]: MCUFWDL=0x{:08X} SYS_FUNC_EN+1=0x{:02X} " + "CPU_DMEM_CON=0x{:08X} DDMA_CH0CTRL=0x{:08X} FIFOPAGE_CTRL2=0x{:08X} " + "BCN_CTRL=0x{:02X} CR+1=0x{:02X}", + tag, _device.rtw_read32(REG_MCUFWDL), + _device.rtw_read8(REG_SYS_FUNC_EN + 1), + _device.rtw_read32(0x1080), + _device.rtw_read32(REG_DDMA_CH0CTRL_8814A), + _device.rtw_read32(REG_FIFOPAGE_CTRL_2_8814A), + _device.rtw_read8(REG_BCN_CTRL), _device.rtw_read8(REG_CR_8814A + 1)); +} + +/* Verbatim port of the vendor kernel's fwdl bracket: + * FirmwareDownload8814A (hal/rtl8814a/rtl8814a_hal_init.c:669-797) with + * HalROMDownloadFWRSVDPage8814A (:469-638) inlined. This is the sequence + * that demonstrably boots the 3081 on this chip (the kernel driver TXes on + * the same adapter). Helpers (_FWDownloadEnable_8814A, _3081Disable/Enable, + * _DDMAReset8814A, _WaitDownLoadRSVDPageOK_3081) are the pre-existing + * kernel-faithful ports. */ +void FirmwareManager::_Fwdl8814_KernelPath(const uint8_t *fw, + uint32_t dmem_size, + uint32_t iram_size) { + _DumpFwdlState8814A("pre-bracket"); + + _FWDownloadEnable_8814A(true); + _3081Disable8814A(); + _DDMAReset8814A(); /* "DDMA reset, suggest by MAC yodar" */ + _DumpFwdlState8814A("fwdl-en+3081-off+ddma-rst"); + + /* === HalROMDownloadFWRSVDPage8814A === */ + const uint8_t bcn_ctrl = _device.rtw_read8(REG_BCN_CTRL); + + /* Set REG_CR bit 8: DMA beacon by SW — ONCE for the whole download (the + * rtw88 path toggles this per-chunk; the kernel does not). */ + uint8_t u1bTmp = _device.rtw_read8(REG_CR_8814A + 1); + _device.rtw_write8(REG_CR_8814A + 1, (uint8_t)(u1bTmp | BIT(0))); + + /* Disable HW beacon protection window during RSVD-page access: + * 0x550[4]=1, 0x550[3]=0. */ + _device.rtw_write8(REG_BCN_CTRL, + (uint8_t)((bcn_ctrl & ~EN_BCN_FUNCTION) | DIS_TSF_UDT)); + + /* 0x422[6]=0: tell HW the queued packet is not a real beacon frame. */ + const uint8_t tmpReg422 = + _device.rtw_read8(REG_FWHW_TXQ_CTRL_8814A + 2); + _device.rtw_write8(REG_FWHW_TXQ_CTRL_8814A + 2, + (uint8_t)(tmpReg422 & ~BIT(6))); + if (tmpReg422 & BIT(6)) { + _logger->info("_Fwdl8814_KernelPath: an adapter was sending beacons"); + } + + /* Head page of the beacon queue. The kernel uses its TX_PAGE_BOUNDARY here + * because _InitQueueReservedPage/LLT ran before fwdl; devourer runs both + * AFTER fwdl, so the boundary is 0 — which degenerates the kernel's source + * formula to the empirically confirmed 0x18780028 (usbmon: rtw88 programs + * REG_DDMA_CH0SA = OCPBASE_TXBUF_3081 + 40). */ + const uint16_t txpktbuf_bndy = 0; + _device.rtw_write16(REG_FIFOPAGE_CTRL_2_8814A, txpktbuf_bndy); + + /* Clear beacon-valid check bit (0x205[7], W1C) — RMW form, kernel-style. */ + const uint8_t bcnValidReg = + _device.rtw_read8(REG_FIFOPAGE_CTRL_2_8814A + 1); + _device.rtw_write8(REG_FIFOPAGE_CTRL_2_8814A + 1, + (uint8_t)(bcnValidReg | BIT(7))); + + const uint32_t MEMOffsetInTxBuf = + OCPBASE_TXBUF_3081 + (uint32_t)txpktbuf_bndy * TX_PAGE_SIZE + + TXDESC_OFFSET_8814A; + + uint32_t max_chunk = MAX_RSVD_PAGE_BUF_SZ_8814A; + if (const char *env_chunk = std::getenv("DEVOURER_8814_FWDL_CHUNK")) { + const unsigned long v = std::strtoul(env_chunk, nullptr, 0); + if (v >= 64 && v <= MAX_RSVD_PAGE_CHUNK_SZ) { + max_chunk = (uint32_t)v; + _logger->info("_Fwdl8814_KernelPath: chunk override {} bytes", max_chunk); + } + } + + auto stream_section = [&](const uint8_t *section_start, + uint32_t section_size, + uint32_t ocp_dest) -> bool { + uint32_t remaining = section_size; + uint32_t pkt_offset = 0; + while (remaining > 0) { + uint32_t chunk; + bool ls; + if (remaining > max_chunk) { + chunk = max_chunk; + ls = false; + /* Kernel quirk: if the would-be final block lands on a multiple of + * 64 (incl. the 40-byte descriptor), shave 4 bytes off this chunk. */ + const uint32_t last_block = remaining - max_chunk; + if (last_block < max_chunk && ((last_block + 40) & 0x3F) == 0) { + chunk -= 4; + } + } else { + chunk = remaining; + ls = true; + } + const bool fs = (pkt_offset == 0); + _SetDownLoadFwRsvdPagePkt_8814A(section_start + pkt_offset, chunk); + if (!_WaitDownLoadRSVDPageOK_3081()) { + _logger->error( + "_Fwdl8814_KernelPath: RSVD-page ack failed @ ocp=0x{:X} " + "pkt_offset={}", + ocp_dest, pkt_offset); + return false; + } + if (!_IDDMADownLoadFW_3081(MEMOffsetInTxBuf, ocp_dest + pkt_offset, + chunk, fs, ls, /*kernel_flags=*/true)) { + _logger->error("_Fwdl8814_KernelPath: IDDMA failed @ ocp=0x{:X} " + "pkt_offset={}", + ocp_dest, pkt_offset); + return false; + } + /* NO per-chunk 0x0204 / 0x0550 / 0x0101 gating — the kernel sets the + * beacon-queue state once before all chunks and restores it once after + * both sections. */ + remaining -= chunk; + pkt_offset += chunk; + } + return true; + }; + + bool ok = stream_section(fw + FW_HEADER_SIZE_8814A, dmem_size, + OCPBASE_DMEM_3081); + if (ok) { + ok = stream_section(fw + FW_HEADER_SIZE_8814A + dmem_size, iram_size, + OCPBASE_IMEM_3081); + } + _DumpFwdlState8814A("sections-done"); + + if (ok) { + /* Restore beacon-queue state (kernel skips this on a failed download — + * mirror that, the boot poll below will fail loudly either way). */ + _device.rtw_write8(REG_BCN_CTRL, bcn_ctrl); + if (tmpReg422 & BIT(6)) { + _device.rtw_write8(REG_FWHW_TXQ_CTRL_8814A + 2, tmpReg422); + } + u1bTmp = _device.rtw_read8(REG_CR_8814A + 1); + _device.rtw_write8(REG_CR_8814A + 1, (uint8_t)(u1bTmp & ~BIT(0))); + + /* FW_DW_RDY (0x0081 BIT6 == REG_MCUFWDL bit14) — only if the chip + * reports both section checksums OK. */ + const uint8_t fwctrl = _device.rtw_read8(REG_MCUFWDL); + if ((fwctrl & DMEM_CHKSUM_OK_8814A) && (fwctrl & IMEM_CHKSUM_OK_8814A)) { + const uint8_t b1 = _device.rtw_read8(REG_MCUFWDL + 1); + _device.rtw_write8(REG_MCUFWDL + 1, (uint8_t)(b1 | BIT(6))); + } else { + _logger->error( + "_Fwdl8814_KernelPath: section checksums not OK in REG_MCUFWDL " + "byte0=0x{:02X} — skipping FW_DW_RDY", + fwctrl); + } + } + _DumpFwdlState8814A("rsvd-done"); + /* === end HalROMDownloadFWRSVDPage8814A === */ + + _3081Enable8814A(); /* release MCU -> fw boots */ + _FWDownloadEnable_8814A(false); + _DumpFwdlState8814A("cpu-kick"); + + 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; + } + _logger->info("8814A firmware boot confirmed: CPU_DL_READY asserted " + "(REG_MCUFWDL=0x{:08X})", + _device.rtw_read32(REG_MCUFWDL)); + + InitializeFirmwareVars8812(); +} + /* Polls REG_FIFOPAGE_CTRL_2_8814A+1 BIT7 for "RSVD page download complete" * after a TX-FIFO write. Upstream's `dump_mgntframe` is synchronous; * devourer's send_packet is async via libusb, so we need a longer window to @@ -628,7 +890,8 @@ void FirmwareManager::_SetDownLoadFwRsvdPagePkt_8814A( * from TX-FIFO source `source` to internal `dest` (DMEM or IMEM). Mirrors * upstream IDDMADownLoadFW_3081. */ bool FirmwareManager::_IDDMADownLoadFW_3081(uint32_t source, uint32_t dest, - uint32_t length, bool fs, bool ls) { + uint32_t length, bool fs, bool ls, + bool kernel_flags) { /* Wait for channel idle before programming a new transfer. */ for (int cnt = 20; cnt > 0; --cnt) { if (!(_device.rtw_read32(REG_DDMA_CH0CTRL_8814A) & DDMA_CH_OWN_8814A)) @@ -661,17 +924,53 @@ bool FirmwareManager::_IDDMADownLoadFW_3081(uint32_t source, uint32_t dest, } if (ls) { - /* Only check the IDDMA checksum status — do NOT write IMEM_DL_RDY / - * DMEM_DL_RDY / *_CHKSUM_OK back into REG_MCUFWDL. The aircrack-ng - * rtl8814au port we originally followed does these writes, but rtw88's - * iddma_download_firmware does not — and the chip is supposed to set - * BIT4 (IMEM_CHKSUM_OK) and BIT6 (DMEM_CHKSUM_OK) itself after a - * successful IDDMA. download_firmware_end_flow then reads MCUFWDL, - * verifies BIT_CHECK_SUM_OK = BIT4|BIT6, and on success writes - * FW_DW_RDY (BIT14) and clears MCUFWDL_EN (BIT0). Force-writing the - * DL_RDY bits ourselves was leaving the chip in a state where the 3081 - * CPU enable wouldn't trigger fw execution (chip stayed at 0x00606078 - * instead of transitioning to 0x0060e078 = BIT15 / FW_INIT_RDY set). */ + if (kernel_flags) { + /* Kernel-faithful last-section handling (IDDMADownLoadFW_3081, + * hal/rtl8814a/rtl8814a_hal_init.c:332-369): on checksum OK, RMW the + * section's DL_RDY + CHKSUM_OK flags into REG_MCUFWDL byte0; on fail, + * clear them. dest < OCPBASE_DMEM_3081 means IMEM. This handshake is + * part of what arms the 3081 boot (issue #95). */ + const uint8_t tmp = _device.rtw_read8(REG_MCUFWDL); + if (!(_device.rtw_read32(REG_DDMA_CH0CTRL_8814A) & + DDMA_CHKSUM_FAIL_8814A)) { + if (dest < OCPBASE_DMEM_3081) { + _device.rtw_write8(REG_MCUFWDL, + (uint8_t)(tmp | IMEM_DL_RDY_8814A | + IMEM_CHKSUM_OK_8814A)); + } else { + _device.rtw_write8(REG_MCUFWDL, + (uint8_t)(tmp | DMEM_DL_RDY_8814A | + DMEM_CHKSUM_OK_8814A)); + } + _logger->info("_IDDMADownLoadFW_3081: {} checksum OK", + dest < OCPBASE_DMEM_3081 ? "imem" : "dmem"); + } else { + const uint32_t v = _device.rtw_read32(REG_DDMA_CH0CTRL_8814A); + _device.rtw_write32(REG_DDMA_CH0CTRL_8814A, + v | DDMA_RST_CHKSUM_STS_8814A); + if (dest < OCPBASE_DMEM_3081) { + _device.rtw_write8(REG_MCUFWDL, + (uint8_t)(tmp & ~(IMEM_DL_RDY_8814A | + IMEM_CHKSUM_OK_8814A))); + } else { + _device.rtw_write8(REG_MCUFWDL, + (uint8_t)(tmp & ~(DMEM_DL_RDY_8814A | + DMEM_CHKSUM_OK_8814A))); + } + _logger->error("_IDDMADownLoadFW_3081: checksum fail (dest=0x{:X})", + dest); + return false; + } + return true; + } + /* rtw88 path: only check the IDDMA checksum status — do NOT write + * IMEM_DL_RDY / DMEM_DL_RDY / *_CHKSUM_OK back into REG_MCUFWDL. rtw88's + * iddma_download_firmware does no such writes — its + * download_firmware_end_flow reads MCUFWDL, verifies BIT4|BIT6, then + * writes FW_DW_RDY (BIT14) and clears MCUFWDL_EN (BIT0). (Historical + * note kept from the mimic experiments: force-writing the DL_RDY bits + * inside the otherwise-rtw88-shaped sequence left the chip stuck at + * 0x00606078.) */ if (_device.rtw_read32(REG_DDMA_CH0CTRL_8814A) & DDMA_CHKSUM_FAIL_8814A) { const uint32_t v = _device.rtw_read32(REG_DDMA_CH0CTRL_8814A); _device.rtw_write32(REG_DDMA_CH0CTRL_8814A, @@ -919,6 +1218,7 @@ bool FirmwareManager::_FWFreeToGo8812(uint32_t min_cnt, uint32_t timeout_ms, auto start = std::chrono::steady_clock::now(); /* polling for FW ready */ + int64_t next_progress_ms = 1000; do { cnt++; value32 = _device.rtw_read32(REG_MCUFWDL); @@ -929,6 +1229,14 @@ bool FirmwareManager::_FWFreeToGo8812(uint32_t min_cnt, uint32_t timeout_ms, if ((value32 & (1u << 15)) != 0) { break; } + /* Per-second progress line so a hardware iteration shows whether the + * register is moving at all while we wait on the 3081. */ + if (since(start).count() >= next_progress_ms) { + _logger->info("_FWFreeToGo: waiting on CPU_DL_READY, " + "REG_MCUFWDL=0x{:08x} ({} ms)", + value32, since(start).count()); + next_progress_ms += 1000; + } } else if ((value32 & ready_bit) != 0) { break; } diff --git a/src/FirmwareManager.h b/src/FirmwareManager.h index 131865d..82c4c9b 100644 --- a/src/FirmwareManager.h +++ b/src/FirmwareManager.h @@ -23,10 +23,19 @@ class FirmwareManager { void _3081Enable8814A(); void _DDMAReset8814A(); void FirmwareDownload_8814A(); + /* Kernel-faithful fwdl bracket (verbatim port of aircrack-ng/rtl8814au + * FirmwareDownload8814A + HalROMDownloadFWRSVDPage8814A). Default path. */ + void _Fwdl8814_KernelPath(const uint8_t *fw, uint32_t dmem_size, + uint32_t iram_size); + /* Legacy rtw88-usbmon-mimic path, selectable via DEVOURER_8814_FWDL=rtw88. + * Bit-identical on the wire to the pre-#95 sequence. */ + void _Fwdl8814_Rtw88Path(const uint8_t *fw, uint32_t dmem_size, + uint32_t iram_size); + void _DumpFwdlState8814A(const char *tag); void _SetDownLoadFwRsvdPagePkt_8814A(const uint8_t *fw_chunk, uint32_t len); bool _WaitDownLoadRSVDPageOK_3081(); bool _IDDMADownLoadFW_3081(uint32_t source, uint32_t dest, uint32_t length, - bool fs, bool ls); + bool fs, bool ls, bool kernel_flags = false); bool WriteFW8812(uint8_t *buffer, uint32_t size); int _PageWrite_8812(uint32_t page, uint8_t *buffer, uint32_t size); bool BlockWrite(uint8_t *buffer, int buffSize); From ec4a25b71d8e084b94d915334eb5b750137e20d4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:37:53 +0300 Subject: [PATCH 2/4] logger: stream byte-sized integers as numbers, not raw chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The minimal {}-formatter streamed uint8_t via `oss << value`, inserting a raw char — register values like 0xF0 became invalid UTF-8 bytes in the log (e.g. the 8814 RFE pin-select line) and crashed tests/regress.py's strict-UTF-8 log parsing, zeroing the devourer cells. Widen unsigned/signed char at the stream site (plain `char` untouched), make regress.py's parsers errors="replace" tolerant anyway, and widen the fwdl state-dump reads explicitly. Co-Authored-By: Claude Opus 4.8 --- src/FirmwareManager.cpp | 8 ++++++-- src/logger.h | 20 ++++++++++++++++++-- tests/regress.py | 8 ++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/FirmwareManager.cpp b/src/FirmwareManager.cpp index 3cbee23..0d6bf9f 100644 --- a/src/FirmwareManager.cpp +++ b/src/FirmwareManager.cpp @@ -657,11 +657,15 @@ void FirmwareManager::_DumpFwdlState8814A(const char *tag) { "CPU_DMEM_CON=0x{:08X} DDMA_CH0CTRL=0x{:08X} FIFOPAGE_CTRL2=0x{:08X} " "BCN_CTRL=0x{:02X} CR+1=0x{:02X}", tag, _device.rtw_read32(REG_MCUFWDL), - _device.rtw_read8(REG_SYS_FUNC_EN + 1), + /* widen u8 reads: the minimal logger streams uint8_t as a raw char, + * which puts non-UTF-8 bytes in the log and breaks text consumers + * (e.g. tests/regress.py). */ + (unsigned)_device.rtw_read8(REG_SYS_FUNC_EN + 1), _device.rtw_read32(0x1080), _device.rtw_read32(REG_DDMA_CH0CTRL_8814A), _device.rtw_read32(REG_FIFOPAGE_CTRL_2_8814A), - _device.rtw_read8(REG_BCN_CTRL), _device.rtw_read8(REG_CR_8814A + 1)); + (unsigned)_device.rtw_read8(REG_BCN_CTRL), + (unsigned)_device.rtw_read8(REG_CR_8814A + 1)); } /* Verbatim port of the vendor kernel's fwdl bracket: diff --git a/src/logger.h b/src/logger.h index fc0cb6b..154af59 100644 --- a/src/logger.h +++ b/src/logger.h @@ -10,6 +10,7 @@ #include #include #include +#include #define ushort uint16_t #define DEVOURER_LOG_TAG "devourer" @@ -47,6 +48,21 @@ printf("\n") #endif +/* Widen byte-sized integers before streaming: `oss << uint8_t` inserts a raw + * char, which puts non-UTF-8 bytes into the log (register values like 0xF0) + * and breaks text consumers such as tests/regress.py. Plain `char` is left + * alone — it is a distinct type and means "character" at our call sites. */ +template +decltype(auto) widen_for_stream(const T& value) +{ + if constexpr (std::is_same_v) + return static_cast(value); + else if constexpr (std::is_same_v) + return static_cast(value); + else + return (value); +} + template void format_helper(std::ostringstream& oss, std::string_view& str, const T& value) @@ -91,11 +107,11 @@ void format_helper(std::ostringstream& oss, if (hex_upper) oss << std::uppercase; if (zero_pad) oss << std::setfill('0'); if (width > 0) oss << std::setw(width); - oss << value; + oss << widen_for_stream(value); oss.flags(saved); oss.fill(saved_fill); } else { - oss << value; + oss << widen_for_stream(value); } str = str.substr(closeBracket + 1); diff --git a/tests/regress.py b/tests/regress.py index 6c1bd05..ad24493 100755 --- a/tests/regress.py +++ b/tests/regress.py @@ -834,7 +834,7 @@ def _terminate(proc: subprocess.Popen, grace: float = 2.0) -> None: def _count_devourer_rx_hits(log_path: Path) -> int: last = 0 try: - for line in log_path.read_text().splitlines(): + for line in log_path.read_text(errors="replace").splitlines(): if "" in line: for tok in line.split(): if tok.startswith("hits="): @@ -854,7 +854,7 @@ def _count_devourer_tx_attempts(log_path: Path) -> tuple[int, int]: last = 0 failures = 0 try: - for line in log_path.read_text().splitlines(): + for line in log_path.read_text(errors="replace").splitlines(): if line.startswith("TX #"): tok = line.split("#", 1)[1].split()[0] try: @@ -870,7 +870,7 @@ def _count_devourer_tx_attempts(log_path: Path) -> tuple[int, int]: def _count_tcpdump_hits(log_path: Path) -> int: try: - return sum(1 for _ in log_path.read_text().splitlines()) + return sum(1 for _ in log_path.read_text(errors="replace").splitlines()) except FileNotFoundError: return 0 @@ -878,7 +878,7 @@ def _count_tcpdump_hits(log_path: Path) -> int: def _count_kernel_tx_sent(log_path: Path) -> int: """inject_beacon.py emits `inject_beacon: sent N frames on IFACE` at exit.""" try: - for line in log_path.read_text().splitlines(): + for line in log_path.read_text(errors="replace").splitlines(): if line.startswith("inject_beacon: sent "): return int(line.split()[2]) except (FileNotFoundError, ValueError): From 01013dbf27878ef747a48bc9a860219869328317 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:45:29 +0300 Subject: [PATCH 3/4] docs: 8814AU ships TX + RX on every band README hardware table: 8814 row flips to TX + RX across 2.4 GHz / UNII-1 / UNII-2-3 (regress matrix green at ch6/ch36/ch100); intro no longer lists it as the band-gapped part. CLAUDE.md: chip-status paragraph reflects the 3081-MCU firmware-boot fix (issue #95), the regression section drops the stale broken-cells list and gains the kernel-TX matrix-interpretation caveat (88XXau host-push beacon inject doesn't emit, judge 8814 TX by devourer-TX cells), and the issue #36 passthrough-cycle gotcha is rewritten as resolved/historical. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 39 ++++++++++++++++++++++----------------- README.md | 13 ++++++------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a86946d..7b677ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,10 +12,11 @@ OpenIPC project for long-range video links. Targets the Realtek "Jaguar" 1st-gen 802.11ac family: **RTL8812AU** (2T2R, reference), **RTL8811AU** (1T1R cut of 8812 silicon — rides the 8812 code path with `RFType=RF_TYPE_1T1R` selected via `REG_SYS_CFG` bit 27), -**RTL8814AU** (4T4R RF / 3-SS baseband; RX solid, TX validated on -fresh-chip single-cell runs but unstable after USB passthrough cycles — -issue #36), and **RTL8821AU** (1T1R AC + BT combo; proper 8821-specific -init flow landed in PR #42, Android hotplug confirmed end-to-end). +**RTL8814AU** (4T4R RF / 3-SS baseband; TX + RX on all bands since the +3081-MCU firmware-boot fix for issue #95 — host-pushed TX needs the +running MCU, and the earlier rtw88-mimic fwdl never booted it), and +**RTL8821AU** (1T1R AC + BT combo; proper 8821-specific init flow landed +in PR #42, Android hotplug confirmed end-to-end). NOT 8821AU's family confusion: it IS Jaguar wave 1 (CHIP_8821 = 7 in Realtek's HalVerDef, shares the enum with CHIP_8812), not Jaguar2 as @@ -54,12 +55,15 @@ sudo python3 tests/regress.py \ --vm-name devourer-testrig --vm-ssh @ ``` -Default channel is `6` (2.4GHz). Devourer's 5GHz path has known broken -cells for 8814 RX, 8821 TX, and 8821 RX — at 2.4GHz every chip combo -except 8814 TX works. Pass `--channel 36` / `--channel 100` to exercise -5GHz; do not assume a single-band matrix is comprehensive. (The repo -history's matrix tables in PR bodies #34/#42/#49 were all captured at -`--channel 100` and document the 5GHz state.) +Default channel is `6` (2.4GHz). Pass `--channel 36` / `--channel 100` +to exercise 5GHz; do not assume a single-band matrix is comprehensive. +(The repo history's matrix tables in PR bodies #34/#42/#49 were all +captured at `--channel 100` and predate the 8814/8821 band fixes.) +Matrix-interpretation caveat: with the 8814 as TX, the `kernel`-TX cells +read 0 at every channel — `aircrack-ng/88XXau` host-push *beacon* +injection (what `inject_beacon.py` does) doesn't emit on that driver +even though its probe-request injection (`aireplay -9`) does. Judge 8814 +TX by the devourer-TX cells. Three specialised modes layered on top of the default 4-cell matrix: @@ -163,13 +167,14 @@ phydm parser. Windows installer, then re-enumerate as the NIC after a mode switch. If `libusb_open_device_with_vid_pid` returns NULL, check `lsusb` — may need `usb_modeswitch` first. -- **RTL8814AU TX is flaky after USB passthrough cycles** (issue #36): - fresh-chip single-cell runs send ~4000 frames with 0 submit failures, - but after one or more virsh attach/detach cycles `libusb_bulk_transfer` - starts returning `LIBUSB_ERROR_IO` on 90%+ of submits. Every full- - matrix run since #34 has reproduced this. `RX = devourer` 8814 cells - are also still 0 (RX path itself is separately broken — not in scope - for #36). +- **RTL8814AU passthrough-cycle TX flakiness is resolved** (issue #36, + historical): virsh attach/detach cycles used to leave 90%+ of + `libusb_bulk_transfer` submits failing with `LIBUSB_ERROR_IO`. Root + cause was the old rtw88-mimic post-fwdl kick's `REG_CR=0` write killing + the DMA-enable bits; the kernel-faithful fwdl path (issue #95 fix) + doesn't have it, and the wedge no longer reproduces after repeated + cycles. If something like it resurfaces, suspect the fwdl path first + (`DEVOURER_8814_FWDL=rtw88` reinstates the old behaviour for A/B). - **rmmod/sysfs-unbind actively de-inits the chip** (RF off, MAC DMA off). After detaching a kernel driver, expect to re-init from cold, not warm. `DEVOURER_SKIP_RESET=1` only helps when firmware state is still intact. diff --git a/README.md b/README.md index fd424bf..8f40082 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,11 @@ The Realtek 11ac driver that simply devours its competitors. Devourer is a userspace re-implementation of Realtek's RTL88xxAU Wi-Fi -driver (Jaguar family: RTL8812AU and RTL8821AU shipping on every band, -RTL8811AU supported via the 8812 code path, RTL8814AU with band- -specific gaps — see table below), speaking to the chip directly -through libusb. No kernel module, no `rtl8812au` DKMS tree — just a -C++20 static library (`WiFiDriver`) plus two demo executables for RX -and TX. It is the OpenIPC project's driver of choice for long-range +driver (Jaguar family: RTL8812AU, RTL8814AU, and RTL8821AU shipping on +every band, RTL8811AU supported via the 8812 code path), speaking to +the chip directly through libusb. No kernel module, no `rtl8812au` +DKMS tree — just a C++20 static library (`WiFiDriver`) plus two demo +executables for RX and TX. It is the OpenIPC project's driver of choice for long-range video links built on top of cheap Realtek 11ac USB radios. ## Hardware landscape @@ -25,7 +24,7 @@ layered on top. | -------------- | --------------- | ------------- | ---------------------- | ---------------------- | ------------------------------------------- | | **RTL8812AU** | 2T2R | TX + RX | TX + RX | TX + RX | VID/PID `0bda:8812`; reference part — works on every channel/band combo | | **RTL8811AU** | 1T1R | TX + RX | TX + RX | TX + RX | 1T1R cut of 8812 silicon; rides 8812 code path with `RFType=RF_TYPE_1T1R` selected from `REG_SYS_CFG` bit 27. Status mirrored from 8812 — not separately exercised | -| **RTL8814AU** | 4T4R, 3-SS max | RX only | RX only | TX + RX | VID/PID `0bda:8813`; 2-SS effective on USB-2. 5 GHz UNII-2/3 TX produces on-air frames after the 8814A-specific band-switch + channel-set chain. 2.4 GHz TX still doesn't reach receivers | +| **RTL8814AU** | 4T4R, 3-SS max | TX + RX | TX + RX | TX + RX | VID/PID `0bda:8813`; 2-SS effective on USB-2 | | **RTL8821AU** | 1T1R AC + BT | TX + RX | TX + RX | TX + RX | OEM-rebadged as TP-Link Archer T2U Plus (`2357:0120`) etc. UNII-2/3 TX has cross-receiver asymmetry against 8812AU peers | Successor families (`Jaguar2` / `Jaguar+` — 8812BU, 8822BU/BE, etc., and From ef06a4d4b8f68f251059a8ad182dff5d0954068c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:50:17 +0300 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20strip=20changelog=20narrative=20?= =?UTF-8?q?=E2=80=94=20current=20state=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md and README describe what the driver does now; git history is the changelog. Drop the resolved-gotcha entry, the "since fix #NNN" phrasing, PR-body references, and the redundant 8812 table note. Keep only operative facts (8814 TX needs the booted 3081 MCU; kernel-TX matrix cells aren't authoritative for 8814). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 25 +++++++------------------ README.md | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7b677ca..a1fe42e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,11 +12,10 @@ OpenIPC project for long-range video links. Targets the Realtek "Jaguar" 1st-gen 802.11ac family: **RTL8812AU** (2T2R, reference), **RTL8811AU** (1T1R cut of 8812 silicon — rides the 8812 code path with `RFType=RF_TYPE_1T1R` selected via `REG_SYS_CFG` bit 27), -**RTL8814AU** (4T4R RF / 3-SS baseband; TX + RX on all bands since the -3081-MCU firmware-boot fix for issue #95 — host-pushed TX needs the -running MCU, and the earlier rtw88-mimic fwdl never booted it), and -**RTL8821AU** (1T1R AC + BT combo; proper 8821-specific init flow landed -in PR #42, Android hotplug confirmed end-to-end). +**RTL8814AU** (4T4R RF / 3-SS baseband; host-pushed TX requires the +on-chip 3081 MCU, which devourer boots during firmware download — +a failed FW-boot poll means dead TX while RX still works), and +**RTL8821AU** (1T1R AC + BT combo). NOT 8821AU's family confusion: it IS Jaguar wave 1 (CHIP_8821 = 7 in Realtek's HalVerDef, shares the enum with CHIP_8812), not Jaguar2 as @@ -57,13 +56,11 @@ sudo python3 tests/regress.py \ Default channel is `6` (2.4GHz). Pass `--channel 36` / `--channel 100` to exercise 5GHz; do not assume a single-band matrix is comprehensive. -(The repo history's matrix tables in PR bodies #34/#42/#49 were all -captured at `--channel 100` and predate the 8814/8821 band fixes.) Matrix-interpretation caveat: with the 8814 as TX, the `kernel`-TX cells read 0 at every channel — `aircrack-ng/88XXau` host-push *beacon* -injection (what `inject_beacon.py` does) doesn't emit on that driver -even though its probe-request injection (`aireplay -9`) does. Judge 8814 -TX by the devourer-TX cells. +injection (what `inject_beacon.py` does) doesn't emit on that driver, +even though its probe-request injection does. Judge 8814 TX by the +devourer-TX cells. Three specialised modes layered on top of the default 4-cell matrix: @@ -167,14 +164,6 @@ phydm parser. Windows installer, then re-enumerate as the NIC after a mode switch. If `libusb_open_device_with_vid_pid` returns NULL, check `lsusb` — may need `usb_modeswitch` first. -- **RTL8814AU passthrough-cycle TX flakiness is resolved** (issue #36, - historical): virsh attach/detach cycles used to leave 90%+ of - `libusb_bulk_transfer` submits failing with `LIBUSB_ERROR_IO`. Root - cause was the old rtw88-mimic post-fwdl kick's `REG_CR=0` write killing - the DMA-enable bits; the kernel-faithful fwdl path (issue #95 fix) - doesn't have it, and the wedge no longer reproduces after repeated - cycles. If something like it resurfaces, suspect the fwdl path first - (`DEVOURER_8814_FWDL=rtw88` reinstates the old behaviour for A/B). - **rmmod/sysfs-unbind actively de-inits the chip** (RF off, MAC DMA off). After detaching a kernel driver, expect to re-init from cold, not warm. `DEVOURER_SKIP_RESET=1` only helps when firmware state is still intact. diff --git a/README.md b/README.md index 8f40082..6ae227b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ layered on top. | Part | RF / streams | 2.4 GHz | 5 GHz UNII-1 (ch36-48) | 5 GHz UNII-2/3 (ch52+) | Notes | | -------------- | --------------- | ------------- | ---------------------- | ---------------------- | ------------------------------------------- | -| **RTL8812AU** | 2T2R | TX + RX | TX + RX | TX + RX | VID/PID `0bda:8812`; reference part — works on every channel/band combo | +| **RTL8812AU** | 2T2R | TX + RX | TX + RX | TX + RX | VID/PID `0bda:8812`; reference part | | **RTL8811AU** | 1T1R | TX + RX | TX + RX | TX + RX | 1T1R cut of 8812 silicon; rides 8812 code path with `RFType=RF_TYPE_1T1R` selected from `REG_SYS_CFG` bit 27. Status mirrored from 8812 — not separately exercised | | **RTL8814AU** | 4T4R, 3-SS max | TX + RX | TX + RX | TX + RX | VID/PID `0bda:8813`; 2-SS effective on USB-2 | | **RTL8821AU** | 1T1R AC + BT | TX + RX | TX + RX | TX + RX | OEM-rebadged as TP-Link Archer T2U Plus (`2357:0120`) etc. UNII-2/3 TX has cross-receiver asymmetry against 8812AU peers |