From d51a27f6592736af674ae831ccc546ee653e24ed Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:44:51 +0300 Subject: [PATCH] jaguar3: 8822C 40/80 MHz bring-up calibrates IQK at 20 MHz, then retunes (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8822CU brought up by devourer directly at 80 MHz received nothing — not even corrupt frames — while the same code path worked on the 8822E and the same chip received 80 MHz fine under both kernel drivers. Root cause: devourer ran IQK with the RF tuned at 80 MHz. The vendor kernel never does that — it calibrates at its bring-up bandwidth and carries the coefficients across `iw set freq ... 80` (usbmon of the 20->80 transition contains no IQK-engine traffic at all). The 8822C IQK executed at 80 MHz loads RxIQC coefficients that leave the receiver unable to sync any frame; DEVOURER_SKIP_IQK alone revived the RX (hardware bisect), and replaying the kernel's captured 20->80 transition onto a 20 MHz bring-up worked for the same reason. Fix: for the 8822C at 40/80 MHz, tune 20 MHz on the target channel, run IQK there (command set follows the calibration width), then retune to the requested bandwidth. 8822E keeps its existing flow. Bench (ch36/central 42, VHT1SS_MCS7/80): CU RX 0 -> 4481 clean EVM -48 from the 8812EU (1545/-42 from the 8812AU); 40 MHz unchanged (4427/-52); EU ladder unchanged; CU hop parity PASS; ctest 19/19. Diagnostic harness: tests/cu_bb_endstate_diff_80.sh (vendor-kernel end-state diff at 80 MHz). Co-Authored-By: Claude Opus 4.8 --- src/jaguar3/RtlJaguar3Device.cpp | 47 +++++++++++-- tests/cu_bb_endstate_diff_80.sh | 113 +++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 5 deletions(-) create mode 100755 tests/cu_bb_endstate_diff_80.sh diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index b0153a8..0d062e6 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -52,10 +52,28 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, _rx_wanted = true; _hal.rtw_hal_init(channel); /* full vendor-source bring-up */ /* Tune the channel/bandwidth (5/10 MHz ChannelWidth re-clocks to narrowband), - * then run IQK calibration (it reads RF18 for the tuned channel). */ + * then run IQK calibration (it reads RF18 for the tuned channel). + * + * The 8822C's IQK must run with the RF at 20 MHz: the vendor kernel only + * ever calibrates at its bring-up bandwidth and carries the coefficients + * across `iw set freq ... 80` (usbmon: the 20->80 transition contains no + * IQK-engine traffic), and devourer's IQK executed AT 80 MHz loads RxIQC + * coefficients that leave the receiver unable to sync ANY frame + * (hardware-bisected: SKIP_IQK alone revives 80 MHz RX). Calibrate at + * 20 MHz on the target channel, then retune to the requested width. */ + const bool iqk_at_20 = _variant == jaguar3::ChipVariant::C8822C && + (channel.ChannelWidth == CHANNEL_WIDTH_40 || + channel.ChannelWidth == CHANNEL_WIDTH_80); _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, - channel.ChannelWidth); - _hal.run_iqk(channel); + iqk_at_20 ? CHANNEL_WIDTH_20 + : channel.ChannelWidth); + SelectedChannel iqk_ch = channel; + if (iqk_at_20) + iqk_ch.ChannelWidth = CHANNEL_WIDTH_20; /* IQK command set follows the RF */ + _hal.run_iqk(iqk_ch); + if (iqk_at_20) + _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, + channel.ChannelWidth); _hal.enable_rx_path(); /* RF into RX mode (IGI toggle) — must follow channel set */ _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ @@ -107,6 +125,14 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, SetAckResponder(*_cfg.rx.ack_responder); /* DEVOURER_ACK_RESPONDER */ apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ — end of both bring-ups, * like Jaguar2's. */ + if (_cfg.debug.bb_dump) { + /* Same MAC+BB end-state dump as InitWrite's, at the end of the RX + * bring-up (kernel rtw_proc read_reg diffing). */ + for (uint32_t a = 0x000; a <= 0x4ffc; a += 0x10) + _logger->info("BBDUMP 0x{:04x} 0x{:08x} 0x{:08x} 0x{:08x} 0x{:08x}", a, + _device.rtw_read32(a), _device.rtw_read32(a + 4), + _device.rtw_read32(a + 8), _device.rtw_read32(a + 12)); + } StartRxLoop(std::move(packetProcessor)); } @@ -463,9 +489,20 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { const bool want_rx = _cfg.rx.enable_with_tx; _rx_wanted = want_rx; _hal.rtw_hal_init(channel); /* full vendor-source bring-up */ + /* 8822C at 40/80 MHz: IQK at 20 MHz, then retune — see Init. */ + const bool iqk_at_20 = _variant == jaguar3::ChipVariant::C8822C && + (channel.ChannelWidth == CHANNEL_WIDTH_40 || + channel.ChannelWidth == CHANNEL_WIDTH_80); _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, - channel.ChannelWidth); - _hal.run_iqk(channel); + iqk_at_20 ? CHANNEL_WIDTH_20 + : channel.ChannelWidth); + SelectedChannel iqk_ch = channel; + if (iqk_at_20) + iqk_ch.ChannelWidth = CHANNEL_WIDTH_20; /* IQK command set follows the RF */ + _hal.run_iqk(iqk_ch); + if (iqk_at_20) + _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, + channel.ChannelWidth); if (want_rx) _hal.enable_rx_path(); /* RF into RX mode — same slot as the Init path */ _hal.dpk_force_bypass_8822e(); /* 8822e rfe 21/22: kernel bypasses DPK (after IQK) */ diff --git a/tests/cu_bb_endstate_diff_80.sh b/tests/cu_bb_endstate_diff_80.sh new file mode 100755 index 0000000..454dc87 --- /dev/null +++ b/tests/cu_bb_endstate_diff_80.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Issue #267: BB end-state diff for the 8822CU at 80 MHz RX — vendor kernel +# (88x2cu_ohd, monitor, iw set freq 5180 80 5210, RX-verified against an AU +# VHT80 flood) vs devourer (rxdemo ch36 DEVOURER_BW=80, delivers nothing). +# Same methodology as tests/eu_bb_endstate_diff.sh. +# sudo tests/cu_bb_endstate_diff_80.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KMOD=88x2cu_ohd +OUT=/tmp/devourer-cu80-endstate +mkdir -p "$OUT" + +cleanup(){ + sudo -n pkill -x rxdemo 2>/dev/null || true + sudo -n pkill -x txdemo 2>/dev/null || true + sudo -n pkill -x tcpdump 2>/dev/null || true + true +} +trap cleanup EXIT INT TERM + +cu_sys(){ + SYS="" + for d in /sys/bus/usb/devices/*/idProduct; do + [ "$(cat "$d" 2>/dev/null)" = "c812" ] && { SYS=$(basename "$(dirname "$d")"); break; } + done + [ -n "$SYS" ] || { echo "ERROR: c812 not on USB"; exit 1; } +} +vbus_cold(){ + cu_sys + sudo -n uhubctl -l "${SYS%.*}" -p "${SYS##*.}" -a cycle -d 3 >/dev/null 2>&1 || \ + echo "WARN: uhubctl cycle failed" + sleep 4 +} + +echo "== kernel side: vendor $KMOD, monitor 80 MHz (ch36/central 42) ==" +# The in-tree rtw88 auto-probes every enumeration — remove it for the vendor +# bind (modprobe -r does not survive re-enumeration, so do it right before). +sudo -n rmmod "$KMOD" 2>/dev/null +sudo -n modprobe -r rtw88_8822cu 2>/dev/null +vbus_cold +sudo -n modprobe -r rtw88_8822cu 2>/dev/null +sudo -n insmod "$ROOT/reference/rtl88x2cu/${KMOD}.ko" 2>/dev/null || true +sleep 5 +IF="" +for d in /sys/class/net/*; do + drv=$(basename "$(readlink -f "$d/device/driver" 2>/dev/null)" 2>/dev/null) + case "$drv" in *88x2cu*) IF=$(basename "$d"); break;; esac +done +[ -n "$IF" ] || { echo "ERROR: no $KMOD netdev (rtw88 raced the probe?)"; exit 1; } +sudo -n ip link set "$IF" down +sudo -n iw dev "$IF" set monitor none +sudo -n ip link set "$IF" up +sudo -n iw dev "$IF" set freq 5180 80 5210 +sleep 2 + +echo "== kernel RX sanity: AU VHT80 flood vs tcpdump ==" +sudo -n timeout 16 tcpdump -i "$IF" "wlan addr2 57:42:75:05:d6:00" \ + >/dev/null 2>"$OUT/ktcp.count" & +TD=$!; sleep 2 +sudo -n env DEVOURER_PID=0x8812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL=36 \ + DEVOURER_HOP_BW=80 DEVOURER_TX_RATE=VHT1SS_MCS7/80 DEVOURER_TX_GAP_US=2000 \ + timeout 10 "$ROOT/build/txdemo" >/dev/null 2>&1 || true +sleep 2; sudo -n pkill -x tcpdump 2>/dev/null; wait "$TD" 2>/dev/null +grep -m1 "captured" "$OUT/ktcp.count" + +PROC="/proc/net/rtl$KMOD/$IF" +[ -e "$PROC/read_reg" ] || PROC="/proc/net/$KMOD/$IF" +[ -e "$PROC/read_reg" ] || { echo "ERROR: no $PROC/read_reg"; exit 1; } +echo "== dumping kernel MAC+BB 0x000..0x4ffc ==" +sudo -n python3 - "$PROC/read_reg" >"$OUT/kernel.dump" <<'PYEOF' +import re, sys +proc = sys.argv[1] +for a in range(0x000, 0x5000, 4): + with open(proc, "w") as f: + f.write(f"{a:x} 4") + with open(proc) as f: + m = re.search(r"=0x(\w+)", f.read()) + print(f"0x{a:04x} 0x{int(m.group(1),16):08x}" if m else f"0x{a:04x} ERR") +PYEOF +echo "kernel dump: $(wc -l <"$OUT/kernel.dump") regs" +sudo -n rmmod "$KMOD" 2>/dev/null + +echo "== devourer side: rxdemo ch36 BW=80 + BB dump ==" +vbus_cold +sudo -n env DEVOURER_PID=0xc812 DEVOURER_VID=0x0bda DEVOURER_CHANNEL=36 \ + DEVOURER_BW=80 DEVOURER_BB_DUMP=1 \ + timeout 40 "$ROOT/build/rxdemo" >/dev/null 2>"$OUT/devourer.err" || true +grep -oE "BBDUMP 0x[0-9a-f]{4} 0x[0-9a-f]{8} 0x[0-9a-f]{8} 0x[0-9a-f]{8} 0x[0-9a-f]{8}" \ + "$OUT/devourer.err" | while read -r _ base v0 v1 v2 v3; do + b=$((base)) + printf "0x%04x %s\n0x%04x %s\n0x%04x %s\n0x%04x %s\n" \ + $b "$v0" $((b+4)) "$v1" $((b+8)) "$v2" $((b+12)) "$v3" +done >"$OUT/devourer.dump" +echo "devourer dump: $(wc -l <"$OUT/devourer.dump") regs" + +python3 - "$OUT/kernel.dump" "$OUT/devourer.dump" <<'PYEOF' +import sys +def load(p): + d = {} + for line in open(p): + parts = line.split() + if len(parts) == 2 and parts[1].startswith("0x"): + d[int(parts[0], 16)] = int(parts[1], 16) + return d +k, u = load(sys.argv[1]), load(sys.argv[2]) +diffs = [(a, k[a], u[a]) for a in sorted(set(k) & set(u)) if k[a] != u[a]] +print(f"{len(diffs)} differing registers of {len(set(k) & set(u))}") +for a, kv, uv in diffs: + tag = "" + if 0x3c00 <= a < 0x4000: tag = " (RF-A win)" + elif 0x4c00 <= a < 0x5000: tag = " (RF-B win)" + print(f"0x{a:04x}: kernel=0x{kv:08x} devourer=0x{uv:08x}{tag}") +PYEOF