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
15 changes: 15 additions & 0 deletions hal/halbb/g6/kestrel_halbb_glue.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ void kestrel_halbb_rx_bringup(struct kestrel_halbb_ctx *ctx) {
/* Enable both RX chains (rx_path_en). Vendor core dispatcher; its 8852C
* case passes hal_com->dbcc_en (0 here) — same as the direct call did. */
halbb_ctrl_rx_path(bb, RF_PATH_AB, HW_PHY_0);

/* 8852C physts MEASUREMENT bring-up. Devourer's minimal halbb bring-up runs
* only halbb_init_reg (BB table) + this, skipping the vendor's
* halbb_dm_init_per_phy measurement inits. On the 8852B the physts engine
* fills reports anyway (its table covers it); on the 8852C the reports arrive
* EMPTY (is_valid=0) without these — halbb_ic_hw_setting_init_8852c sets the
* BB evm/measurement report-enable (0xa10[0]), and the physts CR map + IE
* bitmap must be programmed for the engine to snapshot RSSI/SNR/EVM. */
#ifdef BB_8852C_SUPPORT
if (bb->ic_type == BB_RTL8852C) {
halbb_ic_hw_setting_init_8852c(bb); /* 0xa10[0] evm/measurement rpt-en */
halbb_cr_cfg_physts_init(bb);
halbb_physts_parsing_init(bb);
}
#endif
}

void kestrel_halbb_set_gain(struct kestrel_halbb_ctx *ctx, unsigned char central_ch,
Expand Down
26 changes: 17 additions & 9 deletions src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1920,18 +1920,26 @@ void HalKestrel::bb_reset_all() {
bb_rmw(0x3200, 0x7u << 28, 0x0);
bb_rmw(0x704, 1u << 1, 1);
if (_variant == ChipVariant::C8852C) {
/* cfg_phy_rpt (phy_rpt.c) for the 8852C, matching the working vendor
* driver's R_AX_PPDU_STAT (0xCE40) = 0x0B00020F: RPT_EN(0) | APP_MAC_INFO(1)
* | APP_RX_CNT(2) | APP_PLCP_HDR(3) | DMA_MODE(9, the is_chip_id(8852C)
* bit) | the PPDU filter HAS_A1M/CRC_OK/DMA_OK (24/25/27). Devourer's
* byte-OR only sets RPT_EN; without the filter bits no PPDU gets a status
* report, so the 8852C emits zero physts (rate/RSSI read 0). Also forward
* PPDU-status to the host, not the WLCPU (R_AX_HW_RPT_FWD 0x9C18[1:0]=1). */
/* cfg_phy_rpt (phy_rpt.c) for the 8852C R_AX_PPDU_STAT (0xCE40) =
* 0x0B000201: RPT_EN(0) | DMA_MODE(9, the is_chip_id(8852C) bit) | the PPDU
* report filter HAS_A1M/CRC_OK/DMA_OK (24/25/27). Devourer's byte-OR only
* sets RPT_EN; without the filter bits no PPDU gets a status report, so the
* 8852C emits zero physts.
* NB the vendor's value ALSO sets APP_MAC_INFO(1)/APP_RX_CNT(2)/
* APP_PLCP_HDR(3), which PREPEND those blocks to the report and push the
* physts_hdr_info out of place (devourer parses the physts, not the appended
* MAC info) — with them set, the report reads as an invalid/empty physts
* (is_valid=0). Clearing them makes the report the bare physts, bit-identical
* to the 8852B (is_valid + rssi_avg_td + IE_01), which the RPKT_TYPE_PPDU
* handler + the halbb measurement bring-up (kestrel_halbb_rx_bringup 8852C
* branch) turn into a working per-frame RSSI/SNR passive floor.
* Also forward PPDU-status to the host, not the WLCPU (R_AX_HW_RPT_FWD
* 0x9C18[1:0]=1). */
const uint32_t pre = _device.rtw_read32(0xce40);
_device.rtw_write32(0xce40, 0x0B00020Fu);
_device.rtw_write32(0xce40, 0x0B000201u);
_device.rtw_write32(0x9C18,
(_device.rtw_read32(0x9C18) & ~0x3u) | 0x1u);
_logger->info("Kestrel PPDU_STAT(8852C): 0xCE40 0x{:08x} -> 0x0B00020F", pre);
_logger->info("Kestrel PPDU_STAT(8852C): 0xCE40 0x{:08x} -> 0x0B000201", pre);
} else {
v = _device.rtw_read8(0xce40); /* start phy-sts update */
_device.rtw_write8(0xce40, static_cast<uint8_t>(v | 0x1));
Expand Down
23 changes: 14 additions & 9 deletions src/kestrel/RtlKestrelDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
* (byte0 [4:0] == 1) before trusting the offset: avg_snr is IE byte 8
* [5:0] in dB. Store as raw = dB*2 (the RxQualityAccumulator
* convention snr_db = raw/2). 0 when IE_01 absent (e.g. CCK).
* On-air-validated on the C8852B (passive floor cross-matches the NHM
* floor within ~1 dB). The C8852C physts layout differs (16-byte
* drv_info); IE_01 is not at this offset there, so snr stays 0 and the
* passive floor is null on the C — a follow-up (RSSI/LinkHealth still
* populate). */
* On-air-validated on BOTH dies (passive floor cross-matches the
* NHM floor within ~1 dB): the C8852C physts is bit-identical to the
* C8852B once its measurement engine is brought up (the 8852C branch
* in kestrel_halbb_rx_bringup + the R_AX_PPDU_STAT no-APP-prepend
* config in bb_reset_all) — same header, same IE_01 offset. */
_last_snr = 0;
if (f.payload_len >= 8 + 9 && (f.payload[8] & 0x1f) == 1)
_last_snr = static_cast<uint8_t>((f.payload[16] & 0x3f) * 2);
Expand Down Expand Up @@ -372,10 +372,15 @@ RxEnergy RtlKestrelDevice::GetRxEnergy() {
* env-monitor. Frame-free, BB-driven, no clock-stop -> no wedge.
* ~mntr_time (100 ms) of control-thread wait; opt-in. Guard to a plausible
* idle-floor band so a not-ready/garbage report never emits a fake dBm.
* C8852B only: the NHM triggers on the C8852C but its nhm_pwr reads ~25 dB
* high (an 8852C-specific scaling/reference not yet resolved — on-air, idle
* ch149 reads -68 dBm vs the 8852B's -93), so it stays null on the C rather
* than emit a wrong value. 8852C is a follow-up. */
* C8852B only: the NHM triggers on the C8852C but its nhm_pwr reads ~23 dB
* high (on-air ch36 the NHM reads -70 dBm while the passive floor and the
* 8852B NHM both read -93). The offset is in the NHM's raw HW gain reference,
* not the SW path (halbb_env_mntr treats the 8852C identically to the 8852B)
* and not the front-end gain (the physts RSSI is correct — the passive floor
* cross-matches the 8852B NHM within ~1 dB). The vendor has no 8852C NHM
* offset; it is a deeper RF-cal reference. The C8852C's absolute floor is met
* by the passive rssi-snr floor (GetRxQuality), so the frame-free NHM stays
* 8852B-only rather than emit the wrong -70. */
if (_cfg.rx.abs_noise_floor && _variant == kestrel::ChipVariant::C8852B) {
int8_t nf = 0;
if (_hal.nhm_noise_floor(nf) && nf <= -60 && nf >= -105) {
Expand Down
90 changes: 90 additions & 0 deletions tests/kestrel_physts_floor_onair.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# On-air validation for the Kestrel passive noise floor (RtlKestrelDevice PPDU
# handler -> rssi-snr -> RxQualityAccumulator) on BOTH dies.
#
# The gap this closes: the C8852C emitted EMPTY physts reports
# (is_valid=0, all-zero content) so it had no per-frame RSSI/SNR and a NULL
# passive floor. Root cause was the 8852C physts MEASUREMENT bring-up, not the
# parse: (a) halbb_ic_hw_setting_init_8852c sets the BB evm/measurement
# report-enable (0xa10[0]); (b) the physts CR map + IE bitmap must be programmed
# (halbb_cr_cfg_physts_init + halbb_physts_parsing_init); (c) R_AX_PPDU_STAT
# (0xCE40) must NOT set the APP_MAC_INFO/RX_CNT/PLCP_HDR prepend bits (1/2/3),
# which pushed the physts_hdr_info out of place. With those, the 8852C physts is
# bit-identical to the 8852B (byte0 is_valid, byte3 rssi_avg_td, IE_01 avg_snr),
# so the existing hand-parse works for both dies.
#
# CHECK: with DEVOURER_RXQUALITY on an ambient channel, the passive
# noise_floor_dbm populates (non-null, plausible -80..-100 band) on BOTH dies,
# and the C8852B is unchanged from before.
#
# Usage: sudo -v && tests/kestrel_physts_floor_onair.sh
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${KPFOUT:-/tmp/devourer-kestrel-physts}"
CH="${CH:-6}" # ambient-traffic channel (2.4 GHz; both dies do monitor RX)
DUR="${DUR:-8}"
mkdir -p "$OUT"

VID=0x35bc
C_PID=0x0101 # RTL8832CU / C8852C (the die the fix targets)
B_PID=0x0108 # RTL8852BU / C8852B (regression control)

plugged() { lsusb -d "$(printf '%04x:%04x' "$1" "$2")" >/dev/null 2>&1; }
unbind() { local pid="$1" d p i
for d in /sys/bus/usb/devices/*/idProduct; do p=$(cat "$d" 2>/dev/null) || continue
[ "$p" = "${pid#0x}" ] || continue
for i in "$(dirname "$d")":*; do
[ -e "$i/driver" ] && sudo sh -c "echo '$(basename "$i")' > '$i/driver/unbind'" 2>/dev/null || true
done; done; }

run_die() { # $1=pid $2=tag -> "n snr passiveNF activeNF rssiMax"
sudo env DEVOURER_VID="$VID" DEVOURER_PID="$1" DEVOURER_CHANNEL="$CH" \
DEVOURER_RXQUALITY=1 DEVOURER_RX_NOISE_FLOOR=1 DEVOURER_RX_ENERGY_MS=500 \
timeout "$DUR" "$ROOT/build/rxdemo" >"$OUT/$2.log" 2>/dev/null || true
grep '"ev":"rx.quality"' "$OUT/$2.log" 2>/dev/null | python3 "$OUT/med.py"
}

cat >"$OUT/med.py" <<'PYEOF'
import json,sys,statistics as st
rows=[json.loads(l) for l in sys.stdin]
def med(k):
v=[x[k] for x in rows if isinstance(x.get(k),(int,float))]
return round(st.median(v),1) if v else None
def s(x): return "null" if x is None else str(x)
print("\t".join([str(len(rows)), s(med("snr_mean_db")), s(med("noise_floor_dbm")),
s(med("abs_noise_floor_dbm")), s(med("rssi_max_dbm"))]))
PYEOF

echo "== building rxdemo =="; cmake --build "$ROOT/build" -j --target rxdemo >/dev/null || exit 1
printf "\n%-8s %-7s %-8s %-11s %-10s %-9s\n" DIE qual_ev snr_dB passiveNF activeNF rssiMax
: >"$OUT/summary.tsv"
for spec in "C8852C:$C_PID" "C8852B:$B_PID"; do
name="${spec%%:*}"; pid="${spec##*:}"
plugged "$VID" "$pid" || { printf "%-8s SKIP (not plugged)\n" "$name"; continue; }
unbind "$pid"
read -r n snr nf anf rmax < <(run_die "$pid" "$name")
printf "%-8s %-7s %-8s %-11s %-10s %-9s\n" "$name" "${n:-0}" "${snr:-null}" "${nf:-null}" "${anf:-null}" "${rmax:-null}"
printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$name" "${n:-0}" "${snr:-null}" "${nf:-null}" "${anf:-null}" "${rmax:-null}" >>"$OUT/summary.tsv"
done

echo
python3 - "$OUT/summary.tsv" <<'PYEOF'
import sys
def num(x):
try: return float(x)
except: return None
rows={}
for l in open(sys.argv[1]):
f=l.rstrip("\n").split("\t"); rows[f[0]]=f
for name in ("C8852C","C8852B"):
r=rows.get(name)
if not r: print(f" {name}: no data"); continue
nf=num(r[3])
if nf is None: print(f" {name}: passive floor NULL (FAIL)")
elif -100<=nf<=-80: print(f" {name}: passive floor {nf:.0f} dBm, snr {r[2]} dB -> PASS")
else: print(f" {name}: passive floor {nf} dBm (snr {r[2]}) -> INSPECT")
c,b=rows.get("C8852C"),rows.get("C8852B")
if c and b and num(c[3]) is not None and num(b[3]) is not None:
print(f" cross-die delta {abs(num(c[3])-num(b[3])):.0f} dB")
print(f"\n raw: {sys.argv[1]}")
PYEOF
Loading