From e44288e9ce57fd82cd9b4fd978072c4a632c3240 Mon Sep 17 00:00:00 2001 From: Joseph Albert Nefario Date: Fri, 22 May 2026 08:43:05 +0300 Subject: [PATCH 1/4] demo: cross-adapter TX-validation tooling for the Jaguar family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WiFiDriverTxDemo previously only supported the Termux/Android pattern of taking a USB fd as argv[1] and wrapping it via libusb_wrap_sys_ device. On a regular Linux libusb context that path doesn't apply, and the demo also forked an RX child to run alongside TX on the same USB handle — which races on a libusb context that isn't fork-safe and crashes both processes with "rtw_read: iostream error" before TX even begins. Changes: - txdemo: when argv[1] is absent or non-numeric, fall through to a VID/PID open against the same Realtek PID list as the RX demo (0x8812 / 0x0811 / 0xa811 / 0xb811 / 0x8813). The Termux fd path is preserved when argv[1] is a positive int. Adds DEVOURER_PID, DEVOURER_CHANNEL, DEVOURER_SKIP_RESET env vars (parity with RX demo). - txdemo: gate the fork-RX child behind DEVOURER_TX_WITH_RX=1. Default is TX-only on a single handle. Cross-adapter validation (TX on one adapter, RX on another) is the only configuration that actually works on plain Linux. - txdemo: throttle the TX loop with usleep(2000) and log every Nth TX so it doesn't peg a core or spam stdout. - demo (RX) + txdemo (RX-when-DEVOURER_TX_WITH_RX): detect the txdemo's hardcoded injected-beacon SA (57:42:75:05:d6:00) and log on match. Provides a clean signal for cross- adapter TX validation — each hit is one frame round-tripped over the air. Co-Authored-By: Claude Opus 4.7 (1M context) --- demo/main.cpp | 17 +++++ txdemo/main.cpp | 191 +++++++++++++++++++++++++++++++----------------- 2 files changed, 139 insertions(+), 69 deletions(-) diff --git a/demo/main.cpp b/demo/main.cpp index adbc3dd..298f3c0 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -28,6 +29,22 @@ static void packetProcessor(const Packet &packet) { printf("RX pkt #%d (len=%zu)\n", g_rx_count, packet.Data.size()); fflush(stdout); } + /* TX-validation hook: detect frames whose SA matches the txdemo's hardcoded + * injected beacon (57:42:75:05:d6:00). When running this RX demo against + * one adapter while WiFiDriverTxDemo runs against another on the same + * channel, each hit confirms an injected frame made it over the air. */ + if (packet.Data.size() >= 16) { + static const uint8_t kTxSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; + if (std::memcmp(packet.Data.data() + 10, kTxSa, 6) == 0) { + static int hits = 0; + ++hits; + if (hits <= 10 || hits % 100 == 0) { + printf("txdemo SA match: hits=%d total_rx=%d len=%zu\n", + hits, g_rx_count, packet.Data.size()); + fflush(stdout); + } + } + } } int main() { diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 1460fd0..37cc4d1 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -1,7 +1,11 @@ #include +#include +#include +#include #include #include #include +#include #if defined(_MSC_VER) #include @@ -15,6 +19,7 @@ #include #include #else + #include #include #endif @@ -23,29 +28,33 @@ #include "WiFiDriver.h" #include "logger.h" -#include - - -// #define USB_VENDOR_ID 0x0bda -// #define USB_PRODUCT_ID 0x8812 - -void printHexArray(const uint8_t *array, size_t length) { - for (size_t i = 0; i < length; ++i) { - // Print each byte as a two-digit hexadecimal number - std::cout << "0x" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(array[i]); - - // Print a space between bytes, but not after the last byte - if (i < length - 1) { - std::cout << " "; +#define USB_VENDOR_ID 0x0bda + +/* Known USB product IDs for the Realtek Jaguar family — same set as the RX + * demo (demo/main.cpp). */ +static constexpr uint16_t kRealtekProductIds[] = { + 0x8812, 0x0811, 0xa811, 0xb811, 0x8813, +}; + +static int g_rx_count = 0; +static void packetProcessor(const Packet &packet) { + ++g_rx_count; + /* Surface frames whose source MAC matches the txdemo's injected beacon + * (57:42:75:05:d6:00). The 802.11 header starts at packet.Data[0]; SA is + * at bytes [10..15] for a non-FromDS, non-ToDS frame. */ + if (packet.Data.size() >= 16) { + static const uint8_t kTxSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; + if (std::memcmp(packet.Data.data() + 10, kTxSa, 6) == 0) { + static int hits = 0; + ++hits; + printf("RX from txdemo SA: hits=%d total_rx=%d len=%zu\n", + hits, g_rx_count, packet.Data.size()); + fflush(stdout); } } - std::cout << std::dec << std::endl; // Reset to decimal formatting } -static void packetProcessor(const Packet &packet) {} - -void usb_event_loop(Logger_t _logger,libusb_context* ctx){ +void usb_event_loop(Logger_t _logger, libusb_context *ctx) { while (true) { int r = libusb_handle_events(ctx); if (r < 0) { @@ -56,72 +65,112 @@ void usb_event_loop(Logger_t _logger,libusb_context* ctx){ } int main(int argc, char **argv) { - libusb_context *context; - libusb_device_handle *handle; - libusb_device *device; - struct libusb_device_descriptor desc; - uint8_t usb_frame[10000]; - unsigned char buffer[256]; - struct tx_desc *ptxdesc; - int fd; + libusb_context *context = nullptr; + libusb_device_handle *handle = nullptr; + int rc; auto logger = std::make_shared(); - // fd from argv is provided by termux on Android - // https://wiki.termux.com/wiki/Termux-usb - fd = atoi(argv[1]); - logger->info("got fd {}", fd); - - // libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, - // LIBUSB_LOG_LEVEL_DEBUG); - libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY); - libusb_set_option(NULL, LIBUSB_OPTION_WEAK_AUTHORITY); - - int rc = libusb_init(&context); - - rc = libusb_wrap_sys_device(context, (intptr_t)fd, &handle); - - device = libusb_get_device(handle); - - rc = libusb_get_device_descriptor(device, &desc); + /* Two modes: + * 1. Termux/Android: argv[1] = numeric USB fd (wrapped via + * libusb_wrap_sys_device). + * 2. Linux/macOS: no argv (or non-numeric) — open by VID/PID using the + * same list as the RX demo. DEVOURER_PID=0xNNNN restricts to a single + * PID. */ + long fd = (argc >= 2) ? std::strtol(argv[1], nullptr, 0) : 0; + const bool termux_mode = (fd > 0); + + if (termux_mode) { + logger->info("Termux mode: wrapping fd {}", fd); + libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY); + libusb_set_option(NULL, LIBUSB_OPTION_WEAK_AUTHORITY); + rc = libusb_init(&context); + rc = libusb_wrap_sys_device(context, (intptr_t)fd, &handle); + } else { + rc = libusb_init(&context); + if (rc < 0) return rc; + + const char *pid_env = std::getenv("DEVOURER_PID"); + uint16_t target_pid = 0; + if (pid_env != nullptr) { + target_pid = static_cast(std::strtoul(pid_env, nullptr, 0)); + logger->info("DEVOURER_PID={:04x} (limiting to this PID)", target_pid); + } + for (uint16_t pid : kRealtekProductIds) { + if (target_pid != 0 && pid != target_pid) continue; + handle = libusb_open_device_with_vid_pid(context, USB_VENDOR_ID, pid); + if (handle != NULL) { + logger->info("Opened Realtek device {:04x}:{:04x}", USB_VENDOR_ID, pid); + break; + } + } + if (handle == NULL) { + logger->error("No supported Realtek device found under VID {:04x}", + USB_VENDOR_ID); + libusb_exit(context); + return 1; + } + } - logger->info("Vendor/Product ID:{:04x}:{:04x}", desc.idVendor, + libusb_device *device = libusb_get_device(handle); + libusb_device_descriptor desc{}; + libusb_get_device_descriptor(device, &desc); + logger->info("Vendor/Product ID: {:04x}:{:04x}", desc.idVendor, desc.idProduct); + if (libusb_kernel_driver_active(handle, 0)) { - rc = libusb_detach_kernel_driver(handle, 0); // detach driver - logger->error("libusb_detach_kernel_driver: {}", rc); + rc = libusb_detach_kernel_driver(handle, 0); + if (rc != 0) logger->error("libusb_detach_kernel_driver: {}", rc); + } + + if (!termux_mode && !std::getenv("DEVOURER_SKIP_RESET")) { + libusb_reset_device(handle); } + rc = libusb_claim_interface(handle, 0); + assert(rc == 0); WiFiDriver wifi_driver{logger}; auto rtlDevice = wifi_driver.CreateRtlDevice(handle); - pid_t fpid; - fpid = fork(); - rtlDevice->SetTxPower(40); - if (fpid == 0) { - - rtlDevice->Init(packetProcessor, SelectedChannel{ - .Channel = static_cast(161), - .ChannelOffset = 0, - .ChannelWidth = CHANNEL_WIDTH_20, - }); - - return 1; + int channel = 161; + if (const char *ch_env = std::getenv("DEVOURER_CHANNEL")) { + channel = std::atoi(ch_env); + logger->info("DEVOURER_CHANNEL set — tuning TX to channel {}", channel); } - // Loop for sending packets + rtlDevice->SetTxPower(40); - rtlDevice->InitWrite(SelectedChannel{ - .Channel = static_cast(161), - .ChannelOffset = 0, - .ChannelWidth = CHANNEL_WIDTH_20}); + /* The original txdemo forked an RX child and a TX parent on the same + * libusb handle. That pattern is Termux-specific (libusb_wrap_sys_device + * keeps the kernel fd shared across fork); on a regular Linux libusb + * context after fork(), both processes race on the same URB submission + * queue and the first vendor request after fork tends to fail with + * "rtw_read: iostream error". Skip the fork unless DEVOURER_TX_WITH_RX=1 + * is explicitly set (Termux callers can opt back in). For cross-adapter + * TX validation a second RX process on a separate adapter is what you + * want anyway. */ + if (std::getenv("DEVOURER_TX_WITH_RX")) { + pid_t fpid = fork(); + if (fpid == 0) { + rtlDevice->Init(packetProcessor, + SelectedChannel{ + .Channel = static_cast(channel), + .ChannelOffset = 0, + .ChannelWidth = CHANNEL_WIDTH_20, + }); + return 1; + } + } + rtlDevice->InitWrite(SelectedChannel{ + .Channel = static_cast(channel), + .ChannelOffset = 0, + .ChannelWidth = CHANNEL_WIDTH_20}); sleep(5); - // A new thread starts the libusb event loop - std::thread usb_thread(usb_event_loop,logger,context); + std::thread usb_thread(usb_event_loop, logger, context); uint8_t beacon_frame[] = { 0x00, 0x00, 0x0d, 0x00, 0x00, 0x80, 0x08, 0x00, 0x08, 0x00, 0x37, 0x00, 0x01, // radiotap header @@ -136,16 +185,20 @@ int main(int argc, char **argv) { 0xcd, 0xce, 0x4e, 0x35, 0xd9, 0x85, 0x9a, 0xcf, 0x4d, 0x48, 0x4c, 0x8f, 0x28, 0x6f, 0x10, 0xb0, 0xa9, 0x5d, 0xbf, 0xcb, 0x6f}; - int actual_length = 0; - + long tx_count = 0; while (true) { rc = rtlDevice->send_packet(beacon_frame, sizeof(beacon_frame)); + ++tx_count; + if (tx_count <= 10 || tx_count % 500 == 0) { + printf("TX #%ld rc=%d\n", tx_count, rc); + fflush(stdout); + } + usleep(2000); /* 2 ms — ~500 fps, gentle on USB bulk EP */ } rc = libusb_release_interface(handle, 0); assert(rc == 0); libusb_close(handle); - libusb_exit(context); return 0; } From 0ea7e41ce3d506179a6e2cbaf6a4010fa0e6af23 Mon Sep 17 00:00:00 2001 From: Joseph Albert Nefario Date: Fri, 22 May 2026 08:44:55 +0300 Subject: [PATCH 2/4] =?UTF-8?q?8814AU:=20TX=20FIFO=20bring-up=20=E2=80=94?= =?UTF-8?q?=20page=20allocation,=20auto-LLT,=20REG=5FCR=20preserve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8812-path HalModule init steps that program the TX FIFO silently no-op on the 8814AU. Three separate chip-init bugs that have to be fixed before any TX path can work on this chip: 1) **Page allocation.** _InitQueueReservedPage_8812AUsb and _InitTxBufferBoundary_8812AUsb write 8-bit fields in REG_RQPN / REG_RQPN_NPQ (0x200-0x202) and REG_BCNQ_BDNY / REG_MGQ_BDNY / REG_TRXFF_BNDY (0x424-... 8-bit). That register set is the 8812's ~256-page TX FIFO model. The 8814 has a 2048-page TX FIFO and uses completely different registers: 32-bit REG_FIFOPAGE_INFO_{1..5}_8814A (0x230-0x240) for per-queue page counts, 16-bit REG_TXPKTBUF_BCNQ_BDNY /BCNQ1_BDNY/MGQ_PGBNDY for queue boundaries, plus REG_FIFOPAGE_CTRL_2 for BCNQ head pages. Port _InitQueueReservedPage_8814AUsb + _InitPageBoundary_8814AUsb from upstream hal/rtl8814a/usb/usb_halinit.c. Dispatch on CHIP_8814A so the 8812 path stays untouched. Register addresses + page constants extracted into a local `namespace rtl8814a` because the upstream rtl8814a_spec.h / rtl8814a_hal.h pull in kernel-only deps (drv_conf.h, hal_data.h) we can't satisfy in userspace. 2) **Auto-LLT trigger.** Without HW auto-LLT firing post-fwdl, the chip's per-queue free-page linked-list never gets built — the page counts we just wrote are advertised but no actual pages are linked into the per-queue free lists. TX would have nowhere to land buffer headers even if the rest of bring-up were correct. Upstream's hal/rtl8814a/rtl8814a_hal_init.c::InitLLTTable8814A writes BIT0 of an 8-bit read at 0x208 and polls BIT0 to clear. That is the wrong bit — verified empirically: the trigger never fires and the poll exits in 0 iterations because BIT0 was never set in the chip's response. The generic BIT_AUTO_INIT_LLT in hal_com_reg.h:1424 says BIT(16) of the 32-bit register at 0x208 (aliased REG_TDECTRL). Use a 32-bit RMW with the correct BIT(16) trigger. Verified the fix lands: REG_AUTO_LLT readback shows BIT16 self-cleared within 2ms after our trigger, confirming the chip processed it. 3) **REG_CR preserve.** Some 8812-path init helpers reset REG_CR back to only MACTXEN|MACRXEN (0xC0). Our existing final-state force-write of 0x00FF restored the DMA + protocol + scheduler bits but clobbered any high bits firmware may have set. Read REG_CR observed-value first, then OR-in our minimum-required low byte. Defensive — verified that on this hardware fw doesn't set any high bits, but it's the safer shape. Each of the three is necessary but not by itself sufficient for TX. TX end-to-end validation is still WIP (tracked separately). RX path is unaffected by all three changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/HalModule.cpp | 169 ++++++++++++++++++++++++++++++++++++++++++---- src/HalModule.h | 2 + 2 files changed, 159 insertions(+), 12 deletions(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index e19ce75..c06d405 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -9,6 +9,40 @@ #include "rtl8812a_hal.h" #include "rtl8812a_spec.h" +/* 8814AU register + page constants extracted from upstream + * hal/rtl8814a_spec.h and hal/rtl8814a_hal.h. Inlined here because the + * upstream headers pull in kernel-only deps (drv_conf.h, hal_data.h). */ +namespace rtl8814a { +constexpr uint16_t REG_FIFOPAGE_INFO_1_8814A = 0x0230; +constexpr uint16_t REG_FIFOPAGE_INFO_2_8814A = 0x0234; +constexpr uint16_t REG_FIFOPAGE_INFO_3_8814A = 0x0238; +constexpr uint16_t REG_FIFOPAGE_INFO_4_8814A = 0x023C; +constexpr uint16_t REG_FIFOPAGE_INFO_5_8814A = 0x0240; +constexpr uint16_t REG_RQPN_CTRL_2_8814A = 0x022C; +constexpr uint16_t REG_FIFOPAGE_CTRL_2_8814A = 0x0204; +constexpr uint16_t REG_TXPKTBUF_BCNQ_BDNY_8814A = 0x0424; +constexpr uint16_t REG_TXPKTBUF_BCNQ1_BDNY_8814A = 0x0426; /* spec calls it +2 */ +constexpr uint16_t REG_MGQ_PGBNDY_8814A = 0x047A; +constexpr uint16_t REG_RXFF_PTR_8814A = 0x011C; + +constexpr uint32_t HPQ_PGNUM_8814A = 0x20; /* 32 pages per queue (USB) */ +constexpr uint32_t LPQ_PGNUM_8814A = 0x20; +constexpr uint32_t NPQ_PGNUM_8814A = 0x20; +constexpr uint32_t EPQ_PGNUM_8814A = 0x20; +constexpr uint32_t BCNQ_PAGE_NUM_8814 = 0x08; +constexpr uint32_t WOWLAN_PAGE_NUM_8814 = 0x00; +constexpr uint32_t TXPKT_PGNUM_8814A = + 2048 - BCNQ_PAGE_NUM_8814 - WOWLAN_PAGE_NUM_8814; +constexpr uint32_t PUB_PGNUM_8814A = TXPKT_PGNUM_8814A - HPQ_PGNUM_8814A - + NPQ_PGNUM_8814A - LPQ_PGNUM_8814A - + EPQ_PGNUM_8814A; +constexpr uint16_t TX_PAGE_BOUNDARY_8814A = TXPKT_PGNUM_8814A; +constexpr uint16_t WMM_NORMAL_TX_PAGE_BOUNDARY_8814A = TXPKT_PGNUM_8814A + 1; + +constexpr uint32_t MAX_RX_DMA_BUFFER_SIZE_8814A = 0x5C00; +constexpr uint16_t RX_DMA_BOUNDARY_8814A = MAX_RX_DMA_BUFFER_SIZE_8814A - 1; +} // namespace rtl8814a + #include #include #include @@ -108,11 +142,60 @@ bool HalModule::rtl8812au_hal_init() { PHY_MACConfig8812(); - _InitQueueReservedPage_8812AUsb(); - _InitTxBufferBoundary_8812AUsb(); - _InitQueuePriority_8812AUsb(); - _InitPageBoundary_8812AUsb(); - _InitTransferPageSize_8812AUsb(); + if (is_8814a) { + /* 8814AU has its own TX FIFO page allocation: 2048 total pages vs 8812's + * 256, set via 32-bit FIFOPAGE_INFO_{1..5} regs + 16-bit BCNQ/MGQ page + * boundaries. The 8812 path uses 8-bit REG_RQPN / REG_BCNQ_BDNY etc. + * which silently no-op on 8814 — that leaves HPQ with 0 pages, so MGT + * frames submitted via bulk OUT have nowhere to land and the chip never + * drains the EP (USB bulk OUT times out). */ + _InitQueueReservedPage_8814AUsb(); + /* TX buffer boundary is set inside _InitQueueReservedPage_8814AUsb. Skip + * _InitTxBufferBoundary_8812AUsb. */ + _InitQueuePriority_8812AUsb(); /* dispatches on CHIP_8814A internally */ + _InitPageBoundary_8814AUsb(); + /* _InitTransferPageSize_8814AUsb is a no-op upstream. */ + + /* 8814AU auto-LLT trigger via 32-bit BIT16 of REG_AUTO_LLT_8814A (0x0208, + * aliased as REG_TDECTRL). The generic Realtek bit definition is + * BIT_AUTO_INIT_LLT = BIT(16) (see hal_com_reg.h). The upstream OOT + * code at rtl8814a_hal_init.c::InitLLTTable8814A writes BIT0 of an 8-bit + * read at 0x208 — that's a different bit entirely; empirically the + * trigger never fires (auto-LLT "completes in 0 polls" because BIT0 + * was never set). Use the correct BIT(16) trigger as a 32-bit RMW. + * Without auto-LLT, the chip's TX FIFO page-count regs we just set are + * advertised but no free-page list is linked, so the chip's TX queues + * have nowhere to store inbound bulk-OUT frames and vendor-control + * transfers to OUT EPs time out forever. */ + constexpr uint16_t REG_AUTO_LLT_8814A = 0x0208; + constexpr uint32_t AUTO_INIT_LLT_BIT = 1u << 16; /* renamed: hal_com_reg.h's BIT_AUTO_INIT_LLT macro collides */ + uint32_t llt = _device.rtw_read32(REG_AUTO_LLT_8814A); + _logger->info("8814A auto-LLT pre REG_AUTO_LLT=0x{:08x}", llt); + _device.rtw_write32(REG_AUTO_LLT_8814A, llt | AUTO_INIT_LLT_BIT); + int polls = 0; + do { + llt = _device.rtw_read32(REG_AUTO_LLT_8814A); + if (!(llt & AUTO_INIT_LLT_BIT)) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + ++polls; + } while (polls < 200); + if (llt & AUTO_INIT_LLT_BIT) { + _logger->error("8814A auto-LLT did not complete (REG_AUTO_LLT=0x{:08x} " + "after {} polls)", + llt, polls); + } else { + _logger->info( + "8814A auto-LLT completed in {} polls (REG_AUTO_LLT=0x{:08x})", polls, + llt); + } + } else { + _InitQueueReservedPage_8812AUsb(); + _InitTxBufferBoundary_8812AUsb(); + _InitQueuePriority_8812AUsb(); + _InitPageBoundary_8812AUsb(); + _InitTransferPageSize_8812AUsb(); + } // Get Rx PHY status in order to report RSSI and others. _InitDriverInfoSize_8812A(DRVINFO_SZ); @@ -199,7 +282,19 @@ bool HalModule::rtl8812au_hal_init() { // HW SEQ CTRL // set 0x0 to 0xFF by tynli. Default enable HW SEQ NUM. - _device.rtw_write8(REG_HWSEQ_CTRL, 0xFF); + // On 8814 the chip rejects 8-bit access at offset 0x423 (byte 3 of + // FWHW_TXQ_CTRL@0x420) — the rtw_write8 at this address silently no-ops + // and rtw_read8 returns 0 regardless of the actual byte. Verified by the + // diag dump: rtw_read32(0x420) shows byte3=0x03 but rtw_read8(0x423)=0x00. + // Use a 32-bit aligned RMW so the chip's USB controller handles it as a + // word-sized access. + if (is_8814a) { + uint32_t txqctl = _device.rtw_read32(REG_FWHW_TXQ_CTRL); + txqctl = (txqctl & 0x00FFFFFFu) | (0xFFu << 24); + _device.rtw_write32(REG_FWHW_TXQ_CTRL, txqctl); + } else { + _device.rtw_write8(REG_HWSEQ_CTRL, 0xFF); + } // Disable BAR, suggested by Scott // 2010.04.09 add by hpfan @@ -243,14 +338,25 @@ bool HalModule::rtl8812au_hal_init() { * (clearing DMA + protocol + scheduler) — verified via post-init pyusb * probe showing REG_CR=0xc0. And REG_RXFLTMAP2 read back as 0 instead * of the 0xFFFF we want for monitor-mode data-frame acceptance. Force - * the final state here so RX bulk IN actually moves frames. */ - uint16_t cr_final = (uint16_t)(HCI_TXDMA_EN | HCI_RXDMA_EN | TXDMA_EN | - RXDMA_EN | PROTOCOL_EN | SCHEDULE_EN | - MACTXEN | MACRXEN); + * the final state here so RX bulk IN actually moves frames. + * + * 8814 hypothesis: firmware programs REG_CR after it boots and may set + * bits beyond our 0x00FF mask (ENSEC=BIT9, CALTMR_EN=BIT10 — both in + * the upstream _InitPowerOn_8814AU OR-mask, neither in our cr_final). + * Forcing 0x00FF on 8814 could clobber fw-set high bits that gate TX. + * Read current REG_CR first, then OR in our minimum-required bits + * instead of clobbering the whole word. */ + uint16_t cr_observed = _device.rtw_read16(REG_CR); + uint16_t cr_min = (uint16_t)(HCI_TXDMA_EN | HCI_RXDMA_EN | TXDMA_EN | + RXDMA_EN | PROTOCOL_EN | SCHEDULE_EN | + MACTXEN | MACRXEN); + uint16_t cr_final = static_cast(cr_observed | cr_min); _device.rtw_write16(REG_CR, cr_final); _device.rtw_write16(REG_RXFLTMAP2, 0xFFFF); - _logger->info("post-init final: REG_CR=0x{:04x} REG_RXFLTMAP2=0xFFFF", - cr_final); + _logger->info( + "post-init final: REG_CR observed=0x{:04x} written=0x{:04x} " + "REG_RXFLTMAP2=0xFFFF", + cr_observed, cr_final); return true; } @@ -1035,6 +1141,45 @@ void HalModule::_InitPageBoundary_8812AUsb() { _device.rtw_write16((REG_TRXFF_BNDY + 2), RX_DMA_BOUNDARY_8812); } +void HalModule::_InitQueueReservedPage_8814AUsb() { + using namespace rtl8814a; + /* Port of upstream _InitQueueReservedPage_8814AUsb (hal/rtl8814a/usb/ + * usb_halinit.c). 8814 uses 32-bit FIFOPAGE_INFO regs to set per-queue + * page counts and 16-bit boundary registers. The 8812 8-bit REG_RQPN / + * REG_BCNQ_BDNY equivalents don't exist on 8814, so reusing the 8812 + * path leaves HPQ/NPQ/LPQ with zero pages and TX bulk OUT stalls. */ + _device.rtw_write32(REG_FIFOPAGE_INFO_1_8814A, HPQ_PGNUM_8814A); + _device.rtw_write32(REG_FIFOPAGE_INFO_2_8814A, LPQ_PGNUM_8814A); + _device.rtw_write32(REG_FIFOPAGE_INFO_3_8814A, NPQ_PGNUM_8814A); + _device.rtw_write32(REG_FIFOPAGE_INFO_4_8814A, EPQ_PGNUM_8814A); + _device.rtw_write32(REG_FIFOPAGE_INFO_5_8814A, PUB_PGNUM_8814A); + + _device.rtw_write32(REG_RQPN_CTRL_2_8814A, 0x80000000); + + uint16_t txpktbuf_bndy = registry_priv::wifi_spec + ? WMM_NORMAL_TX_PAGE_BOUNDARY_8814A + : TX_PAGE_BOUNDARY_8814A; + + _device.rtw_write16(REG_TXPKTBUF_BCNQ_BDNY_8814A, txpktbuf_bndy); + _device.rtw_write16(REG_TXPKTBUF_BCNQ1_BDNY_8814A, txpktbuf_bndy); + _device.rtw_write16(REG_MGQ_PGBNDY_8814A, txpktbuf_bndy); + + /* Head page of BCNQ + BCNQ1 packets. */ + _device.rtw_write16(REG_FIFOPAGE_CTRL_2_8814A, txpktbuf_bndy); + _device.rtw_write16(REG_FIFOPAGE_CTRL_2_8814A + 2, txpktbuf_bndy); + + _logger->info( + "8814A queue reserved pages: HPQ/LPQ/NPQ/EPQ={:#x} PUB={:#x} bndy={:#x}", + HPQ_PGNUM_8814A, PUB_PGNUM_8814A, txpktbuf_bndy); +} + +void HalModule::_InitPageBoundary_8814AUsb() { + using namespace rtl8814a; + /* Port of upstream _InitPageBoundary_8814AUsb. Single 16-bit write to + * REG_RXFF_PTR_8814A. The 8812 path writes REG_TRXFF_BNDY+2 instead. */ + _device.rtw_write16(REG_RXFF_PTR_8814A, RX_DMA_BOUNDARY_8814A); +} + void HalModule::_InitTransferPageSize_8812AUsb() { uint8_t value8 = _PSTX(PBP_512); _device.rtw_write8(REG_PBP, value8); diff --git a/src/HalModule.h b/src/HalModule.h index 348627c..0c083e7 100644 --- a/src/HalModule.h +++ b/src/HalModule.h @@ -88,6 +88,8 @@ class HalModule { void _InitNormalChipRegPriority_8814AUsb(uint16_t beQ, uint16_t bkQ, uint16_t viQ, uint16_t voQ, uint16_t mgtQ, uint16_t hiQ); + void _InitQueueReservedPage_8814AUsb(); + void _InitPageBoundary_8814AUsb(); void init_hi_queue_config_8812a_usb(); void _InitPageBoundary_8812AUsb(); void _InitTransferPageSize_8812AUsb(); From 5eda10df61e7eb9fc109da60ef48634d0d56865b Mon Sep 17 00:00:00 2001 From: Joseph Albert Nefario Date: Fri, 22 May 2026 08:45:21 +0300 Subject: [PATCH 3/4] 8814AU: TX-state diagnostic dump + send_packet pre-TX probe + DEVOURER_TX_EP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three diagnostic hooks that make future 8814 TX investigations tractable without rebuilding. All gated behind CHIP_8814A or an opt-in env var; the 8812 path is untouched at runtime. - HalModule::rtl8812au_hal_init: dump the registers that gate USB→TX dataflow at the end of init (REG_CR, TXPAUSE, FWHW_TXQ_CTRL, FIFOPAGE_CTRL_2, MGQ_PGBNDY, FIFOPAGE_INFO_1/5, MCUFWDL, TXDMA_STATUS, TXDMA_OFFSET_CHK, HWSEQ_CTRL via both 8-bit and 32-bit byte-3 access, TCR, RCR). One log line per register — the Logger's homegrown format helper truncates lines with too many placeholders. - RtlUsbAdapter::send_packet: on the first call only, dump CR / TXPAUSE / TXDMA_OFFSET_CHK / FWHW_TXQ_CTRL / MCUFWDL / HCIPWR. Surfaces any clobber between init-end and the actual first TX (e.g. SetMonitorChannel writing registers). - RtlUsbAdapter::send_packet: DEVOURER_TX_EP=0xNN overrides the hardcoded bulk OUT endpoint 0x02. Lets bisecting EP routing without a rebuild (0x03 = NQ, 0x04 = LQ on the 8814's 3-OUT-EP layout). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/HalModule.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/RtlUsbAdapter.cpp | 31 ++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/HalModule.cpp b/src/HalModule.cpp index c06d405..5b32166 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -358,6 +358,43 @@ bool HalModule::rtl8812au_hal_init() { "REG_RXFLTMAP2=0xFFFF", cr_observed, cr_final); + if (is_8814a) { + /* TX-validation diagnostic. Read back the registers that gate USB→TX + * dataflow to confirm what state the chip is actually in at the end of + * init. One log per register to dodge the Logger format helper's + * placeholder-overflow truncation. */ + using namespace rtl8814a; + _logger->info("8814A TX-state CR = 0x{:04x}", _device.rtw_read16(REG_CR)); + _logger->info("8814A TX-state TXPAUSE(0x522) = 0x{:02x}", + _device.rtw_read8(0x0522)); + _logger->info("8814A TX-state FWHW_TXQ_CTRL(0x420) = 0x{:08x}", + _device.rtw_read32(0x0420)); + _logger->info("8814A TX-state FIFOPAGE_CTRL_2 = 0x{:08x}", + _device.rtw_read32(REG_FIFOPAGE_CTRL_2_8814A)); + _logger->info("8814A TX-state MGQ_PGBNDY = 0x{:04x}", + _device.rtw_read16(REG_MGQ_PGBNDY_8814A)); + _logger->info("8814A TX-state FIFOPAGE_INFO_1(HPQ) = 0x{:08x}", + _device.rtw_read32(REG_FIFOPAGE_INFO_1_8814A)); + _logger->info("8814A TX-state FIFOPAGE_INFO_5(PUB) = 0x{:08x}", + _device.rtw_read32(REG_FIFOPAGE_INFO_5_8814A)); + _logger->info("8814A TX-state MCUFWDL = 0x{:08x}", + _device.rtw_read32(0x0080)); + _logger->info("8814A TX-state TXDMA_STATUS(0x210) = 0x{:08x}", + _device.rtw_read32(0x0210)); + _logger->info("8814A TX-state TXDMA_OFFSET_CHK(0x20C) = 0x{:08x}", + _device.rtw_read32(0x020C)); + /* 8-bit read of 0x423 is unreliable on 8814; surface both 8-bit and the + * byte-3 of the 32-bit FWHW_TXQ_CTRL word for comparison. */ + _logger->info("8814A TX-state HWSEQ_CTRL(0x423,8bit) = 0x{:02x}", + _device.rtw_read8(0x0423)); + _logger->info("8814A TX-state HWSEQ_CTRL(byte3 of 0x420 32bit) = 0x{:02x}", + (_device.rtw_read32(REG_FWHW_TXQ_CTRL) >> 24) & 0xFF); + _logger->info("8814A TX-state TCR(0x604) = 0x{:08x}", + _device.rtw_read32(0x0604)); + _logger->info("8814A TX-state RCR(0x608) = 0x{:08x}", + _device.rtw_read32(0x0608)); + } + return true; } diff --git a/src/RtlUsbAdapter.cpp b/src/RtlUsbAdapter.cpp index 99a8122..0057ff2 100644 --- a/src/RtlUsbAdapter.cpp +++ b/src/RtlUsbAdapter.cpp @@ -1,6 +1,7 @@ #include "RtlUsbAdapter.h" #include +#include #if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__) #include #else @@ -350,7 +351,35 @@ bool RtlUsbAdapter::send_packet(uint8_t *packet, size_t length) { return false; } - libusb_fill_bulk_transfer(transfer, _dev_handle, 0x02, packet, length, + /* On the FIRST send only, dump chip state via vendor reads. Surfaces any + * register clobber between init-end and first TX (e.g. SetMonitorChannel + * could be resetting REG_CR or related). */ + static bool first_dump = true; + if (first_dump) { + first_dump = false; + uint16_t cr = rtw_read16(0x0100); + uint8_t txpause = rtw_read8(0x0522); + uint32_t txdma_off_chk = rtw_read32(0x020C); + uint32_t fwhw_txq = rtw_read32(0x0420); + uint32_t mcufwdl = rtw_read32(0x0080); + uint32_t hci_susp = rtw_read32(0xFE10); /* USB_HCPWM / USB suspend ctrl */ + _logger->info("pre-1st-TX: CR=0x{:04x} TXPAUSE=0x{:02x} TXDMA_OFFC=0x{:08x}", + cr, txpause, txdma_off_chk); + _logger->info("pre-1st-TX: FWHW_TXQ=0x{:08x} MCUFWDL=0x{:08x} HCIPWR=0x{:08x}", + fwhw_txq, mcufwdl, hci_susp); + } + + /* DEVOURER_TX_EP=0xNN overrides the default bulk OUT endpoint (0x02 = HQ). + * Diagnostic hook for 8814 TX validation — lets us bisect EP routing + * without rebuilding (0x03=NQ, 0x04=LQ on the 8814's 3-OUT-EP layout). */ + static uint8_t tx_ep = []() { + if (const char *ep_env = std::getenv("DEVOURER_TX_EP")) { + return static_cast(std::strtoul(ep_env, nullptr, 0)); + } + return static_cast(0x02); + }(); + + libusb_fill_bulk_transfer(transfer, _dev_handle, tx_ep, packet, length, transfer_callback, (void *)(_logger.get()), USB_TIMEOUT); auto start = std::chrono::high_resolution_clock::now(); From 352026f5dbc2eb587fb6a84d58177aa174149fc2 Mon Sep 17 00:00:00 2001 From: Joseph Albert Nefario Date: Fri, 22 May 2026 08:51:13 +0300 Subject: [PATCH 4/4] txdemo: replace POSIX usleep with std::this_thread::sleep_for MSVC doesn't ship /usleep so the Windows CI build fails with "identifier not found". Use std::this_thread::sleep_for which is portable and already pulled in by the existing include in this file. Co-Authored-By: Claude Opus 4.7 (1M context) --- txdemo/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 37cc4d1..bb5efb2 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -193,7 +194,7 @@ int main(int argc, char **argv) { printf("TX #%ld rc=%d\n", tx_count, rc); fflush(stdout); } - usleep(2000); /* 2 ms — ~500 fps, gentle on USB bulk EP */ + std::this_thread::sleep_for(std::chrono::milliseconds(2)); /* ~500 fps, gentle on USB bulk EP */ } rc = libusb_release_interface(handle, 0); assert(rc == 0);