diff --git a/hal/halbb/g6/kestrel_halbb_glue.c b/hal/halbb/g6/kestrel_halbb_glue.c index 78e43c0..89d3403 100644 --- a/hal/halbb/g6/kestrel_halbb_glue.c +++ b/hal/halbb/g6/kestrel_halbb_glue.c @@ -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, diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 474755d..248f3af 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -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(v | 0x1)); diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index e8d4727..83624bb 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -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((f.payload[16] & 0x3f) * 2); @@ -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) { diff --git a/tests/kestrel_physts_floor_onair.sh b/tests/kestrel_physts_floor_onair.sh new file mode 100755 index 0000000..5523437 --- /dev/null +++ b/tests/kestrel_physts_floor_onair.sh @@ -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