diff --git a/docs/kestrel-ack-responder.md b/docs/kestrel-ack-responder.md new file mode 100644 index 00000000..c383d893 --- /dev/null +++ b/docs/kestrel-ack-responder.md @@ -0,0 +1,37 @@ +# Hardware ACK responder on Kestrel (RTL8852BU) + +How `SetAckResponder` arms the AX MAC's SIFS ACK on the 8852B, what was +measured to be needed, and what is still open. The bench numbers are the +`ack_responder_ok` row in `src/AdapterCaps.h`. + +## Evidence behind the mechanism + +What `SetAckResponder` / `ClearAckResponder` do and verify is documented once, +on their declaration in `src/kestrel/RtlKestrelDevice.h`. This page keeps what +was measured to get there. + +- **Why it used to measure 0%.** `add_self_sta`'s NO_LINK entry is accepted + over H2C and never committed: the physical entry, read through + `FILTER_MODEL_ADDR`/`INDIR_ACCESS_ENTRY` at 0x18850000, stays valid=0. +- **What commits it** was captured from the vendor Windows driver over + USBPcap: `join_info(INFRA, connect)` THEN the station addr_cam (SHORT + entry, BSSID CAM at index 1). Without the join the entry never commits. The + vendor's surrounding commands (role CLIENT, macid unpause, CMAC/DMAC tables, + H2C REC/DONE_ACK) were each dropped with no change in commit or ACK rate, + so `register_infra_sta` sends only the two. +- **The commit is asynchronous.** Typically read back ~2.2 ms after the H2Cs + (a single read takes about that long over USB), once 19.5 ms. A one-shot + read-back refused healthy arms, so `SetAckResponder` polls for up to 100 ms. +- **Response-abort gates.** The arm clears `R_AX_TRXPTCL_RESP_0` bits 23-28. + On ch149, shared with a neighbour AP, one of three runs without the clear + dropped to 82.4% single-shot; three with it stayed at >= 99.6%. On a quiet + ch161 it made no difference. +- **Clear** is exercised live by rxdemo's `DEVOURER_ACK_DISARM_AFTER_MS`. +- **8852C:** its firmware accepts both commands and leaves the entry invalid, + so the arm refuses. The vendor's 8852C association sends far more before the + addr_cam (BB/RF offload, port configuration); which of it the C needs is open. +- Measured near field (two dongles, injector at -15 dB), single-shot, no CCX: + the numbers are in `AdapterCaps.h`. The responder's own transmissions are + not part of that matrix: with carrier sense off (this die's TX default) a + responder that also sends unicast loses some ACKs to its own queued frames + (the response scheduler abort counter at 0xCC18[23:16] moves). diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index 9d09cad8..dde2baa3 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -46,6 +46,9 @@ #if defined(DEVOURER_HAVE_8733B) #include "rtl8733b/Rtl8733bDevice.h" #endif +#if defined(DEVOURER_HAVE_KESTREL) +#include "kestrel/RtlKestrelDevice.h" +#endif #include "RtlAdapter.h" #include "SignalStop.h" #include "UsbOpen.h" @@ -1704,12 +1707,18 @@ int main(int argc, char **argv) { static_cast(ms)); scheduled = true; } +#endif +#if defined(DEVOURER_HAVE_KESTREL) + if (auto *kestrel = dynamic_cast(rtlDevice)) { + kestrel->ScheduleAckResponderDisarmForTest(static_cast(ms)); + scheduled = true; + } #endif if (!scheduled) { logger->error( - "DEVOURER_ACK_DISARM_AFTER_MS is supported only by RTL8733B " - "and the Jaguar1/CHIP_8812 path (measured on RTL8812AU); " - "refusing {}", + "DEVOURER_ACK_DISARM_AFTER_MS is supported only by RTL8733B, " + "the Jaguar1/CHIP_8812 path (measured on RTL8812AU) and Kestrel " + "(measured on RTL8852BU); refusing {}", devourer::generation_name( rtlDevice->GetAdapterCaps().generation)); return 1; diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index c6d14691..42e6dc5f 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -191,7 +191,14 @@ struct AdapterCaps { * 8822B, 8812C/8822C, 8812E/8822E (the 8811A rides the 8812 die path and * inherits its row), 8733B (1725/1725 frames ACKed at retries_mean 0.00, * and retarget-proof: re-armed on a different MAC, 1728/1728 — - * tests/ack_txreport_matrix.sh run with the 8733B as the responder). + * tests/ack_txreport_matrix.sh run with the 8733B as the responder), + * 8852B (Kestrel has no CCX report, so the soliciting TX is an 8832CU + * counting the ACK frames it receives, one attempt per frame, MCS1 on + * ch161, 3 runs per row: 14230/14430 armed at Init, 14391/14431 armed at + * InitWrite, 14347/14425 retargeted, 0/6440 at the old address after the + * retarget, 0/6194 never armed, 0/6603 after a live disarm). The 8852C + * reads false: its firmware does not commit the station entry, and the + * arm refuses on the read-back. * * The `on`, `retarget`, and legacy `off` rows establish arming, retargeting, * and a never-armed control. A backend-owned `disarmed` row supports only diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index c3d070c7..4fea87e5 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -3,6 +3,7 @@ #include #include +#include #include "ChipVariant.h" #include "KestrelFw.h" @@ -265,6 +266,55 @@ class HalKestrel { return ok; } + /* Register `own` as an associated INFRA station of `bssid` -- what makes the + * firmware COMMIT the physical ADDR_CAM entry, which add_self_sta's NO_LINK + * entry never gets (accepted over H2C, reads back valid=0). Two commands, in + * the order the vendor Windows driver sends them on a real association + * (captured over USBPcap): join_info(INFRA, connect), THEN the station + * addr_cam (SMA=own, TMA=bssid, SHORT entry, BSSID CAM at index 1). The + * join is the step that matters: without it the entry never commits. The + * vendor's surrounding commands (role CLIENT, macid unpause, CMAC and DMAC + * tables) were each dropped on the 8852B with no change in the commit or the + * ACK rate, so they are not sent. The commit is asynchronous; callers verify + * with addr_cam_holds(). The 8852C firmware accepts both commands and does + * not commit. */ + bool register_infra_sta(const uint8_t own[6], const uint8_t bssid[6], + uint8_t macid = 0, uint8_t band = 0, uint8_t port = 0) { + bool ok = _fw.fw_join_info(macid, reg::MAC_AX_NET_TYPE_INFRA, + reg::MAC_AX_SELF_ROLE_CLIENT, + reg::MAC_AX_WIFI_ROLE_STATION, port, band, + /*connect=*/true); + ok = _fw.fw_upd_addr_cam_sta(macid, own, bssid, reg::MAC_AX_NET_TYPE_INFRA, + /*addr_cam_idx=*/0, /*bssid_cam_idx=*/1) && + ok; + return ok; + } + + /* One dword of the MAC's internal table SRAM (rtw89 mac_mem_read): the + * internal address goes to R_AX_FILTER_MODEL_ADDR, the data comes back + * through the R_AX_INDIR_ACCESS_ENTRY window (0x40000, a 32-bit-address + * access). */ + uint32_t read_mac_mem32(uint32_t internal_addr) { + _device.rtw_write32(reg::R_AX_FILTER_MODEL_ADDR, internal_addr); + return _device.rtw_read32_wide(reg::R_AX_INDIR_ACCESS_ENTRY); + } + + /* Whether the PHYSICAL address-CAM entry `idx` is valid and carries `sma` -- + * what the RMAC actually matches A1 against, as opposed to what an H2C asked + * the firmware to write. Layout (rtw89 cam.h): word0 bit0 = VALID, then + * SMA[0..3] in word2 and SMA[4..5] in the low half of word3. */ + bool addr_cam_holds(uint8_t idx, const uint8_t sma[6]) { + const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; + if (!(read_mac_mem32(base) & 0x1u)) + return false; + const uint32_t w2 = read_mac_mem32(base + 8); + const uint32_t w3 = read_mac_mem32(base + 12); + const uint8_t got[6] = {static_cast(w2), static_cast(w2 >> 8), + static_cast(w2 >> 16), static_cast(w2 >> 24), + static_cast(w3), static_cast(w3 >> 8)}; + return std::memcmp(got, sma, 6) == 0; + } + /* Register an associated peer STA (an AP-mode client we schedule UL for): * the same role + ADDR_CAM + CMAC-control chain as add_self_sta but for a * distinct macid and net_type=AP, so the fw tracks the peer and a Trigger's diff --git a/src/kestrel/KestrelFw.cpp b/src/kestrel/KestrelFw.cpp index 9da7abb3..1f3108a7 100644 --- a/src/kestrel/KestrelFw.cpp +++ b/src/kestrel/KestrelFw.cpp @@ -416,6 +416,29 @@ bool KestrelFw::fw_role_maintain(uint8_t macid, uint8_t self_role, return ok; } +bool KestrelFw::fw_join_info(uint8_t macid, uint8_t net_type, uint8_t self_role, + uint8_t wifi_role, uint8_t port, uint8_t band, + bool connect) { + /* One dword; OP is "disconnect", so connect sends OP=0. The HE/UL-OFDMA + * fields (WMM/TGR/ISHESTA/DLBW/TF_MAC_PAD/DL_T_PE) stay 0. */ + uint8_t content[4] = {0}; + const uint32_t op = connect ? 0u : 1u; + const uint32_t w0 = (static_cast(macid) & 0xff) | (op << 8) | + ((static_cast(band) & 0x1) << 9) | + ((static_cast(port) & 0x7) << 21) | + ((static_cast(net_type) & 0x3) << 24) | + ((static_cast(wifi_role) & 0xf) << 26) | + ((static_cast(self_role) & 0x3) << 30); + put_le32(content + 0, w0); + bool ok = send_h2c_cmd(r::FWCMD_H2C_CAT_MAC, r::FWCMD_H2C_CL_MEDIA_RPT, + r::FWCMD_H2C_FUNC_JOININFO, content, sizeof(content)); + _logger->info("Kestrel: join_info (macid={} op={} net_type={} self={} wifi={} " + "port={} band={}) -> {}", + macid, op, net_type, self_role, wifi_role, port, band, + ok ? "sent" : "FAILED"); + return ok; +} + bool KestrelFw::fw_upd_addr_cam(uint8_t macid, const uint8_t self_mac[6], uint8_t net_type, uint8_t addr_cam_idx, uint8_t bssid_cam_idx) { @@ -475,6 +498,51 @@ bool KestrelFw::fw_upd_addr_cam(uint8_t macid, const uint8_t self_mac[6], return ok; } +bool KestrelFw::fw_upd_addr_cam_sta(uint8_t macid, const uint8_t sma[6], + const uint8_t tma[6], uint8_t net_type, + uint8_t addr_cam_idx, uint8_t bssid_cam_idx) { + /* Field layout as fw_upd_addr_cam (rtw89 cam.h), with the station values the + * vendor driver uses: SHORT length, separate SMA/TMA hashes, the BSSID CAM + * index in dword3, macid plus the vendor's bit 14 in dword8, and an embedded + * valid BSSID CAM entry carrying tma. */ + uint8_t sma_hash = 0, tma_hash = 0; + for (int i = 0; i < 6; i++) { + sma_hash ^= sma[i]; + tma_hash ^= tma[i]; + } + uint8_t c[60] = {0}; + auto sw = [](uint32_t v, uint32_t msk, uint32_t sh) { + return (v & msk) << sh; + }; + put_le32(c + 4, sw(addr_cam_idx, 0xff, 0) | sw(0, 0xff, 8) | + sw(r::ADDR_CAM_ENT_SHORT_SIZE, 0xff, 16)); + put_le32(c + 8, 0x1u | sw(net_type, 0x3, 1) | sw(r::MAC_AX_NO_MSK, 0x3, 14) | + sw(sma_hash, 0xff, 16) | sw(tma_hash, 0xff, 24)); + put_le32(c + 12, sw(bssid_cam_idx, 0x3f, 0)); + put_le32(c + 16, sw(sma[0], 0xff, 0) | sw(sma[1], 0xff, 8) | + sw(sma[2], 0xff, 16) | sw(sma[3], 0xff, 24)); + put_le32(c + 20, sw(sma[4], 0xff, 0) | sw(sma[5], 0xff, 8) | + sw(tma[0], 0xff, 16) | sw(tma[1], 0xff, 24)); + put_le32(c + 24, sw(tma[2], 0xff, 0) | sw(tma[3], 0xff, 8) | + sw(tma[4], 0xff, 16) | sw(tma[5], 0xff, 24)); + put_le32(c + 32, sw(macid, 0xff, 0) | (1u << 14)); + put_le32(c + 48, sw(bssid_cam_idx, 0xff, 0) | sw(0, 0xff, 8) | + sw(r::BSSID_CAM_ENT_SIZE, 0xff, 16)); + put_le32(c + 52, 0x1u | sw(r::MAC_AX_NO_MSK, 0x3f, 2) | sw(0, 0x3f, 8) | + sw(tma[0], 0xff, 16) | sw(tma[1], 0xff, 24)); + put_le32(c + 56, sw(tma[2], 0xff, 0) | sw(tma[3], 0xff, 8) | + sw(tma[4], 0xff, 16) | sw(tma[5], 0xff, 24)); + bool ok = send_h2c_cmd(r::FWCMD_H2C_CAT_MAC, r::FWCMD_H2C_CL_ADDR_CAM_UPDATE, + r::FWCMD_H2C_FUNC_ADDRCAM_INFO, c, sizeof(c)); + _logger->info("Kestrel: upd_addr_cam_sta (macid={} net_type={} a_idx={} " + "b_idx={} sma={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} " + "tma={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}) -> {}", + macid, net_type, addr_cam_idx, bssid_cam_idx, sma[0], sma[1], + sma[2], sma[3], sma[4], sma[5], tma[0], tma[1], tma[2], tma[3], + tma[4], tma[5], ok ? "sent" : "FAILED"); + return ok; +} + bool KestrelFw::fw_upd_cctl_basic(uint8_t macid, uint8_t addr_cam_idx, uint16_t datarate, uint8_t ntx_path_en, uint8_t path_map_a, bool bmc) { diff --git a/src/kestrel/KestrelFw.h b/src/kestrel/KestrelFw.h index 072d6413..aa90aa12 100644 --- a/src/kestrel/KestrelFw.h +++ b/src/kestrel/KestrelFw.h @@ -67,6 +67,13 @@ class KestrelFw { bool fw_role_maintain(uint8_t macid, uint8_t self_role, uint8_t wifi_role, uint8_t upd_mode, uint8_t band, uint8_t port); + /* rtw89_fw_h2c_join_info: H2C cat=MAC, class=MEDIA_RPT, func=JOININFO -- + * the firmware's "station connected" op (`connect` sends OP=0). Without it + * the firmware accepts a station's addr_cam H2C and does not commit it. + * Roles are mac_ax_self_role / mac_ax_wifi_role, net_type mac_ax_net_type. */ + bool fw_join_info(uint8_t macid, uint8_t net_type, uint8_t self_role, + uint8_t wifi_role, uint8_t port, uint8_t band, bool connect); + /* mac_upd_addr_cam (addr_cam.c): H2C cat=MAC, class=ADDR_CAM_UPDATE, * func=ADDRCAM_INFO — programs the address-CAM entry (BSSID+SMA+TMA+macid+ * net_type) for `macid`. `self_mac` is the 6-byte transmitter address (our @@ -76,6 +83,13 @@ class KestrelFw { bool fw_upd_addr_cam(uint8_t macid, const uint8_t self_mac[6], uint8_t net_type, uint8_t addr_cam_idx, uint8_t bssid_cam_idx); + /* The station form of the addr_cam, as the vendor Windows driver writes it on + * an association: SMA = our own address, TMA = the AP's BSSID (different), + * the SHORT entry length, and the BSSID CAM entry embedded with BSSID = tma. */ + bool fw_upd_addr_cam_sta(uint8_t macid, const uint8_t sma[6], + const uint8_t tma[6], uint8_t net_type, + uint8_t addr_cam_idx, uint8_t bssid_cam_idx); + /* mac_upd_cctl_info (tblupd.c): H2C cat=MAC, class=FR_EXCHG, func=CCTLINFO_UD * — the per-MACID CMAC control table. Sets the default TX rate, the TX * antenna path enable (ntx_path_en — without it a frame has NO antenna and diff --git a/src/kestrel/MacRegAx.h b/src/kestrel/MacRegAx.h index dfe96b6c..79b4aea9 100644 --- a/src/kestrel/MacRegAx.h +++ b/src/kestrel/MacRegAx.h @@ -626,6 +626,12 @@ constexpr uint8_t USR_TX_RPT_MODE_LAST_PKT = 2; * | WIFI_ROLE[16:13] | BAND[18:17] | PORT[21:19]. */ constexpr uint8_t FWCMD_H2C_CL_MEDIA_RPT = 0x8; constexpr uint8_t FWCMD_H2C_FUNC_FWROLE_MAINTAIN = 0x4; +/* JOININFO (same MEDIA_RPT class, func 0): the fw "STA connected" op. One dword + * w0 = MACID[7:0] | OP[8] (0=connect,1=disconnect) | BAND[9] | WMM[11:10] | + * TGR[12] | ISHESTA[13] | DLBW[15:14] | TF_MAC_PAD[17:16] | DL_T_PE[20:18] | + * PORT_ID[23:21] | NET_TYPE[25:24] | WIFI_ROLE[29:26] | SELF_ROLE[31:30] + * (rtw89 fw.c rtw89_fw_h2c_join_info, fw.h RTW89_H2C_JOININFO_W0_*). */ +constexpr uint8_t FWCMD_H2C_FUNC_JOININFO = 0x0; constexpr uint8_t H2C_FWROLE_SELF_ROLE_SH = 8; /* mask 0x3 */ constexpr uint8_t H2C_FWROLE_UPD_MODE_SH = 10; /* mask 0x7 */ constexpr uint8_t H2C_FWROLE_WIFI_ROLE_SH = 13; /* mask 0xf */ @@ -679,6 +685,11 @@ constexpr uint8_t FWCMD_H2C_FUNC_DCTLINFO_UD = 0x1; /* addr_cam entry geometry (addr_cam.h): 8852B uses the LONG entry (0x40). */ constexpr uint8_t ADDR_CAM_ENT_LONG_SIZE = 0x40; +/* The entry length the vendor driver writes for an associated station. */ +constexpr uint8_t ADDR_CAM_ENT_SHORT_SIZE = 0x20; +/* ADDR_CAM in the MAC table SRAM (rtw89 ADDR_CAM_BASE_ADDR), read through the + * FILTER_MODEL_ADDR / INDIR_ACCESS_ENTRY window. */ +constexpr uint32_t ADDR_CAM_MEM_BASE = 0x18850000; constexpr uint8_t BSSID_CAM_ENT_SIZE = 0x08; /* mac_ax_addr_msk_sel (mac_def.h). */ constexpr uint8_t MAC_AX_NO_MSK = 0; @@ -1567,6 +1578,17 @@ constexpr uint32_t WMAC_SPEC_SIFS_CCK = 0xA; constexpr uint8_t B_AX_WMAC_SPEC_SIFS_OFDM_SH = 8; constexpr uint32_t B_AX_WMAC_SPEC_SIFS_OFDM_MSK = 0xff; constexpr uint32_t WMAC_SPEC_SIFS_OFDM_52B = 0x11; +/* Response (ACK/CTS/BA) abort gates in the same register (rtw89 reg.h): the + * RMAC drops a SIFS response when an armed check reads the medium busy. */ +constexpr uint32_t B_AX_RSP_CHK_CCA = 1u << 23; +constexpr uint32_t B_AX_RSP_CHK_EDCCA = 1u << 24; +constexpr uint32_t B_AX_RSP_CHK_BTCCA = 1u << 25; +constexpr uint32_t B_AX_RSP_CHK_SEC_CCA_20 = 1u << 26; +constexpr uint32_t B_AX_RSP_CHK_SEC_CCA_40 = 1u << 27; +constexpr uint32_t B_AX_RSP_CHK_SEC_CCA_80 = 1u << 28; +constexpr uint32_t B_AX_RSP_CHK_ALL = + B_AX_RSP_CHK_CCA | B_AX_RSP_CHK_EDCCA | B_AX_RSP_CHK_BTCCA | + B_AX_RSP_CHK_SEC_CCA_20 | B_AX_RSP_CHK_SEC_CCA_40 | B_AX_RSP_CHK_SEC_CCA_80; constexpr uint16_t R_AX_RXTRIG_TEST_USER_2 = 0xCCB0; constexpr uint32_t B_AX_RXTRIG_FCSCHK_EN = 1u << 20; constexpr uint16_t R_AX_TRXPTCL_RRSR_CTL_0 = 0xCC08; diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 4f0d8731..62e6cf49 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -4,6 +4,8 @@ #include #include /* INT_MIN = "no radiotap DBM_TX_POWER" sentinel */ #include +#include +#include #include #include #include @@ -91,6 +93,9 @@ RtlKestrelDevice::RtlKestrelDevice(RtlAdapter device, Logger_t logger, } RtlKestrelDevice::~RtlKestrelDevice() { + /* Cancel + join a pending test disarm before the transport is quiesced + * under it (Stop() does the same; a caller may skip Stop()). */ + _ack_disarm_thread = {}; /* Inert on this generation (its send path is synchronous), but the invariant * belongs to every device: nothing gets released while a transfer the * transport still owns is outstanding. */ @@ -236,6 +241,25 @@ void RtlKestrelDevice::Init(Action_ParsedRadioPacket packetProcessor, _logger->error("Kestrel: monitor bring-up failed"); return; } + arm_configured_ack_responder(); /* DEVOURER_ACK_RESPONDER */ + /* Measurement-only live disarm, started after the arm so even a zero delay + * cannot clear before it. The stop token bounds shutdown on a long delay. */ + if (_ack_disarm_after_ms) { + const uint32_t delay_ms = *_ack_disarm_after_ms; + _ack_disarm_after_ms.reset(); + _ack_disarm_thread = std::jthread([this, delay_ms](std::stop_token stop) { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(delay_ms); + while (!stop.stop_requested() && + std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + if (stop.stop_requested()) + return; + _logger->info("DEVOURER_ACK_DISARM_AFTER_MS: disarming Kestrel ACK " + "responder {} ms after completed bring-up", delay_ms); + ClearAckResponder(); + }); + } StartRxLoop(std::move(packetProcessor)); } @@ -341,6 +365,7 @@ void RtlKestrelDevice::InitWrite(SelectedChannel channel) { * inside every send — no drain thread. */ if (_device.is_usb()) start_wp_drain(); + arm_configured_ack_responder(); /* DEVOURER_ACK_RESPONDER */ } void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { @@ -506,6 +531,132 @@ bool RtlKestrelDevice::GetCcaGates(bool &primary_disabled, return true; } +namespace { +/* The BSSID half of the station the ACK responder registers. Nothing matches + * on it (A1 alone selects the response), it only has to be a unicast address + * distinct from the responder's own. */ +constexpr uint8_t kAckResponderBssid[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; + +std::string mac_text(const uint8_t *m) { + char b[18]; + std::snprintf(b, sizeof b, "%02x:%02x:%02x:%02x:%02x:%02x", m[0], m[1], m[2], + m[3], m[4], m[5]); + return b; +} +} // namespace + +bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { + namespace r = kestrel::reg; + const uint8_t *m = mac.bytes.data(); + if (m[0] & 0x01u) { + _logger->error("Kestrel: ACK responder needs a UNICAST MAC (I/G set in " + "{:02x}) - not armed", m[0]); + return false; + } + if (!_brought_up) { + _logger->error("Kestrel: ACK responder refused until bring-up " + "(Init/InitWrite) has finished"); + return false; + } + std::lock_guard lock(_ack_mutex); + uint8_t bssid[6]; + std::memcpy(bssid, kAckResponderBssid, sizeof bssid); + if (std::memcmp(bssid, m, sizeof bssid) == 0) + bssid[5] ^= 0x02; + /* Clear the response-abort CCA gates: armed, the RMAC drops the SIFS ACK + * whenever a check reads the medium busy (measured: a co-channel neighbour + * AP, docs/kestrel-ack-responder.md). Saved once so a re-arm keeps the original. */ + const uint32_t resp0 = _device.rtw_read32(r::R_AX_TRXPTCL_RESP_0); + if (!_ack_responder) + _ack_resp0_saved = resp0; + const bool sent = _hal.register_infra_sta(m, bssid); + _device.rtw_write32(r::R_AX_TRXPTCL_RESP_0, resp0 & ~r::B_AX_RSP_CHK_ALL); + /* The H2Cs going out says nothing about the firmware committing them: read + * the physical CAM entry the RMAC matches against. The firmware applies the + * queue asynchronously, so poll for a bounded time rather than read once. */ + const auto t0 = std::chrono::steady_clock::now(); + bool committed = false; + int polls = 0; + while (sent && !(committed = _hal.addr_cam_holds(/*idx=*/0, m)) && + std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(100)) { + ++polls; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + const auto waited_us = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + _logger->debug("Kestrel: ACK responder CAM read-back {} after {} us ({} " + "re-reads)", committed ? "matched" : "did not match", + waited_us, polls); + if (!sent || !committed) { + _logger->error("Kestrel: ACK responder for {} did not arm: the firmware " + "{} the ADDR_CAM entry (H2C {})", + mac_text(m), + sent ? "accepted but did not commit" : "refused", + sent ? "sent" : "failed"); + /* Roll back. On a retarget the previous address may still be committed + * (a refused H2C leaves it in place), so verify it is gone too; whatever + * is still held stays recorded for ClearAckResponder to retry. */ + devourer::MacAddr held[2] = {mac}; + size_t n = 1; + if (_ack_responder && _ack_responder->bytes != mac.bytes) + held[n++] = *_ack_responder; + _ack_responder = disarm_ack_locked({held, n}); + return false; + } + _ack_responder = mac; + _logger->info("Kestrel: hardware ACK responder armed for {} (ADDR_CAM " + "entry 0 committed, read back after {} us)", mac_text(m), + waited_us); + return true; +} + +void RtlKestrelDevice::ClearAckResponder() { + std::lock_guard lock(_ack_mutex); + if (!_ack_responder) + return; + const devourer::MacAddr mac = *_ack_responder; + /* Forgotten only once the CAM read-back confirms it; a failed clear stays + * armed on record so the next call retries. */ + _ack_responder = disarm_ack_locked({&mac, 1}); +} + +std::optional +RtlKestrelDevice::disarm_ack_locked(std::span held) { + namespace r = kestrel::reg; + /* Back to what InitWrite registers for macid 0: the NO_LINK self entry, + * which the firmware does not commit -- so the RMAC stops answering. */ + _hal.register_sta_role(0, 0, 0); + _hal.add_self_sta(kInjectSA, /*macid=*/0); + _device.rtw_write32(r::R_AX_TRXPTCL_RESP_0, _ack_resp0_saved); + /* The rewrite is as asynchronous as the arm: give it the same bound. */ + const auto still_held = [&]() -> const devourer::MacAddr * { + for (const auto &a : held) + if (_hal.addr_cam_holds(/*idx=*/0, a.bytes.data())) + return &a; + return nullptr; + }; + const auto t0 = std::chrono::steady_clock::now(); + const devourer::MacAddr *left; + while ((left = still_held()) && + std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(100)) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (left) { + _logger->warn("Kestrel: ACK responder clear did not take: ADDR_CAM entry " + "0 still holds {}, the MAC may keep answering for it " + "(ClearAckResponder retries)", mac_text(left->bytes.data())); + return *left; + } + _logger->info("Kestrel: hardware ACK responder disarmed (ADDR_CAM entry 0 " + "no longer holds {})", mac_text(held.front().bytes.data())); + return std::nullopt; +} + +void RtlKestrelDevice::arm_configured_ack_responder() { + if (_cfg.rx.ack_responder && !SetAckResponder(*_cfg.rx.ack_responder)) + throw std::runtime_error( + "Kestrel: configured ACK responder could not be armed"); +} + RxEnergy RtlKestrelDevice::GetRxEnergy(bool with_nhm) { RxEnergy e; /* no phydm FA/CCA/IGI DIG monitor on Kestrel */ /* DEVOURER_RX_NOISE_FLOOR — active/frame-free absolute floor via the halbb NHM @@ -937,14 +1088,17 @@ devourer::AdapterCaps RtlKestrelDevice::GetAdapterCaps() { * computed and discarded. No phydm FA/CCA/IGI monitor either. */ c.busy_airtime_ok = false; c.rx_energy_ok = false; - /* Hardware ARQ: SetAckResponder is not implemented on the AX generation - * (matrix-measured 0% closure) — that flag stays false. The retry knob IS + /* Hardware ARQ: SetAckResponder arms on the 8852B -- a committed INFRA + * station entry (HalKestrel::register_infra_sta), verified by CAM read-back. + * The 8852C firmware does not commit that entry, so its arm refuses and the + * flag stays false there. The retry knob IS * wired (WD DATA_TXCNT_LMT per frame, attempts-semantics folded to the * N-retries contract in send_packet; witness-measured on the 8832CU: * limits {0,2} -> modal {1,3} on-air copies exactly, limit 8 -> an 8/9 * near-tie consistent with ~90% witness capture of a 9-copy truth — * obedient, no wedge). */ c.tx_retry_limit_ok = true; + c.ack_responder_ok = _variant == kestrel::ChipVariant::C8852B; 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) */ diff --git a/src/kestrel/RtlKestrelDevice.h b/src/kestrel/RtlKestrelDevice.h index ce5fa081..76982311 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include "TxMode.h" @@ -67,6 +69,7 @@ class RtlKestrelDevice : public IRtlRadio { * demo shutdown). The destructor joins too, but the demos destroy the * device object after libusb_exit, so Stop() is the ordered join point. */ void Stop() override { + _ack_disarm_thread = {}; /* cancel + join a pending test disarm */ _rx_stop = true; stop_wp_drain(); pcie_quiesce(); @@ -83,6 +86,32 @@ class RtlKestrelDevice : public IRtlRadio { * IRtlRadio contract; SetCcaMode(d) == SetCcaGates(d, d). */ bool SetCcaGates(bool primary_disabled, bool edcca_disabled) override; bool GetCcaGates(bool &primary_disabled, bool &edcca_disabled) override; + /* Hardware ACK responder (IRadio contract). The AX MAC has no Jaguar-style + * port identity register to retarget: the RMAC answers for an address only + * once the firmware has COMMITTED a valid ADDR_CAM entry for it, and it does + * that only for a station registered the way a real association registers + * one (HalKestrel::register_infra_sta). The arm programs that station with + * `mac` as its own address, clears the response-abort CCA gates, and is + * verified by reading the physical CAM entry back -- false when the entry + * did not commit, which is the 8852C today. Post-bring-up only. Clear puts + * macid 0 back to the bring-up NO_LINK entry and verifies the CAM no + * longer holds `mac`; an address still held after a failed clear (or a + * failed retarget, which checks the old address too) stays recorded, so a + * later ClearAckResponder retries it. Arm and clear are serialized. + * + * The responder shares macid 0 / ADDR_CAM entry 0 with injection: while + * armed, injected frames (whose WD carries macid 0) go out under the + * station context the arm registered, and clear restores the bring-up + * entry. A second macid for the responder is untested: whether the + * firmware commits an entry other than 0 has not been measured. */ + bool SetAckResponder(const devourer::MacAddr &mac) override; + void ClearAckResponder() override; + /* rxdemo's live-disarm measurement hook (DEVOURER_ACK_DISARM_AFTER_MS): the + * delay starts once Init has finished the bring-up, including a configured + * SetAckResponder, never from process start. */ + void ScheduleAckResponderDisarmForTest(uint32_t delay_ms) { + _ack_disarm_after_ms = delay_ms; + } bool send_packet(const uint8_t *packet, size_t length) override; devourer::TxStats GetTxStats() override { return _device.GetTxStats(); } SelectedChannel GetSelectedChannel() override { return _channel; } @@ -275,6 +304,22 @@ class RtlKestrelDevice : public IRtlRadio { * ACH0 is channel 0, so "TX is up" is its own flag, not a non-zero handle. */ bool _tx_up = false; bool _brought_up = false; /* Init/InitWrite completed the MAC bring-up */ + /* Armed ACK responder address, and the R_AX_TRXPTCL_RESP_0 value the arm + * replaced (restored by ClearAckResponder). */ + std::optional _ack_responder; + uint32_t _ack_resp0_saved = 0; + /* Serializes SetAckResponder/ClearAckResponder: the test disarm below + * calls ClearAckResponder from its own thread. */ + std::mutex _ack_mutex; + /* Rewrite macid 0 to the NO_LINK entry, restore RESP_0 and poll the CAM + * until it holds none of `held`. Returns the address still held on + * timeout, nullopt once gone. Caller holds _ack_mutex. */ + std::optional + disarm_ack_locked(std::span held); + std::optional _ack_disarm_after_ms; + std::jthread _ack_disarm_thread; + /* DEVOURER_ACK_RESPONDER: arm at the end of bring-up, throw if refused. */ + void arm_configured_ack_responder(); uint8_t _tx_mgmt_q = 0; uint8_t _tx_data_q = 0; bool _tx_data_ok = false; /* data frames may use _tx_data_q */