diff --git a/src/BeamformingSounder.h b/src/BeamformingSounder.h index 9bcc26c..17e3f20 100644 --- a/src/BeamformingSounder.h +++ b/src/BeamformingSounder.h @@ -52,6 +52,8 @@ enum : uint16_t { kSndNdpStandby = 0x071B, kBbCsiContent = 0x09B4, /* Jaguar-1 only (clock divider on Jaguar-3) */ kOurAid = 0x1680, /* Jaguar-2/3 only: our AID [11:0] */ + kBbpsfCtrl = 0x06DC, /* CSI GID select (BIT30 + gid BIT12) */ + kCsiRrsr = 0x1678, /* CSI response-rate set (= 0x550) */ }; enum : uint8_t { @@ -133,6 +135,49 @@ inline void arm_beamformee(RtlUsbAdapter& dev, const uint8_t beamformer_mac[6], dev.rtw_write32(cfg.csi_content_reg, cfg.csi_content_val); } +/* Beamformer-side steering entry — the TX-BF *apply* half, layered on top of + * arm_sounder() (which already set SND_PTCL_CTRL, the TXBF_CTRL NDPA-enables + + * P_AID=0, and BFMEE_SEL entry-0/TXUSER_ID0). This configures the entry so the + * WMAC accepts the beamformee's Compressed Beamforming Report and builds the V + * matrix from it IN HARDWARE (software never touches coefficients — see + * reference/rtl88x2cu/hal/rtl8822c/rtl8822c_bf_monitor.c bf_monitor_config_*). + * `peer_mac` = the beamformee we steer toward (each side writes the other's MAC + * into BFMER0_INFO). `rx_nss` = GET_RX_NSS (2 on the 2T2R 8822B/C/E). Registers + * are family-shared, so this is generation-neutral like arm_sounder. Apply is + * NOT enabled here — call apply_vmatrix() once a CBR has been ingested. */ +inline void arm_beamformer_entry(RtlUsbAdapter& dev, const uint8_t peer_mac[6], + uint8_t rx_nss = 2, uint8_t g_id = 0) { + for (uint16_t i = 0; i < 6; ++i) + dev.rtw_write8(kBfmer0Info + i, peer_mac[i]); + /* Expected CSI-report dims: nc=rx_nss-1, nr=1, grouping=0, codebook=1 (vht), + * coeff=3 (bf_monitor_config_beamformer). */ + const uint8_t nc = rx_nss ? static_cast(rx_nss - 1) : 0; + const uint16_t csi_param = + static_cast((3u << 10) | (1u << 8) | (1u << 3) | nc); + dev.rtw_write16(kCsiRptParam20, csi_param); + dev.rtw_write8(kSndNdpStandby, 0x70); /* ndp_rx_standby_timer */ + uint32_t bbpsf = dev.rtw_read(kBbpsfCtrl) | (1u << 30); + bbpsf = (g_id == 63) ? (bbpsf | (1u << 12)) : (bbpsf & ~(1u << 12)); + dev.rtw_write(kBbpsfCtrl, bbpsf); + dev.rtw_write(kCsiRrsr, 0x550); +} + +/* Flip the V-matrix apply toggle: TXBF_CTRL[11:9] per bandwidth (BIT9=20, + * +BIT10=40, +BIT11=80) + DIS_NDP_BFEN (BIT15). Enable ONLY after the WMAC has + * ingested a valid CBR (bf_monitor_enable_txbf gates on csi_matrix_len>0) — + * enabling with no V degrades the link. `bw`: 0=20, 1=40, 2=80. */ +inline void apply_vmatrix(RtlUsbAdapter& dev, bool enable, uint8_t bw) { + uint32_t txbf = dev.rtw_read(kTxbfCtrl); + txbf &= ~((1u << 9) | (1u << 10) | (1u << 11)); + if (enable) { + txbf |= (1u << 9); + if (bw >= 1) txbf |= (1u << 10); + if (bw >= 2) txbf |= (1u << 11); + txbf |= (1u << 15); /* BIT_DIS_NDP_BFEN */ + } + dev.rtw_write(kTxbfCtrl, txbf); +} + /* Jaguar-2/3 MU-beamformee registers (8822B/C/E). MU-BF group table. */ enum : uint16_t { kMuTxCtl = 0x14C0, /* [10:8] STA-table index, [5:0] STA validity */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 18132b7..f7ebf28 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -187,6 +187,27 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { p.Data = std::span(const_cast(f.frame), f.frame_len); if (!p.RxAtrib.crc_err) _rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]); + /* TX-BF apply gate (DEVOURER_BF_TXBF): a VHT Compressed Beamforming + * Report (category 0x15, action 0x00) from the target peer (addr2) means + * the WMAC just ingested a fresh V matrix — enable the apply toggle so + * subsequent TX is steered. Enabling it before any CBR (no V) degrades + * the link, so this is the gate. One-shot; V refreshes on each periodic + * re-sound automatically. */ + if (_bf_txbf_armed && !p.RxAtrib.crc_err && f.frame_len >= 26) { + const uint8_t *fb = f.frame; + if (fb[24] == 0x15 && fb[25] == 0x00 && + std::memcmp(fb + 10, _bf_peer, 6) == 0) { + _bf_cbr_count.fetch_add(1, std::memory_order_relaxed); + if (!_bf_apply_on.load(std::memory_order_relaxed)) { + std::lock_guard lk(_reg_mu); + devourer::bf::apply_vmatrix( + _device, true, static_cast(_channel.ChannelWidth)); + _bf_apply_on.store(true, std::memory_order_relaxed); + _logger->info("Jaguar3 BF: CBR from peer ingested — TXBF apply " + "ENABLED (steering subsequent TX)"); + } + } + } _packetProcessor(p); } if (++frames <= 5) @@ -390,6 +411,27 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { devourer::bf::arm_sounder(_device, /*snd_ptcl_ctrl=*/0xDB); _logger->info("Jaguar3 BF sounder armed (beamformer side)"); } + + /* DEVOURER_BF_TXBF=aa:bb:cc:dd:ee:ff — TX beamforming APPLY (needs the sounder + * above + DEVOURER_TX_WITH_RX so the beamformer's RX can receive the peer's + * report). Configure the beamformer entry for `peer_mac` so the WMAC accepts + * and ingests the peer's Compressed Beamforming Report into the V matrix (in + * hardware). The apply toggle is NOT flipped here — it is enabled from the RX + * loop once a CBR from the peer is seen (a blind apply with no V degrades the + * link). Periodic re-sounding (DEVOURER_TX_NDPA=N>1) keeps V fresh. */ + if (const char *bftx = std::getenv("DEVOURER_BF_TXBF")) { + unsigned m[6]; + if (std::sscanf(bftx, "%x:%x:%x:%x:%x:%x", &m[0], &m[1], &m[2], &m[3], &m[4], + &m[5]) == 6) { + for (int i = 0; i < 6; ++i) _bf_peer[i] = static_cast(m[i]); + devourer::bf::arm_beamformer_entry(_device, _bf_peer, /*rx_nss=*/2); + _bf_txbf_armed = true; + _logger->info("Jaguar3 BF TXBF entry configured for peer {} — apply gates " + "on the first CBR (needs DEVOURER_TX_WITH_RX)", bftx); + } else { + _logger->error("DEVOURER_BF_TXBF — bad MAC '{}'", bftx); + } + } /* TX-only: the bring-up enables accept-all RX (monitor_rx_cfg), but the TX * path never reads bulk-IN — so on a trafficked channel the on-chip RX FIFO * fills with unread frames and throttles TX. Close the RX filter so no @@ -1004,6 +1046,32 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) { bwidth = static_cast(tp.bwidth); } + /* DEVOURER_TX_NDPA=N — beamforming-sounding probe: mark injected frames as + * NDPA so the armed sounding engine (DEVOURER_BF_ARM_SOUNDER, InitWrite) + * follows each with a hardware NDP. N is the PERIOD: 1 (or non-numeric) = + * every frame (self-sounding capture, the original behaviour); N>1 = every Nth + * frame, so the interleaved data frames stay steerable for TX beamforming. */ + static const int ndpa_period = []() -> int { + const char *e = std::getenv("DEVOURER_TX_NDPA"); + if (!e) return 0; + int p = std::atoi(e); + return p > 0 ? p : 1; + }(); + bool ndpa = false; + if (ndpa_period > 0) { + ndpa = (_ndpa_ctr % static_cast(ndpa_period)) == 0; + ++_ndpa_ctr; + } + /* Periodic sounding (N>1, the TX-BF case) needs a 2-stream NDP to fill the + * 2-antenna V matrix, so force those NDPA frames to VHT-2SS regardless of the + * data rate (measured: a 1SS NDP yields no CBR); the interleaved data frames + * keep their radiotap rate (1SS/HT), which is what gets steered. N==1 + * (every-frame self-sounding capture) keeps the caller's rate unchanged. */ + if (ndpa && ndpa_period > 1) { + fixed_rate = MGN_VHT2SS_MCS0; + vht = true; + } + uint8_t bw_desc = (bwidth == CHANNEL_WIDTH_40) ? 1 : (bwidth == CHANNEL_WIDTH_80) ? 2 : 0; @@ -1025,10 +1093,6 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) { * the kernel's descriptor for group-addressed frames. */ const uint8_t *dot11 = packet + radiotap_length; bool bmc = frame_len >= 6 && (dot11[4] & 0x01); - /* DEVOURER_TX_NDPA=1 — beamforming self-sounding probe: mark injected frames - * as NDPA so the armed sounding engine (DEVOURER_BF_ARM_SOUNDER, InitWrite) - * follows each with a hardware NDP. Same knob as the Jaguar-1 path. */ - static const bool ndpa = std::getenv("DEVOURER_TX_NDPA") != nullptr; /* STBC guard (IRtlDevice contract) — 8822C/8822E are 2T2R so this never * fires today, but keeps the invariant uniform across families: never air an * STBC frame the chip can't do. */ diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index ffec5ee..b3b0c29 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -163,6 +163,16 @@ class RtlJaguar3Device : public IRtlDevice { bool _brought_up = false; /* Rolling per-frame RX link-quality aggregate (GetRxQuality). */ devourer::RxQualityAccumulator _rxq; + /* Frame counter for periodic NDPA sounding (DEVOURER_TX_NDPA=N). */ + uint64_t _ndpa_ctr = 0; + /* TX beamforming apply state (DEVOURER_BF_TXBF). The entry is configured at + * InitWrite (arm_beamformer_entry); the apply toggle is enabled from the RX + * loop only after a Compressed Beamforming Report from _bf_peer is ingested + * (blind apply degrades). */ + bool _bf_txbf_armed = false; + std::atomic _bf_apply_on{false}; + std::atomic _bf_cbr_count{0}; + uint8_t _bf_peer[6] = {0}; /* dis_cca sticky state — re-applied after SetMonitorChannel (the channel set * rewrites the BB CCA registers). Caller holds _reg_mu. */ bool _cca_disabled = false; diff --git a/tests/txbf_apply_onair.sh b/tests/txbf_apply_onair.sh new file mode 100755 index 0000000..a8e6398 --- /dev/null +++ b/tests/txbf_apply_onair.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# TX beamforming APPLY on Jaguar3 (8822E/C): does steering injected TX toward a +# sounded peer lift the peer's RX signal? Closed loop: +# A (beamformer, 8822EU): TX+RX, arms the sounder, injects VHT1SS data + a +# periodic VHT2SS NDPA (DEVOURER_TX_NDPA=64), and — when DEVOURER_BF_TXBF is +# set — configures the beamformer entry and enables the V-matrix apply toggle +# from its RX loop the moment it ingests the peer's Compressed Beamforming +# Report (the CBR gate; a blind apply with no V degrades the link). +# B (beamformee + sensor, 8822CU): responds to the NDPA with a CBR and measures +# A's frames via GetRxQuality (). +# A/B: apply OFF (no DEVOURER_BF_TXBF) vs ON. PASS = ON lifts B's snr/rssi and +# A logs "TXBF apply ENABLED". Low TX power dodges the near-field RSSI ceiling. +# +# Usage: sudo -v && tests/txbf_apply_onair.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${TXBF_OUT:-/tmp/devourer-txbf}" +CH="${CH:-100}" +DUR="${DUR:-13}" +REPS="${REPS:-2}" +BFER=57:42:75:05:d6:00 # A's self-MAC / NDPA TA +BFEE=00:e0:4c:88:22:ce # B's beamformee MAC (programmed by arm_bfee) +# Beamformer must have a healthy RX while injecting (to ingest the CBR): the +# 8822CU does; the 8822EU's TX+RX desenses its own RX (0x41e8 quirk), so it can't +# ingest — hence 8822CU=beamformer, 8822EU=beamformee (RX-only, no desense). +A_PID=0xc812 A_VID=0x0bda # 8822CU beamformer (TX+RX) +B_PID=0xa81a B_VID=0x0bda # 8822EU beamformee + sensor +TXPWR="${TXPWR:-16}" +mkdir -p "$OUT" + +cleanup() { + pkill -x WiFiDriverTxDem 2>/dev/null || true + pkill -x WiFiDriverDemo 2>/dev/null || true + true +} +trap cleanup EXIT INT TERM +plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; } +for d in "$A_PID $A_VID beamformer-8822EU" "$B_PID $B_VID beamformee-8822CU"; do + read -r p v name <<<"$d" + plugged "$p" "$v" || { echo "SKIP: $name ($p:$v) not plugged"; exit 0; } +done + +echo "== building ==" +cmake --build "$ROOT/build" -j --target WiFiDriverTxDemo WiFiDriverDemo >/dev/null || exit 1 + +# One cell. $1 = apply(0|1) $2 = tag. Echoes "rssi snr applied". +run_cell() { + local apply="$1" tag="$2" + : >"$OUT/$tag-b.log"; : >"$OUT/$tag-a.log" + sudo -n env DEVOURER_PID="$B_PID" DEVOURER_VID="$B_VID" DEVOURER_CHANNEL="$CH" \ + DEVOURER_BF_ARM_BFEE="$BFER" DEVOURER_RXQUALITY=1 DEVOURER_RX_ENERGY_MS=1000 \ + timeout $((DUR + 20)) "$ROOT/build/WiFiDriverDemo" 2>&1 \ + | tee "$OUT/$tag-b.full.log" | grep --line-buffered devourer-rxquality \ + >"$OUT/$tag-b.log" & + local bpid=$! + # Jaguar3 beamformee init is long (DLFW + cals ~10-15s) — wait for the RX + # loop banner + the beamformee arm before sounding, else no CBRs. + for _ in $(seq 50); do + grep -q "entering RX loop" "$OUT/$tag-b.full.log" 2>/dev/null && break + sleep 0.5 + done + sleep 2 + local extra=() + [ "$apply" = 1 ] && extra=(DEVOURER_BF_TXBF="$BFEE") + sudo -n env DEVOURER_PID="$A_PID" DEVOURER_VID="$A_VID" DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_WITH_RX=thread DEVOURER_BF_ARM_SOUNDER="$BFER" \ + DEVOURER_TX_NDPA_RA="$BFEE" DEVOURER_TX_NDPA=64 DEVOURER_TX_RATE=VHT1SS_MCS0 \ + DEVOURER_TX_PWR="$TXPWR" DEVOURER_TX_GAP_US=1000 "${extra[@]}" \ + timeout "$DUR" "$ROOT/build/WiFiDriverTxDemo" >"$OUT/$tag-a.log" 2>&1 || true + sudo -n pkill -x WiFiDriverDemo 2>/dev/null || true + wait "$bpid" 2>/dev/null || true + sleep 2 + # median rssi_mean / snr_mean over settled windows (frames>0) + python3 - "$OUT/$tag-b.log" <<'PYEOF' +import re, statistics, sys +r, s = [], [] +for line in open(sys.argv[1], errors="replace"): + fr = re.search(r"frames=(\d+)", line) + if not fr or int(fr.group(1)) == 0: continue + m = re.search(r"rssi_mean_dbm=(-?\d+)", line) + n = re.search(r"snr_mean_db=(-?\d+\.?\d*)", line) + if m: r.append(int(m.group(1))) + if n: s.append(float(n.group(1))) +def med(a): return statistics.median(a) if a else float("nan") +print(f"{med(r):.1f} {med(s):.1f}") +PYEOF +} + +applied_any=0 +declare -a OFF_R OFF_S ON_R ON_S +for rep in $(seq 1 "$REPS"); do + read -r r s <<<"$(run_cell 0 "off$rep")"; OFF_R+=("$r"); OFF_S+=("$s") + read -r r s <<<"$(run_cell 1 "on$rep")"; ON_R+=("$r"); ON_S+=("$s") + grep -q "apply ENABLED" "$OUT/on$rep-a.log" && applied_any=1 + printf "rep%s OFF rssi=%s snr=%s | ON rssi=%s snr=%s | apply=%s cbr_gate=%s\n" \ + "$rep" "${OFF_R[-1]}" "${OFF_S[-1]}" "${ON_R[-1]}" "${ON_S[-1]}" \ + "$(grep -c 'apply ENABLED' "$OUT/on$rep-a.log")" \ + "$(grep -c 'entry configured' "$OUT/on$rep-a.log")" +done + +echo +python3 - "$applied_any" "${OFF_S[@]}" "__" "${ON_S[@]}" <<'PYEOF' +import sys, statistics +applied = sys.argv[1] == "1" +args = sys.argv[2:] +sep = args.index("__") +off = [float(x) for x in args[:sep]] +on = [float(x) for x in args[sep+1:]] +d = statistics.median(on) - statistics.median(off) +print(f"OFF snr median: {statistics.median(off):.1f} dB") +print(f"ON snr median: {statistics.median(on):.1f} dB") +print(f"delta (ON - OFF): {d:+.1f} dB apply-gate fired: {applied}") +if applied and d >= 1.5: + print("PASS: CBR-gated TXBF apply lifts the peer's SNR") +elif applied and d <= -1.0: + print("FAIL: apply fired but DEGRADED the peer (V/steering wrong)") +elif not applied: + print("FAIL: apply gate never fired (A did not ingest a CBR)") +else: + print("INCONCLUSIVE: apply fired, delta within noise") +PYEOF