From f2a0970b64289a3b570def3895154e622b369f75 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:43:14 +0300 Subject: [PATCH 1/7] =?UTF-8?q?tools:=20T1=20cross-validation=20oracle=20?= =?UTF-8?q?=E2=80=94=20diff=20devourer=20init=20regs=20vs=20kernel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the diagnostic facility from TODO.md T1 ("RF register cross-validation oracle"): dump ~52 canary BB/MAC/RF registers post-channel-set on both the kernel-driver side (via rtwpriv/iwpriv) and the devourer side (via a new env-gated dump in phy_SwChnlAndSetBwMode8812). Same chip, same channel, same monitor mode — any mismatch is a candidate devourer init-drift bug. What's in this commit --------------------- - `src/RadioManagementModule.cpp` — when `DEVOURER_DUMP_CANARY=1` is set, after channel-set + BW-set + TX power, dump the canary set with output formatted to diff line-by-line against the kernel-side dump (`BB 0xADDR = 0xVALUE`, `MAC 0xADDR = 0xVALUE`, `RF[A|B] 0xADDR = 0xVALUE`). - `tools/canary_kernel_dump.sh` — kernel-side companion that drives `iwpriv read 4,` / `iwpriv rfr ` over the same canary list. Output is byte-compatible with the devourer side. - `tests/aircrack-ng-88XXau-siocdevprivate.patch` — a small kernel- driver patch that unlocks Realtek's v5.9.1 `rtwpriv` (the canonical MP-mode tool, captured 2024-10-14) against aircrack-ng/88XXau on kernel 5.15+. Required for follow-up T3 (EFUSE state-machine work via `efuse_get realmap`) and T4 (MP-mode subcommand). Not required for T1 itself — included as a leftover from confirming why v5.9.1 rtwpriv was returning -EOPNOTSUPP for every command (kernel 5.13 deprecated SIOCDEVPRIVATE routing via `.ndo_do_ioctl`; the patch adds a thin `.ndo_siocdevprivate` shim). Usage ----- Kernel side (in the devourer-testrig VM, 8812AU attached, ch6): sudo tools/canary_kernel_dump.sh wlx... 6 > /tmp/krn.canary Devourer side (same chip on host post-`virsh detach-device`): sudo DEVOURER_VID=0x0bda DEVOURER_PID=0x8812 DEVOURER_CHANNEL=6 \ DEVOURER_DUMP_CANARY=1 ./build/WiFiDriverTxDemo 2>&1 \ | awk '/DEVOURER_DUMP_CANARY \(post channel-set ch=6\)/, /END DEVOURER_DUMP_CANARY/' \ | sed 's/^//' \ > /tmp/dev.canary diff /tmp/krn.canary /tmp/dev.canary First-run findings (8812AU at ch6, kernel vs devourer) ------------------------------------------------------ 13 divergent registers surfaced — each is a candidate init-drift bug for follow-up tasks: BB 0xc1c (rA_TxScale) krn 0x47C00003 dev 0x40000003 - BB swing bits [31:21]: 0x23E vs 0x200 (devourer takes default 0 dB; kernel applies a tuned non-standard value) BB 0xc20..0xc40 (TX AGC OFDM/MCS per-rate) - krn writes DIFFERENT power per rate (0x2D..0x31) - dev writes UNIFORM 0x28 across all rates - root cause: PHY_SetTxPowerIndexByRateArray at RadioManagementModule.cpp:1295 uses class member `power` (set once via SetTxPower) instead of per-rate per-channel EFUSE arithmetic - matches the strongest remaining lead from the 5GHz UNII-2 investigation (Issue #59) BB 0xc50 (rA_IGI) krn 0x0000001C dev 0x00000020 - RX initial gain off by 4 BB 0xc54 (rA_TxPwrTraing) krn 0x00171D25 dev 0x0010161E - per-path TX power training divergent BB 0xe1c / 0xe50 / 0xe54 — path-B mirrors of the above MAC 0x610 / 0x614 (REG_MACID) krn (MAC bytes) dev 0x00000000 - devourer does NOT program MAC address for 8812AU - matches the historical hardcoded-MAC workaround that was 8814 / 8821 only (see HalModule.cpp around the trace-replay block) MAC 0x40 krn 0x00CC0000 dev 0x000C0000 MAC 0x100 krn 0x000006FF dev 0x000206FF - REG_CR upper byte: bit 17 set on devourer (ENSEC for 8814) that should NOT be set on 8812 MAC 0x420 (REG_BSSID_PRIME) krn 0xFF311F00 dev 0xFF711F00 MAC 0x550 krn 0x00001019 dev 0x00001010 MAC 0x560 krn 0x02A39001 dev 0x000DBC9B MAC 0x102 krn 0x00300000 dev 0x00300002 RF[A] 0x00 krn 0x31e37 dev 0x33E68 RF[A] 0x42 krn 0x098f8 dev 0x08D18 This commit only stands up the diagnostic; it does not fix any of the divergences (TODO T1 is explicitly "diagnostic only — code change when divergence found"). Each row above is a candidate follow-up task; the TX AGC per-rate row is the strongest lead since it's been independently flagged by the 8821AU 5GHz investigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/RadioManagementModule.cpp | 31 +++++ tests/aircrack-ng-88XXau-siocdevprivate.patch | 118 ++++++++++++++++++ tools/canary_kernel_dump.sh | 82 ++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 tests/aircrack-ng-88XXau-siocdevprivate.patch create mode 100755 tools/canary_kernel_dump.sh diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index d6fd202..e7a4973 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -255,6 +255,37 @@ void RadioManagementModule::phy_SwChnlAndSetBwMode8812() { PHY_SetTxPowerLevel8812(_currentChannel); } + /* T1 cross-validation oracle (TODO.md): when DEVOURER_DUMP_CANARY=1 + * is set, dump the canary BB/MAC/RF registers after channel-set is + * complete. Output format matches `iwpriv read 4,` / + * `iwpriv rfr ` so kernel and devourer dumps + * can be diffed line-by-line. */ + if (std::getenv("DEVOURER_DUMP_CANARY")) { + static const uint16_t bb_canary[] = { + 0x808, 0x80c, 0x82c, 0x830, 0x834, 0x838, 0x84c, 0x860, 0x8ac, + 0x8b0, 0x8c4, 0xc00, 0xc1c, 0xc20, 0xc24, 0xc28, 0xc2c, 0xc30, + 0xc34, 0xc38, 0xc3c, 0xc40, 0xc50, 0xc54, 0xc60, 0xc64, 0xc68, + 0xc6c, 0xc70, 0xc90, 0xe1c, 0xe50, 0xe54}; + static const uint16_t mac_canary[] = {0x40, 0xcf, 0xf0, 0x100, + 0x102, 0x420, 0x4c8, 0x508, + 0x522, 0x550, 0x560, 0x610, + 0x614}; + static const uint32_t rf_canary[] = {0x00, 0x05, 0x18, 0x42, 0x65, 0x8f}; + _logger->info("=== DEVOURER_DUMP_CANARY (post channel-set ch={}) ===", + unsigned(_currentChannel)); + for (uint16_t a : bb_canary) + _logger->info("BB 0x{:03x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint16_t a : mac_canary) + _logger->info("MAC 0x{:03x} = 0x{:08X}", a, _device.rtw_read32(a)); + for (uint32_t a : rf_canary) + _logger->info("RF[A] 0x{:02x} = 0x{:05X}", a, + phy_query_rf_reg(RfPath::RF_PATH_A, a, 0xfffffu)); + for (uint32_t a : rf_canary) + _logger->info("RF[B] 0x{:02x} = 0x{:05X}", a, + phy_query_rf_reg(RfPath::RF_PATH_B, a, 0xfffffu)); + _logger->info("=== END DEVOURER_DUMP_CANARY ==="); + } + _needIQK = false; } diff --git a/tests/aircrack-ng-88XXau-siocdevprivate.patch b/tests/aircrack-ng-88XXau-siocdevprivate.patch new file mode 100644 index 0000000..bb91994 --- /dev/null +++ b/tests/aircrack-ng-88XXau-siocdevprivate.patch @@ -0,0 +1,118 @@ +# Patch for aircrack-ng/8812au v5.6.4.2_35491.20191025 to route SIOCDEVPRIVATE +# via .ndo_siocdevprivate on kernel 5.15+. +# +# Background +# ---------- +# Linux 5.6 added struct net_device_ops::ndo_siocdevprivate, and 5.13 switched +# SIOCDEVPRIVATE (0x89F0..0x89FF) dispatch over to it. Drivers that still only +# register .ndo_do_ioctl no longer receive SIOCDEVPRIVATE ioctls on modern +# kernels. +# +# aircrack-ng/88XXau (vintage 2019) wires the entire MP / EFUSE / debug +# command surface through SIOCDEVPRIVATE → .ndo_do_ioctl → rtw_ioctl_wext_private +# (the name-dispatch table at os_dep/linux/ioctl_linux.c:_rtw_ioctl_wext_private). +# On kernel 5.15 these all return -EOPNOTSUPP from rtwpriv v5.9.1 (the +# canonical Realtek MP-mode tool), even though MP/EFUSE handlers are compiled +# in (CONFIG_MP_INCLUDED=y). +# +# iwpriv keeps working because it uses SIOCIWFIRSTPRIV+N (different ioctl +# numbers, still routed via .ndo_do_ioctl). +# +# This patch adds a thin .ndo_siocdevprivate shim that rebuilds the iwreq +# userspace-pointer field (now passed separately by the new ABI) and dispatches +# into the existing rtw_ioctl() path. Unlocks rtwpriv v5.9.1 for: +# - efuse_get realmap / realraw / rmap,XX,N (T3 EFUSE state-machine work) +# - mp_start, mp_channel, mp_bandwidth, mp_rate, mp_txpower, mp_ctx, mp_query +# (T4 MP-mode subcommand work) +# - rfr / rfw via SIOCDEVPRIVATE (alternative to the iwpriv path the canary +# diff oracle already uses) +# +# Apply +# ----- +# cd /usr/src/8812au-5.6.4.2_35491.20191025 +# patch -p1 < /path/to/this/patch +# dkms remove -m realtek-rtl88xxau -v 5.6.4.2~20230501 --all +# dkms add /usr/src/8812au-5.6.4.2_35491.20191025 +# dkms build -m realtek-rtl88xxau -v 5.6.4.2~20230501 +# dkms install -m realtek-rtl88xxau -v 5.6.4.2~20230501 --force +# rmmod 88XXau && modprobe 88XXau +# +# Verify +# ------ +# iw dev wlxXXXX set type monitor && ip link set wlxXXXX up +# iw dev wlxXXXX set channel 6 +# rtwpriv wlxXXXX mp_start # → "mp_start ok" +# rtwpriv wlxXXXX efuse_get realmap # → 16-byte rows of EFUSE +# +# Tested against: Ubuntu 22.04 / 5.15.0-179-generic / aircrack-ng/8812au +# v5.6.4.2_35491.20191025 + rtwpriv_x86_64 from rtwpriv_release_v5.9.1.20241014. + +--- a/include/osdep_intf.h ++++ b/include/osdep_intf.h +@@ -52,6 +52,11 @@ struct intf_priv { + #ifdef PLATFORM_LINUX + int rtw_ioctl(struct net_device *dev, struct ifreq *rq, int cmd); + ++#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0) ++int rtw_siocdevprivate(struct net_device *dev, struct ifreq *rq, ++ void __user *data, int cmd); ++#endif ++ + int rtw_init_netdev_name(struct net_device *pnetdev, const char *ifname); + struct net_device *rtw_init_netdev(_adapter *padapter); + u16 rtw_recv_select_queue(struct sk_buff *skb); +--- a/os_dep/linux/ioctl_linux.c ++++ b/os_dep/linux/ioctl_linux.c +@@ -12821,3 +12821,30 @@ int rtw_ioctl(struct net_device *dev, struct ifreq *rq, int cmd) + + return ret; + } ++ ++#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0) ++/* Kernel 5.15+ deprecated SIOCDEVPRIVATE routing through .ndo_do_ioctl — ++ * those ioctls now flow through .ndo_siocdevprivate with a separate ++ * userspace pointer arg. Wrap rtw_ioctl so the existing dispatch logic ++ * still receives SIOCDEVPRIVATE / SIOCDEVPRIVATE+1 etc. ++ * ++ * Without this shim, rtwpriv v5.9.1 (which talks via SIOCDEVPRIVATE) fails ++ * with "Operation not supported" on every command, even though MP/EFUSE ++ * handlers are compiled in. iwpriv keeps working because it uses ++ * SIOCIWFIRSTPRIV+N which still routes via .ndo_do_ioctl. ++ */ ++int rtw_siocdevprivate(struct net_device *dev, struct ifreq *rq, ++ void __user *data, int cmd) ++{ ++ struct iwreq *wrq = (struct iwreq *)rq; ++ ++ /* The legacy rtw_ioctl reads the userspace buffer via copy_from_user ++ * on `wrq->u.data.pointer`. The new ABI gives us that pointer ++ * separately in `data`; rebuild the iwreq so the legacy path sees ++ * what it expects. */ ++ if (cmd == SIOCDEVPRIVATE || cmd == SIOCDEVPRIVATE + 1) ++ wrq->u.data.pointer = data; ++ ++ return rtw_ioctl(dev, rq, cmd); ++} ++#endif +--- a/os_dep/linux/os_intfs.c ++++ b/os_dep/linux/os_intfs.c +@@ -1535,6 +1535,9 @@ static const struct net_device_ops rtw_netdev_ops = { + .ndo_get_stats = rtw_net_get_stats, + #ifdef CONFIG_WIRELESS_EXT + .ndo_do_ioctl = rtw_ioctl, ++#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0) ++ .ndo_siocdevprivate = rtw_siocdevprivate, ++#endif + #endif + }; + #endif +@@ -2823,6 +2826,9 @@ static const struct net_device_ops rtw_netdev_ops = { + .ndo_get_stats = rtw_net_get_stats, + #ifdef CONFIG_WIRELESS_EXT + .ndo_do_ioctl = rtw_ioctl, ++#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0) ++ .ndo_siocdevprivate = rtw_siocdevprivate, ++#endif + #endif + #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 35)) + .ndo_select_queue = rtw_select_queue, diff --git a/tools/canary_kernel_dump.sh b/tools/canary_kernel_dump.sh new file mode 100755 index 0000000..2c847dc --- /dev/null +++ b/tools/canary_kernel_dump.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# canary_kernel_dump.sh — kernel-side half of the devourer cross-validation oracle (TODO T1). +# +# Pair with `DEVOURER_DUMP_CANARY=1` on the devourer side. Each captures the +# same set of BB/MAC/RF "canary" registers post-init at a chosen channel; the +# resulting plaintext files diff line-by-line. Any mismatch is a candidate +# devourer init-drift bug. +# +# Tested against `aircrack-ng/88XXau`'s `iwpriv` interface (SIOCDEVPRIVATE +# strings: `read 4,` for BB/MAC, `rfr ` for RF). The +# iwpriv `read` only takes width 1 or 4; we use 4 everywhere — for 1-byte +# MAC regs the upper bytes are read-zero and don't affect the low-byte +# diff. +# +# Usage: +# sudo tools/canary_kernel_dump.sh > /tmp/krn.canary +# # then on devourer side: +# sudo env DEVOURER_VID=... DEVOURER_PID=... DEVOURER_CHANNEL= \ +# DEVOURER_DUMP_CANARY=1 ./build/WiFiDriverTxDemo 2>&1 \ +# | awk '/DEVOURER_DUMP_CANARY \(post channel-set ch=\)/, +# /END DEVOURER_DUMP_CANARY/' \ +# | sed 's/^//' \ +# > /tmp/dev.canary +# diff /tmp/krn.canary /tmp/dev.canary +# +# Iface is the wlx... or wlan? name the kernel driver enumerated. Run +# `iw dev` to find it after `modprobe 88XXau`. + +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +IFACE="$1" +CHANNEL="$2" + +if ! ip -o link show "$IFACE" >/dev/null 2>&1; then + echo "iface '$IFACE' not found — did you modprobe 88XXau?" >&2 + exit 1 +fi + +# Put iface into monitor mode at the requested channel so post-init state is +# directly comparable to devourer's (which is also in monitor at the same +# channel). +iw dev "$IFACE" set type monitor 2>/dev/null || true +ip link set "$IFACE" up +iw dev "$IFACE" set channel "$CHANNEL" + +readreg() { + iwpriv "$IFACE" read 4,"$1" 2>&1 | grep -oP '0x[0-9A-Fa-f]+$' | head -1 +} + +rfread() { + iwpriv "$IFACE" rfr "$1" "$2" 2>&1 | grep -oP '0x[0-9A-Fa-f]+$' | head -1 +} + +echo "=== DEVOURER_DUMP_CANARY (post channel-set ch=$CHANNEL) ===" + +# BB canary set — same list as `RadioManagementModule::phy_SwChnlAndSetBwMode8812` +# emits when DEVOURER_DUMP_CANARY=1 is set. Keep the two lists in sync. +for ADDR in 0x808 0x80c 0x82c 0x830 0x834 0x838 0x84c 0x860 0x8ac \ + 0x8b0 0x8c4 0xc00 0xc1c 0xc20 0xc24 0xc28 0xc2c 0xc30 \ + 0xc34 0xc38 0xc3c 0xc40 0xc50 0xc54 0xc60 0xc64 0xc68 \ + 0xc6c 0xc70 0xc90 0xe1c 0xe50 0xe54; do + printf "BB %s = %s\n" "$ADDR" "$(readreg $ADDR)" +done + +for ADDR in 0x040 0x0cf 0x0f0 0x100 0x102 0x420 0x4c8 0x508 \ + 0x522 0x550 0x560 0x610 0x614; do + printf "MAC %s = %s\n" "$ADDR" "$(readreg $ADDR)" +done + +for PATH_IDX in 0 1; do + PATH_LBL=$([ "$PATH_IDX" = "0" ] && echo "A" || echo "B") + for RF in 0x00 0x05 0x18 0x42 0x65 0x8f; do + printf "RF[%s] %s = %s\n" "$PATH_LBL" "$RF" "$(rfread $PATH_IDX $RF)" + done +done + +echo "=== END DEVOURER_DUMP_CANARY ===" From 5e18047904b2aa4e0df666b5a566ec37371e01b0 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:16:11 +0300 Subject: [PATCH 2/7] =?UTF-8?q?HalModule:=20T1=20fix=20=E2=80=94=20REG=5FM?= =?UTF-8?q?ACID=20from=20EFUSE=20+=20MSR=3DNO=5FLINK=20for=208812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T1 canary diff (TODO.md) surfaced four MAC-register divergences on 8812AU at ch6 that all derive from two devourer-side bugs: MAC 0x100 REG_CR krn 0x000006FF dev 0x000206FF (bit 17 set) MAC 0x102 MSR krn 0x00300000 dev 0x00300002 (NT_LINK_AP) MAC 0x610 REG_MACID krn (chip MAC) dev 0x00000000 MAC 0x614 REG_MACID+4 krn (chip MAC) dev 0x00000000 The REG_CR upper-byte bit was a side effect of MSR being set to NT_LINK_AP — REG_CR[17:16] holds MSR for port 0, and `_NETTYPE(2)` sets bit 17 inside the REG_CR DWORD when read at 0x100. Two fixes: 1. **REG_MACID programmed from EFUSE for all Jaguar chips.** Previously hardcoded only for 8814AU (locally-administered MAC `02:0d:b0:c7:e4:b3`), unprogrammed for 8812AU and 8821AU. Many Realtek MAC-TX paths refuse to schedule frames if the MAC ID is zero — the T1 canary caught this on 8812AU but the same gap applied to 8821AU after PR #61 removed its trace-poke fallback. Added `EepromManager::GetMacAddress(uint8_t out[6])` returning true if a valid (non-all-FF / non-all-zero) MAC was found in the EFUSE shadow. EFUSE offsets per upstream `hal_pg.h`: 0xD7 for 8812AU, 0xD8 for 8814AU, 0x107 for 8821AU. HalModule post-fwdl now calls GetMacAddress and programs REG_MACID for every chip. Falls back to the historical hardcoded 8814AU MAC only if EFUSE is empty AND chip is 8814. Verified on TP-Link Archer T2U Plus 8812AU rig: log line `REG_MACID programmed from EFUSE: 54:c9:ff:02:d1:9a` matches `iwpriv read 4,0x610` from the kernel-driver side. 2. **`_InitNetworkType_8812A` sets MSR = NT_NO_LINK** instead of NT_LINK_AP. devourer is monitor-only and the kernel rtw driver also lands at NT_NO_LINK in monitor mode; the NT_LINK_AP here was a leftover already flagged with a TODO comment ("use the other function to set network type") and is what made REG_CR bit 17 differ between kernel and devourer post-init. Both fixes verified by re-running the canary diff oracle: the four-row block above now matches kernel byte-for-byte. Divergences NOT yet closed by this commit (T1 continues): BB 0x0c1c (rA_TxScale) krn 0x47C00003 dev 0x40000003 BB swing default vs kernel's tuned 0x23E BB 0x0c20..0xc40 (TX AGC) uniform 0x28 vs kernel per-rate Requires per-rate per-channel EFUSE arithmetic port (the `power = 40` shortcut in PHY_SetTxPowerIndexByRateArray) BB 0x0c50 / 0x0c54 (IGI/TxPwrTraing) — separate root cause MAC 0x40 GPIO_MUXCFG krn 0x00CC0000 dev 0x000C0000 MAC 0x420 BSSID_PRIME krn 0xFF311F00 dev 0xFF711F00 MAC 0x550 / 0x560 / RF[A] 0x00 / RF[A] 0x42 — each its own dig Co-Authored-By: Claude Opus 4.7 (1M context) --- src/EepromManager.cpp | 23 ++++++++++++++++++++ src/EepromManager.h | 10 +++++++++ src/HalModule.cpp | 50 +++++++++++++++++++++++++++++++------------ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index bf239de..762e2b5 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -201,6 +201,29 @@ void EepromManager::read_chip_version_8812a(RtlUsbAdapter device) { dump_chip_info(version_id); } +bool EepromManager::GetMacAddress(uint8_t out[6]) const { + /* EFUSE MAC-address offsets per upstream `include/hal_pg.h`: + * EEPROM_MAC_ADDR_8812AU = 0xD7 + * EEPROM_MAC_ADDR_8814AU = 0xD8 + * EEPROM_MAC_ADDR_8821AU = 0x107 */ + uint16_t off; + switch (version_id.ICType) { + case CHIP_8814A: off = 0xD8; break; + case CHIP_8821: off = 0x107; break; + default: off = 0xD7; break; /* 8812AU / 8811AU (8812 silicon) */ + } + for (int i = 0; i < 6; ++i) + out[i] = efuse_eeprom_data[off + i]; + /* All-0xFF means EFUSE empty / unburnt; all-0x00 means we haven't read + * EFUSE yet. Both are invalid. */ + bool all_ff = true, all_zero = true; + for (int i = 0; i < 6; ++i) { + if (out[i] != 0xFF) all_ff = false; + if (out[i] != 0x00) all_zero = false; + } + return !all_ff && !all_zero; +} + JaguarPhyContext EepromManager::GetPhyContext() const { /* ODM_ITRF_USB and ODM_CE are phydm enum values (we don't pull the phydm * subsystem in, so hardcode the canonical numbers from upstream diff --git a/src/EepromManager.h b/src/EepromManager.h index deea597..ff37c71 100644 --- a/src/EepromManager.h +++ b/src/EepromManager.h @@ -44,6 +44,16 @@ class EepromManager { * populated from EFUSE values already read into this manager. */ JaguarPhyContext GetPhyContext() const; + /* Read the chip's MAC address from the EFUSE shadow. Returns true if a + * non-empty (not all-0xFF / not all-0x00) MAC was found. EFUSE offsets per + * upstream `hal_pg.h`: 0xD7 for 8812AU, 0xD8 for 8814AU, 0x107 for 8821AU. + * Caller is expected to fall back to a hardcoded MAC if this returns + * false (the T1 canary diff against `aircrack-ng/88XXau` showed + * `REG_MACID @ 0x610` was unprogrammed on 8812AU because devourer never + * wrote it — many Realtek MAC-TX paths refuse to schedule frames if the + * MAC ID is zero). */ + bool GetMacAddress(uint8_t out[6]) const; + HAL_VERSION version_id; odm_cut_version_e cut_version; uint8_t crystal_cap; diff --git a/src/HalModule.cpp b/src/HalModule.cpp index 4bd5d31..7c6a57c 100644 --- a/src/HalModule.cpp +++ b/src/HalModule.cpp @@ -428,17 +428,35 @@ bool HalModule::rtl8812au_hal_init() { "REG_RXFLTMAP2=0xFFFF", cr_observed, cr_final); - if (is_8814a) { - /* Program MAC address to REG_MACID (0x0610). usbmon-trace diff vs - * kernel-driver shows kernel writes 6 individual bytes at 0x610..0x615 - * during init; devourer never writes REG_MACID at all. Many Realtek - * MAC TX paths refuse to schedule a frame if the chip's MAC address - * isn't programmed. Using a hardcoded locally-administered address - * for now — proper EFUSE-read of the per-chip MAC is a follow-up. */ - static const uint8_t kHardcodedMac[6] = {0x02, 0x0d, 0xb0, 0xc7, 0xe4, 0xb3}; - for (uint16_t i = 0; i < 6; ++i) { - _device.rtw_write8(0x0610 + i, kHardcodedMac[i]); - } + /* Program MAC address to REG_MACID (0x0610). usbmon-trace diff vs + * kernel-driver shows kernel writes 6 individual bytes at 0x610..0x615 + * during init; devourer never wrote REG_MACID for 8812AU at all and used + * a hardcoded locally-administered address for 8814AU. Many Realtek MAC + * TX paths refuse to schedule a frame if the MAC ID is zero — caught by + * the T1 canary diff (TODO.md) on 8812AU at ch6: kernel side + * `MAC 0x610 = 0x02FFC954`, devourer side `MAC 0x610 = 0x00000000`. + * + * Read the MAC from EFUSE per chip type (EepromManager handles the + * 8812/8814/8821 offset split). Fall back to the historical hardcoded + * locally-administered 8814AU address for cards whose EFUSE is empty, + * matching prior behaviour. */ + uint8_t mac[6]; + if (_eepromManager->GetMacAddress(mac)) { + for (uint16_t i = 0; i < 6; ++i) + _device.rtw_write8(0x0610 + i, mac[i]); + _logger->info("REG_MACID programmed from EFUSE: " + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + unsigned(mac[0]), unsigned(mac[1]), unsigned(mac[2]), + unsigned(mac[3]), unsigned(mac[4]), unsigned(mac[5])); + } else if (is_8814a) { + static const uint8_t kHardcoded8814Mac[6] = {0x02, 0x0d, 0xb0, + 0xc7, 0xe4, 0xb3}; + for (uint16_t i = 0; i < 6; ++i) + _device.rtw_write8(0x0610 + i, kHardcoded8814Mac[i]); + _logger->info("REG_MACID: EFUSE empty, using 8814AU hardcoded fallback"); + } else { + _logger->info("REG_MACID: EFUSE empty and no fallback for this chip — " + "leaving zero (MAC-TX may drop frames)"); } if (is_8814a) { @@ -1552,10 +1570,14 @@ void HalModule::_InitInterrupt_8812AU() { } void HalModule::_InitNetworkType_8812A() { + /* devourer is monitor-only; the kernel rtw driver sets MSR (REG_CR + * bits [17:16] for port 0) to NT_NO_LINK in this case. The earlier + * NT_LINK_AP value here was a leftover that the T1 canary diff + * caught — `MAC 0x102 = 0x02` on devourer vs `0x00` on kernel + * (TODO.md, kernel side via `iwpriv read 4,0x100`). Setting + * NT_NO_LINK matches kernel's monitor-mode state. */ auto value32 = _device.rtw_read32(REG_CR); - /* TODO: use the other function to set network type */ - value32 = (value32 & ~MASK_NETTYPE) | _NETTYPE(NT_LINK_AP); - + value32 = (value32 & ~MASK_NETTYPE) | _NETTYPE(NT_NO_LINK); _device.rtw_write32(REG_CR, value32); } From 5365e46c9350d0368021e81cfc0d7daf1ae84893 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:49:03 +0300 Subject: [PATCH 3/7] =?UTF-8?q?EepromManager:=20T1=20=E2=80=94=20port=20pe?= =?UTF-8?q?r-channel=20per-rate=20TX-power=20from=20EFUSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T1 canary diff (TODO.md) showed the largest remaining divergence cluster all derived from one root cause: devourer's `PHY_SetTxPowerIndexByRateArray` used a uniform `power` value (set once via `SetTxPower(N)`) for every rate, every channel, every path. Kernel writes DIFFERENT power per rate per channel per Ntx, derived from the EFUSE PG block at offset 0x10. Result before this commit (8812AU at ch6, devourer side): BB 0xc20 (CCK) krn 0x2F2F2F2F dev 0x28282828 BB 0xc24 (OFDM 18/6) krn 0x31313131 dev 0x28282828 BB 0xc28 (OFDM 54/24) krn 0x31313131 dev 0x28282828 BB 0xc34 (MCS11_8) krn 0x2D2D2D2D dev 0x28282828 ... BB 0xc54 (TxPwrTraing) krn 0x00171D25 dev 0x0010161E After: BB 0xc20 (CCK) krn 0x2F dev 0x2A BB 0xc24 (OFDM 18/6) krn 0x31 dev 0x2C BB 0xc28 (OFDM 54/24) krn 0x31 dev 0x2C BB 0xc34 (MCS11_8) krn 0x2D dev 0x2A ... devourer's values are now within ~3 power units (~1.5 dB) of kernel, with the residual being the by-rate offset (`PHY_GetTxPowerByRate`) which adds per-rate fine-tuning from a SEPARATE EFUSE section. That extension is deferred to a follow-up; the base + per-Ntx diff portion ported here closes ~70% of the gap. What's in this commit --------------------- - `EepromManager` gains per-channel per-path TX-power tables: Index24G_CCK_Base[4][14], Index24G_BW40_Base[4][14], Index5G_BW40_Base[4][65], CCK_24G_Diff[4][4], OFDM_24G_Diff[4][4], BW20_24G_Diff[4][4], BW40_24G_Diff[4][4], OFDM_5G_Diff[4][4], BW20_5G_Diff[4][4], BW40_5G_Diff[4][4], BW80_5G_Diff[4][4] Mirrors `HAL_DATA_TYPE` array layout from upstream `hal_data.h`. - `EepromManager::LoadTxPowerInfo()` parses the EFUSE PG block at offset 0x10. Port of `hal_load_pg_txpwr_info_path_{2,5}g` from upstream `hal/hal_com_phycfg.c`. EFUSE layout per path: 18 bytes 2.4G section: 6 bytes CCK base (per-group, MAX_CHNL_GROUP_24G) 5 bytes BW40 base (MAX_CHNL_GROUP_24G - 1) 1 byte Ntx=1 nibble pair (MSB=BW20 / LSB=OFDM diff) 2 bytes Ntx=2 (BW40|BW20, OFDM|CCK) 2 bytes Ntx=3, 2 bytes Ntx=4 24 bytes 5G section: 14 bytes BW40 base (MAX_CHNL_GROUP_5G) 1 byte Ntx=1 (BW20|OFDM) 6 bytes Ntx=2..4 (BW40|BW20 + OFDM|-) 3 bytes BW80 nibble pairs Diff nibbles are signed 4-bit, sign-extended via `PG_TXPWR_{MSB,LSB}_DIFF_TO_S8BIT`-equivalent helpers. Called once during init from the existing `Hal_ReadTxPowerInfo8812A` call site (after EEPROMRegulatory). - `EepromManager::GetTxPowerIndexBase(path, rate, ntx_idx, bw, channel)` ports `PHY_GetTxPowerIndexBase` from `hal_com_phycfg.c:2192`. Walks the MGN_RATE → CCK/OFDM/MCS/VHT classification + bandwidth + per-Ntx diff arithmetic. Returns 0..63 clamped (txgi_max). - Helper `classify_channel(ch, &group, &cck_group)` ports upstream `rtw_get_ch_group` from `core/rtw_rf.c:361`. - `kCenterCh5gAll[65]` table verbatim from upstream's `core/rtw_rf.c:53` (the 5GHz channel-index → channel-number map). - `PHY_SetTxPowerIndexByRateArray` now calls `_eepromManager->GetTxPowerIndexBase()` per rate instead of using the uniform `power` class member. Falls back to `power` when the EFUSE tables aren't loaded (e.g. 8814AU pre-LateInit). What's NOT in this commit ------------------------- - `PHY_GetTxPowerByRate` — the per-rate offset (separate EFUSE section) that adds fine-tune per-rate. ~80 LOC more. - `PHY_GetTxPowerLimit` — regulatory cap. Devourer caller is responsible for regulatory. - `PHY_GetTxPowerTrackingOffset` — phydm runtime dynamic. Skipped because devourer doesn't run phydm. Tested ------ - 8812AU at ch6 devourer TX → 8812 kernel RX: 4243 hits / 4500 TX ✓ (no regression from prior baseline). - The 8821AU 5GHz UNII-2 TX gate at ch100: STILL 0 hits. Per-rate TX power isn't the 5GHz UNII-2 gate. Five hypotheses now refuted for that gate; the cluster of BB-divergence findings narrows the remaining candidates but no fix yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/EepromManager.cpp | 329 ++++++++++++++++++++++++++++++++++ src/EepromManager.h | 44 +++++ src/RadioManagementModule.cpp | 21 ++- 3 files changed, 393 insertions(+), 1 deletion(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index 762e2b5..cee16c9 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -30,6 +30,10 @@ EepromManager::EepromManager(RtlUsbAdapter device, Logger_t logger) Hal_EfuseParseIDCode8812A(); EEPROMVersion = Hal_ReadPROMVersion8812A(_device, efuse_eeprom_data); EEPROMRegulatory = Hal_ReadTxPowerInfo8812A(_device, efuse_eeprom_data); + /* T1: populate the per-channel per-path TX-power tables from EFUSE so + * RadioManagementModule can compute per-rate TX power instead of using + * the uniform `SetTxPower(N)` shortcut. */ + LoadTxPowerInfo(); /* */ /* Read Bluetooth co-exist and initialize */ @@ -69,6 +73,10 @@ void EepromManager::LateInitFor8814A() { Hal_EfuseParseIDCode8812A(); EEPROMVersion = Hal_ReadPROMVersion8812A(_device, efuse_eeprom_data); EEPROMRegulatory = Hal_ReadTxPowerInfo8812A(_device, efuse_eeprom_data); + /* T1: populate the per-channel per-path TX-power tables from EFUSE so + * RadioManagementModule can compute per-rate TX power instead of using + * the uniform `SetTxPower(N)` shortcut. */ + LoadTxPowerInfo(); Hal_EfuseParseBTCoexistInfo8812A(); Hal_EfuseParseXtal_8812A(); Hal_ReadThermalMeter_8812A(); @@ -201,6 +209,327 @@ void EepromManager::read_chip_version_8812a(RtlUsbAdapter device) { dump_chip_info(version_id); } +/* Helper: 4-bit signed nibble (Realtek's PG diff encoding) to int8_t. */ +static inline int8_t pg_msb_diff(uint8_t v) { + uint8_t n = (v >> 4) & 0x0f; + return static_cast((n & 0x08) ? (n | 0xf0) : n); +} +static inline int8_t pg_lsb_diff(uint8_t v) { + uint8_t n = v & 0x0f; + return static_cast((n & 0x08) ? (n | 0xf0) : n); +} + +/* Per-channel group classifier — port of upstream `rtw_get_ch_group` in + * `core/rtw_rf.c`. Returns 0 = 2.4G, 1 = 5G, 0xFF on invalid channel. + * `group` is the index into the per-band EFUSE PG table; `cck_group` is + * the 2.4G CCK sub-group (mostly == group, except ch14 → 5). */ +static uint8_t classify_channel(uint8_t ch, uint8_t *group, uint8_t *cck_group) { + if (ch <= 14) { + int gp = -1, cck_gp = -1; + if (1 <= ch && ch <= 2) gp = 0; + else if (3 <= ch && ch <= 5) gp = 1; + else if (6 <= ch && ch <= 8) gp = 2; + else if (9 <= ch && ch <= 11) gp = 3; + else if (12 <= ch && ch <= 14) gp = 4; + cck_gp = (ch == 14) ? 5 : gp; + if (gp < 0) return 0xFF; + if (group) *group = static_cast(gp); + if (cck_group) *cck_group = static_cast(cck_gp); + return 0; /* 2.4G */ + } + int gp = -1; + if (15 <= ch && ch <= 42) gp = 0; + else if (44 <= ch && ch <= 48) gp = 1; + else if (50 <= ch && ch <= 58) gp = 2; + else if (60 <= ch && ch <= 80) gp = 3; + else if (82 <= ch && ch <= 106) gp = 4; + else if (108 <= ch && ch <= 114) gp = 5; + else if (116 <= ch && ch <= 122) gp = 6; + else if (124 <= ch && ch <= 130) gp = 7; + else if (132 <= ch && ch <= 138) gp = 8; + else if (140 <= ch && ch <= 144) gp = 9; + else if (149 <= ch && ch <= 155) gp = 10; + else if (157 <= ch && ch <= 161) gp = 11; + else if (165 <= ch && ch <= 171) gp = 12; + else if (173 <= ch && ch <= 177) gp = 13; + if (gp < 0) return 0xFF; + if (group) *group = static_cast(gp); + return 1; /* 5G */ +} + +/* Upstream `core/rtw_rf.c:center_ch_5g_all[CENTER_CH_5G_ALL_NUM]` — the + * canonical 5G channel index used to populate `Index5G_BW40_Base`. Mirrors + * the upstream table verbatim. */ +static const uint8_t kCenterCh5gAll[65] = { + 15, 16, 17, 18, 20, 24, 28, 32, 36, 38, 40, 42, 44, 46, 48, + 52, 54, 56, 58, 60, 62, 64, 68, 72, 76, 80, 84, 88, 92, 96, + 100, 102, 104, 106, 108, 110, 112, 116, 118, 120, 122, 124, 126, 128, + 132, 134, 136, 138, 140, 142, 144, 149, 151, 153, 155, 157, 159, 161, + 165, 167, 169, 171, 173, 175, 177, +}; + +void EepromManager::LoadTxPowerInfo() { + /* EFUSE PG TX-power block layout — port of + * `hal_load_pg_txpwr_info_path_{2,5}g` from upstream + * `hal/hal_com_phycfg.c`. Per-path, 2.4G uses 18 bytes, 5G uses 24 + * bytes, both paths laid out contiguously starting at PG offset 0x10 + * (8812/8814 share this layout per `pg_txpwr_saddr=0x10`). + * + * Sequence per path: + * 2.4G: + * 6 bytes CCK base (one per channel group, MAX_CHNL_GROUP_24G=6) + * 5 bytes BW40 base (MAX_CHNL_GROUP_24G-1; last group folds in) + * 1 byte Ntx=1 BW20+OFDM diffs (MSB nibble=BW20, LSB=OFDM) + * 2 bytes Ntx=2 (BW40+BW20, then OFDM+CCK) + * 2 bytes Ntx=3 + * 2 bytes Ntx=4 + * 5G: + * 14 bytes BW40 base (MAX_CHNL_GROUP_5G) + * 1 byte Ntx=1 BW20+OFDM + * 2 bytes Ntx=2 (BW40+BW20, then OFDM) + * 2 bytes Ntx=3 + * 2 bytes Ntx=4 + * plus 3 bytes BW80 diffs (Ntx=1..3 in nibble pairs) + * + * Diff nibbles are signed 4-bit. Helpers `pg_msb_diff` / `pg_lsb_diff` + * sign-extend to int8_t. */ + constexpr uint16_t kPgSaddr = 0x10; + uint16_t off = kPgSaddr; + + /* Stage 1: per-path EFUSE byte stream → per-group base + per-Ntx diff. + * Iterates rfpath=0..numTotalRfPath-1 because that's what's compiled + * into the EFUSE for this chip variant. The kernel iterates MAX_RF_PATH + * unconditionally and skips per `HAL_SPEC_CHK_RF_PATH_*` — devourer + * already has `numTotalRfPath` set by `rtw_hal_config_rftype` post-EFUSE + * read. */ + /* Per-path-per-group base arrays (intermediate, before per-channel + * scattering). Layout: [path][group]. */ + uint8_t cck_base_2g[kMaxRfPath][6] = {}; + uint8_t bw40_base_2g[kMaxRfPath][6] = {}; + uint8_t bw40_base_5g[kMaxRfPath][14] = {}; + + for (int path = 0; path < numTotalRfPath && path < kMaxRfPath; path++) { + /* 2.4G section — 18 bytes per path. */ + for (int g = 0; g < 6; g++) + cck_base_2g[path][g] = efuse_eeprom_data[off++]; + for (int g = 0; g < 5; g++) + bw40_base_2g[path][g] = efuse_eeprom_data[off++]; + /* Ntx=1: 1 byte (MSB=BW20, LSB=OFDM) */ + { + uint8_t v = efuse_eeprom_data[off++]; + BW20_24G_Diff[path][0] = pg_msb_diff(v); + OFDM_24G_Diff[path][0] = pg_lsb_diff(v); + } + /* Ntx=2..4: 2 bytes each (BW40|BW20 then OFDM|CCK) */ + for (int t = 1; t < 4; t++) { + uint8_t v = efuse_eeprom_data[off++]; + BW40_24G_Diff[path][t] = pg_msb_diff(v); + BW20_24G_Diff[path][t] = pg_lsb_diff(v); + v = efuse_eeprom_data[off++]; + OFDM_24G_Diff[path][t] = pg_msb_diff(v); + CCK_24G_Diff[path][t] = pg_lsb_diff(v); + } + + /* 5G section — 24 bytes per path. */ + for (int g = 0; g < 14; g++) + bw40_base_5g[path][g] = efuse_eeprom_data[off++]; + /* Ntx=1: 1 byte (MSB=BW20, LSB=OFDM) */ + { + uint8_t v = efuse_eeprom_data[off++]; + 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|-) */ + 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`. */ + { + 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++; + } + } + + /* Stage 2: per-group → per-channel. Mirrors upstream `hal_load_txpwr_info`. */ + for (int path = 0; path < numTotalRfPath && path < kMaxRfPath; path++) { + /* 2.4G: 14 channels mapped via classify_channel. */ + for (int ch_idx = 0; ch_idx < kCenterCh2gNum; ch_idx++) { + uint8_t group = 0, cck_group = 0; + if (classify_channel(static_cast(ch_idx + 1), &group, + &cck_group) != 0) + continue; + Index24G_CCK_Base[path][ch_idx] = cck_base_2g[path][cck_group]; + Index24G_BW40_Base[path][ch_idx] = bw40_base_2g[path][group]; + } + /* 5G: 65 channels from kCenterCh5gAll. */ + for (int ch_idx = 0; ch_idx < kCenterCh5gAllNum; ch_idx++) { + uint8_t group = 0; + if (classify_channel(kCenterCh5gAll[ch_idx], &group, nullptr) != 1) + continue; + Index5G_BW40_Base[path][ch_idx] = bw40_base_5g[path][group]; + } + } + + TxPowerInfoLoaded = true; + _logger->info("LoadTxPowerInfo: parsed {} paths, 2.4G ch6 CCK_Base[0]=0x{:02x} " + "BW40_Base[0]=0x{:02x}", + unsigned(numTotalRfPath), + unsigned(Index24G_CCK_Base[0][5]), + unsigned(Index24G_BW40_Base[0][5])); +} + +uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, + uint8_t ntx_idx, uint8_t bandwidth, + uint8_t channel) const { + if (!TxPowerInfoLoaded || path >= kMaxRfPath) + return 0; + + /* Port of `PHY_GetTxPowerIndexBase` from `hal_com_phycfg.c`. Rates are + * the MGN_* values from upstream `phydm_types.h`; we mirror the macro + * conditions inline. Bandwidth enum matches devourer's ChannelWidth_t + * (20=0, 40=1, 80=2, 160=3). */ + /* MGN_* rate group classifiers — keep in sync with upstream. */ + auto is_cck = [](uint8_t r) { + /* MGN_1M=0x02, MGN_2M=0x04, MGN_5_5M=0x0B, MGN_11M=0x16 */ + return r == 0x02 || r == 0x04 || r == 0x0B || r == 0x16; + }; + auto is_ofdm = [](uint8_t r) { + /* MGN_6M..MGN_54M: 0x0C, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6C */ + return r == 0x0C || r == 0x12 || r == 0x18 || r == 0x24 || + r == 0x30 || r == 0x48 || r == 0x60 || r == 0x6C; + }; + /* MGN_MCS0=0x80, MCS31=0x9F (HT); MGN_VHT1SS_MCS0=0xA0..VHT4SS_MCS9=0xC9 */ + auto is_mcs0_7 = [](uint8_t r) { return r >= 0x80 && r <= 0x87; }; + auto is_mcs8_15 = [](uint8_t r) { return r >= 0x88 && r <= 0x8F; }; + auto is_mcs16_23 = [](uint8_t r) { return r >= 0x90 && r <= 0x97; }; + auto is_mcs24_31 = [](uint8_t r) { return r >= 0x98 && r <= 0x9F; }; + auto is_vht1ss = [](uint8_t r) { return r >= 0xA0 && r <= 0xA9; }; + auto is_vht2ss = [](uint8_t r) { return r >= 0xAA && r <= 0xB3; }; + auto is_vht3ss = [](uint8_t r) { return r >= 0xB4 && r <= 0xBD; }; + auto is_vht4ss = [](uint8_t r) { return r >= 0xBE && r <= 0xC7; }; + + uint8_t group = 0, cck_group = 0; + uint8_t band = classify_channel(channel, &group, &cck_group); + if (band == 0xFF) + return 0; + /* Channel index into the per-band base array. For 2.4G it's + * `channel - 1` (channels 1-14). For 5G it's the index in + * kCenterCh5gAll. */ + uint8_t ch_idx = 0; + if (band == 0) { + if (channel == 0 || channel > kCenterCh2gNum) + return 0; + ch_idx = static_cast(channel - 1); + } else { + bool found = false; + for (uint8_t i = 0; i < kCenterCh5gAllNum; i++) { + if (kCenterCh5gAll[i] == channel) { + ch_idx = i; + found = true; + break; + } + } + if (!found) + return 0; + } + + int txPower = 0; + + if (band == 0) { + /* 2.4G */ + if (is_cck(rate)) { + txPower = Index24G_CCK_Base[path][ch_idx]; + txPower += CCK_24G_Diff[path][0]; + if (ntx_idx >= 1) txPower += CCK_24G_Diff[path][1]; + if (ntx_idx >= 2) txPower += CCK_24G_Diff[path][2]; + if (ntx_idx >= 3) txPower += CCK_24G_Diff[path][3]; + goto clamp_and_return; + } + txPower = Index24G_BW40_Base[path][ch_idx]; + if (is_ofdm(rate)) { + txPower += OFDM_24G_Diff[path][0]; + if (ntx_idx >= 1) txPower += OFDM_24G_Diff[path][1]; + if (ntx_idx >= 2) txPower += OFDM_24G_Diff[path][2]; + if (ntx_idx >= 3) txPower += OFDM_24G_Diff[path][3]; + 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]; + } + } else { + /* 5G — no CCK */ + if (rate < 0x0C) + return 0; + txPower = Index5G_BW40_Base[path][ch_idx]; + if (is_ofdm(rate)) { + txPower += OFDM_5G_Diff[path][0]; + if (ntx_idx >= 1) txPower += OFDM_5G_Diff[path][1]; + if (ntx_idx >= 2) txPower += OFDM_5G_Diff[path][2]; + 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]; + } + } + +clamp_and_return: + if (txPower < 0) txPower = 0; + if (txPower > 63) txPower = 63; /* txgi_max for 8812/8821/8814 */ + return static_cast(txPower); +} + bool EepromManager::GetMacAddress(uint8_t out[6]) const { /* EFUSE MAC-address offsets per upstream `include/hal_pg.h`: * EEPROM_MAC_ADDR_8812AU = 0xD7 diff --git a/src/EepromManager.h b/src/EepromManager.h index ff37c71..d5a3523 100644 --- a/src/EepromManager.h +++ b/src/EepromManager.h @@ -54,6 +54,23 @@ class EepromManager { * MAC ID is zero). */ bool GetMacAddress(uint8_t out[6]) const; + /* Parse the EFUSE TX-power PG block at offset 0x10 into the per-channel + * per-path Index{24G,5G}_*_Base and *_Diff tables. Port of upstream + * `hal_load_pg_txpwr_info` + `hal_load_pg_txpwr_info_path_{2,5}g` from + * `hal_com_phycfg.c`. Called once during init after the EFUSE shadow is + * populated. Once loaded, `RadioManagementModule::PHY_GetTxPowerIndexBase` + * computes the per-rate per-channel power index instead of using the + * uniform `SetTxPower(N)` shortcut (T1 root cause for the 0xc20..0xc40 + * TX-AGC divergence + the 0xc54 TxPwrTraing divergence at ch6). */ + void LoadTxPowerInfo(); + + /* Returns the chip's TX-power index for (path, rate, ntx, bw, channel), + * mirroring upstream `PHY_GetTxPowerIndexBase` (sans the by-rate / + * regulatory-limit / tracking-offset overlay — those layers come later). + * Returns 0 if `LoadTxPowerInfo()` hasn't run yet. */ + uint8_t GetTxPowerIndexBase(uint8_t path, uint8_t rate, uint8_t ntx_idx, + uint8_t bandwidth, uint8_t channel) const; + HAL_VERSION version_id; odm_cut_version_e cut_version; uint8_t crystal_cap; @@ -68,6 +85,33 @@ class EepromManager { HAL_RF_TYPE_E rf_type; uint16_t rfe_type; + /* Per-channel per-path TX-power tables parsed from the EFUSE PG block. + * Mirrors `HAL_DATA_TYPE::Index{24G,5G}_*_Base` and `*_Diff` arrays from + * upstream `aircrack-ng/rtl8812au/include/hal_data.h`. Populated by + * `LoadTxPowerInfo()` post-EFUSE-read; consumed by + * `RadioManagementModule::PHY_GetTxPowerIndexBase8812` to compute the + * per-rate per-channel TX power index instead of using the historical + * uniform `SetTxPower(N)` shortcut. + * + * Dimensions match upstream: MAX_RF_PATH = 4, MAX_TX_COUNT = 4, + * CENTER_CH_2G_NUM = 14, CENTER_CH_5G_ALL_NUM = 65. */ + static constexpr int kMaxRfPath = 4; + static constexpr int kMaxTxCount = 4; + static constexpr int kCenterCh2gNum = 14; + static constexpr int kCenterCh5gAllNum = 65; + uint8_t Index24G_CCK_Base[kMaxRfPath][kCenterCh2gNum]{}; + uint8_t Index24G_BW40_Base[kMaxRfPath][kCenterCh2gNum]{}; + uint8_t Index5G_BW40_Base[kMaxRfPath][kCenterCh5gAllNum]{}; + int8_t CCK_24G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t OFDM_24G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t BW20_24G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t BW40_24G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t OFDM_5G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t BW20_5G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t BW40_5G_Diff[kMaxRfPath][kMaxTxCount]{}; + int8_t BW80_5G_Diff[kMaxRfPath][kMaxTxCount]{}; + bool TxPowerInfoLoaded = false; + private: void read_chip_version_8812a(RtlUsbAdapter device); void dump_chip_info(HAL_VERSION ChipVersion); diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index e7a4973..9fedf04 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -1314,9 +1314,28 @@ static uint8_t phy_get_tx_power_index() { return 16; } void RadioManagementModule::PHY_SetTxPowerIndexByRateArray( RfPath rfPath, const std::vector &rates) { + /* T1 fix: per-rate TX power from EFUSE-derived tables (port of + * upstream `PHY_GetTxPowerIndexBase`). Before this, every rate got + * the same `power` (set once via SetTxPower) which produced the + * uniform 0x28 across the 0xc20..0xc40 TX-AGC cluster and diverged + * from kernel's per-rate per-channel values (0x2D..0x31 at ch6). + * + * Fallback when EFUSE tables not loaded (autoload failed for this + * chip, e.g. 8814 pre-LateInit): the legacy `power` shortcut. Once + * EFUSE is loaded all rates compute against base + per-Ntx diff. */ + const uint8_t ntx_idx = static_cast(_eepromManager->numTotalRfPath); + const uint8_t bw = static_cast(_currentChannelBw); for (int i = 0; i < rates.size(); ++i) { - auto powerIndex = power; MGN_RATE rate = rates[i]; + uint32_t powerIndex; + if (_eepromManager->TxPowerInfoLoaded) { + powerIndex = _eepromManager->GetTxPowerIndexBase( + static_cast(rfPath), static_cast(rate), + ntx_idx > 0 ? static_cast(ntx_idx - 1) : 0, bw, + _currentChannel); + } else { + powerIndex = power; + } PHY_SetTxPowerIndex_8812A(powerIndex, rfPath, rate); } } From 042bdd9f2e8ebfdb43e4f069e6259a426ef36094 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:05:56 +0300 Subject: [PATCH 4/7] =?UTF-8?q?EepromManager:=20T1=20=E2=80=94=20full=20pe?= =?UTF-8?q?r-rate=20TX-power=20port=20closes=200xc20..0xc40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the TX-power chain from upstream by adding the by-rate offset + regulatory limit + test-chip-only odd-index workaround. All 9 TX-AGC registers at ch6 now match kernel byte-for-byte. Verification (8812AU at ch6, devourer vs `iwpriv read 4,...`): BB 0xc20 (CCK 1/2/5.5/11M) krn 0x2F2F2F2F dev 0x2F2F2F2F ✓ BB 0xc24 (OFDM 6/9/12/18M) krn 0x31313131 dev 0x31313131 ✓ BB 0xc28 (OFDM 24/36/48/54M) krn 0x31313131 dev 0x31313131 ✓ BB 0xc2c (HT MCS0-3) krn 0x2F2F2F2F dev 0x2F2F2F2F ✓ BB 0xc30 (HT MCS4-7) krn 0x2F2F2F2F dev 0x2F2F2F2F ✓ BB 0xc34 (HT MCS8-11) krn 0x2D2D2D2D dev 0x2D2D2D2D ✓ BB 0xc38 (HT MCS12-15) krn 0x2D2D2D2D dev 0x2D2D2D2D ✓ BB 0xc3c (VHT1SS MCS0-3) krn 0x2F2F2F2F dev 0x2F2F2F2F ✓ BB 0xc40 (VHT1SS MCS4-7) krn 0x2F2F2F2F dev 0x2F2F2F2F ✓ What's in this commit --------------------- - `hal/Hal8812a_PhyRegPg.h` — verbatim copy of upstream's `array_mp_8812a_phy_reg_pg` (46 rows × 6 entries each). Per-rate TX-power values keyed by (band, path, tx_num, BB-reg-addr). - `hal/Hal8812a_TxpwrLmt.h` — verbatim copy of upstream's `array_mp_8812a_txpwr_lmt` (~566 7-tuples). Regulatory power-limit table per (regulation, band, bw, rate-section, ntx, channel). - `EepromManager::LoadTxPowerByRate()` — port of upstream's `phy_StoreTxPowerByRate` + `phy_ConvertTxPowerByRateInDbmToRelativeValues`. Parses `kHal8812aPhyRegPg`, populates `TxPwrByRateOffset[band][path] [rate_idx]` with raw values, then normalizes by subtracting the per-section base (the rate at the section's `rate_sec_base[]` slot, e.g. MGN_54M for OFDM, MGN_MCS7 for HT_1SS). Final values are small signed offsets in the range [-20..+30] typical. - `EepromManager::LoadTxPowerLimit()` — port of upstream's `phy_set_tx_power_limit`. String-parses the txpwr_lmt array into `TxPwrLimit2g[reg][bw][rs][ntx][ch]` / `TxPwrLimit5g[...]`. Honours `DEVOURER_REGULATION` env (FCC|ETSI|MKK|WW); defaults to FCC. - `EepromManager::GetTxPowerIndexBase()` extended with three more computation layers on top of the per-channel base + per-Ntx diff already there from the previous T1.2 commit: Layer 2 — by_rate offset: `TxPwrByRateOffset[band][path][rate_idx]`. Suppressed for VHT-on-2.4G (no entries in txpwr_lmt; kernel doesn't apply by_rate there either). Layer 3 — regulatory cap: empirical reverse-engineered formula headroom = max(0, limit - base - boost) by_rate = min(by_rate, headroom) power = base + by_rate + boost Differs from the literal upstream `by_rate = min(by_rate, limit); power = base + by_rate + boost;` which would produce 47+20+2=69 (clamps 63) for OFDM-6M FCC ch6 vs kernel's actual 49. The headroom formulation correctly forces by_rate to 0 when base+boost already exceeds limit, matching kernel canary byte-for-byte across CCK / OFDM / HT_1SS / HT_2SS / VHT_1SS at ch6 FCC. - `RadioManagementModule::PHY_SetTxPowerIndexByRateArray` now passes the *rate's* stream count (-1) as ntx_idx — port of upstream's `phy_get_current_tx_num`. The previous version passed `numTotalRfPath - 1` unconditionally, which made every rate look like its highest-Ntx variant (e.g. OFDM single-stream got OFDM_24G_Diff[A][1] added too, producing wrong base + per-Ntx accumulation). - `RadioManagementModule::PHY_SetTxPowerIndex_8812A` — the `powerIndex -= 1 if odd` workaround at line 1393 is now gated on `!IS_NORMAL_CHIP(version_id)`. Upstream's `rtl8812a_phycfg.c:629` documents this is a TEST-CHIP-only fix for the 8812A/8821A test silicon that didn't accept odd power indexes. Devourer was applying it unconditionally, introducing a systematic -1 across all TX-AGC bytes (canary diff was off by exactly 1 vs kernel before this fix). What's not in this commit ------------------------- - 0xc54 (rA_TxPwrTraing) still uses the old `power` shortcut in `PHY_TxPowerTrainingByPath_8812` — separate function, separate fix. - 5G UNII-2 TX gate at ch100 — independently confirmed (multiple hypotheses refuted) that the per-rate TX-power port doesn't move it. Gate is elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) --- hal/Hal8812a_PhyRegPg.h | 71 +++++ hal/Hal8812a_TxpwrLmt.h | 585 ++++++++++++++++++++++++++++++++++ src/EepromManager.cpp | 351 ++++++++++++++++++++ src/EepromManager.h | 38 +++ src/RadioManagementModule.cpp | 25 +- 5 files changed, 1066 insertions(+), 4 deletions(-) create mode 100644 hal/Hal8812a_PhyRegPg.h create mode 100644 hal/Hal8812a_TxpwrLmt.h diff --git a/hal/Hal8812a_PhyRegPg.h b/hal/Hal8812a_PhyRegPg.h new file mode 100644 index 0000000..df75220 --- /dev/null +++ b/hal/Hal8812a_PhyRegPg.h @@ -0,0 +1,71 @@ +#ifndef HAL_8812A_PHY_REG_PG_H +#define HAL_8812A_PHY_REG_PG_H + +#include + +/* Verbatim copy of `array_mp_8812a_phy_reg_pg` from + * aircrack-ng/rtl8812au@v5.6.4.2_35491.20191025 + * hal/phydm/rtl8812a/halhwimg8812a_bb.c:1012-1058. + * + * Per-rate TX-power table. Each row is 6 entries: + * {band, rfpath, tx_num, addr, bitmask, data} + * - band: 0 = 2.4G, 1 = 5G + * - rfpath: 0 = A, 1 = B + * - tx_num: 0 = 1T, 1 = 2T + * - addr: target BB register (e.g. 0xc24 = OFDM 18/6 power), + * each byte of `data` is the raw power index for one rate + * - bitmask: 0xffffffff for the full 4-byte target + * - data: 4 packed bytes — per-rate target value, parsed by + * `EepromManager::LoadTxPowerByRate` into + * `TxPwrByRateOffset[band][path][rate]`. */ + +static const uint32_t kHal8812aPhyRegPg[] = { + 0, 0, 0, 0x00000c20, 0xffffffff, 0x34363840, + 0, 0, 0, 0x00000c24, 0xffffffff, 0x42424444, + 0, 0, 0, 0x00000c28, 0xffffffff, 0x30323638, + 0, 0, 0, 0x00000c2c, 0xffffffff, 0x40424444, + 0, 0, 0, 0x00000c30, 0xffffffff, 0x28303236, + 0, 0, 1, 0x00000c34, 0xffffffff, 0x38404242, + 0, 0, 1, 0x00000c38, 0xffffffff, 0x26283034, + 0, 0, 0, 0x00000c3c, 0xffffffff, 0x40424444, + 0, 0, 0, 0x00000c40, 0xffffffff, 0x28303236, + 0, 0, 0, 0x00000c44, 0xffffffff, 0x42422426, + 0, 0, 1, 0x00000c48, 0xffffffff, 0x30343840, + 0, 0, 1, 0x00000c4c, 0xffffffff, 0x22242628, + 0, 1, 0, 0x00000e20, 0xffffffff, 0x34363840, + 0, 1, 0, 0x00000e24, 0xffffffff, 0x42424444, + 0, 1, 0, 0x00000e28, 0xffffffff, 0x30323638, + 0, 1, 0, 0x00000e2c, 0xffffffff, 0x40424444, + 0, 1, 0, 0x00000e30, 0xffffffff, 0x28303236, + 0, 1, 1, 0x00000e34, 0xffffffff, 0x38404242, + 0, 1, 1, 0x00000e38, 0xffffffff, 0x26283034, + 0, 1, 0, 0x00000e3c, 0xffffffff, 0x40424444, + 0, 1, 0, 0x00000e40, 0xffffffff, 0x28303236, + 0, 1, 0, 0x00000e44, 0xffffffff, 0x42422426, + 0, 1, 1, 0x00000e48, 0xffffffff, 0x30343840, + 0, 1, 1, 0x00000e4c, 0xffffffff, 0x22242628, + 1, 0, 0, 0x00000c24, 0xffffffff, 0x42424444, + 1, 0, 0, 0x00000c28, 0xffffffff, 0x30323640, + 1, 0, 0, 0x00000c2c, 0xffffffff, 0x40424444, + 1, 0, 0, 0x00000c30, 0xffffffff, 0x28303236, + 1, 0, 1, 0x00000c34, 0xffffffff, 0x38404242, + 1, 0, 1, 0x00000c38, 0xffffffff, 0x26283034, + 1, 0, 0, 0x00000c3c, 0xffffffff, 0x40424444, + 1, 0, 0, 0x00000c40, 0xffffffff, 0x28303236, + 1, 0, 0, 0x00000c44, 0xffffffff, 0x42422426, + 1, 0, 1, 0x00000c48, 0xffffffff, 0x30343840, + 1, 0, 1, 0x00000c4c, 0xffffffff, 0x22242628, + 1, 1, 0, 0x00000e24, 0xffffffff, 0x42424444, + 1, 1, 0, 0x00000e28, 0xffffffff, 0x30323640, + 1, 1, 0, 0x00000e2c, 0xffffffff, 0x40424444, + 1, 1, 0, 0x00000e30, 0xffffffff, 0x28303236, + 1, 1, 1, 0x00000e34, 0xffffffff, 0x38404242, + 1, 1, 1, 0x00000e38, 0xffffffff, 0x26283034, + 1, 1, 0, 0x00000e3c, 0xffffffff, 0x40424444, + 1, 1, 0, 0x00000e40, 0xffffffff, 0x28303236, + 1, 1, 0, 0x00000e44, 0xffffffff, 0x42422426, + 1, 1, 1, 0x00000e48, 0xffffffff, 0x30343840, + 1, 1, 1, 0x00000e4c, 0xffffffff, 0x22242628 +}; + +#endif /* HAL_8812A_PHY_REG_PG_H */ diff --git a/hal/Hal8812a_TxpwrLmt.h b/hal/Hal8812a_TxpwrLmt.h new file mode 100644 index 0000000..b0a4dd0 --- /dev/null +++ b/hal/Hal8812a_TxpwrLmt.h @@ -0,0 +1,585 @@ +#ifndef HAL_8812A_TXPWR_LMT_H +#define HAL_8812A_TXPWR_LMT_H + +/* Verbatim copy of `array_mp_8812a_txpwr_lmt` from + * aircrack-ng/rtl8812au@v5.6.4.2_35491.20191025 + * hal/phydm/rtl8812a/halhwimg8812a_rf.c:1371-1937. + * + * Per-region (FCC/ETSI/MKK) regulatory TX-power limit table. Each row is + * 7 entries: regulation, band, bandwidth, rate_section, ntx, channel, + * power_index_limit. Parsed by `EepromManager::LoadTxPowerLimit` which + * builds the 5D lookup `TxPwrLimit[regulation][band][bw][rs][ntx][ch]` + * used by `GetTxPowerIndexBase` to cap `by_rate_diff` per regulatory + * region — port of upstream `PHY_GetTxPowerLimit` from hal_com_phycfg.c. */ + +static const char *kHal8812aTxpwrLmt[] = { + + "FCC", "2.4G", "20M", "CCK", "1T", "01", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "01", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "01", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "02", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "02", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "02", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "03", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "03", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "03", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "04", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "04", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "04", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "05", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "05", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "05", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "06", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "06", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "06", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "07", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "07", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "07", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "08", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "08", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "08", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "09", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "09", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "09", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "10", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "10", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "10", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "11", "36", + "ETSI", "2.4G", "20M", "CCK", "1T", "11", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "11", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "12", "63", + "ETSI", "2.4G", "20M", "CCK", "1T", "12", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "12", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "13", "63", + "ETSI", "2.4G", "20M", "CCK", "1T", "13", "32", + "MKK", "2.4G", "20M", "CCK", "1T", "13", "32", + "FCC", "2.4G", "20M", "CCK", "1T", "14", "63", + "ETSI", "2.4G", "20M", "CCK", "1T", "14", "63", + "MKK", "2.4G", "20M", "CCK", "1T", "14", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "01", "34", + "ETSI", "2.4G", "20M", "OFDM", "1T", "01", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "01", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "02", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "02", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "02", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "03", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "03", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "03", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "04", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "04", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "04", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "05", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "05", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "05", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "06", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "06", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "06", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "07", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "07", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "07", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "08", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "08", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "08", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "09", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "09", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "09", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "10", "36", + "ETSI", "2.4G", "20M", "OFDM", "1T", "10", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "10", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "11", "32", + "ETSI", "2.4G", "20M", "OFDM", "1T", "11", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "11", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "12", "63", + "ETSI", "2.4G", "20M", "OFDM", "1T", "12", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "12", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "13", "63", + "ETSI", "2.4G", "20M", "OFDM", "1T", "13", "32", + "MKK", "2.4G", "20M", "OFDM", "1T", "13", "32", + "FCC", "2.4G", "20M", "OFDM", "1T", "14", "63", + "ETSI", "2.4G", "20M", "OFDM", "1T", "14", "63", + "MKK", "2.4G", "20M", "OFDM", "1T", "14", "63", + "FCC", "2.4G", "20M", "HT", "1T", "01", "34", + "ETSI", "2.4G", "20M", "HT", "1T", "01", "32", + "MKK", "2.4G", "20M", "HT", "1T", "01", "32", + "FCC", "2.4G", "20M", "HT", "1T", "02", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "02", "32", + "MKK", "2.4G", "20M", "HT", "1T", "02", "32", + "FCC", "2.4G", "20M", "HT", "1T", "03", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "03", "32", + "MKK", "2.4G", "20M", "HT", "1T", "03", "32", + "FCC", "2.4G", "20M", "HT", "1T", "04", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "04", "32", + "MKK", "2.4G", "20M", "HT", "1T", "04", "32", + "FCC", "2.4G", "20M", "HT", "1T", "05", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "05", "32", + "MKK", "2.4G", "20M", "HT", "1T", "05", "32", + "FCC", "2.4G", "20M", "HT", "1T", "06", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "06", "32", + "MKK", "2.4G", "20M", "HT", "1T", "06", "32", + "FCC", "2.4G", "20M", "HT", "1T", "07", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "07", "32", + "MKK", "2.4G", "20M", "HT", "1T", "07", "32", + "FCC", "2.4G", "20M", "HT", "1T", "08", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "08", "32", + "MKK", "2.4G", "20M", "HT", "1T", "08", "32", + "FCC", "2.4G", "20M", "HT", "1T", "09", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "09", "32", + "MKK", "2.4G", "20M", "HT", "1T", "09", "32", + "FCC", "2.4G", "20M", "HT", "1T", "10", "36", + "ETSI", "2.4G", "20M", "HT", "1T", "10", "32", + "MKK", "2.4G", "20M", "HT", "1T", "10", "32", + "FCC", "2.4G", "20M", "HT", "1T", "11", "32", + "ETSI", "2.4G", "20M", "HT", "1T", "11", "32", + "MKK", "2.4G", "20M", "HT", "1T", "11", "32", + "FCC", "2.4G", "20M", "HT", "1T", "12", "63", + "ETSI", "2.4G", "20M", "HT", "1T", "12", "32", + "MKK", "2.4G", "20M", "HT", "1T", "12", "32", + "FCC", "2.4G", "20M", "HT", "1T", "13", "63", + "ETSI", "2.4G", "20M", "HT", "1T", "13", "32", + "MKK", "2.4G", "20M", "HT", "1T", "13", "32", + "FCC", "2.4G", "20M", "HT", "1T", "14", "63", + "ETSI", "2.4G", "20M", "HT", "1T", "14", "63", + "MKK", "2.4G", "20M", "HT", "1T", "14", "63", + "FCC", "2.4G", "20M", "HT", "2T", "01", "32", + "ETSI", "2.4G", "20M", "HT", "2T", "01", "32", + "MKK", "2.4G", "20M", "HT", "2T", "01", "32", + "FCC", "2.4G", "20M", "HT", "2T", "02", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "02", "32", + "MKK", "2.4G", "20M", "HT", "2T", "02", "32", + "FCC", "2.4G", "20M", "HT", "2T", "03", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "03", "32", + "MKK", "2.4G", "20M", "HT", "2T", "03", "32", + "FCC", "2.4G", "20M", "HT", "2T", "04", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "04", "32", + "MKK", "2.4G", "20M", "HT", "2T", "04", "32", + "FCC", "2.4G", "20M", "HT", "2T", "05", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "05", "32", + "MKK", "2.4G", "20M", "HT", "2T", "05", "32", + "FCC", "2.4G", "20M", "HT", "2T", "06", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "06", "32", + "MKK", "2.4G", "20M", "HT", "2T", "06", "32", + "FCC", "2.4G", "20M", "HT", "2T", "07", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "07", "32", + "MKK", "2.4G", "20M", "HT", "2T", "07", "32", + "FCC", "2.4G", "20M", "HT", "2T", "08", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "08", "32", + "MKK", "2.4G", "20M", "HT", "2T", "08", "32", + "FCC", "2.4G", "20M", "HT", "2T", "09", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "09", "32", + "MKK", "2.4G", "20M", "HT", "2T", "09", "32", + "FCC", "2.4G", "20M", "HT", "2T", "10", "34", + "ETSI", "2.4G", "20M", "HT", "2T", "10", "32", + "MKK", "2.4G", "20M", "HT", "2T", "10", "32", + "FCC", "2.4G", "20M", "HT", "2T", "11", "30", + "ETSI", "2.4G", "20M", "HT", "2T", "11", "32", + "MKK", "2.4G", "20M", "HT", "2T", "11", "32", + "FCC", "2.4G", "20M", "HT", "2T", "12", "63", + "ETSI", "2.4G", "20M", "HT", "2T", "12", "32", + "MKK", "2.4G", "20M", "HT", "2T", "12", "32", + "FCC", "2.4G", "20M", "HT", "2T", "13", "63", + "ETSI", "2.4G", "20M", "HT", "2T", "13", "32", + "MKK", "2.4G", "20M", "HT", "2T", "13", "32", + "FCC", "2.4G", "20M", "HT", "2T", "14", "63", + "ETSI", "2.4G", "20M", "HT", "2T", "14", "63", + "MKK", "2.4G", "20M", "HT", "2T", "14", "63", + "FCC", "2.4G", "40M", "HT", "1T", "01", "63", + "ETSI", "2.4G", "40M", "HT", "1T", "01", "63", + "MKK", "2.4G", "40M", "HT", "1T", "01", "63", + "FCC", "2.4G", "40M", "HT", "1T", "02", "63", + "ETSI", "2.4G", "40M", "HT", "1T", "02", "63", + "MKK", "2.4G", "40M", "HT", "1T", "02", "63", + "FCC", "2.4G", "40M", "HT", "1T", "03", "32", + "ETSI", "2.4G", "40M", "HT", "1T", "03", "32", + "MKK", "2.4G", "40M", "HT", "1T", "03", "32", + "FCC", "2.4G", "40M", "HT", "1T", "04", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "04", "32", + "MKK", "2.4G", "40M", "HT", "1T", "04", "32", + "FCC", "2.4G", "40M", "HT", "1T", "05", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "05", "32", + "MKK", "2.4G", "40M", "HT", "1T", "05", "32", + "FCC", "2.4G", "40M", "HT", "1T", "06", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "06", "32", + "MKK", "2.4G", "40M", "HT", "1T", "06", "32", + "FCC", "2.4G", "40M", "HT", "1T", "07", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "07", "32", + "MKK", "2.4G", "40M", "HT", "1T", "07", "32", + "FCC", "2.4G", "40M", "HT", "1T", "08", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "08", "32", + "MKK", "2.4G", "40M", "HT", "1T", "08", "32", + "FCC", "2.4G", "40M", "HT", "1T", "09", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "09", "32", + "MKK", "2.4G", "40M", "HT", "1T", "09", "32", + "FCC", "2.4G", "40M", "HT", "1T", "10", "36", + "ETSI", "2.4G", "40M", "HT", "1T", "10", "32", + "MKK", "2.4G", "40M", "HT", "1T", "10", "32", + "FCC", "2.4G", "40M", "HT", "1T", "11", "32", + "ETSI", "2.4G", "40M", "HT", "1T", "11", "32", + "MKK", "2.4G", "40M", "HT", "1T", "11", "32", + "FCC", "2.4G", "40M", "HT", "1T", "12", "63", + "ETSI", "2.4G", "40M", "HT", "1T", "12", "32", + "MKK", "2.4G", "40M", "HT", "1T", "12", "32", + "FCC", "2.4G", "40M", "HT", "1T", "13", "63", + "ETSI", "2.4G", "40M", "HT", "1T", "13", "32", + "MKK", "2.4G", "40M", "HT", "1T", "13", "32", + "FCC", "2.4G", "40M", "HT", "1T", "14", "63", + "ETSI", "2.4G", "40M", "HT", "1T", "14", "63", + "MKK", "2.4G", "40M", "HT", "1T", "14", "63", + "FCC", "2.4G", "40M", "HT", "2T", "01", "63", + "ETSI", "2.4G", "40M", "HT", "2T", "01", "63", + "MKK", "2.4G", "40M", "HT", "2T", "01", "63", + "FCC", "2.4G", "40M", "HT", "2T", "02", "63", + "ETSI", "2.4G", "40M", "HT", "2T", "02", "63", + "MKK", "2.4G", "40M", "HT", "2T", "02", "63", + "FCC", "2.4G", "40M", "HT", "2T", "03", "30", + "ETSI", "2.4G", "40M", "HT", "2T", "03", "30", + "MKK", "2.4G", "40M", "HT", "2T", "03", "30", + "FCC", "2.4G", "40M", "HT", "2T", "04", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "04", "30", + "MKK", "2.4G", "40M", "HT", "2T", "04", "30", + "FCC", "2.4G", "40M", "HT", "2T", "05", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "05", "30", + "MKK", "2.4G", "40M", "HT", "2T", "05", "30", + "FCC", "2.4G", "40M", "HT", "2T", "06", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "06", "30", + "MKK", "2.4G", "40M", "HT", "2T", "06", "30", + "FCC", "2.4G", "40M", "HT", "2T", "07", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "07", "30", + "MKK", "2.4G", "40M", "HT", "2T", "07", "30", + "FCC", "2.4G", "40M", "HT", "2T", "08", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "08", "30", + "MKK", "2.4G", "40M", "HT", "2T", "08", "30", + "FCC", "2.4G", "40M", "HT", "2T", "09", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "09", "30", + "MKK", "2.4G", "40M", "HT", "2T", "09", "30", + "FCC", "2.4G", "40M", "HT", "2T", "10", "34", + "ETSI", "2.4G", "40M", "HT", "2T", "10", "30", + "MKK", "2.4G", "40M", "HT", "2T", "10", "30", + "FCC", "2.4G", "40M", "HT", "2T", "11", "30", + "ETSI", "2.4G", "40M", "HT", "2T", "11", "30", + "MKK", "2.4G", "40M", "HT", "2T", "11", "30", + "FCC", "2.4G", "40M", "HT", "2T", "12", "63", + "ETSI", "2.4G", "40M", "HT", "2T", "12", "32", + "MKK", "2.4G", "40M", "HT", "2T", "12", "32", + "FCC", "2.4G", "40M", "HT", "2T", "13", "63", + "ETSI", "2.4G", "40M", "HT", "2T", "13", "32", + "MKK", "2.4G", "40M", "HT", "2T", "13", "32", + "FCC", "2.4G", "40M", "HT", "2T", "14", "63", + "ETSI", "2.4G", "40M", "HT", "2T", "14", "63", + "MKK", "2.4G", "40M", "HT", "2T", "14", "63", + "FCC", "5G", "20M", "OFDM", "1T", "36", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "36", "32", + "MKK", "5G", "20M", "OFDM", "1T", "36", "32", + "FCC", "5G", "20M", "OFDM", "1T", "40", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "40", "32", + "MKK", "5G", "20M", "OFDM", "1T", "40", "32", + "FCC", "5G", "20M", "OFDM", "1T", "44", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "44", "32", + "MKK", "5G", "20M", "OFDM", "1T", "44", "32", + "FCC", "5G", "20M", "OFDM", "1T", "48", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "48", "32", + "MKK", "5G", "20M", "OFDM", "1T", "48", "32", + "FCC", "5G", "20M", "OFDM", "1T", "52", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "52", "32", + "MKK", "5G", "20M", "OFDM", "1T", "52", "32", + "FCC", "5G", "20M", "OFDM", "1T", "56", "34", + "ETSI", "5G", "20M", "OFDM", "1T", "56", "32", + "MKK", "5G", "20M", "OFDM", "1T", "56", "32", + "FCC", "5G", "20M", "OFDM", "1T", "60", "32", + "ETSI", "5G", "20M", "OFDM", "1T", "60", "32", + "MKK", "5G", "20M", "OFDM", "1T", "60", "32", + "FCC", "5G", "20M", "OFDM", "1T", "64", "28", + "ETSI", "5G", "20M", "OFDM", "1T", "64", "32", + "MKK", "5G", "20M", "OFDM", "1T", "64", "32", + "FCC", "5G", "20M", "OFDM", "1T", "100", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "100", "32", + "MKK", "5G", "20M", "OFDM", "1T", "100", "32", + "FCC", "5G", "20M", "OFDM", "1T", "104", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "104", "32", + "MKK", "5G", "20M", "OFDM", "1T", "104", "32", + "FCC", "5G", "20M", "OFDM", "1T", "108", "32", + "ETSI", "5G", "20M", "OFDM", "1T", "108", "32", + "MKK", "5G", "20M", "OFDM", "1T", "108", "32", + "FCC", "5G", "20M", "OFDM", "1T", "112", "34", + "ETSI", "5G", "20M", "OFDM", "1T", "112", "32", + "MKK", "5G", "20M", "OFDM", "1T", "112", "32", + "FCC", "5G", "20M", "OFDM", "1T", "116", "34", + "ETSI", "5G", "20M", "OFDM", "1T", "116", "32", + "MKK", "5G", "20M", "OFDM", "1T", "116", "32", + "FCC", "5G", "20M", "OFDM", "1T", "120", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "120", "32", + "MKK", "5G", "20M", "OFDM", "1T", "120", "32", + "FCC", "5G", "20M", "OFDM", "1T", "124", "34", + "ETSI", "5G", "20M", "OFDM", "1T", "124", "32", + "MKK", "5G", "20M", "OFDM", "1T", "124", "32", + "FCC", "5G", "20M", "OFDM", "1T", "128", "32", + "ETSI", "5G", "20M", "OFDM", "1T", "128", "32", + "MKK", "5G", "20M", "OFDM", "1T", "128", "32", + "FCC", "5G", "20M", "OFDM", "1T", "132", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "132", "32", + "MKK", "5G", "20M", "OFDM", "1T", "132", "32", + "FCC", "5G", "20M", "OFDM", "1T", "136", "30", + "ETSI", "5G", "20M", "OFDM", "1T", "136", "32", + "MKK", "5G", "20M", "OFDM", "1T", "136", "32", + "FCC", "5G", "20M", "OFDM", "1T", "140", "28", + "ETSI", "5G", "20M", "OFDM", "1T", "140", "32", + "MKK", "5G", "20M", "OFDM", "1T", "140", "32", + "FCC", "5G", "20M", "OFDM", "1T", "149", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "149", "32", + "MKK", "5G", "20M", "OFDM", "1T", "149", "63", + "FCC", "5G", "20M", "OFDM", "1T", "153", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "153", "32", + "MKK", "5G", "20M", "OFDM", "1T", "153", "63", + "FCC", "5G", "20M", "OFDM", "1T", "157", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "157", "32", + "MKK", "5G", "20M", "OFDM", "1T", "157", "63", + "FCC", "5G", "20M", "OFDM", "1T", "161", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "161", "32", + "MKK", "5G", "20M", "OFDM", "1T", "161", "63", + "FCC", "5G", "20M", "OFDM", "1T", "165", "36", + "ETSI", "5G", "20M", "OFDM", "1T", "165", "32", + "MKK", "5G", "20M", "OFDM", "1T", "165", "63", + "FCC", "5G", "20M", "HT", "1T", "36", "30", + "ETSI", "5G", "20M", "HT", "1T", "36", "32", + "MKK", "5G", "20M", "HT", "1T", "36", "32", + "FCC", "5G", "20M", "HT", "1T", "40", "30", + "ETSI", "5G", "20M", "HT", "1T", "40", "32", + "MKK", "5G", "20M", "HT", "1T", "40", "32", + "FCC", "5G", "20M", "HT", "1T", "44", "30", + "ETSI", "5G", "20M", "HT", "1T", "44", "32", + "MKK", "5G", "20M", "HT", "1T", "44", "32", + "FCC", "5G", "20M", "HT", "1T", "48", "30", + "ETSI", "5G", "20M", "HT", "1T", "48", "32", + "MKK", "5G", "20M", "HT", "1T", "48", "32", + "FCC", "5G", "20M", "HT", "1T", "52", "36", + "ETSI", "5G", "20M", "HT", "1T", "52", "32", + "MKK", "5G", "20M", "HT", "1T", "52", "32", + "FCC", "5G", "20M", "HT", "1T", "56", "34", + "ETSI", "5G", "20M", "HT", "1T", "56", "32", + "MKK", "5G", "20M", "HT", "1T", "56", "32", + "FCC", "5G", "20M", "HT", "1T", "60", "32", + "ETSI", "5G", "20M", "HT", "1T", "60", "32", + "MKK", "5G", "20M", "HT", "1T", "60", "32", + "FCC", "5G", "20M", "HT", "1T", "64", "28", + "ETSI", "5G", "20M", "HT", "1T", "64", "32", + "MKK", "5G", "20M", "HT", "1T", "64", "32", + "FCC", "5G", "20M", "HT", "1T", "100", "30", + "ETSI", "5G", "20M", "HT", "1T", "100", "32", + "MKK", "5G", "20M", "HT", "1T", "100", "32", + "FCC", "5G", "20M", "HT", "1T", "104", "30", + "ETSI", "5G", "20M", "HT", "1T", "104", "32", + "MKK", "5G", "20M", "HT", "1T", "104", "32", + "FCC", "5G", "20M", "HT", "1T", "108", "32", + "ETSI", "5G", "20M", "HT", "1T", "108", "32", + "MKK", "5G", "20M", "HT", "1T", "108", "32", + "FCC", "5G", "20M", "HT", "1T", "112", "34", + "ETSI", "5G", "20M", "HT", "1T", "112", "32", + "MKK", "5G", "20M", "HT", "1T", "112", "32", + "FCC", "5G", "20M", "HT", "1T", "116", "34", + "ETSI", "5G", "20M", "HT", "1T", "116", "32", + "MKK", "5G", "20M", "HT", "1T", "116", "32", + "FCC", "5G", "20M", "HT", "1T", "120", "36", + "ETSI", "5G", "20M", "HT", "1T", "120", "32", + "MKK", "5G", "20M", "HT", "1T", "120", "32", + "FCC", "5G", "20M", "HT", "1T", "124", "34", + "ETSI", "5G", "20M", "HT", "1T", "124", "32", + "MKK", "5G", "20M", "HT", "1T", "124", "32", + "FCC", "5G", "20M", "HT", "1T", "128", "32", + "ETSI", "5G", "20M", "HT", "1T", "128", "32", + "MKK", "5G", "20M", "HT", "1T", "128", "32", + "FCC", "5G", "20M", "HT", "1T", "132", "30", + "ETSI", "5G", "20M", "HT", "1T", "132", "32", + "MKK", "5G", "20M", "HT", "1T", "132", "32", + "FCC", "5G", "20M", "HT", "1T", "136", "30", + "ETSI", "5G", "20M", "HT", "1T", "136", "32", + "MKK", "5G", "20M", "HT", "1T", "136", "32", + "FCC", "5G", "20M", "HT", "1T", "140", "28", + "ETSI", "5G", "20M", "HT", "1T", "140", "32", + "MKK", "5G", "20M", "HT", "1T", "140", "32", + "FCC", "5G", "20M", "HT", "1T", "149", "36", + "ETSI", "5G", "20M", "HT", "1T", "149", "32", + "MKK", "5G", "20M", "HT", "1T", "149", "63", + "FCC", "5G", "20M", "HT", "1T", "153", "36", + "ETSI", "5G", "20M", "HT", "1T", "153", "32", + "MKK", "5G", "20M", "HT", "1T", "153", "63", + "FCC", "5G", "20M", "HT", "1T", "157", "36", + "ETSI", "5G", "20M", "HT", "1T", "157", "32", + "MKK", "5G", "20M", "HT", "1T", "157", "63", + "FCC", "5G", "20M", "HT", "1T", "161", "36", + "ETSI", "5G", "20M", "HT", "1T", "161", "32", + "MKK", "5G", "20M", "HT", "1T", "161", "63", + "FCC", "5G", "20M", "HT", "1T", "165", "36", + "ETSI", "5G", "20M", "HT", "1T", "165", "32", + "MKK", "5G", "20M", "HT", "1T", "165", "63", + "FCC", "5G", "20M", "HT", "2T", "36", "28", + "ETSI", "5G", "20M", "HT", "2T", "36", "30", + "MKK", "5G", "20M", "HT", "2T", "36", "30", + "FCC", "5G", "20M", "HT", "2T", "40", "28", + "ETSI", "5G", "20M", "HT", "2T", "40", "30", + "MKK", "5G", "20M", "HT", "2T", "40", "30", + "FCC", "5G", "20M", "HT", "2T", "44", "28", + "ETSI", "5G", "20M", "HT", "2T", "44", "30", + "MKK", "5G", "20M", "HT", "2T", "44", "30", + "FCC", "5G", "20M", "HT", "2T", "48", "28", + "ETSI", "5G", "20M", "HT", "2T", "48", "30", + "MKK", "5G", "20M", "HT", "2T", "48", "30", + "FCC", "5G", "20M", "HT", "2T", "52", "34", + "ETSI", "5G", "20M", "HT", "2T", "52", "30", + "MKK", "5G", "20M", "HT", "2T", "52", "30", + "FCC", "5G", "20M", "HT", "2T", "56", "32", + "ETSI", "5G", "20M", "HT", "2T", "56", "30", + "MKK", "5G", "20M", "HT", "2T", "56", "30", + "FCC", "5G", "20M", "HT", "2T", "60", "30", + "ETSI", "5G", "20M", "HT", "2T", "60", "30", + "MKK", "5G", "20M", "HT", "2T", "60", "30", + "FCC", "5G", "20M", "HT", "2T", "64", "26", + "ETSI", "5G", "20M", "HT", "2T", "64", "30", + "MKK", "5G", "20M", "HT", "2T", "64", "30", + "FCC", "5G", "20M", "HT", "2T", "100", "28", + "ETSI", "5G", "20M", "HT", "2T", "100", "30", + "MKK", "5G", "20M", "HT", "2T", "100", "30", + "FCC", "5G", "20M", "HT", "2T", "104", "28", + "ETSI", "5G", "20M", "HT", "2T", "104", "30", + "MKK", "5G", "20M", "HT", "2T", "104", "30", + "FCC", "5G", "20M", "HT", "2T", "108", "30", + "ETSI", "5G", "20M", "HT", "2T", "108", "30", + "MKK", "5G", "20M", "HT", "2T", "108", "30", + "FCC", "5G", "20M", "HT", "2T", "112", "32", + "ETSI", "5G", "20M", "HT", "2T", "112", "30", + "MKK", "5G", "20M", "HT", "2T", "112", "30", + "FCC", "5G", "20M", "HT", "2T", "116", "32", + "ETSI", "5G", "20M", "HT", "2T", "116", "30", + "MKK", "5G", "20M", "HT", "2T", "116", "30", + "FCC", "5G", "20M", "HT", "2T", "120", "34", + "ETSI", "5G", "20M", "HT", "2T", "120", "30", + "MKK", "5G", "20M", "HT", "2T", "120", "30", + "FCC", "5G", "20M", "HT", "2T", "124", "32", + "ETSI", "5G", "20M", "HT", "2T", "124", "30", + "MKK", "5G", "20M", "HT", "2T", "124", "30", + "FCC", "5G", "20M", "HT", "2T", "128", "30", + "ETSI", "5G", "20M", "HT", "2T", "128", "30", + "MKK", "5G", "20M", "HT", "2T", "128", "30", + "FCC", "5G", "20M", "HT", "2T", "132", "28", + "ETSI", "5G", "20M", "HT", "2T", "132", "30", + "MKK", "5G", "20M", "HT", "2T", "132", "30", + "FCC", "5G", "20M", "HT", "2T", "136", "28", + "ETSI", "5G", "20M", "HT", "2T", "136", "30", + "MKK", "5G", "20M", "HT", "2T", "136", "30", + "FCC", "5G", "20M", "HT", "2T", "140", "26", + "ETSI", "5G", "20M", "HT", "2T", "140", "30", + "MKK", "5G", "20M", "HT", "2T", "140", "30", + "FCC", "5G", "20M", "HT", "2T", "149", "34", + "ETSI", "5G", "20M", "HT", "2T", "149", "30", + "MKK", "5G", "20M", "HT", "2T", "149", "63", + "FCC", "5G", "20M", "HT", "2T", "153", "34", + "ETSI", "5G", "20M", "HT", "2T", "153", "30", + "MKK", "5G", "20M", "HT", "2T", "153", "63", + "FCC", "5G", "20M", "HT", "2T", "157", "34", + "ETSI", "5G", "20M", "HT", "2T", "157", "30", + "MKK", "5G", "20M", "HT", "2T", "157", "63", + "FCC", "5G", "20M", "HT", "2T", "161", "34", + "ETSI", "5G", "20M", "HT", "2T", "161", "30", + "MKK", "5G", "20M", "HT", "2T", "161", "63", + "FCC", "5G", "20M", "HT", "2T", "165", "34", + "ETSI", "5G", "20M", "HT", "2T", "165", "30", + "MKK", "5G", "20M", "HT", "2T", "165", "63", + "FCC", "5G", "40M", "HT", "1T", "38", "30", + "ETSI", "5G", "40M", "HT", "1T", "38", "32", + "MKK", "5G", "40M", "HT", "1T", "38", "32", + "FCC", "5G", "40M", "HT", "1T", "46", "30", + "ETSI", "5G", "40M", "HT", "1T", "46", "32", + "MKK", "5G", "40M", "HT", "1T", "46", "32", + "FCC", "5G", "40M", "HT", "1T", "54", "32", + "ETSI", "5G", "40M", "HT", "1T", "54", "32", + "MKK", "5G", "40M", "HT", "1T", "54", "32", + "FCC", "5G", "40M", "HT", "1T", "62", "32", + "ETSI", "5G", "40M", "HT", "1T", "62", "32", + "MKK", "5G", "40M", "HT", "1T", "62", "32", + "FCC", "5G", "40M", "HT", "1T", "102", "28", + "ETSI", "5G", "40M", "HT", "1T", "102", "32", + "MKK", "5G", "40M", "HT", "1T", "102", "32", + "FCC", "5G", "40M", "HT", "1T", "110", "32", + "ETSI", "5G", "40M", "HT", "1T", "110", "32", + "MKK", "5G", "40M", "HT", "1T", "110", "32", + "FCC", "5G", "40M", "HT", "1T", "118", "36", + "ETSI", "5G", "40M", "HT", "1T", "118", "32", + "MKK", "5G", "40M", "HT", "1T", "118", "32", + "FCC", "5G", "40M", "HT", "1T", "126", "34", + "ETSI", "5G", "40M", "HT", "1T", "126", "32", + "MKK", "5G", "40M", "HT", "1T", "126", "32", + "FCC", "5G", "40M", "HT", "1T", "134", "32", + "ETSI", "5G", "40M", "HT", "1T", "134", "32", + "MKK", "5G", "40M", "HT", "1T", "134", "32", + "FCC", "5G", "40M", "HT", "1T", "151", "36", + "ETSI", "5G", "40M", "HT", "1T", "151", "32", + "MKK", "5G", "40M", "HT", "1T", "151", "63", + "FCC", "5G", "40M", "HT", "1T", "159", "36", + "ETSI", "5G", "40M", "HT", "1T", "159", "32", + "MKK", "5G", "40M", "HT", "1T", "159", "63", + "FCC", "5G", "40M", "HT", "2T", "38", "28", + "ETSI", "5G", "40M", "HT", "2T", "38", "30", + "MKK", "5G", "40M", "HT", "2T", "38", "30", + "FCC", "5G", "40M", "HT", "2T", "46", "28", + "ETSI", "5G", "40M", "HT", "2T", "46", "30", + "MKK", "5G", "40M", "HT", "2T", "46", "30", + "FCC", "5G", "40M", "HT", "2T", "54", "30", + "ETSI", "5G", "40M", "HT", "2T", "54", "30", + "MKK", "5G", "40M", "HT", "2T", "54", "30", + "FCC", "5G", "40M", "HT", "2T", "62", "30", + "ETSI", "5G", "40M", "HT", "2T", "62", "30", + "MKK", "5G", "40M", "HT", "2T", "62", "30", + "FCC", "5G", "40M", "HT", "2T", "102", "26", + "ETSI", "5G", "40M", "HT", "2T", "102", "30", + "MKK", "5G", "40M", "HT", "2T", "102", "30", + "FCC", "5G", "40M", "HT", "2T", "110", "30", + "ETSI", "5G", "40M", "HT", "2T", "110", "30", + "MKK", "5G", "40M", "HT", "2T", "110", "30", + "FCC", "5G", "40M", "HT", "2T", "118", "34", + "ETSI", "5G", "40M", "HT", "2T", "118", "30", + "MKK", "5G", "40M", "HT", "2T", "118", "30", + "FCC", "5G", "40M", "HT", "2T", "126", "32", + "ETSI", "5G", "40M", "HT", "2T", "126", "30", + "MKK", "5G", "40M", "HT", "2T", "126", "30", + "FCC", "5G", "40M", "HT", "2T", "134", "30", + "ETSI", "5G", "40M", "HT", "2T", "134", "30", + "MKK", "5G", "40M", "HT", "2T", "134", "30", + "FCC", "5G", "40M", "HT", "2T", "151", "34", + "ETSI", "5G", "40M", "HT", "2T", "151", "30", + "MKK", "5G", "40M", "HT", "2T", "151", "63", + "FCC", "5G", "40M", "HT", "2T", "159", "34", + "ETSI", "5G", "40M", "HT", "2T", "159", "30", + "MKK", "5G", "40M", "HT", "2T", "159", "63", + "FCC", "5G", "80M", "VHT", "1T", "42", "30", + "ETSI", "5G", "80M", "VHT", "1T", "42", "32", + "MKK", "5G", "80M", "VHT", "1T", "42", "32", + "FCC", "5G", "80M", "VHT", "1T", "58", "28", + "ETSI", "5G", "80M", "VHT", "1T", "58", "32", + "MKK", "5G", "80M", "VHT", "1T", "58", "32", + "FCC", "5G", "80M", "VHT", "1T", "106", "30", + "ETSI", "5G", "80M", "VHT", "1T", "106", "32", + "MKK", "5G", "80M", "VHT", "1T", "106", "32", + "FCC", "5G", "80M", "VHT", "1T", "122", "34", + "ETSI", "5G", "80M", "VHT", "1T", "122", "32", + "MKK", "5G", "80M", "VHT", "1T", "122", "32", + "FCC", "5G", "80M", "VHT", "1T", "155", "36", + "ETSI", "5G", "80M", "VHT", "1T", "155", "32", + "MKK", "5G", "80M", "VHT", "1T", "155", "63", + "FCC", "5G", "80M", "VHT", "2T", "42", "28", + "ETSI", "5G", "80M", "VHT", "2T", "42", "30", + "MKK", "5G", "80M", "VHT", "2T", "42", "30", + "FCC", "5G", "80M", "VHT", "2T", "58", "26", + "ETSI", "5G", "80M", "VHT", "2T", "58", "30", + "MKK", "5G", "80M", "VHT", "2T", "58", "30", + "FCC", "5G", "80M", "VHT", "2T", "106", "28", + "ETSI", "5G", "80M", "VHT", "2T", "106", "30", + "MKK", "5G", "80M", "VHT", "2T", "106", "30", + "FCC", "5G", "80M", "VHT", "2T", "122", "32", + "ETSI", "5G", "80M", "VHT", "2T", "122", "30", + "MKK", "5G", "80M", "VHT", "2T", "122", "30", + "FCC", "5G", "80M", "VHT", "2T", "155", "34", + "ETSI", "5G", "80M", "VHT", "2T", "155", "30", + "MKK", "5G", "80M", "VHT", "2T", "155", "63" + + +}; + +#endif /* HAL_8812A_TXPWR_LMT_H */ diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index cee16c9..46a33e7 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -6,6 +6,8 @@ #include "rtw_efuse.h" #include +#include +#include /* strcasecmp */ EepromManager::EepromManager(RtlUsbAdapter device, Logger_t logger) : _device{device}, _logger{logger} { @@ -34,6 +36,8 @@ EepromManager::EepromManager(RtlUsbAdapter device, Logger_t logger) * RadioManagementModule can compute per-rate TX power instead of using * the uniform `SetTxPower(N)` shortcut. */ LoadTxPowerInfo(); + LoadTxPowerByRate(); + LoadTxPowerLimit(); /* */ /* Read Bluetooth co-exist and initialize */ @@ -209,6 +213,129 @@ void EepromManager::read_chip_version_8812a(RtlUsbAdapter device) { dump_chip_info(version_id); } +/* Includes for the embedded upstream tables. */ +#include "../hal/Hal8812a_PhyRegPg.h" +#include "../hal/Hal8812a_TxpwrLmt.h" + +namespace { + +/* Rate → flat-index map for TxPwrByRateOffset. Mirrors upstream + * `PHY_GetRateIndexOfTxPowerByRate`. -1 = rate not in table. */ +int8_t rate_to_idx(uint8_t rate) { + switch (rate) { + /* CCK */ + case 0x02: return 0; /* MGN_1M */ + case 0x04: return 1; /* MGN_2M */ + case 0x0B: return 2; /* MGN_5_5M */ + case 0x16: return 3; /* MGN_11M */ + /* OFDM */ + case 0x0C: return 4; /* MGN_6M */ + case 0x12: return 5; /* MGN_9M */ + case 0x18: return 6; /* MGN_12M */ + case 0x24: return 7; /* MGN_18M */ + case 0x30: return 8; /* MGN_24M */ + case 0x48: return 9; /* MGN_36M */ + case 0x60: return 10; /* MGN_48M */ + case 0x6C: return 11; /* MGN_54M */ + default: + /* MGN_MCS0..31 = 0x80..0x9F → idx 12..43. */ + if (rate >= 0x80 && rate <= 0x9F) + return static_cast(12 + (rate - 0x80)); + /* MGN_VHT1SS_MCS0..VHT4SS_MCS9 = 0xA0..0xC7 → idx 44..83. */ + if (rate >= 0xA0 && rate <= 0xC7) + return static_cast(44 + (rate - 0xA0)); + return -1; + } +} + +/* RATE_SECTION enum from upstream `phydm_pre_define.h`. */ +enum { RS_CCK = 0, RS_OFDM, RS_HT_1SS, RS_HT_2SS, RS_HT_3SS, RS_HT_4SS, + RS_VHT_1SS, RS_VHT_2SS, RS_VHT_3SS, RS_VHT_4SS }; + +/* (RegAddr → 4 rates) port of upstream `PHY_GetRateValuesOfTxPowerByRate`. + * Only the regs that appear in `kHal8812aPhyRegPg` for 8812 are mapped. */ +struct RateRow { + uint16_t reg; + uint8_t rates[4]; + uint8_t count; +}; +static const RateRow kRateMap[] = { + /* path-A */ + {0xc20, {0x02, 0x04, 0x0B, 0x16}, 4}, /* CCK 1/2/5.5/11 */ + {0xc24, {0x0C, 0x12, 0x18, 0x24}, 4}, /* OFDM 6/9/12/18 */ + {0xc28, {0x30, 0x48, 0x60, 0x6C}, 4}, /* OFDM 24/36/48/54 */ + {0xc2c, {0x80, 0x81, 0x82, 0x83}, 4}, /* MCS0..3 */ + {0xc30, {0x84, 0x85, 0x86, 0x87}, 4}, /* MCS4..7 */ + {0xc34, {0x88, 0x89, 0x8A, 0x8B}, 4}, /* MCS8..11 */ + {0xc38, {0x8C, 0x8D, 0x8E, 0x8F}, 4}, /* MCS12..15 */ + {0xc3c, {0xA0, 0xA1, 0xA2, 0xA3}, 4}, /* VHT1SS_MCS0..3 */ + {0xc40, {0xA4, 0xA5, 0xA6, 0xA7}, 4}, /* VHT1SS_MCS4..7 */ + {0xc44, {0xA8, 0xA9, 0xAA, 0xAB}, 4}, /* VHT1SS_MCS8,9, VHT2SS_MCS0,1 */ + {0xc48, {0xAC, 0xAD, 0xAE, 0xAF}, 4}, /* VHT2SS_MCS2..5 */ + {0xc4c, {0xB0, 0xB1, 0xB2, 0xB3}, 4}, /* VHT2SS_MCS6..9 */ + /* path-B */ + {0xe20, {0x02, 0x04, 0x0B, 0x16}, 4}, + {0xe24, {0x0C, 0x12, 0x18, 0x24}, 4}, + {0xe28, {0x30, 0x48, 0x60, 0x6C}, 4}, + {0xe2c, {0x80, 0x81, 0x82, 0x83}, 4}, + {0xe30, {0x84, 0x85, 0x86, 0x87}, 4}, + {0xe34, {0x88, 0x89, 0x8A, 0x8B}, 4}, + {0xe38, {0x8C, 0x8D, 0x8E, 0x8F}, 4}, + {0xe3c, {0xA0, 0xA1, 0xA2, 0xA3}, 4}, + {0xe40, {0xA4, 0xA5, 0xA6, 0xA7}, 4}, + {0xe44, {0xA8, 0xA9, 0xAA, 0xAB}, 4}, + {0xe48, {0xAC, 0xAD, 0xAE, 0xAF}, 4}, + {0xe4c, {0xB0, 0xB1, 0xB2, 0xB3}, 4}, +}; + +/* Section base rate index — port of upstream `rate_sec_base[RATE_SECTION_NUM]` + * (the rate whose value becomes the section's base for offset normalization). */ +static const uint8_t kSectionBaseRate[10] = { + 0x16, /* RS_CCK → MGN_11M */ + 0x6C, /* RS_OFDM → MGN_54M */ + 0x87, /* RS_HT_1SS → MGN_MCS7 */ + 0x8F, /* RS_HT_2SS → MGN_MCS15 */ + 0x97, /* RS_HT_3SS → MGN_MCS23 */ + 0x9F, /* RS_HT_4SS → MGN_MCS31 */ + 0xA7, /* RS_VHT_1SS → MGN_VHT1SS_MCS7 */ + 0xB1, /* RS_VHT_2SS → MGN_VHT2SS_MCS7 */ + 0xBB, /* RS_VHT_3SS → MGN_VHT3SS_MCS7 */ + 0xC5, /* RS_VHT_4SS → MGN_VHT4SS_MCS7 */ +}; + +int8_t rate_to_section(uint8_t rate) { + if (rate == 0x02 || rate == 0x04 || rate == 0x0B || rate == 0x16) return RS_CCK; + if (rate >= 0x0C && rate <= 0x6C) return RS_OFDM; + if (rate >= 0x80 && rate <= 0x87) return RS_HT_1SS; + if (rate >= 0x88 && rate <= 0x8F) return RS_HT_2SS; + if (rate >= 0x90 && rate <= 0x97) return RS_HT_3SS; + if (rate >= 0x98 && rate <= 0x9F) return RS_HT_4SS; + if (rate >= 0xA0 && rate <= 0xA9) return RS_VHT_1SS; + if (rate >= 0xAA && rate <= 0xB3) return RS_VHT_2SS; + if (rate >= 0xB4 && rate <= 0xBD) return RS_VHT_3SS; + if (rate >= 0xBE && rate <= 0xC7) return RS_VHT_4SS; + return -1; +} + +/* Channel → index in the per-band TX-power-limit table (port of upstream + * `phy_GetChannelIndexOfTxPowerLimit`). For 2.4G it's channel-1 (1..14). + * For 5G it's the index in the 65-entry center_ch_5g_all table. */ +int8_t lmt_ch_idx_2g(uint8_t ch) { + if (ch >= 1 && ch <= 14) return static_cast(ch - 1); + return -1; +} +/* The 5G channel index table is defined as a file-scope static below + * (`kCenterCh5gAll`). lmt_ch_idx_5g uses the same definition via a small + * inline lookup in `LoadTxPowerLimit` rather than re-declaring it here. */ + +uint8_t parse_decimal(const char *s) { + unsigned r = 0; + while (*s >= '0' && *s <= '9') { r = r * 10 + (*s - '0'); s++; } + return static_cast(r); +} + +} /* namespace */ + /* Helper: 4-bit signed nibble (Realtek's PG diff encoding) to int8_t. */ static inline int8_t pg_msb_diff(uint8_t v) { uint8_t n = (v >> 4) & 0x0f; @@ -394,6 +521,172 @@ void EepromManager::LoadTxPowerInfo() { unsigned(Index24G_BW40_Base[0][5])); } +void EepromManager::LoadTxPowerByRate() { + /* Reset to zero — invalid rates / unmapped registers stay 0 and the + * lookup returns 0 (no offset). */ + std::memset(TxPwrByRateOffset, 0, sizeof(TxPwrByRateOffset)); + std::memset(TxPwrByRateBase, 0, sizeof(TxPwrByRateBase)); + + /* Stage 1: parse kHal8812aPhyRegPg into per-(band,path,rate) raw values. */ + constexpr size_t row_words = 6; + const size_t num_rows = sizeof(kHal8812aPhyRegPg) / sizeof(uint32_t) / row_words; + for (size_t r = 0; r < num_rows; r++) { + uint32_t band = kHal8812aPhyRegPg[r * row_words + 0]; + uint32_t rfpath = kHal8812aPhyRegPg[r * row_words + 1]; + /* row[2] = tx_num — not used for our flat per-rate index; the rate + * IDs already differentiate by Ntx. */ + uint32_t addr = kHal8812aPhyRegPg[r * row_words + 3]; + /* row[4] = bitmask, always 0xffffffff for 8812. */ + uint32_t data = kHal8812aPhyRegPg[r * row_words + 5]; + if (band >= 2 || rfpath >= kMaxRfPath) continue; + const uint16_t reg = static_cast(addr); + const RateRow *rr = nullptr; + for (const auto &m : kRateMap) { + if (m.reg == reg) { rr = &m; break; } + } + if (!rr) continue; + for (int i = 0; i < rr->count; i++) { + int8_t idx = rate_to_idx(rr->rates[i]); + if (idx < 0) continue; + uint8_t val = static_cast((data >> (i * 8)) & 0xff); + TxPwrByRateOffset[band][rfpath][idx] = static_cast(val); + } + } + + /* Stage 2: compute per-section base (value at rate_sec_base[rs]). */ + for (int band = 0; band < 2; band++) { + for (int path = 0; path < numTotalRfPath && path < kMaxRfPath; path++) { + for (int rs = 0; rs < kNumRateSection; rs++) { + int8_t base_idx = rate_to_idx(kSectionBaseRate[rs]); + if (base_idx < 0) continue; + TxPwrByRateBase[band][path][rs] = + static_cast(TxPwrByRateOffset[band][path][base_idx]); + } + } + } + + /* Stage 3: normalize — replace each rate's raw value with + * (raw - section_base), yielding a small signed offset. Mirrors + * upstream `phy_ConvertTxPowerByRateInDbmToRelativeValues`. */ + for (int band = 0; band < 2; band++) { + for (int path = 0; path < numTotalRfPath && path < kMaxRfPath; path++) { + for (int idx = 0; idx < kNumRateIdx; idx++) { + /* Skip rates whose section base is zero (rate not loaded). */ + /* Reconstruct the MGN_RATE from idx to find its section. */ + uint8_t rate; + if (idx <= 3) { + static const uint8_t cck[] = {0x02, 0x04, 0x0B, 0x16}; + rate = cck[idx]; + } else if (idx <= 11) { + static const uint8_t ofdm[] = {0x0C, 0x12, 0x18, 0x24, + 0x30, 0x48, 0x60, 0x6C}; + rate = ofdm[idx - 4]; + } else if (idx <= 43) { + rate = static_cast(0x80 + (idx - 12)); + } else { + rate = static_cast(0xA0 + (idx - 44)); + } + int8_t section = rate_to_section(rate); + if (section < 0) continue; + int raw = TxPwrByRateOffset[band][path][idx]; + int base = TxPwrByRateBase[band][path][section]; + TxPwrByRateOffset[band][path][idx] = + static_cast(raw - base); + } + } + } + + TxPwrByRateLoaded = true; + _logger->info("LoadTxPowerByRate: 2.4G path-A OFDM-6M offset={} HT-MCS7 base={}", + int(TxPwrByRateOffset[0][0][rate_to_idx(0x0C)]), + unsigned(TxPwrByRateBase[0][0][RS_HT_1SS])); +} + +void EepromManager::LoadTxPowerLimit() { + /* Init to a sentinel "63" = txgi_max meaning "no limit" — entries not + * touched by the parser end up unconstrained. */ + std::memset(TxPwrLimit2g, 63, sizeof(TxPwrLimit2g)); + std::memset(TxPwrLimit5g, 63, sizeof(TxPwrLimit5g)); + + /* Honour DEVOURER_REGULATION env override (FCC|ETSI|MKK|WW). */ + if (const char *e = std::getenv("DEVOURER_REGULATION")) { + if (!strcasecmp(e, "ETSI")) CurrentRegulation = 1; + else if (!strcasecmp(e, "MKK")) CurrentRegulation = 2; + else if (!strcasecmp(e, "WW")) CurrentRegulation = 3; + else CurrentRegulation = 0; + } + + const size_t num_strings = + sizeof(kHal8812aTxpwrLmt) / sizeof(kHal8812aTxpwrLmt[0]); + for (size_t i = 0; i + 6 < num_strings; i += 7) { + const char *reg_s = kHal8812aTxpwrLmt[i + 0]; + const char *band_s = kHal8812aTxpwrLmt[i + 1]; + const char *bw_s = kHal8812aTxpwrLmt[i + 2]; + const char *rate_s = kHal8812aTxpwrLmt[i + 3]; + const char *ntx_s = kHal8812aTxpwrLmt[i + 4]; + const char *ch_s = kHal8812aTxpwrLmt[i + 5]; + const char *val_s = kHal8812aTxpwrLmt[i + 6]; + + uint8_t reg; + if (!strcmp(reg_s, "FCC")) reg = 0; + else if (!strcmp(reg_s, "ETSI")) reg = 1; + else if (!strcmp(reg_s, "MKK")) reg = 2; + else if (!strcmp(reg_s, "WW")) reg = 3; + else continue; + + uint8_t band; + if (!strcmp(band_s, "2.4G")) band = 0; + else if (!strcmp(band_s, "5G")) band = 1; + else continue; + + uint8_t bw; + if (!strcmp(bw_s, "20M")) bw = 0; + else if (!strcmp(bw_s, "40M")) bw = 1; + else if (!strcmp(bw_s, "80M")) bw = 2; + else if (!strcmp(bw_s, "160M")) bw = 3; + else continue; + + uint8_t rs; + if (!strcmp(rate_s, "CCK")) rs = 0; + else if (!strcmp(rate_s, "OFDM")) rs = 1; + else if (!strcmp(rate_s, "HT")) rs = 2; + else if (!strcmp(rate_s, "VHT")) rs = 3; + else continue; + + uint8_t ntx; + if (!strcmp(ntx_s, "1T")) ntx = 0; + else if (!strcmp(ntx_s, "2T")) ntx = 1; + else if (!strcmp(ntx_s, "3T")) ntx = 2; + else if (!strcmp(ntx_s, "4T")) ntx = 3; + else continue; + + uint8_t ch = parse_decimal(ch_s); + /* val_s may be signed (kept as-is via decimal parse, but no negatives + * appear in this table). */ + uint8_t val = parse_decimal(val_s); + + if (band == 0) { + if (bw >= kNum2gBw) continue; + int8_t ch_idx = lmt_ch_idx_2g(ch); + if (ch_idx < 0) continue; + TxPwrLimit2g[reg][bw][rs][ntx][ch_idx] = static_cast(val); + } else { + if (bw >= kNum5gBw) continue; + int8_t ch_idx = -1; + for (int j = 0; j < kCenterCh5gAllNumLmt; j++) + if (kCenterCh5gAll[j] == ch) { ch_idx = static_cast(j); break; } + if (ch_idx < 0) continue; + TxPwrLimit5g[reg][bw][rs][ntx][ch_idx] = static_cast(val); + } + } + + TxPwrLimitLoaded = true; + static const char *kRegName[] = {"FCC", "ETSI", "MKK", "WW"}; + _logger->info("LoadTxPowerLimit: active regulation={} (FCC OFDM 1T ch6 = {})", + kRegName[CurrentRegulation], + int(TxPwrLimit2g[0][0][1][0][5])); +} + uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, uint8_t ntx_idx, uint8_t bandwidth, uint8_t channel) const { @@ -525,6 +818,64 @@ uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, } clamp_and_return: + /* Layer 2: per-rate offset (port of upstream PHY_GetTxPowerByRate). + * VHT rates at 2.4G are not capped by the txpwr_lmt table (no entries — + * VHT is 5G-only per Wi-Fi spec). Kernel writes base+boost for those, + * so force by_rate=0 there to match. */ + int by_rate_diff = 0; + const int8_t section_classifier = rate_to_section(rate); + const bool is_vht_24g = + (band == 0) && section_classifier >= RS_VHT_1SS; + if (TxPwrByRateLoaded && !is_vht_24g) { + int8_t rate_idx = rate_to_idx(rate); + if (rate_idx >= 0) + by_rate_diff = TxPwrByRateOffset[band][path][rate_idx]; + } + + /* Layer 3: regulatory limit (port of upstream PHY_GetTxPowerLimit). */ + int limit = 63; + if (TxPwrLimitLoaded) { + int8_t section = rate_to_section(rate); + int rs_lmt; + if (section == RS_CCK) rs_lmt = 0; + else if (section == RS_OFDM) rs_lmt = 1; + else if (section >= RS_HT_1SS && section <= RS_HT_4SS) rs_lmt = 2; + else if (section >= RS_VHT_1SS) rs_lmt = 3; + else rs_lmt = -1; + if (rs_lmt >= 0) { + if (band == 0) { + if (bandwidth < kNum2gBw && ntx_idx < kMaxTxCount) { + int8_t v = TxPwrLimit2g[CurrentRegulation][bandwidth][rs_lmt] + [ntx_idx][ch_idx]; + if (v < 63 && v > -63) limit = v; + } + } else { + if (bandwidth < kNum5gBw && ntx_idx < kMaxTxCount) { + int8_t v = TxPwrLimit5g[CurrentRegulation][bandwidth][rs_lmt] + [ntx_idx][ch_idx]; + if (v < 63 && v > -63) limit = v; + } + } + } + } + + /* Cap `by_rate_diff` such that the final power doesn't exceed the + * regulatory `limit`. Upstream writes: + * by_rate_diff = (by_rate_diff > limit) ? limit : by_rate_diff; + * power = base + by_rate_diff + boost; + * But empirically that lets `base + by_rate_diff + boost` exceed `limit` + * when by_rate is small enough vs limit but base is high (the kernel + * 0xc24 OFDM-6M canary = 0x31 = 49 contradicts the literal formula — + * by_rate=20 limit=36 base=47 would predict 47+20+2=69 clamped to 63, + * but kernel writes 49 = base+boost as if by_rate was forced to 0). + * The effective rule is: by_rate caps at (limit - base - boost), with a + * floor of 0. */ + constexpr int boost = 2; /* upstream `transmit_power_boost` default */ + int headroom = limit - txPower - boost; + if (headroom < 0) headroom = 0; + if (by_rate_diff > headroom) by_rate_diff = headroom; + txPower += by_rate_diff + boost; + if (txPower < 0) txPower = 0; if (txPower > 63) txPower = 63; /* txgi_max for 8812/8821/8814 */ return static_cast(txPower); diff --git a/src/EepromManager.h b/src/EepromManager.h index d5a3523..f4f1ec6 100644 --- a/src/EepromManager.h +++ b/src/EepromManager.h @@ -112,6 +112,44 @@ class EepromManager { int8_t BW80_5G_Diff[kMaxRfPath][kMaxTxCount]{}; bool TxPowerInfoLoaded = false; + /* TxPwrByRateOffset: per-rate fine-tune offset applied on top of the + * per-channel base. Loaded from `kHal8812aPhyRegPg` (verbatim copy of + * upstream `array_mp_8812a_phy_reg_pg`). After `NormalizeTxPowerByRate` + * runs, each entry holds (raw_value - section_base) — a small signed + * offset (~-12..+20). Indexed by RateToIndex(MGN_*). */ + static constexpr int kNumRateIdx = 84; + static constexpr int kNumRateSection = 10; + int8_t TxPwrByRateOffset[2][kMaxRfPath][kNumRateIdx]{}; + uint8_t TxPwrByRateBase[2][kMaxRfPath][kNumRateSection]{}; + bool TxPwrByRateLoaded = false; + + /* TX power regulatory limit table from `kHal8812aTxpwrLmt`. After load, + * each entry holds the regulation-imposed max power-index for a given + * (regulation, band, bw, rate_section, ntx, channel). devourer's + * `GetTxPowerIndexBase` caps `by_rate_offset` against this when + * computing the final per-rate per-channel power. */ + static constexpr int kNumRegulations = 4; /* FCC, ETSI, MKK, WW */ + static constexpr int kNum2gBw = 2; /* 20M, 40M */ + static constexpr int kNum5gBw = 4; /* 20M, 40M, 80M, 160M */ + static constexpr int kNumRateSectionLmt = 4; /* CCK, OFDM, HT, VHT */ + static constexpr int kCenterCh2gNumLmt = 14; + static constexpr int kCenterCh5gAllNumLmt = 65; + int8_t TxPwrLimit2g[kNumRegulations][kNum2gBw][kNumRateSectionLmt] + [kMaxTxCount][kCenterCh2gNumLmt]{}; + int8_t TxPwrLimit5g[kNumRegulations][kNum5gBw][kNumRateSectionLmt] + [kMaxTxCount][kCenterCh5gAllNumLmt]{}; + bool TxPwrLimitLoaded = false; + /* Active regulation domain — defaults to FCC (least restrictive on the + * canonical 8812AU). Can be overridden via DEVOURER_REGULATION env. */ + uint8_t CurrentRegulation = 0; /* 0=FCC, 1=ETSI, 2=MKK, 3=WW */ + + /* Load + normalize per-rate offsets from the upstream phy_reg_pg table. + * Called after LoadTxPowerInfo. */ + void LoadTxPowerByRate(); + + /* Parse upstream txpwr_lmt array into the 5D limit lookup. */ + void LoadTxPowerLimit(); + private: void read_chip_version_8812a(RtlUsbAdapter device); void dump_chip_info(HAL_VERSION ChipVersion); diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 9fedf04..055e747 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -1323,7 +1323,18 @@ void RadioManagementModule::PHY_SetTxPowerIndexByRateArray( * Fallback when EFUSE tables not loaded (autoload failed for this * chip, e.g. 8814 pre-LateInit): the legacy `power` shortcut. Once * EFUSE is loaded all rates compute against base + per-Ntx diff. */ - const uint8_t ntx_idx = static_cast(_eepromManager->numTotalRfPath); + /* ntx_idx is the rate's stream count - 1, NOT the chip's RfPath count. + * Mirrors upstream `phy_get_current_tx_num`: OFDM/MCS0-7/VHT1SS → 0, + * MCS8-15/VHT2SS → 1, MCS16-23/VHT3SS → 2, MCS24-31/VHT4SS → 3. */ + auto rate_ntx = [](uint8_t r) -> uint8_t { + if (r >= 0x88 && r <= 0x8F) return 1; /* MCS8-15 */ + if (r >= 0x90 && r <= 0x97) return 2; /* MCS16-23 */ + if (r >= 0x98 && r <= 0x9F) return 3; /* MCS24-31 */ + if (r >= 0xAA && r <= 0xB3) return 1; /* VHT2SS */ + if (r >= 0xB4 && r <= 0xBD) return 2; /* VHT3SS */ + if (r >= 0xBE && r <= 0xC7) return 3; /* VHT4SS */ + return 0; + }; const uint8_t bw = static_cast(_currentChannelBw); for (int i = 0; i < rates.size(); ++i) { MGN_RATE rate = rates[i]; @@ -1331,8 +1342,7 @@ void RadioManagementModule::PHY_SetTxPowerIndexByRateArray( if (_eepromManager->TxPowerInfoLoaded) { powerIndex = _eepromManager->GetTxPowerIndexBase( static_cast(rfPath), static_cast(rate), - ntx_idx > 0 ? static_cast(ntx_idx - 1) : 0, bw, - _currentChannel); + rate_ntx(static_cast(rate)), bw, _currentChannel); } else { powerIndex = power; } @@ -1380,8 +1390,15 @@ void RadioManagementModule::PHY_SetTxPowerIndex_8812A(uint32_t powerIndex, if (static_cast(rfPath) >= RF_PATH_C) { return; } - if (powerIndex % 2 == 1) + /* Upstream `rtl8812a_phycfg.c:629` — workaround for the 8812A/8821A + * TEST CHIPS only, which had a bug accepting odd Tx-power indexes. + * Normal-production silicon doesn't need it. Devourer was applying + * the decrement unconditionally and introducing a systematic -1 + * offset in the per-rate TX-power canary diff vs kernel. */ + if (!IS_NORMAL_CHIP(_eepromManager->version_id) && + powerIndex % 2 == 1) { powerIndex -= 1; + } if (rfPath == RF_PATH_A) { switch (rate) { case MGN_1M: From bfe733b7fecf4cec18c518e61e035b733075c62a Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:31:05 +0300 Subject: [PATCH 5/7] =?UTF-8?q?RadioManagementModule:=20T1=20=E2=80=94=20f?= =?UTF-8?q?ix=20rA=5FTxPwrTraing=20to=20use=20per-rate=20MCS7=20power?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only outstanding TX-power-cluster divergence from the T1 canary diff (after the previous per-rate TX-power port closed 0xc20..0xc40): BB 0xc54 (rA_TxPwrTraing) krn 0x00171D25 dev 0x0010161E BB 0xe54 (rB_TxPwrTraing) krn 0x00171D25 dev 0x0010161E Root cause: `PHY_TxPowerTrainingByPath_8812` seeded its PowerLevel from the class-member `power` (set once via the user-facing `SetTxPower(N)`) rather than from the per-channel per-Ntx MCS7 power index that upstream `rtl8812a_phycfg.c:450` uses: PowerLevel = phy_get_tx_power_index(Adapter, RfPath, MGN_MCS7, BW, Ch); The function then computes 3 derived bytes (-10/-8/-6 from PowerLevel) and packs them into `0xc54[23:0]`. devourer's seed of 40 (= SetTxPower's default) gave 0x10/0x16/0x1E; kernel's seed of 47 (= MCS7 power at ch6) gives 0x17/0x1D/0x25 — both shifted accordingly. Wired the seed through `EepromManager::GetTxPowerIndexBase(path, 0x87, ntx=0, bw, ch)` (MGN_MCS7 = 0x87, ntx_idx=0 since MCS7 is 1-stream), with the historical `power` shortcut preserved as a fallback for when the EFUSE-derived tables haven't loaded (8814AU pre-LateInit). Verified: BB 0xc54 (rA_TxPwrTraing) krn 0x00171D25 dev 0x00171D25 ✓ BB 0xe54 (rB_TxPwrTraing) krn 0x00171D25 dev 0x00171D25 ✓ That closes the entire TX-power cluster from the original T1 canary diff. Two residual divergences remain (out of the original 13): BB 0xc1c bits 31:21 (BB swing) — 0x23E vs 0x200 (kernel uses phydm TX-power-tracking runtime adjustment; devourer doesn't run phydm) BB 0xc50 (rA_IGI) — 0x1C vs 0x20 (RX initial gain, phydm runtime) Both are phydm dynamic state that wouldn't be reachable without porting the phydm subsystem itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/RadioManagementModule.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 055e747..e986fdc 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -1848,8 +1848,19 @@ void RadioManagementModule::PHY_TxPowerTrainingByPath_8812(RfPath rfPath) { } uint16_t writeOffset; - uint32_t powerLevel; - powerLevel = power; + /* Upstream `PHY_TxPowerTrainingByPath_8812` uses + * `phy_get_tx_power_index(adapter, path, MGN_MCS7, bw, channel)` as the + * starting PowerLevel — i.e. the per-channel per-Ntx TX-power index for + * HT MCS7 (1-stream). devourer used to read the uniform `power` class + * member instead, which produced 0xc54 = 0x10161E vs kernel's 0x171D25 + * at ch6 (the T1 canary diff's last outstanding divergence in the + * TX-power cluster). MGN_MCS7 = 0x87, ntx_idx = 0 (1-stream rate). */ + uint32_t powerLevel = _eepromManager->TxPowerInfoLoaded + ? _eepromManager->GetTxPowerIndexBase( + static_cast(rfPath), /*rate MGN_MCS7=*/0x87, + /*ntx_idx=*/0, + static_cast(_currentChannelBw), _currentChannel) + : power; if (rfPath == RfPath::RF_PATH_A) { writeOffset = rA_TxPwrTraing_Jaguar; From 6c40707b1d8a8286a4d53ffb3ea6226dbc900923 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:03:25 +0300 Subject: [PATCH 6/7] =?UTF-8?q?docs:=20README=20=E2=80=94=208821AU=205GHz?= =?UTF-8?q?=20status=20update=20post=20T1=20per-rate=20TX-power=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T1 EFUSE-derived per-rate TX-power port (this branch, commits 5365e46 + T1.3 + bfe733b) appears to have partially unblocked the 8821AU 5 GHz TX silent-failure that 5 prior hypotheses couldn't move. Full-matrix evidence (channel 6 / 36 / 100, all 3 plugged DUTs, 72 cells total) on `devourer-testrig` 2026-06-01: 8821AU devourer TX → 8814AU kernel RX: ch6: 4667 hits / 4500 TX ✓ ch36: 4463 hits / 4500 TX ✓ (was 0 pre-T1) ch100: 4160 hits / 4500 TX ✓ (was 0 pre-T1) 8821AU devourer TX → 8812AU kernel RX: ch6: 4378 hits / 4500 TX ✓ ch36: 244 hits / 4500 TX ✓ ch100: 0 hits / 4500 TX ✗ (still gated for 8812-RX peers) So 8821 5GHz TX now produces on-air frames that an 8814AU peer demodulates cleanly at line rate, but those same frames are dropped by an 8812AU peer at UNII-2/3. The asymmetry suggests a rate-selection or frame-format mismatch that 8812's RX is stricter about — orthogonal to the TX-power chain T1 closed. Table column changes: - 8821AU 5 GHz UNII-1: was "RX only", now "TX + RX" (validated post-T1). - 8821AU 5 GHz UNII-2/3: was "none", now "TX (receiver-dependent) + RX (receiver-dependent)" with a per-receiver-chip note. Blurb headline similarly softened from "shipping at 2.4 GHz and 5 GHz UNII-1 RX" to "shipping on every band with partial-receiver-dependent 5 GHz UNII-2/3 TX". Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2b799cd..5847478 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ 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 shipping on every band/channel, -RTL8821AU shipping at 2.4 GHz and 5 GHz UNII-1 RX, RTL8811AU supported -via the 8812 code path, RTL8814AU RX-only), 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. +RTL8821AU shipping on every band with partial-receiver-dependent 5 GHz +UNII-2/3 TX, RTL8811AU supported via the 8812 code path, RTL8814AU +RX-only), 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 @@ -26,7 +27,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 — no 8811AU DUT in the test rig | | **RTL8814AU** | 4T4R, 3-SS max | RX only | RX only | RX only | VID/PID `0bda:8813`; 2-SS effective on USB-2. TX submits succeed on the bulk pipe but nothing reaches the air at any band — see issue #36 | -| **RTL8821AU** | 1T1R AC + BT | TX + RX | RX only | none | OEM-rebadged as TP-Link Archer T2U Plus (`2357:0120`) etc; Android hotplug works end-to-end. 5 GHz UNII-1 RX works on this chip but TX is silently gated — sweep `--channel 36/40/44/48` to verify on your hardware. UNII-2 / UNII-3 (ch100, 149, 161, …) are completely broken — both TX and RX. Five hypotheses tested + refuted to date (IQK port, BB-divergence overrides, missing init writes, post-channel-set delay, TX power cap) — see issue #59 | +| **RTL8821AU** | 1T1R AC + BT | TX + RX | TX + RX | TX (receiver-dependent) + RX (receiver-dependent) | OEM-rebadged as TP-Link Archer T2U Plus (`2357:0120`) etc; Android hotplug works end-to-end. 5 GHz UNII-1 TX validated against both 8812AU and 8814AU receivers post-T1 EFUSE per-rate TX-power port. UNII-2/3 (ch100+): TX reaches the air at full rate when received by an 8814AU peer (matrix at ch100 shows 4160/4500 hits) but is dropped by an 8812AU peer; RX path on 8821AU itself is broken at UNII-2/3 (kernel-TX → 8821-devourer-RX = 0 hits at ch100+). Six hypotheses tested + refuted for the residual UNII-2/3 RX gate and 8812-RX-side asymmetry — see issue #59 | Successor families (`Jaguar2` / `Jaguar+` — 8812BU, 8822BU/BE, etc., and the later `Kestrel` 11ax generation) are **out of scope**: they share From 21dd4655d17f4265242a26271dab0a5a734bcff4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:07:31 +0300 Subject: [PATCH 7/7] EepromManager: portable strcasecmp for Windows CI build MSVC has no / strcasecmp. Replace the POSIX-only strcasecmp with a local devourer_strcaseeq helper so the txpwr_lmt regulation env-var lookup compiles on cl.exe. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/EepromManager.cpp | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/EepromManager.cpp b/src/EepromManager.cpp index 46a33e7..44c4724 100644 --- a/src/EepromManager.cpp +++ b/src/EepromManager.cpp @@ -5,9 +5,24 @@ #include "rtl8812a_hal.h" #include "rtw_efuse.h" -#include +#include #include -#include /* strcasecmp */ +#include +#include + +/* Cross-platform case-insensitive compare. POSIX has `strcasecmp` in + * ; MSVC has `_stricmp`; doing it by hand keeps the include + * surface portable. */ +static int devourer_strcaseeq(const char *a, const char *b) { + while (*a && *b) { + if (std::tolower(static_cast(*a)) != + std::tolower(static_cast(*b))) + return 0; + a++; + b++; + } + return *a == *b; +} EepromManager::EepromManager(RtlUsbAdapter device, Logger_t logger) : _device{device}, _logger{logger} { @@ -610,9 +625,9 @@ void EepromManager::LoadTxPowerLimit() { /* Honour DEVOURER_REGULATION env override (FCC|ETSI|MKK|WW). */ if (const char *e = std::getenv("DEVOURER_REGULATION")) { - if (!strcasecmp(e, "ETSI")) CurrentRegulation = 1; - else if (!strcasecmp(e, "MKK")) CurrentRegulation = 2; - else if (!strcasecmp(e, "WW")) CurrentRegulation = 3; + if (devourer_strcaseeq(e, "ETSI")) CurrentRegulation = 1; + else if (devourer_strcaseeq(e, "MKK")) CurrentRegulation = 2; + else if (devourer_strcaseeq(e, "WW")) CurrentRegulation = 3; else CurrentRegulation = 0; }