diff --git a/CMakeLists.txt b/CMakeLists.txt index eb7a6ddd..ad63549c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -610,7 +610,7 @@ add_test(NAME tone_mask_math COMMAND ToneMaskSelftest) # Headless guard for the active idle-noise-floor math (src/NoiseFloorMath.h): the # phydm sign/pwdb helpers the Jaguar1 0x0FA0 debug-port measurement uses. Pure -# math, header-only (issue #202). +# math, header-only. add_executable(NoiseFloorMathSelftest tests/noise_floor_math_selftest.cpp) target_include_directories(NoiseFloorMathSelftest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) diff --git a/docs/rx-spectrum-sensing.md b/docs/rx-spectrum-sensing.md index 4b04dc5f..f55cf77a 100644 --- a/docs/rx-spectrum-sensing.md +++ b/docs/rx-spectrum-sensing.md @@ -63,6 +63,15 @@ Two noise floors are exposed on `GetRxQuality()`: - **Jaguar3 (8822C/8822E)** — no vendor idle-noise path (the report dispatch excludes the 8822C), so `abs_noise_floor_dbm` is always null; the passive floor is J3's only floor. + - **Kestrel (8852B/8852C, Wi-Fi 6)** — the cleanest source: Realtek's halbb + **NHM env-monitor** (`halbb_env_mntr_trigger`/`result` → `nhm_pwr − 110`), a + proper frame-free BB measurement with no clock-stop → no wedge. On-air the + **8852B reads a correct idle floor (−93 dBm, cross-matches the passive floor + within ~1 dB)**; the 8852C triggers but its `nhm_pwr` reads ~25 dB high (an + 8852C scaling difference, unresolved) so it stays null there. Kestrel also + gets the passive floor + LinkHealth (per-frame RSSI from the physts header + + SNR from IE_01) on the 8852B; the 8852C physts layout differs (SNR unparsed → + passive floor null there). Both 8852C gaps are follow-ups. Validation: `tests/rx_noise_floor_active_onair.sh` (anti-wedge + sanity / cross-chip agreement — a B210 injected-noise sweep isn't used because the bench diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index c4a2eb17..3de22143 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -1208,7 +1208,7 @@ int main() { ev.f("igi", e.igi); else ev.f("igi", nullptr); - if (e.valid_noise_floor) /* active absolute floor, #202 (opt-in) */ + if (e.valid_noise_floor) /* active absolute floor (opt-in) */ ev.f("abs_noise_floor_dbm", e.abs_noise_floor_dbm); else ev.f("abs_noise_floor_dbm", nullptr); @@ -1294,7 +1294,7 @@ int main() { ev.f("noise_floor_dbm", nullptr); /* Active/frame-free absolute floor (DEVOURER_RX_NOISE_FLOOR): the * companion to the passive floor above. Null unless opted-in AND the - * generation supports it (Jaguar2 live / Jaguar1 8812A CAL). #202. */ + * generation supports it (Jaguar2 live / Jaguar1 8812A CAL). */ if (q.abs_nf_valid) ev.f("abs_noise_floor_dbm", q.abs_noise_floor_dbm); else diff --git a/examples/streamtx/main.cpp b/examples/streamtx/main.cpp index 661d17bf..23746261 100644 --- a/examples/streamtx/main.cpp +++ b/examples/streamtx/main.cpp @@ -210,7 +210,7 @@ int main(int argc, char **argv) { auto stream_cfg = devourer_config_from_env(); /* FPV downlink default: disable the MAC carrier-sense gate so the video TX * punches through co-channel traffic instead of deferring — on-air ~1.5-2.2x - * inject-rate recovery under a co-channel transmitter (issue #199, SetCcaMode / + * inject-rate recovery under a co-channel transmitter (SetCcaMode / * DEVOURER_DIS_CCA). The link owns the channel, so CSMA back-off only stutters * it. Explicit DEVOURER_DIS_CCA=0 still forces standard carrier-sense back on. */ if (std::getenv("DEVOURER_DIS_CCA") == nullptr) diff --git a/hal/halbb/g6/kestrel_halbb_glue.c b/hal/halbb/g6/kestrel_halbb_glue.c index 00a1f4dd..78e43c0c 100644 --- a/hal/halbb/g6/kestrel_halbb_glue.c +++ b/hal/halbb/g6/kestrel_halbb_glue.c @@ -9,6 +9,7 @@ struct kestrel_halbb_ctx { struct rtw_hal_com_t hal; struct rtw_phl_com_t phl; struct bb_cmn_info cmn; + int env_mntr_ready; /* lazy halbb_env_mntr init done */ }; struct kestrel_halbb_ctx *kestrel_halbb_create(struct kestrel_halbb_bridge *br, @@ -39,6 +40,9 @@ struct kestrel_halbb_ctx *kestrel_halbb_create(struct kestrel_halbb_bridge *br, c->hal.chip_id = CHIP_WIFI6_8852C; } c->bb.rx_path = RF_PATH_AB; + c->bb.cr_type = BB_CLIENT; /* USB client CR bank; without it the env-monitor + * CR-init switch falls through and the NHM + * ready-bit address stays 0 (report never rdy) */ c->bb.bb0_cr_offset = 0; /* bridge adds the BB window (wIndex=1) */ /* halbb_init_cr_default guards on this flag; it asserts exactly the wiring * done above (hal_com/phl_com/bb_cmn_hooker present). */ @@ -118,6 +122,59 @@ void kestrel_halbb_ctrl_tx_path(struct kestrel_halbb_ctx *ctx) { #endif } +/* One-shot active/frame-free NHM absolute noise floor. See the header. */ +int kestrel_halbb_env_mntr_nhm(struct kestrel_halbb_ctx *ctx, + unsigned short mntr_time_ms, + signed char *nhm_pwr_dbm) { + struct bb_info *bb; + struct ccx_para_info para; + struct env_trig_rpt trig; + struct env_mntr_rpt rpt; + void *drv; + u16 mtime; + int i; + + if (!ctx || !nhm_pwr_dbm) + return 0; + bb = &ctx->bb; + drv = bb->hal_com->drv_priv; + mtime = mntr_time_ms ? mntr_time_ms : 100; + + /* Lazy env-monitor init: the RX bring-up flow (kestrel_halbb_init_reg) only + * runs halbb_init_reg, so the NHM CR addresses (incl. nhm_rdy) and the ccx/nhm + * state are still zero. Run the two vendor inits once before the first + * trigger. */ + if (!ctx->env_mntr_ready) { + halbb_cr_cfg_env_mntr_init(bb); + halbb_env_mntr_init(bb); + ctx->env_mntr_ready = 1; + } + + /* One-shot NHM in the vendor's IEEE-11k idle-noise mode. A zeroed para gives + * nhm_incld_cca=NHM_EXCLUDE_CCA + nav/rssi gating DISABLED (all enum 0). */ + _os_mem_set(0, ¶, 0, sizeof(para)); + para.rac_lv = RAC_LV_4; /* debug priority: always wins the racing arbiter */ + para.mntr_time = mtime; /* ms; drives ccx_period (must be nonzero) */ + para.nhm_app = NHM_DBG_11K; /* 802.11k noise-floor thresholds */ + + if (!(halbb_env_mntr_trigger(bb, ¶, &trig, HW_PHY_0) & NHM_SUCCESS)) + return 0; + + /* The measurement runs mntr_time in HW; wait the window, then poll the ready + * bit (halbb_env_mntr_result returns NHM_SUCCESS once nhm_rdy is set). */ + _os_delay_ms(drv, mtime); + for (i = 0; i < 8; ++i) { + _os_mem_set(0, &rpt, 0, sizeof(rpt)); + if ((halbb_env_mntr_result(bb, &rpt, HW_PHY_0) & NHM_SUCCESS) && + rpt.nhm_pwr != ENV_MNTR_FAIL_BYTE) { + *nhm_pwr_dbm = (signed char)((int)rpt.nhm_pwr - 110); /* 110+x dBm */ + return 1; + } + _os_delay_ms(drv, 3); + } + return 0; +} + void kestrel_halbb_destroy(struct kestrel_halbb_ctx *ctx) { if (ctx) free(ctx); diff --git a/hal/halbb/g6/kestrel_halbb_glue.h b/hal/halbb/g6/kestrel_halbb_glue.h index cb0810c3..92f01697 100644 --- a/hal/halbb/g6/kestrel_halbb_glue.h +++ b/hal/halbb/g6/kestrel_halbb_glue.h @@ -67,6 +67,16 @@ void kestrel_halbb_ctrl_bw_ch(struct kestrel_halbb_ctx *ctx, unsigned char pri_c * model, so this is a no-op there. Single-PHY, RF_PATH_A. */ void kestrel_halbb_ctrl_tx_path(struct kestrel_halbb_ctx *ctx); +/* One-shot active/frame-free NHM absolute noise floor. Runs a manual IEEE-11k + * NHM measurement over `mntr_time_ms` (0 -> 100) and returns the + * idle in-band power as signed dBm (nhm_pwr - 110). Frame-free, BB-driven, no + * clock-stop -> no RX wedge. Lazily runs halbb_cr_cfg_env_mntr_init + + * halbb_env_mntr_init on first call (the RX bring-up path does not). Returns 1 + * on a valid reading (fills *nhm_pwr_dbm), 0 on timeout/failure. */ +int kestrel_halbb_env_mntr_nhm(struct kestrel_halbb_ctx *ctx, + unsigned short mntr_time_ms, + signed char *nhm_pwr_dbm); + #ifdef __cplusplus } #endif diff --git a/hal/halbb/g6/kestrel_halbb_helpers.c b/hal/halbb/g6/kestrel_halbb_helpers.c index 7ea6ac88..12f14e99 100644 --- a/hal/halbb/g6/kestrel_halbb_helpers.c +++ b/hal/halbb/g6/kestrel_halbb_helpers.c @@ -220,3 +220,12 @@ void halbb_tssi_ctrl_set_bandedge_cfg(struct bb_info *bb, enum tssi_bandedge_cfg bandedge_cfg) { (void)bb; (void)phy_idx; (void)bandedge_cfg; } + +/* BE (11BE / WiFi7) primary-sub-band helper referenced by halbb_env_mntr.c's + * EDCCA-opt path only in its 11BE branch - dead code on the AX Kestrel ICs + * (halbb_api_be.c is not vendored). A link-only stub, never called at runtime. */ +u8 halbb_get_prim_sb(struct bb_info *bb, u8 central_ch, u8 pri_ch, + enum channel_width bw) { + (void)bb; (void)central_ch; (void)pri_ch; (void)bw; + return 0; +} diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index fc354b5d..78e2e4d6 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -101,7 +101,7 @@ struct DeviceConfig { * OFF by default: the vendor idle-noise measurement adds ~10 ms of USB * round-trips. Jaguar2 measures it live (wedge-free HW idle-noise report); * Jaguar1 8812A/8821A measure it RX-idle (a CAL bracket); Jaguar3 and others - * leave it invalid (no vendor path). Issue #202. */ + * leave it invalid (no vendor path). */ bool abs_noise_floor = false; /* env: DEVOURER_IGI — Jaguar2 fixed initial-gain index override, 7 bits * (unset = 0x40, the FA-rate-validated default). */ @@ -209,7 +209,7 @@ struct DeviceConfig { /* env: DEVOURER_DIS_CCA — Jaguar2/3 MAC carrier-sense disable at bring-up * (primary CCA 0x520[14] + EDCCA [15]): injected/beacon TX stops deferring to * a busy channel and punches through co-channel traffic. Runtime equivalent: - * SetCcaMode. Default-on on the streamtx FPV downlink (issue #199). */ + * SetCcaMode. Default-on on the streamtx FPV downlink. */ bool disable_cca = false; /* env: DEVOURER_TXPKT_STEP_QDB — Jaguar3 per-packet power-bank step size * in quarter-dB: the dB weight of one 0x1e70 offset-index step diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index 83c30be0..258e7baf 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -389,14 +389,16 @@ class IRtlDevice { * co-channel transmitter. Measured on-air (Jaguar3, 8822EU/8812CU): monitor * injection otherwise defers ~40-60% to a co-channel 802.11 flooder; clearing * the gate recovers ~1.5-2.2x, back to ~90% of the unimpeded rate - * (tests/dis_cca_tx_onair.sh, issue #199). The energy bit [15] alone is null + * (tests/dis_cca_tx_onair.sh). The energy bit [15] alone is null * against a decodable preamble — the primary-CCA bit [14] is what recovers the * inject path. This is the MAC-gate only; the vendor's BB CCA-off writes are * NOT applied (they deafen the RX). Also DEVOURER_DIS_CCA at construction. - * Implemented on Jaguar2/3; a DELIBERATE no-op on Jaguar1, whose baseband EDCCA - * is already disabled by its init table (0x8A4 = 0x7F7F7F7F) and whose - * hardware-beacon downlink measures ~0.34 µs RMS on a crowded channel with no - * MAC-side gate. */ + * Implemented on Jaguar2/3 and Kestrel (the AX R_AX_CCA_CFG_0 all-CCA-EN field — + * on Kestrel injection is already CCA-off by default via sch_tx_en, so this is a + * runtime toggle rather than the deferral fix it is on Jaguar); a DELIBERATE + * no-op on Jaguar1, whose baseband EDCCA is already disabled by its init table + * (0x8A4 = 0x7F7F7F7F) and whose hardware-beacon downlink measures ~0.34 µs RMS + * on a crowded channel with no MAC-side gate. */ virtual void SetCcaMode(bool disabled) { (void)disabled; } /* Shift the next hardware beacon TBTT by `microseconds` (>0 = later/retard, diff --git a/src/NoiseFloorMath.h b/src/NoiseFloorMath.h index 46ca914d..258ff8e0 100644 --- a/src/NoiseFloorMath.h +++ b/src/NoiseFloorMath.h @@ -1,7 +1,7 @@ /* Verbatim ports of the two phydm_math_lib helpers the active idle-noise-floor * measurement needs (reference/rtl8812au/hal/phydm/phydm_math_lib.c). Kept in a * standalone header so the per-generation noise-floor code and a selftest share - * one copy. See docs/rx-spectrum-sensing.md and issue #202. + * one copy. See docs/rx-spectrum-sensing.md. * * These drive the Jaguar1 (8812A/8821A) active-sampling path: the 0x0FA0 debug * port packs rx I/Q as 10-bit signed (sign_conversion), and the per-sample power diff --git a/src/RxQuality.h b/src/RxQuality.h index b74e9577..1640423b 100644 --- a/src/RxQuality.h +++ b/src/RxQuality.h @@ -55,7 +55,7 @@ struct RxQuality { * floor above: the vendor idle-noise monitor, measured with no wanted signal * (site survey / channel selection). Present only when the caller opted in * (DEVOURER_RX_NOISE_FLOOR) and the generation supports it (Jaguar2 live, - * Jaguar1 8812A/8821A RX-idle CAL; false elsewhere). Issue #202. */ + * Jaguar1 8812A/8821A RX-idle CAL; false elsewhere). */ int8_t abs_noise_floor_dbm = 0; bool abs_nf_valid = false; diff --git a/src/RxSense.h b/src/RxSense.h index 4e36378b..7d175c43 100644 --- a/src/RxSense.h +++ b/src/RxSense.h @@ -46,7 +46,7 @@ struct RxEnergy { * (~10 ms of USB round-trips), so it is only filled when the caller opted in * (DEVOURER_RX_NOISE_FLOOR). Jaguar2 fills it live (HW idle-noise report, * wedge-free); Jaguar1 8812A/8821A fill it from an RX-idle CAL measurement; - * Jaguar3 and others leave it invalid (no vendor path). Issue #202. */ + * Jaguar3 and others leave it invalid (no vendor path). */ bool valid_noise_floor = false; int8_t abs_noise_floor_dbm = 0; }; diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 74ae9899..d0c91faf 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -4,7 +4,7 @@ #include "EepromManager.h" #include "Hal8812PhyReg.h" #include "NhmReader.h" -#include "NoiseFloorMath.h" /* active idle-noise-floor sign/pwdb helpers (#202) */ +#include "NoiseFloorMath.h" /* active idle-noise-floor sign/pwdb helpers */ #include "RadioManagementModule.h" #include "AckResponder.h" /* hardware ACK responder recipe */ #include "RadiotapPeek.h" /* send_packets batch pre-parse */ @@ -301,7 +301,7 @@ RxEnergy RtlJaguarDevice::GetRxEnergy() { }, e); - /* Active absolute floor (#202): the debug-port measurement wedges live RX, so + /* Active absolute floor: the debug-port measurement wedges live RX, so * it is NOT re-run here — GetRxEnergy just surfaces the RX-idle CAL taken at * bring-up (measure_idle_noise_floor). Left invalid on the 8814A / when off. */ if (_cfg.rx.abs_noise_floor) { @@ -313,7 +313,7 @@ RxEnergy RtlJaguarDevice::GetRxEnergy() { /* Vendor odm_inband_noise_monitor_ac (8812/8821 active-sampling path). MUST run * RX-idle: the clock-stop (0x8B4[6]) + BB/PMAC/CCK resets collide with a live - * bulk-IN RX DMA (#202). Reads the path-A RX I/Q off debug port 0x0FA0, forms + * bulk-IN RX DMA. Reads the path-A RX I/Q off debug port 0x0FA0, forms * pwdb = 10*log10(I^2+Q^2), averages VALID_CNT idle samples in [-27,0) dB, and * offsets to dBm: noise = avg + 12(ADC backoff) + 0x1C(IGI ref) - 110 - 3. */ void RtlJaguarDevice::measure_idle_noise_floor() { @@ -1280,7 +1280,7 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, /* DEVOURER_RX_NOISE_FLOOR — measure the absolute idle floor RX-idle here, * before StartRxLoop starts the bulk-IN DMA (the active-sampling path wedges a - * live RX). 8812A/8821A only; the 8814A uses a different vendor path (#202). */ + * live RX). 8812A/8821A only; the 8814A uses a different vendor path. */ if (_cfg.rx.abs_noise_floor && _eepromManager->version_id.ICType != CHIP_8814A) measure_idle_noise_floor(); diff --git a/src/jaguar1/RtlJaguarDevice.h b/src/jaguar1/RtlJaguarDevice.h index a4443f3b..1c5c25aa 100644 --- a/src/jaguar1/RtlJaguarDevice.h +++ b/src/jaguar1/RtlJaguarDevice.h @@ -289,7 +289,7 @@ class RtlJaguarDevice : public IRtlDevice { void SetRxPathMask(uint8_t mask); int GetRxPathMask(); - /* Active idle-noise-floor CAL (#202, 8812A/8821A only). The vendor + /* Active idle-noise-floor CAL (8812A/8821A only). The vendor * odm_inband_noise_monitor_ac debug-port path stops CK320/CK88 and resets * BB/PMAC/CCK, which wedges a live bulk-IN RX DMA — so it MUST run RX-idle. * Called at bring-up (before StartRxLoop); result cached and surfaced via diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 5455f527..e958fc13 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -870,7 +870,7 @@ RxEnergy RtlJaguar2Device::GetRxEnergy() { }, e); - /* DEVOURER_RX_NOISE_FLOOR — active/frame-free absolute floor (#202). The + /* DEVOURER_RX_NOISE_FLOOR — active/frame-free absolute floor. The * vendor phydm_idle_noise_measure_ac: the BB maintains an idle-time power * report at 0x0FF0 (path-A [7:0], path-B [15:8], sval = byte>>1); freeze it * (0x9E4[30]=1) for a clean read, then noise = -110 + IGI(0xC50/0xE50) + mean. @@ -879,7 +879,7 @@ RxEnergy RtlJaguar2Device::GetRxEnergy() { * * BEST-EFFORT on the tested 8812BU (1x1 8822B cut): the report is only rarely * populated in devourer's monitor bring-up — most reads return the 0x80/0x00 - * "no idle sample" sentinels. Investigated (issue #202): there is no vendor + * "no idle sample" sentinels. Investigated: there is no vendor * enable step for it (the report is HW-automatic and the vendor runs the * measurement from a full associated init, not monitor mode); the sentinel * rate is channel-independent, oversampling barely helps, and pausing DIG @@ -1709,7 +1709,7 @@ void RtlJaguar2Device::SetCcaMode(bool disabled) { std::lock_guard lk(_reg_mu); /* Both MAC carrier-sense bits in REG_TX_PTCL_CTRL: primary CCA 0x520[14] + * EDCCA [15], plus EDCCA_MSK_COUNTDOWN 0x524[11]. The primary-CCA bit is the - * one that stops TX deferring to a co-channel transmitter (issue #199); 0x520 + * one that stops TX deferring to a co-channel transmitter; 0x520 * is the same HalMAC layout as the on-air-validated Jaguar3. */ uint32_t v520 = _device.rtw_read(0x0520); uint32_t v524 = _device.rtw_read(0x0524); diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 457f8784..b73c4568 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1097,7 +1097,7 @@ RxEnergy RtlJaguar3Device::GetRxEnergy() { _device.phy_set_bb_reg(0x1eb4, 1u << 25, 0x0); _device.phy_set_bb_reg(0x1d2c, 1u << 31, 0x1); - /* No active absolute noise floor on Jaguar3 (#202): the vendor 8822C driver + /* No active absolute noise floor on Jaguar3: the vendor 8822C driver * has no idle-noise path (phydm_noisemonitor.c dispatches the report only to * 8822B/8821C and returns 0 for the 8822C), so e.valid_noise_floor stays * false. The passive rssi-snr floor (RxQuality.noise_floor_dbm) is J3's only @@ -1116,8 +1116,8 @@ RxEnergy RtlJaguar3Device::GetRxEnergy() { * flooder vs a far-channel USB-contention control; tests/dis_cca_tx_onair.sh). * Clearing [14] removes that deferral (~1.5x recovery, back to ~90% of the * unimpeded rate) so injected/beacon TX punches through a busy channel — the - * "keep transmitting through interference" lever the OpenIPC-FPV community wants - * (issue #199). The energy bit [15] alone is null against a decodable preamble; + * "keep transmitting through interference" lever the OpenIPC-FPV community wants. + * The energy bit [15] alone is null against a decodable preamble; * it is kept because it also relaxes deferral to non-802.11 in-band energy. * * The vendor dis_cca recipe ALSO writes three BB registers (0x1a9c[20], diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 413ab99c..474755da 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -1691,6 +1691,16 @@ void HalKestrel::vnd_bb_ctrl_bw_ch(uint8_t pri_ch, uint8_t center, kestrel_halbb_ctrl_bw_ch(_halbb_ctx, pri_ch, center, hbw, band_type); } +bool HalKestrel::nhm_noise_floor(int8_t &dbm, uint16_t mntr_time_ms) { + if (!_halbb_ctx) + return false; + signed char v = 0; + if (!kestrel_halbb_env_mntr_nhm(_halbb_ctx, mntr_time_ms, &v)) + return false; + dbm = static_cast(v); + return true; +} + HalKestrel::~HalKestrel() { if (_halrf_ctx) kestrel_halrf_destroy(_halrf_ctx); diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index 638ca154..dfc91eb6 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -508,6 +508,10 @@ class HalKestrel { * connects the BB IFFT output to the TX chain on the 8852C (glue no-op on * the 8852B, which selects the TX antenna per-STA via the CMAC model). */ void vnd_bb_ctrl_tx_path(); + /* Active/frame-free NHM absolute noise floor: one-shot halbb env-monitor. + * Returns true + fills `dbm` on a valid reading, false otherwise (report not + * ready / no halbb ctx). */ + bool nhm_noise_floor(int8_t &dbm, uint16_t mntr_time_ms = 100); ::kestrel_halbb_ctx *_halbb_ctx = nullptr; void *_halbb_bridge = nullptr; /* heap kestrel_halbb_bridge, outlives ctx */ static unsigned int halbb_r32(void *dev, unsigned int addr); diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 153bea1f..e8d47277 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -214,6 +214,10 @@ void RtlKestrelDevice::InitWrite(SelectedChannel channel) { * pure-monitor RX path keeps CCA on and can actually hear frames. */ _hal.set_cca_on(_cfg.debug.kestrel_cca_on); EnableTxScheduler(); + /* DEVOURER_DIS_CCA: EnableTxScheduler already cleared the CCA gates for TX; + * this just re-asserts it explicitly (idempotent) when the knob is set. */ + if (_cfg.tuning.disable_cca) + SetCcaMode(true); _tx_mgmt_ep = _device.nth_bulk_out_ep(0); /* B0MG -> BULKOUTID0 */ _tx_data_ep = _device.nth_bulk_out_ep(3); /* ACH0 -> BULKOUTID3 */ if (_tx_mgmt_ep == 0) { @@ -288,6 +292,20 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { * for the following WIFI frame(s). */ _last_rssi[0] = static_cast(f.payload[4] >> 1); _last_rssi[1] = static_cast(f.payload[5] >> 1); + /* Per-frame SNR for the passive floor: the 8-byte header is followed + * by the IEs; IE_01 (OFDM/HE info, physts_ie_1_info) is + * the first for a decodable OFDM/HE PPDU. Self-validate on its ie_hdr + * (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). */ + _last_snr = 0; + if (f.payload_len >= 8 + 9 && (f.payload[8] & 0x1f) == 1) + _last_snr = static_cast((f.payload[16] & 0x3f) * 2); } else if (f.rpkt_type == kestrel::RPKT_TYPE_WIFI && packetProcessor) { Packet p{}; p.RxAtrib.pkt_len = static_cast(f.payload_len); @@ -300,6 +318,12 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { p.RxAtrib.tsfl = f.freerun_cnt; p.RxAtrib.rssi[0] = _last_rssi[0]; p.RxAtrib.rssi[1] = _last_rssi[1]; + p.RxAtrib.snr[0] = _last_snr; + /* Feed the windowed RX-quality aggregate (passive rssi-snr floor + + * LinkHealth). rssi_raw = dBm+110 (== _last_rssi), snr_raw = dB*2; + * a frame with no IE_01 SNR (snr=0) still counts toward RSSI. */ + if (_last_rssi[0] > 0) + _rxq.add(_last_rssi[0], _last_snr, 0); p.Data = std::span(const_cast(f.payload), f.payload_len); packetProcessor(p); @@ -322,6 +346,52 @@ void RtlKestrelDevice::SetMonitorChannel(SelectedChannel channel) { _hal.set_channel(channel.Channel, channel.ChannelWidth, channel.ChannelOffset, channel.Band); } +void RtlKestrelDevice::SetCcaMode(bool disabled) { + /* RMW the MAC carrier-sense gate: R_AX_CCA_CFG_0 B_AX_CCA_ALL_EN = primary CCA + * + sec20/40/80 + EDCCA (bits 0-4). Same field EnableTxScheduler clears for TX + * injection, so this composes with it (last writer wins). Only 0xC340 — NOT the + * 0xC390 per-phase EDCCA machinery (the Jaguar3 lesson: deeper CCA writes deafen + * RX; B_AX_CCA_ALL_EN already covers EDCCA via bit 4). No lock: Kestrel is + * lock-free on registers (the RX/WP-drain thread only touches the bulk-IN ep). + * The TX default is already CCA-off; carrier-sense TX needs the un-ported + * IQK/DPK cal to fully engage (see HalKestrel::sch_tx_en). */ + namespace r = kestrel::reg; + uint32_t v = _device.rtw_read32(r::R_AX_CCA_CFG_0); + if (disabled) + v &= ~r::B_AX_CCA_ALL_EN; + else + v |= r::B_AX_CCA_ALL_EN; + _device.rtw_write32(r::R_AX_CCA_CFG_0, v); + _logger->info("Kestrel: MAC carrier-sense {} (CCA_CFG_0=0x{:08x})", + disabled ? "DISABLED (dis_cca)" : "enabled", v); +} + +RxEnergy RtlKestrelDevice::GetRxEnergy() { + RxEnergy e; /* no phydm FA/CCA/IGI DIG monitor on Kestrel */ + /* DEVOURER_RX_NOISE_FLOOR — active/frame-free absolute floor via the halbb NHM + * 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. */ + if (_cfg.rx.abs_noise_floor && _variant == kestrel::ChipVariant::C8852B) { + int8_t nf = 0; + if (_hal.nhm_noise_floor(nf) && nf <= -60 && nf >= -105) { + e.abs_noise_floor_dbm = nf; + e.valid_noise_floor = true; + } + } + return e; +} + +devourer::RxQuality RtlKestrelDevice::GetRxQuality() { + /* Fuse the per-frame aggregate (passive rssi-snr floor + LinkHealth, fed by + * the RX loop via _rxq) with the active NHM floor from GetRxEnergy. */ + return devourer::build_rx_quality(_rxq.snapshot(), GetRxEnergy()); +} + void RtlKestrelDevice::FastRetune(uint8_t channel, bool /*cache_rf*/) { /* Lean path only for a same-band 20 MHz hop (RF channel switch); anything else * (band change or wide/narrow BW, where the center + BB config differ) takes diff --git a/src/kestrel/RtlKestrelDevice.h b/src/kestrel/RtlKestrelDevice.h index 42f9ac59..0f09c8b2 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -11,6 +11,7 @@ #include "DeviceConfig.h" #include "IRtlDevice.h" #include "RtlAdapter.h" +#include "RxQuality.h" /* RxQualityAccumulator + build_rx_quality */ #include "SelectedChannel.h" #include "ChipVariant.h" @@ -67,6 +68,11 @@ class RtlKestrelDevice : public IRtlDevice { stop_wp_drain(); } void SetMonitorChannel(SelectedChannel channel) override; + /* Disable / restore the MAC carrier-sense gate (R_AX_CCA_CFG_0 all-CCA-EN: + * primary + sec20/40/80 + EDCCA). Injection is already CCA-off by default here + * (EnableTxScheduler clears these gates), so this is a runtime toggle, not the + * co-channel-deferral fix it is on Jaguar. */ + void SetCcaMode(bool disabled) override; bool send_packet(const uint8_t *packet, size_t length) override; devourer::TxStats GetTxStats() override { return _device.GetTxStats(); } SelectedChannel GetSelectedChannel() override { return _channel; } @@ -107,6 +113,14 @@ class RtlKestrelDevice : public IRtlDevice { * the efuse baseline. Control-thread only (does an RF read). */ devourer::ThermalStatus GetThermalStatus() override; + /* Frame-free RX energy snapshot. On Kestrel this carries only the active + * absolute noise floor (halbb NHM env-monitor) when DEVOURER_RX_NOISE_FLOOR is + * set — there is no phydm FA/CCA/IGI DIG monitor on this generation. */ + RxEnergy GetRxEnergy() override; + /* Windowed RX link-quality: the passive rssi-snr floor + LinkHealth verdict + * (fed per frame via _rxq) fused with the active NHM floor from GetRxEnergy. */ + devourer::RxQuality GetRxQuality() override; + /* Arm the AX HW beacon engine (mac_send_bcn_h2c + AP port timing). Requires a * prior InitWrite. `beacon` is a full 802.11 beacon; the MAC airs it every * `interval_tu` TU with the live TSF inserted. */ @@ -207,6 +221,12 @@ class RtlKestrelDevice : public IRtlDevice { /* Per-chain RSSI (RSSI% = dBm+110) cached from the last PPDU-status physts * header, attached to the following WIFI frame(s) in the aggregate. */ uint8_t _last_rssi[2] = {0, 0}; + /* Per-frame SNR (raw U(8,1) = dB*2), cached from the physts header alongside + * RSSI for the passive noise floor (rssi_dbm - snr_db). 0 = unknown. */ + uint8_t _last_snr = 0; + /* Windowed RX link-quality accumulator (passive noise floor + LinkHealth), + * fed per decoded frame from the RX loop; drained by GetRxQuality. */ + devourer::RxQualityAccumulator _rxq; }; #endif /* RTL_KESTREL_DEVICE_H */ diff --git a/tests/dis_cca_tx_onair.sh b/tests/dis_cca_tx_onair.sh index a2b4b599..e845b0ac 100755 --- a/tests/dis_cca_tx_onair.sh +++ b/tests/dis_cca_tx_onair.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # TX-SIDE test for the dis_cca / carrier-sense-disable knob (SetCcaMode, -# DEVOURER_DIS_CCA) — the experiment issue #199 asks for. The sibling +# DEVOURER_DIS_CCA) — the TX-side experiment. The sibling # tests/dis_cca_onair.sh measured a NULL, but it ran the DUT as an RX (decoding a # beacon through B210 AWGN) and never exercised the TX side. Carrier sense gates TX # *deferral*, not RX decode, so the real test drives the DUT as a TX injector and diff --git a/tests/rx_noise_floor_active_onair.sh b/tests/rx_noise_floor_active_onair.sh index 7b9bec75..16a69dc3 100755 --- a/tests/rx_noise_floor_active_onair.sh +++ b/tests/rx_noise_floor_active_onair.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # On-air validation for the ACTIVE (frame-free) absolute noise floor -# (DEVOURER_RX_NOISE_FLOOR, RxQuality.abs_noise_floor_dbm) — issue #202. +# (DEVOURER_RX_NOISE_FLOOR, RxQuality.abs_noise_floor_dbm). # -# The #202 blocker was that the vendor active measurement WEDGES live RX (the +# The blocker was that the vendor active measurement WEDGES live RX (the # 8812AU live-poll delivered 0 frames on 2 of 6 runs). devourer measures it # RX-idle instead: # - Jaguar1 (8812A/8821A): the debug-port active-sampling path (clock-stop + @@ -15,11 +15,11 @@ # reads and null on others; poll until valid. When valid it cross-matches the # Jaguar1 floor on the same channel. # - Jaguar3 (8822C/8822E): no vendor idle-noise path -> always null (the -# passive rssi-snr floor #201 is its only floor). +# passive rssi-snr floor is its only floor). # # Two checks (a monotonic-vs-injected-noise sweep is NOT included: the bench B210 # is too weakly coupled to the RTL front ends to move the floor above the -# measurement variance — the same near-field limit noted in #199/#201): +# measurement variance — the same near-field limit the passive-floor bench hit): # (A) ANTI-WEDGE (the acceptance gate) — with the knob ON, a live RX keeps # delivering frames across N runs (0 wedges), vs the issue's 2/6. # (B) SANITY + CROSS-CHIP — the reported floor sits in a plausible band and the @@ -79,7 +79,7 @@ for r in $(seq 1 "$RUNS"); do printf " run %d (knob ON): %s frames\n" "$r" "$f" [ "${f:-0}" -le 0 ] && zeros=$((zeros + 1)); sleep 1 done -echo " => $zeros/$RUNS wedged (0-frame) runs [issue #202 live-poll: 2/6]" +echo " => $zeros/$RUNS wedged (0-frame) runs [live-poll baseline: 2/6]" [ "$zeros" -eq 0 ] && echo " PASS: wedge-free" || echo " FAIL: CAL wedged RX" echo