From c1d186816f3a8512227b1ed85b9d4ae16dd5ccc4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:18:01 +0300 Subject: [PATCH] Kestrel per-packet TX power: radiotap DBM_TX_POWER via fixed-dBm rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AX WD descriptor carries no per-frame power field (both wd_body layouts; vendor mac_ax has none either), but Kestrel's power write is the cheapest in the tree: the fixed-dBm BB target (halbb_set_txpwr_dbm form, 0x09a4[16] arm + 0x4594[30:22] s(9,2)) is 2 RMWs at 0.25 dB resolution. Since Kestrel submits one bulk transfer per frame, send_packet now honours a radiotap DBM_TX_POWER dB-delta by rewriting that target between frames when the requested level changes — free while it stays constant, no run-splitting machinery needed. A field-less frame restores the SetTxPowerOffsetQdb session offset; the trim is clamped to the same [0, 23] dBm PA window; the hal offset member stays the single source of truth, so the existing set_channel/fast_retune re-apply keeps the last-written level sticky across channel switches. Global, like the J1 BB-swing lever: a HW beacon airing between frames follows. Caps: per_packet_txpower=true, steps=0 (continuous), step_qdb=1, min/max derived from the PA window around the session base. DEVOURER_TX_PKT_OFSET now fans out to Kestrel too (nominal LUT dB onto the session fixed-dBm offset, radiotap still overriding per frame); the on-air harness gains a TX_PWR knob (Kestrel reads DEVOURER_TX_PWR as whole dBm — 14 gives the sweep headroom the Jaguar index 40 default would clamp away). On-air (ch36, MCS3, 14 dBm base, 8822BU chip-RSSI ground): 8852BU 5/5 LUT-mapped cells + 2/2 radiotap cells correct (+3/+6 exact; deep cuts over-deliver ~3 raw at the uncalibrated low-dBm end); 8832CU 5/5 + 2/2 near-exact (+3->+3, +6->+6, -3->-3, -7->-6, -11->-10). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 18 +++++++++--- examples/tx/main.cpp | 14 +++++++--- src/AdapterCaps.h | 20 ++++++++------ src/kestrel/RtlKestrelDevice.cpp | 47 +++++++++++++++++++++++++++++++- src/kestrel/RtlKestrelDevice.h | 7 ++++- tests/txpkt_pwr_ofset_onair.sh | 8 ++++-- 6 files changed, 94 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b38f7b65..e5823748 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,9 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first — see RX bulk-IN delivery requires the USB RXAGG engine enabled (`B_AX_RXAGG_EN`). TX power is a fixed BB dBm (`halbb_set_txpwr_dbm`, default 20 dBm, `DEVOURER_TX_PWR` override) with a runtime - `SetTxPowerOffsetQdb` lever; `ReadTsf` reads the per-port MAC TSF; + `SetTxPowerOffsetQdb` lever and per-packet radiotap `DBM_TX_POWER` + (fixed-dBm rewrite between frames — see **Per-packet TX power** under + Configuration); `ReadTsf` reads the per-port MAC TSF; `StartBeacon` drives the AX HW beacon engine. **HE ER SU + DCM extended range** (both dies): per-packet via radiotap-HE FORMAT=EXT_SU or `DEVOURER_TX_RATE=.../ER[/DCM]`; RX classifies the format in @@ -399,13 +401,21 @@ the 0 dB baseline). On-air-validated on 8822CU + 8822EU, sticky across `SetMonitorChannel`/`FastRetune`; the E compresses deep cuts (≈−6 dB floor, same TSSI reshape as its offset slope). **8814A** — the same 3-bit LUT field at the 8822B position (dword5 [30:28]), `SetTxPacketPowerStep` on -`RtlJaguarDevice`. **8812AU/8821AU** — no descriptor field exists (per-rate +`RtlJaguarDevice`. **Kestrel** — no WD-descriptor field; `send_packet` +honours the radiotap delta by rewriting the fixed-dBm BB target between +frames (2 BB RMWs on value change, free while constant; 0.25 dB steps, +clamped to the 0..23 dBm PA window around the `DEVOURER_TX_PWR` base — +whole dBm on this family; global, so a HW beacon airing between frames +follows the last-written level; a field-less frame restores the +`SetTxPowerOffsetQdb` session offset). **8812AU/8821AU** — no descriptor +field exists (per-rate selection via radiotap is their only per-packet lever); the compensating fast lever is `FastSetTxPowerOffsetQdb` (BB-swing TxScale `0xc1c/0xe1c`: global per-burst, 1–4 writes, 0.5 dB steps, −12..+2 dB, folded through the 8812A thermal tracker; on-air-validated on the 8812AU). Sweep harnesses: -`tests/txpkt_pwr_ofset_onair.sh` (TX_PID/TX_VID select the DUT), -`tests/txpkt_fastswing_onair.sh`, `tests/txpkt_hop_persist.sh`. +`tests/txpkt_pwr_ofset_onair.sh` (TX_PID/TX_VID select the DUT; Kestrel +DUTs need `TX_PWR=14`-style dBm bases), `tests/txpkt_fastswing_onair.sh`, +`tests/txpkt_hop_persist.sh`. Per-packet unequal error protection: `svctx` classifies stdin HEVC NALs by temporal layer and injects each at its ladder's rate diff --git a/examples/tx/main.cpp b/examples/tx/main.cpp index 18fcc270..947c9839 100644 --- a/examples/tx/main.cpp +++ b/examples/tx/main.cpp @@ -563,11 +563,13 @@ int main(int argc, char **argv) { /* DEVOURER_TX_PKT_OFSET=N — default per-packet TX-power LUT step written * into every TX descriptor: 0=none, 1=-3dB, 2=-7dB, 3=-11dB, 4=+3dB, - * 5=+6dB. One env var, three families: Jaguar2 8822B/8821C + * 5=+6dB. One env var, four families: Jaguar2 8822B/8821C * (descriptor TXPWR_OFSET, on-air-confirmed), Jaguar1 8814A (dword5 - * [30:28], same LUT), and Jaguar3 8822C/8822E (the step's nominal dB is - * mapped onto a programmable 0x1e70 offset bank). The measurement driver - * for tests/txpkt_pwr_ofset_onair.sh. */ + * [30:28], same LUT), Jaguar3 8822C/8822E (the step's nominal dB is + * mapped onto a programmable 0x1e70 offset bank), and Kestrel 8852B/8852C + * (no descriptor field — the nominal dB rides the session fixed-dBm + * offset, which a radiotap DBM_TX_POWER still overrides per frame). The + * measurement driver for tests/txpkt_pwr_ofset_onair.sh. */ if (const char *p = std::getenv("DEVOURER_TX_PKT_OFSET")) { const uint8_t step = static_cast(std::strtol(p, nullptr, 0)); #if defined(DEVOURER_HAVE_JAGUAR2) @@ -583,6 +585,10 @@ int main(int argc, char **argv) { jag3->SetTxPacketPowerOffsetQdb(4 * devourer::txpkt_pwr_db_for_step(step)); #endif + if (rtlDevice->GetAdapterCaps().generation == + devourer::ChipGeneration::Kestrel) + rtlDevice->SetTxPowerOffsetQdb(4 * + devourer::txpkt_pwr_db_for_step(step)); (void)step; } diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index c72a275f..ff55c6b2 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -139,25 +139,29 @@ struct AdapterCaps { bool ldpc_rx_flag = false; /* --- feature flags --- */ - /* Per-packet TX power: the TX descriptor carries a per-frame - * power trim, driven by radiotap DBM_TX_POWER (dB delta vs the calibrated - * table) or a session default. Two hardware shapes: + /* Per-packet TX power: a per-frame power trim driven by radiotap + * DBM_TX_POWER (dB delta vs the calibrated table / session base) or a + * session default. Three hardware shapes: * - Jaguar2 (8822B/8821C) + 8814A: a fixed 6-rung LUT in the descriptor * ({0,-3,-7,-11,+3,+6} dB) — per_pkt_txpwr_steps = 6, step_qdb = 0. * - Jaguar3 (8822C/8822E): a 2-bit bank selector; the banks are * programmable 7-bit signed offsets (0x1e70) in per_pkt_txpwr_step_qdb * units (nominally 4 = 1 dB), 2 concurrent non-zero levels — * per_pkt_txpwr_steps = 0 (continuous), min/max give the travel. - * per_pkt_txpwr_measured stays false until the family's descriptor path - * has been proven to move on-air power (tests/txpkt_pwr_ofset_onair.sh) — - * the honest flag for the vendor-defined-but-unvalidated 8814A/Jaguar3 - * fields. */ + * - Kestrel (8852B/8852C): no descriptor field; the fixed-dBm BB target + * is rewritten between frames on value change (2 RMWs, free while + * constant; global, so HW beacons follow) — per_pkt_txpwr_steps = 0, + * step_qdb = 1 (0.25 dB). + * per_pkt_txpwr_measured stays false until the family's path has been + * proven to move on-air power (tests/txpkt_pwr_ofset_onair.sh) — the + * honest flag for a vendor-defined-but-unvalidated field (the 8814A + * today). */ bool per_packet_txpower = false; uint8_t per_pkt_txpwr_steps = 0; /* 6 = LUT rungs; 0 = continuous qdb */ uint8_t per_pkt_txpwr_step_qdb = 0; /* Jaguar3 bank step (qdB); 0 for LUT */ int16_t per_pkt_txpwr_min_qdb = 0; /* most negative per-packet trim */ int16_t per_pkt_txpwr_max_qdb = 0; /* most positive per-packet trim */ - bool per_pkt_txpwr_measured = false; /* on-air-confirmed (J2 today) */ + bool per_pkt_txpwr_measured = false; /* on-air-confirmed for this family */ bool narrowband_ok = false; /* 5/10 MHz re-clock (Jaguar2/Jaguar3) */ uint8_t xtal_cap_max = 0; /* crystal-cap trim range top (0 = no trim; * 0x3f on Jaguar1/2, 0x7f on Jaguar3) */ diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 3de86a6e..153bea1f 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -1,5 +1,6 @@ #include "RtlKestrelDevice.h" +#include /* INT_MIN = "no radiotap DBM_TX_POWER" sentinel */ #include #include #include @@ -458,6 +459,8 @@ int RtlKestrelDevice::SetTxPowerOffsetQdb(int qdb) { if (eff < kKestrelTxMinQdb) eff = kKestrelTxMinQdb; if (eff > kKestrelTxMaxQdb) eff = kKestrelTxMaxQdb; const int16_t applied = static_cast(eff - base); + _sess_pwr_qdb = applied; /* restore target for frames without a radiotap + * DBM_TX_POWER field (see send_packet) */ _hal.set_txpwr_offset_qdb(applied); _logger->info("Kestrel: SetTxPowerOffsetQdb({}) -> applied {} qdB " "(effective {} dBm)", @@ -630,7 +633,24 @@ devourer::AdapterCaps RtlKestrelDevice::GetAdapterCaps() { c.bw_mask = devourer::bw_mask_for_generation(c.generation); if (_variant == kestrel::ChipVariant::C8852C) c.bw_mask |= devourer::kBw160; /* 8852C-only (vendor bw_sup BW_CAP_160M) */ - c.per_packet_txpower = false; /* AX power is TSSI, not the J2 descriptor LUT */ + /* Per-packet TX power: no WD-descriptor field exists on AX — a radiotap + * DBM_TX_POWER dB-delta is honoured per frame by rewriting the fixed-dBm BB + * target between frames (2 RMWs on value change, free while constant; + * global, so a HW beacon airing between frames follows). Continuous + * quarter-dB, clamped to the [0, 23] dBm PA window around the session + * base. */ + c.per_packet_txpower = true; + c.per_pkt_txpwr_steps = 0; /* continuous qdB, not a LUT */ + c.per_pkt_txpwr_step_qdb = 1; + { + const int16_t base = _hal.txpwr_base_qdb(); + c.per_pkt_txpwr_min_qdb = static_cast(kKestrelTxMinQdb - base); + c.per_pkt_txpwr_max_qdb = static_cast(kKestrelTxMaxQdb - base); + } + c.per_pkt_txpwr_measured = true; /* on-air: both dies, LUT-mapped session + * sweep 5/5 + radiotap cells 2/2 + * (tests/txpkt_pwr_ofset_onair.sh, ch36, + * 14 dBm base, chip-RSSI ground) */ c.narrowband_ok = true; /* 5/10 MHz BB small-BW (SDR-validated); FastSetBandwidth toggle */ c.fastretune_ok = true; /* lean intra-band 20 MHz retune (FastRetune) */ /* HE ER SU + DCM: both dies (AX_TXD_DATA_ER/_BW_ER/_DCM in the TX WD; RX @@ -687,6 +707,8 @@ bool RtlKestrelDevice::send_packet(const uint8_t *packet, size_t length) { bool he = false; /* HE rate set directly on tr.rate (no MGN_HE code) */ bool rate_from_radiotap = false; /* a per-packet radiotap rate overrides the * SetTxMode default */ + int pkt_pwr_db = INT_MIN; /* radiotap DBM_TX_POWER dB-delta vs the session + * base; INT_MIN = field absent (session offset) */ kestrel::TxRate tr{}; /* defaults: 6M, 20MHz, LGI */ auto *rth = reinterpret_cast( const_cast(packet)); @@ -698,6 +720,11 @@ bool RtlKestrelDevice::send_packet(const uint8_t *packet, size_t length) { mgn = *it.this_arg; rate_from_radiotap = true; break; + case IEEE80211_RADIOTAP_DBM_TX_POWER: + /* Per-packet TX power: a signed dB delta vs the session base, applied + * as a fixed-dBm BB rewrite before this frame airs (see below). */ + pkt_pwr_db = *reinterpret_cast(it.this_arg); + break; case IEEE80211_RADIOTAP_MCS: { rate_from_radiotap = true; uint8_t flags = it.this_arg[1]; @@ -847,6 +874,24 @@ bool RtlKestrelDevice::send_packet(const uint8_t *packet, size_t length) { } if (!he) tr.rate = kestrel::mgn_to_ax_rate(mgn); + /* Per-packet TX power: the AX WD descriptor has no power field, but the + * fixed-dBm BB target is a 2-RMW rewrite — reprogram it between frames when + * the requested level changes (free while it stays constant), and restore + * the session offset for a frame without the field. Global, like the J1 + * BB-swing lever: a HW beacon airing between frames follows the last-written + * level. Clamped to the same PA-valid window as SetTxPowerOffsetQdb. */ + { + int16_t target = _sess_pwr_qdb; + if (pkt_pwr_db != INT_MIN) { + const int16_t base = _hal.txpwr_base_qdb(); + int eff = base + pkt_pwr_db * 4; + if (eff < kKestrelTxMinQdb) eff = kKestrelTxMinQdb; + if (eff > kKestrelTxMaxQdb) eff = kKestrelTxMaxQdb; + target = static_cast(eff - base); + } + if (target != _hal.txpwr_offset_qdb()) + _hal.set_txpwr_offset_qdb(target); + } /* Route by 802.11 frame type: data frames ride the AC0 queue (BULKOUTID3) so * they exercise the data power-by-rate path; mgmt/beacon uses the MG0 queue * (BULKOUTID0). Falls back to the mgmt ep if the data ep was not resolved. */ diff --git a/src/kestrel/RtlKestrelDevice.h b/src/kestrel/RtlKestrelDevice.h index c6694002..42f9ac59 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -78,7 +78,9 @@ class RtlKestrelDevice : public IRtlDevice { /* Runtime TX-power lever: fixed-dBm BB power (halbb_set_txpwr_dbm) with a * quarter-dB offset relative to the DEVOURER_TX_PWR base. Sticky across - * SetMonitorChannel. */ + * SetMonitorChannel. Per-packet: a radiotap DBM_TX_POWER dB-delta on a + * frame overrides this for that frame via a fixed-dBm rewrite in + * send_packet (2 BB RMWs on value change, free while constant). */ devourer::TxPowerCaps GetTxPowerCaps() override; int SetTxPowerOffsetQdb(int qdb) override; devourer::TxPowerState GetTxPowerState() override; @@ -199,6 +201,9 @@ class RtlKestrelDevice : public IRtlDevice { uint8_t _tx_data_ep = 0; /* AC0 data bulk-OUT ep (BULKOUTID3) */ uint16_t _tx_seq = 0; /* rolling 12-bit wifi sequence for injected frames */ std::optional _tx_mode_default; /* SetTxMode default */ + int16_t _sess_pwr_qdb = 0; /* offset applied by SetTxPowerOffsetQdb — the + * restore target for frames without a radiotap + * DBM_TX_POWER field */ /* 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}; diff --git a/tests/txpkt_pwr_ofset_onair.sh b/tests/txpkt_pwr_ofset_onair.sh index 3caada88..c8819067 100755 --- a/tests/txpkt_pwr_ofset_onair.sh +++ b/tests/txpkt_pwr_ofset_onair.sh @@ -20,10 +20,14 @@ # TX_BUS/TX_PORT USB topology pin when VID:PID is ambiguous (8814A) # GROUND_PID/GROUND_VID RSSI sensor identity (default 8812CU 0bda:c812) # CH channel (default 36; 8822E DUTs are 5 GHz-only TX) +# TX_PWR DUT base power (default 40 = TXAGC index on the +# Jaguar families; Kestrel reads it as whole dBm, +# clamped 0..23 — use TX_PWR=14 for offset headroom) set -u ROOT="$(cd "$(dirname "$0")/.." && pwd)" OUT="${TXPKT_OUT:-/tmp/devourer-txpkt-ofset}" CH="${CH:-36}" +TX_PWR="${TX_PWR:-40}" TX_PID="${TX_PID:-0x012d}" TX_VID="${TX_VID:-0x2357}" TX_BUS="${TX_BUS:-}" TX_PORT="${TX_PORT:-}" GROUND_PID="${1:-0xc812}" GROUND_VID="${GROUND_VID:-0x0bda}" @@ -61,7 +65,7 @@ for step in $STEPS; do t0="$(date +%s.%N)" # Fixed rate MCS3, fixed base power, only the per-packet offset varies. sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" DEVOURER_CHANNEL="$CH" \ - DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR=40 DEVOURER_TX_PKT_OFSET="$step" \ + DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR="$TX_PWR" DEVOURER_TX_PKT_OFSET="$step" \ DEVOURER_TX_GAP_US=1500 "${TX_EXTRA[@]}" \ timeout 12 "$ROOT/build/txdemo" >/dev/null 2>&1 || true t1="$(date +%s.%N)" @@ -85,7 +89,7 @@ for pair in "0:0" "6:6" "-11:-11"; do db="${pair%%:*}" t0="$(date +%s.%N)" sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" DEVOURER_CHANNEL="$CH" \ - DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR=40 DEVOURER_TX_PKT_PWR_DB="$db" \ + DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR="$TX_PWR" DEVOURER_TX_PKT_PWR_DB="$db" \ DEVOURER_TX_GAP_US=1500 "${TX_EXTRA[@]}" \ timeout 12 "$ROOT/build/txdemo" >/dev/null 2>&1 || true t1="$(date +%s.%N)"