From 3f21f539b890f42e26315b98cba380e7783a0aaf Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:06:17 +0300 Subject: [PATCH] kestrel: FastRetune firmware IO-offload for the 8852B same-sub-band hop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rtw89 has no channel-switch H2C, but its mac_ax firmware exposes a generic register IO-offload (FW_OFLD/CMD_OFLD_REG): a buffer of masked-write commands the on-chip CPU replays locally. Batch the whole same-sub-band hop — the RF18/ 0xcf channel-set writes, halbb_bb_reset_all_8852b, and the fixed-dBm TX-power target — into ONE H2C, collapsing ~20 per-hop USB register round-trips into a single bulk-OUT. Host-side hop cost drops ~9.35 ms -> ~0.15 ms. KestrelFw::reg_write_ofld packs the 16-byte fwcmd_cmd_ofld command buffer; fast_retune_ofld_8852b builds the RF+BB+MAC+DELAY write-list for the hop. The firmware applies each masked write as (reg & ~mask) | ((val << ctz(mask)) & mask), so BB/MAC entries carry the raw field value (RF uses the full MASKRF, shift 0). The offload frees the host, not the radio: the direct path's ~5 ms of bb_reset USB round-trips were incidentally covering the RF synth's ~1.5 ms VCO settle. Removing them exposes that floor — a frame airing sooner lands wrong-channel (on-air settle sweep: 1.0 ms = 20 wrong/2000, >=1.5 ms = 0). Net time-to-usable-channel ~1.7 ms with the host free during the settle. dwelltx's default settle window is raised 500 us -> 2000 us to cover it. Default on (DeviceConfig.tuning.kestrel_fastretune_ofld, DEVOURER_KFR_OFLD=0 forces the self-pacing direct path); armed on all bring-up paths, a no-op on the 8852C and the bucket-crossing synth-relock path. Validated on-air (8852BU): 6020-slot 3-channel soak, zero wrong-channel, 97.6% delivery. tests/dwell1_ab.py gains --kfr-ofld; tests/kfr_ofld_smoke.sh is the no-oracle timing check. Co-Authored-By: Claude Opus 4.8 --- docs/frequency-hopping.md | 20 +++++- examples/common/env_config.cpp | 2 + examples/dwelltx/main.cpp | 6 +- src/DeviceConfig.h | 11 +++ src/kestrel/CLAUDE.md | 26 +++++++ src/kestrel/HalKestrel.cpp | 114 +++++++++++++++++++++++++++---- src/kestrel/HalKestrel.h | 6 ++ src/kestrel/KestrelFw.cpp | 33 +++++++++ src/kestrel/KestrelFw.h | 17 +++++ src/kestrel/RtlKestrelDevice.cpp | 3 + tests/dwell1_ab.py | 4 ++ tests/kfr_ofld_smoke.sh | 71 +++++++++++++++++++ 12 files changed, 298 insertions(+), 15 deletions(-) create mode 100755 tests/kfr_ofld_smoke.sh diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index 1c47692a..b29819ec 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -336,7 +336,7 @@ every generation overrides it with a lean path built from the tricks above: | RTL8821CU (Jaguar2) | ~30 ms | **~0.55 ms** | code-covered, no HW | ~0.5 ms | 1 | | RTL8822CU (Jaguar3) | ~12 ms | **~1.9 ms** | **~0.8 ms** | ~0.21 ms | 9 | | RTL8812EU (Jaguar3) | ~12 ms | **~2.4 ms** | **~0.8 ms** (8822E) | ~0.27 ms | 9 | -| RTL8852BU (Kestrel/AX) | ~90 ms | **~9 ms** | — | ~1–2 ms | ~4 | +| RTL8852BU (Kestrel/AX) | ~90 ms | **~9 ms** | **~0.15 ms** (1 H2C) | ~1–2 ms | ~4 | The Kestrel (rtw89/AX) hop needed the most work to reach 11ac-ish territory, but the same techniques applied. The vendored halrf channel setting does an @@ -361,6 +361,24 @@ architecture (`SCAN_OFFLOAD` and MCC are the only rtw89 channel primitives). A Kestrel dwell-1 slot is ~30 ms (approaching the 8822B's ~20 ms) — the N-channel data plane runs on AX at a usable hop rate. +There is, however, a firmware *register* IO-offload (`DEVOURER_KFR_OFLD`, +default on, 8852B). rtw89 has no channel-switch primitive, but its mac_ax firmware +does expose a generic register-write offload (`FW_OFLD`/`CMD_OFLD_REG`): a +buffer of masked write commands the on-chip CPU replays locally. The whole +same-sub-band hop — the RF18/0xcf channel-set writes, the BB reset, and the +fixed-dBm power target — packs into ONE H2C, so ~20 per-hop USB register +round-trips collapse into a single bulk-OUT. The host-side hop cost drops from +~9 ms to **~0.15 ms**. The catch is honest and instructive: the offload frees +the *host*, not the *radio*. The direct path's ~5 ms of BB-reset round-trips +were incidentally covering the RF synth's settle; removing them exposes a +**~1.5 ms VCO settle floor** below which a frame airs mid-retune. So the +caller must honour a ~1.5 ms settle before it TXes on the new channel — the +time-to-usable-channel is ~1.7 ms (still ~5×), but the host is free to prepare +the next frame or service RX during the settle instead of blocking on EP0. This +changes the `FastRetune` timing contract (it returns before the channel is +usable); the hop demos' admission window already honours the settle, and +`DEVOURER_KFR_OFLD=0` restores the self-pacing direct path. + (Median `hop.dwell` switch_us over a 1/6/11 hop set; per-stage numbers from `DEVOURER_HOP_PROF=1`. Every hop microsecond is USB round-trips: one register read or write is one synchronous control transfer, whose latency is a property diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index a5087f71..2791eb86 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -118,6 +118,8 @@ devourer::DeviceConfig devourer_config_from_env() { cfg.tuning.disable_cca = env_flag("DEVOURER_DIS_CCA"); if (env_long("DEVOURER_FASTRETUNE_FW", &v) && v >= 0) cfg.tuning.fastretune_fw = static_cast(v); + if (env_long("DEVOURER_KFR_OFLD", &v) && v >= 0) + cfg.tuning.kestrel_fastretune_ofld = static_cast(v); /* Jaguar3 per-packet power-bank step size (qdB per 0x1e70 offset-index * step; default 4 = 1 dB) — bench slope-calibration override. */ if (env_long("DEVOURER_TXPKT_STEP_QDB", &v) && v > 0) diff --git a/examples/dwelltx/main.cpp b/examples/dwelltx/main.cpp index 1f15af17..3d3ced70 100644 --- a/examples/dwelltx/main.cpp +++ b/examples/dwelltx/main.cpp @@ -155,7 +155,11 @@ int main() { } const long slot_ms = env_long("DEVOURER_DWELL_SLOT_MS", 20); const long total_slots = env_long("DEVOURER_DWELL_SLOTS", 0); - const long settle_us = env_long("DEVOURER_DWELL_SETTLE_US", 500); + // Post-switch settle before admitting a frame. On the Kestrel 8852B the + // default IO-offload hop returns before the ~1.5 ms RF synth settle (the host + // no longer self-paces it via slow register writes), so the window must + // cover that floor or a frame airs mid-retune. 2 ms is safe on every chip. + const long settle_us = env_long("DEVOURER_DWELL_SETTLE_US", 2000); const long guard_us = env_long("DEVOURER_DWELL_GUARD_US", 1000); const long airtime_us = env_long("DEVOURER_DWELL_AIRTIME_US", 300); const long late_us = env_long("DEVOURER_DWELL_LATE_US", 0); diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index aca7d726..3b351c6b 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -225,6 +225,17 @@ struct DeviceConfig { * bring-up band's — active power knobs re-fold). Bench + protocol: * docs/experiments/kernel-channel-switch-offload.md. */ int fastretune_fw = 0; + /* env: DEVOURER_KFR_OFLD — Kestrel FastRetune firmware IO-offload (mac_ax + * fwofld.c): batch the whole same-sub-band hop (RF18/0xcf channel set + BB + * reset + fixed TX power) into ONE FW_OFLD/CMD_OFLD_REG H2C the on-chip fw + * replays, collapsing ~20 USB register round-trips into one bulk-OUT + * (~9 ms -> ~0.15 ms host-side). Default 1 (on); 0 forces the direct + * write-only path (self-paces the ~1.5 ms RF synth settle instead of + * returning before it). No rtw89 firmware channel-switch H2C exists, so + * this offloads the raw register writes, not a fw switch primitive. The + * hop returns before the synth settles — the caller honours a ~1.5 ms + * settle before TX (the demos' admission window). 8852B only. */ + int kestrel_fastretune_ofld = 1; /* 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: diff --git a/src/kestrel/CLAUDE.md b/src/kestrel/CLAUDE.md index 96c06720..cb9a1cb8 100644 --- a/src/kestrel/CLAUDE.md +++ b/src/kestrel/CLAUDE.md @@ -112,6 +112,32 @@ here (H2C 0x1D is 11ac HalMAC; rtw89 has only `SCAN_OFFLOAD` + MCC). The dwell-1 / N-channel data plane (`examples/dwelltx`) runs on Kestrel at ~30 ms slots (soak: 2000 hops, zero wrong-channel, ~97 % delivery). +### Firmware IO-offload hop (`DEVOURER_KFR_OFLD`, default on) + +`fast_retune_ofld_8852b` ships the *entire* same-sub-band hop — the six RF18/ +0xcf channel-set writes, `halbb_bb_reset_all_8852b`, and the fixed-dBm TX-power +target — as ONE `FW_OFLD`/`CMD_OFLD_REG` H2C (mac_ax `fwofld.c`) the on-chip +firmware replays locally. `KestrelFw::reg_write_ofld` packs the 16-byte +`fwcmd_cmd_ofld` command buffer (src RF a-die / BB / MAC, `WRITE`/`DELAY` +types, LC on the last, `cmd_num` running); the firmware applies each masked +write as `(reg & ~mask) | ((val << ctz(mask)) & mask)` — so BB/MAC entries +carry the **raw** field value (pre-shifting double-shifts and mis-tunes), +while RF writes use the full `MASKRF` (shift 0). This collapses ~20 per-hop USB +register round-trips into a single bulk-OUT: host-side hop cost **~9.3 ms → +~0.15 ms (~45×)**, zero wrong-channel over a 6000-hop soak. + +The offload does not speed up the RF synth — it only frees the host. The +direct path's ~5 ms of `bb_reset` USB round-trips incidentally covered the VCO +settle; collapsing them to ~0.15 ms exposes the true **~1.5 ms synth-settle +floor** (below it, a frame airs mid-retune → wrong-channel; ≥1.5 ms is clean, +`DEVOURER_DWELL_SETTLE_US` on the demo). Net time-to-usable-channel ~1.7 ms +(~5.5×), with the host free to prep the next frame / service RX during the +settle. The hop returns before the synth settles, so this changes the +`FastRetune` timing contract — the caller honours the settle (the demos' +admission window). Default on; `DEVOURER_KFR_OFLD=0` forces the self-pacing +direct path (for A/B or a suspect chip). A bucket crossing (verified synth +relock) and the 8852C fall through to the direct steps. + ## TX power A fixed BB dBm (`halbb_set_txpwr_dbm`, default 20 dBm, `DEVOURER_TX_PWR` diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 0378f182..559846e2 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -1744,9 +1744,12 @@ bool HalKestrel::fast_rf_channel_8852b(uint8_t channel, bool relock) { rf_wrf(0, 0xd3, 1u << 8, 0x0); rf_wrf(0, 0xb1, r::MASKRF, b1); fast_lck_check_8852b(); - } else { - rf_wrf(0, 0x18, r::MASKRF, rf18_dav); + cf_toggle(0, _kfr_cf_a); + rf_wrf(1, 0x18, r::MASKRF, rf18_dav); + cf_toggle(1, _kfr_cf_b); + return true; } + rf_wrf(0, 0x18, r::MASKRF, rf18_dav); cf_toggle(0, _kfr_cf_a); /* path-B DAV. The DDV (d-die, 0x10018) window is not populated on the * single-die 8852B — skipping it costs no channel accuracy (soak-confirmed) @@ -1756,6 +1759,84 @@ bool HalKestrel::fast_rf_channel_8852b(uint8_t channel, bool relock) { return true; } +namespace { +/* count-trailing-zeros for the RMW field shift. */ +inline uint8_t ctz32(uint32_t m) { + uint8_t s = 0; + while (m && !(m & 1)) { + m >>= 1; + ++s; + } + return s; +} +} // namespace + +bool HalKestrel::fast_retune_ofld_8852b(uint8_t channel) { + if (_variant != ChipVariant::C8852B || !_halbb_ctx) + return false; + /* Prime the a-die RF18/0xcf dwords once per epoch — the only reads on this + * path; everything after is composed write-only and shipped as one H2C. */ + if (!_kfr_primed) { + _kfr_rf18_dav = rf_rrf(0, 0x18, r::MASKRF); + _kfr_cf_a = rf_rrf(0, 0xcf, r::MASKRF); + _kfr_cf_b = rf_rrf(1, 0xcf, r::MASKRF); + _kfr_primed = true; + } + uint32_t rf18_dav = (_kfr_rf18_dav & ~0x3e3ffu) | channel; + if (channel > 14) + rf18_dav |= (1u << 16) | (1u << 8); + rf18_dav = (rf18_dav & 0xf0fffu) | (1u << 12); + _kfr_rf18_dav = rf18_dav; + + using O = KestrelFw::OfldWrite; + const uint8_t RF = 1, BB = 0, MAC = 2, DLY = 2; + /* The fw applies a masked write as (reg & ~mask) | ((val << ctz(mask)) & + * mask) — it does the field shift itself (matching halbb_fw_set_reg, which + * forwards the RAW field value). So pass the unshifted field value, NOT + * pre-positioned. (RF writes use the full MASKRF, shift 0, so they are + * identical either way.) */ + auto bb = [](uint16_t off, uint32_t msk, uint32_t val) { + return O{BB, 0, off, val, msk, 0}; + }; + const int16_t dbm_q2 = static_cast(_txpwr_dbm_q2 + _txpwr_offset_qdb); + /* One batch = the whole same-sub-band hop the fw replays locally: + * - RF18 (path A/B, a-die) + 0xcf clear/set latch toggle (channel set), + * - halbb_bb_reset_all_8852b (BB RMWs the fw applies under mask on-chip; the + * 1/2 us settles become DELAY ops; 0xce40 phy-sts stop/start is a MAC RMW), + * - halbb_set_txpwr_dbm_8852b (fixed-dBm arm + s(9,2) target). + * DDV (d-die 0x10018) is unpopulated on the single-die 8852B, so path-B is + * a-die too. This is the 8852B-only non-relock path; a sub-band crossing + * keeps the direct synth-relock (verified lock) path. */ + const O cmds[] = { + {RF, 0, 0x18, rf18_dav, r::MASKRF, 0}, + {RF, 0, 0xcf, _kfr_cf_a & ~1u, r::MASKRF, 0}, + {RF, 0, 0xcf, _kfr_cf_a | 1u, r::MASKRF, 0}, + {RF, 1, 0x18, rf18_dav, r::MASKRF, 0}, + {RF, 1, 0xcf, _kfr_cf_b & ~1u, r::MASKRF, 0}, + {RF, 1, 0xcf, _kfr_cf_b | 1u, r::MASKRF, 0}, + /* bb_reset_all (8852B) */ + bb(0x2344, 1u << 31, 1), + bb(0xc3c, 1u << 9, 1), + bb(0x1200, 0x7u << 28, 0x7), + bb(0x3200, 0x7u << 28, 0x7), + {0, 0, 0, 1, 0, DLY}, /* delay 1us */ + {MAC, 0, 0xce40, 0u, 0x1u, 0}, /* stop phy-sts (clear bit0) */ + {0, 0, 0, 2, 0, DLY}, /* delay 2us */ + bb(0x704, 1u << 1, 1), + bb(0x704, 1u << 1, 0), + bb(0x1200, 0x7u << 28, 0x0), + bb(0x3200, 0x7u << 28, 0x0), + bb(0x704, 1u << 1, 1), + {MAC, 0, 0xce40, 0x1u, 0x1u, 0}, /* start phy-sts (set bit0) */ + bb(0x2344, 1u << 31, 0), + bb(0xc3c, 1u << 9, 0), + /* set_txpwr_dbm */ + bb(0x09a4, 1u << 16, 1), + bb(0x4594, 0x7fc00000u, static_cast(dbm_q2) & 0x1ffu), + }; + return _fw.reg_write_ofld(cmds, sizeof(cmds) / sizeof(cmds[0])); +} + void HalKestrel::vnd_bb_set_gain(uint8_t channel, uint8_t band_type) { if (_halbb_ctx) kestrel_halbb_set_gain(_halbb_ctx, channel, band_type); @@ -1799,15 +1880,6 @@ HalKestrel::~HalKestrel() { } namespace { -/* count-trailing-zeros for the RMW field shift. */ -inline uint8_t ctz32(uint32_t m) { - uint8_t s = 0; - while (m && !(m & 1)) { - m >>= 1; - ++s; - } - return s; -} constexpr uint32_t BB_WIN = 0x10000; /* bb0_cr_offset (wIndex=1) */ } /* namespace */ @@ -1976,6 +2048,19 @@ void HalKestrel::fast_retune(uint8_t channel) { * ctl_ch's per-hop RF18/0xcf reads and, within a sub-band, the ~13 ms synth * LCK poll. Falls back to the vendored tune on the 8852C or a cold cache; * the synth-lock diagnostic reads are skipped either way (diag=false). */ + using kfr_clock = std::chrono::steady_clock; + auto t0 = kfr_clock::now(); + /* Fully-offloaded same-sub-band hop: the whole RF+bb_reset+txpwr write burst + * ships as ONE FW_OFLD H2C the fw replays on-chip, collapsing ~20 USB + * register round-trips into a single bulk-OUT. Only the 8852B non-relock + * path; a bucket crossing (or the 8852C) falls through to the direct steps. */ + if (_kfr_ofld && !bucket_changed && fast_retune_ofld_8852b(channel)) { + _logger->debug("Kestrel FastRetune: ch{} (offloaded) {}us", channel, + std::chrono::duration_cast( + kfr_clock::now() - t0) + .count()); + return; + } if (!fast_rf_channel_8852b(channel, /*relock=*/bucket_changed)) vnd_rf_tune(band_type, channel, CHANNEL_WIDTH_20, /*diag=*/false); if (bucket_changed) { @@ -1984,8 +2069,11 @@ void HalKestrel::fast_retune(uint8_t channel) { } bb_reset_all(); set_txpwr_dbm(static_cast(_txpwr_dbm_q2 + _txpwr_offset_qdb)); - _logger->debug("Kestrel FastRetune: ch{} ({})", channel, - band_type == 0 ? "2.4G" : band_type == 2 ? "6G" : "5G"); + _logger->debug("Kestrel FastRetune: ch{} ({}) {}us", channel, + band_type == 0 ? "2.4G" : band_type == 2 ? "6G" : "5G", + std::chrono::duration_cast( + kfr_clock::now() - t0) + .count()); } void HalKestrel::fast_set_bw(ChannelWidth_t bw) { diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index 39e9e2fe..681782a7 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -564,16 +564,22 @@ class HalKestrel { * Returns false (caller falls back to vnd_rf_tune) on the 8852C or a cold * cache. Invalidated by every full set_channel. */ bool fast_rf_channel_8852b(uint8_t channel, bool relock); + /* Whole same-sub-band hop (RF18/0xcf + bb_reset + fixed TX power) as ONE + * FW_OFLD H2C the fw replays on-chip — collapses the ~20 per-hop USB + * register round-trips into a single bulk-OUT. 8852B, non-relock only. */ + bool fast_retune_ofld_8852b(uint8_t channel); void fast_lck_check_8852b(); /* halrf_lck_check_8852b (synth-lock verify) */ bool _kfr_primed = false; uint32_t _kfr_rf18_dav = 0; /* a-die RF18 compose base */ uint32_t _kfr_cf_a = 0, _kfr_cf_b = 0; /* RF 0xcf full value/path */ + bool _kfr_ofld = false; /* batch the same-band hop writes via fw IO-offload */ struct kestrel_halrf_ctx *_halrf_ctx = nullptr; bool _halrf_rfk_inited = false; /* NCTL engine loaded (one-time, lazy) */ public: ~HalKestrel(); void set_cca_on(bool on) { _cca_on = on; } + void set_kfr_ofld(bool on) { _kfr_ofld = on; } }; } /* namespace kestrel */ diff --git a/src/kestrel/KestrelFw.cpp b/src/kestrel/KestrelFw.cpp index 4d10c8f9..b7b31c7e 100644 --- a/src/kestrel/KestrelFw.cpp +++ b/src/kestrel/KestrelFw.cpp @@ -568,6 +568,39 @@ bool KestrelFw::fw_send_beacon(const uint8_t *body, uint32_t len, uint8_t macid, return ok; } +bool KestrelFw::reg_write_ofld(const OfldWrite *cmds, size_t n) { + /* Build the fwofld.c command buffer verbatim: N contiguous 16-byte + * fwcmd_cmd_ofld entries, cmd_num running 0..N-1, LC on the last, then send + * it as ONE FW_OFLD/CMD_OFLD_REG H2C. The fw replays the write-list on-chip. + * v0 layout: dword0 = src|type|lc|path|cmd_num|offset, dword1 = id|base_offset + * (0 a-die / 1 d-die for RF, else offset[31:16]), dword2 = value, + * dword3 = mask. type is fixed WRITE (masked RMW on-chip). */ + if (n == 0 || n > 0x7f) + return false; + std::vector buf(n * 16, 0); + for (size_t i = 0; i < n; i++) { + const OfldWrite &c = cmds[i]; + const uint32_t base = (c.src == 3) ? 1u /* RF d-die */ + : (c.src == 1) ? 0u /* RF a-die */ + : ((uint32_t)c.offset >> 16) & 0xffff; + const uint32_t d0 = + ((uint32_t)(c.src & 0x3)) | + ((uint32_t)(c.type & 0x3) << 2) /* WRITE=0 / DELAY=2 */ | + ((i + 1 == n) ? (1u << 4) : 0u) /* LC on last */ | + ((uint32_t)(c.path & 0x3) << 5) | + ((uint32_t)(i & 0x7f) << 8) /* cmd_num */ | + ((uint32_t)(c.offset & 0xffff) << 16); + const uint32_t d1 = (0u & 0xffff) /* id */ | (base << 16); + put_le32(buf.data() + i * 16 + 0, d0); + put_le32(buf.data() + i * 16 + 4, d1); + put_le32(buf.data() + i * 16 + 8, c.value); + put_le32(buf.data() + i * 16 + 12, c.mask); + } + return send_h2c_cmd(r::FWCMD_H2C_CAT_MAC, r::FWCMD_H2C_CL_FW_OFLD, + r::FWCMD_H2C_FUNC_CMD_OFLD_REG, buf.data(), + static_cast(buf.size())); +} + bool KestrelFw::send_h2c_cmd(uint8_t cat, uint8_t h2c_class, uint8_t func, const uint8_t *content, uint32_t len) { /* Serialize: this mutates the shared _txbuf scratch + the _h2c_seq counter and diff --git a/src/kestrel/KestrelFw.h b/src/kestrel/KestrelFw.h index e000d0d8..5919b06b 100644 --- a/src/kestrel/KestrelFw.h +++ b/src/kestrel/KestrelFw.h @@ -143,6 +143,23 @@ class KestrelFw { bool fw_upd_cctl_bf(uint8_t macid, uint8_t addr_cam_idx, const devourer::StaBfCaps &bf); + /* Firmware register IO-offload (mac_ax fwofld.c): batch a write-list into one + * FW_OFLD H2C (func CMD_OFLD_REG) that the on-chip fw replays locally, so N + * register writes cost ONE USB round-trip instead of N. Each entry is a + * masked write; src selects the target space (RF a-die/d-die, BB, MAC), path + * the RF chain. The last command carries the LC (list-complete) bit. Used by + * the FastRetune hot path to collapse the per-hop RF18/0xcf write burst. + * Fire-and-forget: the C2H IO_OFLD_RESULT is not awaited (write-only batch).*/ + struct OfldWrite { + uint8_t src; /* RTW_MAC_{RF=1, RF_DDIE=3, BB=0, MAC=2}_CMD_OFLD */ + uint8_t path; /* RF path (0=A, 1=B); 0 for BB/MAC */ + uint16_t offset; /* register / RF address */ + uint32_t value; /* pre-shifted into the mask field (DELAY: microseconds) */ + uint32_t mask; + uint8_t type = 0; /* RTW_MAC_{WRITE=0, DELAY=2}_OFLD */ + }; + bool reg_write_ofld(const OfldWrite *cmds, size_t n); + private: /* Generic H2C over CH12: [WD 24B][fwcmd_hdr 8B][content]. */ bool send_h2c_cmd(uint8_t cat, uint8_t h2c_class, uint8_t func, diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 1392ec5d..2534db06 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -67,6 +67,9 @@ RtlKestrelDevice::RtlKestrelDevice(RtlAdapter device, Logger_t logger, else _logger->info("Kestrel: die-id 0x{:02x} ({}), cut {}", info.die_id, die_name(info.die_id), info.cut); + /* FastRetune firmware IO-offload (8852B same-sub-band hop). Armed here so it + * covers every bring-up path; a no-op on the 8852C / relock path. */ + _hal.set_kfr_ofld(_cfg.tuning.kestrel_fastretune_ofld != 0); /* DEVOURER_TX_PWR on Kestrel = the fixed BB TX power in whole dBm (distinct * from the Jaguar2 TXAGC-index meaning). Applied at every set_channel. */ if (_cfg.tx.power_index.has_value()) diff --git a/tests/dwell1_ab.py b/tests/dwell1_ab.py index f6a0f681..a48b8e21 100644 --- a/tests/dwell1_ab.py +++ b/tests/dwell1_ab.py @@ -251,6 +251,8 @@ def spawn_dwelltx(dut, chans, a, out_dir): "DEVOURER_DWELL_SLOTS": str(a.slots + 20), # +warmup "DEVOURER_HOP_FAST": str(a.hop_fast), }) + if a.kfr_ofld: + env["DEVOURER_KFR_OFLD"] = str(a.kfr_ofld) if a.seed: env["DEVOURER_HOP_SEED"] = a.seed if a.tx_pwr: @@ -312,6 +314,8 @@ def main(): ap.add_argument("--slots", type=int, default=5000) ap.add_argument("--fw", type=int, default=1) ap.add_argument("--hop-fast", type=int, default=1) + ap.add_argument("--kfr-ofld", type=int, default=0, + help="Kestrel FastRetune fw IO-offload (DEVOURER_KFR_OFLD)") ap.add_argument("--late-us", type=int, default=0) ap.add_argument("--settle-us", type=int, default=0, help="post-switch admission delay; fw switch needs " diff --git a/tests/kfr_ofld_smoke.sh b/tests/kfr_ofld_smoke.sh new file mode 100755 index 00000000..3afa61fb --- /dev/null +++ b/tests/kfr_ofld_smoke.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# kfr_ofld_smoke.sh — de-risk the Kestrel FastRetune firmware IO-offload. +# +# Runs the dwelltx dwell-1 hopper on a Kestrel (8852BU) DUT over a same-sub-band +# 5 GHz pair (offload path is the non-relock hop), first with the direct +# write-only path (DEVOURER_KFR_OFLD=0) then with the fw IO-offload +# (DEVOURER_KFR_OFLD=1). No oracle — this only answers: does the offloaded H2C +# get accepted (no fw error, no crash), and does per-hop switch_us drop? The +# oracle-attributed delivery/wrong-channel A/B is dwell1_ab.py. +set -u + +# DUT must be an 8852B (die-id 0x51) — the offload path is 8852B-only; an +# 8852C (die 0x52) falls through to the vendored tune. VID:PID overridable. +DUT_VID="${DUT_VID:-0x35bc}" +DUT_PID="${DUT_PID:-0x0108}" # TP-Link Archer TX20U Nano (RTL8852BU) +REPO="$(cd "$(dirname "$0")/.." && pwd)" +DWELLTX="$REPO/build/dwelltx" +OUT="/tmp/kfr-ofld-smoke" +CHANS="36,40" # both 5G-L (gain bucket 1) -> same-sub-band, offload path +SLOTS=300 +SLOT_MS=30 +TXPWR=12 # near-field: back off so the chip isn't railed +# The offload collapses the host hop cost but not the ~1.5 ms RF synth settle; +# the oracle-attributed correctness A/B (delivery + wrong-channel) is +# dwell1_ab.py --kfr-ofld with --settle-us >= 1500. + +mkdir -p "$OUT" +declare -a PIDS=() +cleanup() { for p in "${PIDS[@]}"; do kill "$p" 2>/dev/null; done; } +trap cleanup EXIT INT TERM + +run_one() { + local ofld="$1" tag="$2" + echo "=== KFR_OFLD=$ofld ($tag) ===" + DEVOURER_VID="$DUT_VID" DEVOURER_PID="$DUT_PID" \ + DEVOURER_EVENTS=stdout DEVOURER_LOG_LEVEL=info \ + DEVOURER_KFR_OFLD="$ofld" \ + DEVOURER_DWELL_CHANNELS="$CHANS" \ + DEVOURER_DWELL_SLOT_MS="$SLOT_MS" \ + DEVOURER_DWELL_SLOTS="$SLOTS" \ + DEVOURER_HOP_FAST=1 \ + DEVOURER_TX_PWR="$TXPWR" \ + "$DWELLTX" >"$OUT/dwelltx.$tag.jsonl" 2>"$OUT/dwelltx.$tag.stderr" + local rc=$? + echo "exit=$rc" + echo "-- fw/error lines (stderr) --" + grep -iE 'error|fail|ofld|offload|timeout' "$OUT/dwelltx.$tag.stderr" | head -12 + echo "-- switch_us (median / p90 / max over same-sub-band hops) --" + python3 - "$OUT/dwelltx.$tag.jsonl" <<'PY' +import json,sys,statistics +sw=[] +for l in open(sys.argv[1]): + l=l.strip() + if '"dwell.slot"' not in l: continue + try: e=json.loads(l) + except: continue + if 'switch_us' in e: sw.append(e['switch_us']) +sw=sw[3:] # drop warmup +if not sw: + print(" no dwell.slot switch_us samples"); sys.exit() +sw_s=sorted(sw) +p90=sw_s[int(len(sw_s)*0.9)] +print(f" n={len(sw)} median={statistics.median(sw)/1000:.2f}ms " + f"p90={p90/1000:.2f}ms max={max(sw)/1000:.2f}ms") +PY + echo +} + +run_one 0 direct +sleep 2 +run_one 1 offload