Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/BeamformingSounder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<uint8_t>(rx_nss - 1) : 0;
const uint16_t csi_param =
static_cast<uint16_t>((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<uint32_t>(kBbpsfCtrl) | (1u << 30);
bbpsf = (g_id == 63) ? (bbpsf | (1u << 12)) : (bbpsf & ~(1u << 12));
dev.rtw_write<uint32_t>(kBbpsfCtrl, bbpsf);
dev.rtw_write<uint32_t>(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<uint32_t>(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<uint32_t>(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 */
Expand Down
72 changes: 68 additions & 4 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
p.Data = std::span<uint8_t>(const_cast<uint8_t *>(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<std::mutex> lk(_reg_mu);
devourer::bf::apply_vmatrix(
_device, true, static_cast<uint8_t>(_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)
Expand Down Expand Up @@ -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<uint8_t>(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
Expand Down Expand Up @@ -1004,6 +1046,32 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) {
bwidth = static_cast<ChannelWidth_t>(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<uint64_t>(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;
Expand All @@ -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. */
Expand Down
10 changes: 10 additions & 0 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> _bf_apply_on{false};
std::atomic<uint64_t> _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;
Expand Down
121 changes: 121 additions & 0 deletions tests/txbf_apply_onair.sh
Original file line number Diff line number Diff line change
@@ -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 (<devourer-rxquality>).
# 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
Loading