From 7684beac2ba7270c6a2b691566e5fddd729b1e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Wed, 14 Oct 2026 21:41:25 -0300 Subject: [PATCH 1/3] kestrel: hardware ACK responder on the 8852C On the 8852C, SetAckResponder refused: the firmware ignores the addr_cam H2C altogether. After it neither the address CAM entry nor the BSSID CAM entry it carries is written, and nothing changed that: a byte-for-byte replay of the vendor Windows driver's association H2Cs, a join_info alone, a 64-byte addr_cam. The same replay commits on the 8852B. The RMAC still answers from the CAM, so on this die the arm writes the station entry from the host (HalKestrel::write_addr_cam_sta), the way the vendor's mac_upd_addr_cam does when the firmware is not running: the same short entry the 8852B firmware commits, VALID cleared first (a retarget rewrites a live entry), then the body, then VALID. No H2C is sent, and the clear is the host clear alone (no join_info disconnect for a station the firmware never registered). - The responder's slot is per die (ack_slot()): 127 on the 8852B (unchanged) and 63 on the 8852C, its last entry: the CAM reads 0xdeadbeef from entry 64 up. RegisterPeerSta/RegisterBeamformee keep refusing it. - The response-abort gate clear is mandatory on this die: its EDCCA reads its own floor as busy, and with the gates armed every ACK was dropped. - The station entry encoding moves to KestrelAddrCam.h and is shared by the 8852B's addr_cam H2C (byte-identical payload) and the 8852C's host write; tests/kestrel_addr_cam_selftest.cpp pins it against hardware read-backs (an entry the 8852B firmware committed, one the host wrote on an 8852C). - ack_responder_ok is true on both dies; the SetAckResponder contract and the flag's comment say that on the 8852C a true return does not guarantee every bring-up answers (see below). Measured, TX50UH (RTL8832CU) responder, TX20U Nano (RTL8852BU) soliciting and counting the ACKs it receives, ch161, MCS1, one attempt per frame, 10 s per cell, 3 runs per row: 13831/14041 armed at Init, 14239/14408 retargeted, armed at InitWrite 98.9% and 97.0% (third run a dead session), 0/5993 at the old address, 0/6062 never armed, 0/5965 after a live disarm; arm/retarget/clear/re-arm correct in the live cycle; injection delivery unchanged armed vs unarmed. The RX descriptor shows every frame to the responder matching the host-written entry. 8852B regression on this code: 99.5-99.8% armed and retargeted, 0 after a disarm, cycle correct, slot 127. Not solved: some 8852C bring-ups answer no ACK for the whole session while the entry reads back and the adapter airs its own frames. It tracks run time (0 of 12 sessions straight after a long power-off, 3 of the next 12; up to three in four after a day of back-to-back runs). The MAC configuration registers are identical to a good session's; the difference is that the MAC classifies the soliciting frames as its own BSS's (WMAC_NAV_UP_INFO, PS_RXINFO). Response NAV checks, the BSSID CAM, the injector, host RX cost and arm timing are ruled out; the doc lists what was measured. Full MSVC build and ctest pass (79 tests with the new selftest, 4 skipped as on master). --- CMakeLists.txt | 10 +++ docs/kestrel-ack-responder.md | 104 +++++++++++++++++++++++----- examples/rx/main.cpp | 2 +- src/AdapterCaps.h | 12 +++- src/kestrel/HalKestrel.h | 30 +++++++- src/kestrel/KestrelAddrCam.h | 46 ++++++++++++ src/kestrel/KestrelFw.cpp | 26 +++---- src/kestrel/RtlKestrelDevice.cpp | 98 ++++++++++++++------------ src/kestrel/RtlKestrelDevice.h | 39 ++++++++--- tests/kestrel_addr_cam_selftest.cpp | 54 +++++++++++++++ 10 files changed, 326 insertions(+), 95 deletions(-) create mode 100644 src/kestrel/KestrelAddrCam.h create mode 100644 tests/kestrel_addr_cam_selftest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a3c66b10..9bcbab81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -923,6 +923,16 @@ target_link_libraries(ToneMaskSelftest PRIVATE devourer) add_test(NAME tone_mask_math COMMAND ToneMaskSelftest) +# Headless guard for the Kestrel station address-CAM encoder shared by the +# 8852B's addr_cam H2C and the 8852C's host write; expected words are +# hardware read-backs (src/kestrel/KestrelAddrCam.h). +add_executable(KestrelAddrCamSelftest + tests/kestrel_addr_cam_selftest.cpp +) +target_link_libraries(KestrelAddrCamSelftest PRIVATE devourer) + +add_test(NAME kestrel_addr_cam COMMAND KestrelAddrCamSelftest) + # Headless guard for the radio contract: a radio implementing only IRadio's # pure-virtual core must compile without any Realtek type, its not-ported # defaults must hold, and it must not be an IRtlRadio. A Realtek-typed pure diff --git a/docs/kestrel-ack-responder.md b/docs/kestrel-ack-responder.md index 7a1e55c2..2732a44c 100644 --- a/docs/kestrel-ack-responder.md +++ b/docs/kestrel-ack-responder.md @@ -1,7 +1,7 @@ -# Hardware ACK responder on Kestrel (RTL8852BU) +# Hardware ACK responder on Kestrel (RTL8852BU, RTL8832CU) -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 +How `SetAckResponder` arms the AX MAC's SIFS ACK on both Kestrel dies, 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 @@ -26,20 +26,18 @@ was measured to get there. 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. -- **Own macid.** The station sits at macid 127 / ADDR_CAM entry 127 (the last - entry of the 0x7f search range), apart from the injection context at macid 0 - / entry 0 and from the low slots peer registrations take (`RegisterPeerSta` - callers use 1, 2, ...; it and `RegisterBeamformee` refuse 127). The - firmware commits entry 127 in the same ~2.2 ms as entry 1 or 0. With the - station at entry 1 during development, entry 0 read back byte-identical - before the arm, after it and after the clear. Sharing entry 0 (the previous - layout) had no measurable cost: a witness counted the same >= 2900 of 3000 - injected frames armed and unarmed, on both layouts. The separation is for - the injection context's integrity, not a delivery fix. +- **Own slot.** The slot itself is documented at `ack_slot()`. Measured: the + 8852B firmware commits entry 127 in the same ~2.2 ms as entry 1 or 0; the + 8852C's CAM reads back 0xdeadbeef from entry 64 up, which is why its slot is + 63. With the station at entry 1 during development, entry 0 read back + byte-identical before the arm, after it and after the clear. Sharing entry 0 + (the previous layout) had no measurable cost: a witness counted the same >= + 2900 of 3000 injected frames armed and unarmed, on both layouts. - **Unexplained cell.** In about 55 armed cells across development (station at entry 1), one read the entry back committed (after 10 ms rather than ~2.2 ms) and answered no ACKs; other slow commits were fine and it has not - reproduced. + reproduced. It has the shape of the 8852C bring-ups described below, much + rarer. - **Clear needs the host.** Measured at entry 1: after `join_info(disconnect)` and a NO_LINK addr_cam, the entry stays VALID with the responder's address; a role REMOVE, or role CREATE plus NO_LINK, leaves it valid too. Clearing @@ -47,11 +45,83 @@ was measured to get there. H2Cs land asynchronously, the clear repeats until the entry has read back empty for 50 ms, and only that counts as disarmed. rxdemo's `DEVOURER_ACK_DISARM_AFTER_MS` exercises it live. -- **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. +- **8852C:** see the next two sections. - 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). + +## 8852C: the host writes the entry + +The 8852C firmware ignores the addr_cam H2C altogether. After it, neither the +address CAM entry nor the BSSID CAM entry it carries is written: the address +CAM still holds the uninitialised SRAM it powered up with, and the BSSID CAM +stays zero. Nothing changed that, including a byte-for-byte replay of the +vendor Windows driver's four association H2Cs (CMAC table, DMAC table, a +12-byte join_info, addr_cam), a 12-byte join_info alone, and a 64-byte +addr_cam (the vendor Linux `struct fwcmd_addrcam_info`). The same replay +commits on the 8852B. Both dies report non-secure (`is_sec_ic=0`), and the two +ways of reading the window (per-dword address, or the vendor's page base plus +offset) agree, so the read-back itself is not the issue. + +The RMAC still answers from the CAM, so on this die the arm writes the entry +from the host instead, the vendor's no-firmware path (`mac_upd_addr_cam`'s +indirect SRAM write); what it writes and how is at +`HalKestrel::write_addr_cam_sta`, and the entry encoding is shared with the +8852B's H2C (`KestrelAddrCam.h`, pinned by a selftest against hardware +read-backs). Measured on its own: + +- **Only the address CAM entry matters.** Leaving out the join_info H2C, the + addr_cam H2C or the BSSID CAM entry, alone or all together, kept the + single-shot rate at 97.6-99.4%. Host-writing the BSSID CAM entry the vendor + path also writes changed nothing measurable. +- **The response-abort gates are mandatory here.** This die's EDCCA reads its + own floor as busy (-67 dBm, `busy=1` at every tune), and with the gates + armed the RMAC dropped every ACK: 0/2060 and 0/2019, against >= 97.6% with + them cleared. +- **The lookup hits the entry.** The long RX descriptor (dword 5: + ADDR_CAM_VLD, addr_cam index, macid) shows every frame addressed to the + responder matching the host-written entry (macid 63; the index reads 126, + the entry counted in 32-byte units). +- **Clear** reads back empty and the soliciting peer then gets no ACK (the + live-disarm row in `AdapterCaps.h`). + +## 8852C: bring-ups that answer no ACKs + +Some 8852C bring-ups answer no ACK at all: the responder arms, the entry reads +back, and the adapter answers nothing for the whole session, where a good +session answers >= 97.5%. How often tracks how long the adapter has been +running. Over a day of back-to-back sessions it rose from about one in ten to +three in four (10 of 83 sessions overall); after the adapter was unplugged for +a long while, the first 12 sessions in a row answered 97.5-99.4%, and 3 of the +next 12 armed sessions (the `AdapterCaps.h` matrix) came up dead. The RF +thermal meter read delta 2 rising to 4, too coarse to say more. So it looks +thermal or uptime-related, though temperature was not varied on its own. It is +a property of the bring-up, not of the arm: in one failed session three +separate injector processes in a row all saw 0, and no session has ever +recovered or failed partway. Measured in failed sessions: + +- Every frame addressed to the responder still matches the entry in the RX + descriptor (ADDR_CAM_VLD, macid 63), and the adapter airs its own frames + normally: only the SIFS response is missing. +- The response abort counters (0xCC14 NAV, 0xCC18 CCA/EDCCA/scheduler) stay at + 0: the hardware does not attempt the response and abort it. +- Every MAC configuration register read (0xC000, 0xC300, 0xC400, 0xC600, + 0xC800, 0xCA00, 0xCC00, 0xCE00 and the BB path-com block at 0xD800) is + identical to a good session's; only counters differ. +- What does differ: `R_AX_WMAC_NAV_UP_INFO` (0xCC84) records intra-BSS NAV + updates and `R_AX_PS_RXINFO` (0xCEA0) flags port-0 data, i.e. the MAC + classifies the soliciting frames as its own BSS's traffic, which it does not + in a good session. +- Ruled out by measurement: the response NAV checks (`R_AX_RSP_CHK_SIG` bits + 19-21 cleared, still fails), the BSSID CAM (all zero in good and failed + sessions alike; writing entry 1 valid, still fails), the injector, the + host's per-frame RX cost, the arm's timing relative to the RX loop, and the + slot. + +What decides it is not found. The 8852B shows the same shape far more rarely +(about one bring-up in 55). A caller that needs the ACK on the 8852C should +treat a soliciting peer's retries as the tell and bring the adapter up again; +when that keeps failing, letting the adapter cool down is what brought it back +here. diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index dde2baa3..e95de965 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -1718,7 +1718,7 @@ int main(int argc, char **argv) { logger->error( "DEVOURER_ACK_DISARM_AFTER_MS is supported only by RTL8733B, " "the Jaguar1/CHIP_8812 path (measured on RTL8812AU) and Kestrel " - "(measured on RTL8852BU); refusing {}", + "(measured on RTL8852BU and RTL8832CU); refusing {}", devourer::generation_name( rtlDevice->GetAdapterCaps().generation)); return 1; diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index e69bd25c..571fa210 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -199,9 +199,15 @@ struct AdapterCaps { * 14419/14432 retargeted, 0/6597 at the old address after the retarget, * 0/6575 never armed, 0/6561 after a live disarm; one cell in about 55 * across the development runs, at entry 1, read the entry back committed - * and answered no ACKs, not reproduced since). The 8852C reads false: its - * firmware does not commit the station entry, and the arm refuses on the - * read-back. + * and answered no ACKs, not reproduced since), 8852C (the same bench with + * the roles swapped, the 8852B soliciting; the entry is host-written at + * macid / ADDR_CAM entry 63, docs/kestrel-ack-responder.md; 3 runs per row: + * 13831/14041 armed at Init, 14239/14408 retargeted, armed at InitWrite + * 98.9% and 97.0% in two runs with the third a dead session, 0/5993 at the + * old address, 0/6062 never armed, 0/5965 after a live disarm; some + * bring-ups answer no ACK at all, more often the longer the adapter has + * run: 3 of the 12 armed sessions in that matrix, 0 of the 12 run before it + * straight after a long power-off). * * 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 d02cd0f5..c6db5920 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -7,6 +7,7 @@ #include "ChipVariant.h" #include "KestrelFw.h" +#include "KestrelAddrCam.h" #include "MacRegAx.h" /* mac_ax role enums */ #include "RtlAdapter.h" #include "SelectedChannel.h" /* ChannelWidth_t */ @@ -276,8 +277,9 @@ class HalKestrel { * 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. */ + * with addr_cam_holds(). The 8852C firmware ignores the addr_cam H2C + * altogether (neither CAM entry is written), so that die takes + * write_addr_cam_sta() instead. */ bool register_infra_sta(const uint8_t own[6], const uint8_t bssid[6], uint8_t macid, uint8_t addr_cam_idx, uint8_t bssid_cam_idx, uint8_t band = 0, @@ -320,6 +322,30 @@ class HalKestrel { write_mac_mem32(reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE, 0u); } + /* Write the station entry register_infra_sta asks the firmware for straight + * into the physical address CAM -- the vendor's no-firmware path + * (mac_upd_addr_cam's indirect SRAM write), for the 8852C, whose firmware + * ignores the addr_cam H2C. The same SHORT entry the 8852B firmware commits: + * VALID, net_type INFRA, the SMA/TMA hashes (XOR of the address bytes), + * `bssid_cam_idx`, SMA=own, TMA=bssid, `macid` plus the vendor's bit 14. + * Only the address CAM: the vendor path also writes the BSSID CAM entry, + * but host-writing it changed nothing measurable (the response rate, and + * the bring-ups that answer no ACKs, both stayed). Synchronous; callers + * verify with addr_cam_holds(). */ + void write_addr_cam_sta(uint8_t idx, const uint8_t own[6], + const uint8_t bssid[6], uint8_t macid, + uint8_t bssid_cam_idx) { + const auto w = addr_cam_sta_entry(own, bssid, reg::MAC_AX_NET_TYPE_INFRA, + bssid_cam_idx, macid); + const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; + /* VALID off first (a retarget rewrites a live entry), then the body, then + * VALID last, so the RMAC never matches a half-written entry. */ + write_mac_mem32(base, 0u); + for (int i = 7; i >= 1; --i) + write_mac_mem32(base + 4 * i, w[i]); + write_mac_mem32(base, w[0]); + } + /* 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 diff --git a/src/kestrel/KestrelAddrCam.h b/src/kestrel/KestrelAddrCam.h new file mode 100644 index 00000000..a9415fb9 --- /dev/null +++ b/src/kestrel/KestrelAddrCam.h @@ -0,0 +1,46 @@ +#pragma once + +/* The station address-CAM entry (rtw89 cam.h layout, SHORT length) as eight + * dwords. One encoder for both ways an entry reaches the hardware: the + * addr_cam H2C body (KestrelFw::fw_upd_addr_cam_sta, dwords 2..9 of its + * payload), which the 8852B firmware commits, and the host's own write into + * the physical CAM (HalKestrel::write_addr_cam_sta) on the 8852C, whose + * firmware ignores that H2C. Pure, so tests/kestrel_addr_cam_selftest.cpp + * pins it headless against an entry read back from a live 8852C. */ + +#include +#include + +#include "MacRegAx.h" + +namespace kestrel { + +/* VALID, `net_type`, no address mask, the SMA/TMA hashes (XOR of the six + * address bytes, the vendor's unmasked form), `bssid_cam_idx`, SMA = `sma`, + * TMA = `tma`, and `macid` plus the vendor's bit 14. */ +inline std::array addr_cam_sta_entry(const uint8_t sma[6], + const uint8_t tma[6], + uint8_t net_type, + uint8_t bssid_cam_idx, + uint8_t macid) { + uint32_t sh = 0, th = 0; + for (int i = 0; i < 6; ++i) { + sh ^= sma[i]; + th ^= tma[i]; + } + const auto b = [](const uint8_t *p, int i, int sh) { + return uint32_t{p[i]} << sh; + }; + return { + 0x1u | (uint32_t{net_type} & 0x3u) << 1 | + (uint32_t{reg::MAC_AX_NO_MSK} & 0x3u) << 14 | sh << 16 | th << 24, + uint32_t{bssid_cam_idx} & 0x3fu, + b(sma, 0, 0) | b(sma, 1, 8) | b(sma, 2, 16) | b(sma, 3, 24), + b(sma, 4, 0) | b(sma, 5, 8) | b(tma, 0, 16) | b(tma, 1, 24), + b(tma, 2, 0) | b(tma, 3, 8) | b(tma, 4, 16) | b(tma, 5, 24), + 0u, + uint32_t{macid} | (1u << 14), + 0u}; +} + +} // namespace kestrel diff --git a/src/kestrel/KestrelFw.cpp b/src/kestrel/KestrelFw.cpp index 1f3108a7..07a49cf5 100644 --- a/src/kestrel/KestrelFw.cpp +++ b/src/kestrel/KestrelFw.cpp @@ -1,4 +1,5 @@ #include "KestrelFw.h" +#include "KestrelAddrCam.h" #include #include @@ -502,30 +503,19 @@ 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]; - } + * vendor driver uses: SHORT length, the entry from addr_cam_sta_entry (the + * same words the host writes on the 8852C), and an embedded valid BSSID CAM + * entry carrying tma. */ 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)); + const auto w = kestrel::addr_cam_sta_entry(sma, tma, net_type, + bssid_cam_idx, macid); + for (size_t i = 0; i < w.size(); ++i) + put_le32(c + 8 + 4 * i, w[i]); 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) | diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index cab9eb20..fe0257b1 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -536,16 +536,9 @@ namespace { * 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}; -/* The responder's own station: a macid and ADDR_CAM entry apart from the - * injection context InitWrite programs at macid 0 / entry 0 (which the WD of - * every injected frame keeps pointing at), and from the low slots peer - * registrations take by convention (RegisterPeerSta callers use 1, 2, ...): - * the last entry of the 0x7f ADDR_CAM search range, reserved against - * RegisterPeerSta / RegisterBeamformee. BSSID CAM 1, apart from the injection - * context's index 0 (peers use 0 too, and only the addr entry selects the - * response). */ -constexpr uint8_t kAckMacid = 127; -constexpr uint8_t kAckAddrCamIdx = 127; +/* BSSID CAM entry of the responder's station: 1, apart from the injection + * context's index 0 (peers use 0 too, and only the address entry selects the + * response). The macid / ADDR_CAM slot is per die: ack_slot(). */ constexpr uint8_t kAckBssidCamIdx = 1; std::string mac_text(const uint8_t *m) { @@ -580,16 +573,24 @@ bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { 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, kAckMacid, kAckAddrCamIdx, - kAckBssidCamIdx); + /* The 8852B firmware commits the station entry from H2C; the 8852C + * firmware ignores the addr_cam H2C, so on that die the host writes it. */ + const bool host_cam = host_written_ack_cam(); + bool sent = true; + if (host_cam) + _hal.write_addr_cam_sta(ack_slot(), m, bssid, ack_slot(), + kAckBssidCamIdx); + else + sent = _hal.register_infra_sta(m, bssid, ack_slot(), ack_slot(), + kAckBssidCamIdx); _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. */ + /* Neither the H2Cs going out nor the host write says what the RMAC will + * match: read the physical CAM entry back. The firmware applies its 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(kAckAddrCamIdx, m)) && + while (sent && !(committed = _hal.addr_cam_holds(ack_slot(), m)) && std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(100)) { ++polls; std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -600,11 +601,15 @@ bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { "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"); + if (host_cam) + _logger->error("Kestrel: ACK responder for {} did not arm: the host-" + "written ADDR_CAM entry does not read back", mac_text(m)); + else + _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. */ @@ -617,8 +622,9 @@ bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { } _ack_responder = mac; _logger->info("Kestrel: hardware ACK responder armed for {} (macid {}, " - "ADDR_CAM entry {} committed, read back after {} us)", - mac_text(m), kAckMacid, kAckAddrCamIdx, waited_us); + "ADDR_CAM entry {} {}, read back after {} us)", + mac_text(m), ack_slot(), ack_slot(), + host_cam ? "written by the host" : "committed", waited_us); return true; } @@ -635,15 +641,18 @@ void RtlKestrelDevice::ClearAckResponder() { std::optional RtlKestrelDevice::disarm_ack_locked(std::span held) { namespace r = kestrel::reg; - /* Disconnect the responder's station in the firmware; the injection - * context at macid 0 is never touched. */ - _hal.unregister_infra_sta(held.front().bytes.data(), kAckMacid, - kAckAddrCamIdx, kAckBssidCamIdx); + /* Disconnect the responder's station in the firmware (the 8852C never + * registered one: its entry is host-written and its firmware does not touch + * the CAM); the injection context at macid 0 is never touched. */ + if (!host_written_ack_cam()) + _hal.unregister_infra_sta(held.front().bytes.data(), ack_slot(), + ack_slot(), kAckBssidCamIdx); _device.rtw_write32(r::R_AX_TRXPTCL_RESP_0, _ack_resp0_saved); /* The 8852B firmware keeps a disconnected station's entry VALID (measured: * neither join_info(disconnect) + a NO_LINK addr_cam, nor a role REMOVE, * nor role CREATE + NO_LINK clears it), so the host clears VALID itself, as - * the vendor's no-firmware mac_upd_addr_cam path writes the CAM. The H2Cs + * the vendor's no-firmware mac_upd_addr_cam path writes the CAM; on the + * 8852C that host clear is the whole disarm. The H2Cs * above land asynchronously (2-20 ms measured for the arm) and rewrite the * entry, so the clear repeats until the entry has read back empty for 50 ms * straight, well past that tail. The 150 ms bound only ends the wait while @@ -651,7 +660,7 @@ RtlKestrelDevice::disarm_ack_locked(std::span held) { * full 50 ms, so the worst case is about 200 ms. */ const auto still_held = [&]() -> const devourer::MacAddr * { for (const auto &a : held) - if (_hal.addr_cam_holds(kAckAddrCamIdx, a.bytes.data())) + if (_hal.addr_cam_holds(ack_slot(), a.bytes.data())) return &a; return nullptr; }; @@ -666,7 +675,7 @@ RtlKestrelDevice::disarm_ack_locked(std::span held) { bool stable = false; for (;;) { if (first || left) { - _hal.clear_addr_cam_entry(kAckAddrCamIdx); + _hal.clear_addr_cam_entry(ack_slot()); clean_since = clock::now(); first = false; } @@ -686,12 +695,12 @@ RtlKestrelDevice::disarm_ack_locked(std::span held) { if (!stable) { _logger->warn("Kestrel: ACK responder clear did not take: ADDR_CAM entry " "{} still holds {}, the MAC may keep answering for it " - "(ClearAckResponder retries)", kAckAddrCamIdx, + "(ClearAckResponder retries)", ack_slot(), mac_text(last_held->bytes.data())); return *last_held; } _logger->info("Kestrel: hardware ACK responder disarmed (ADDR_CAM entry {} " - "no longer holds {})", kAckAddrCamIdx, + "no longer holds {})", ack_slot(), mac_text(held.front().bytes.data())); return std::nullopt; } @@ -1089,11 +1098,11 @@ bool RtlKestrelDevice::ConfigureUlOfdma(const devourer::UlOfdmaConfig &cfg) { bool RtlKestrelDevice::peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const { - if (macid != kAckMacid && addr_cam_idx != kAckAddrCamIdx) + if (macid != ack_slot() && addr_cam_idx != ack_slot()) return false; _logger->error("Kestrel: {} refused: macid {} / ADDR_CAM entry {} overlaps " "the ACK responder's reserved macid {} / entry {}", - who, macid, addr_cam_idx, kAckMacid, kAckAddrCamIdx); + who, macid, addr_cam_idx, ack_slot(), ack_slot()); return true; } @@ -1147,17 +1156,18 @@ 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 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). */ + /* Hardware ARQ: SetAckResponder arms on both dies, verified by CAM read-back + * -- the 8852B through a firmware-committed INFRA station entry + * (HalKestrel::register_infra_sta), the 8852C through a host-written one + * (HalKestrel::write_addr_cam_sta), its firmware ignoring the H2C. True on + * the 8852C means the arm works and was measured, not that every bring-up + * answers: some answer no ACK (see SetAckResponder). 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.ack_responder_ok = true; 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 95ae505d..0e716a9a 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -91,19 +91,24 @@ class RtlKestrelDevice : public IRtlRadio { * 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, on its own macid 127 / ADDR_CAM entry 127 so - * the injection context at macid 0 / entry 0 (what every injected frame's + * `mac` as its own address, on its own macid / ADDR_CAM entry (ack_slot()) + * so the injection context at macid 0 / entry 0 (what every injected frame's * WD points at) is left as bring-up programmed it, and RegisterPeerSta / * RegisterBeamformee refuse that slot so a peer cannot take it; 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 disconnects that station and clears - * the entry's VALID bit from the host (the 8852B firmware leaves a - * disconnected station's entry valid), and succeeds only once the entry - * has read back empty for 50 ms; an address still held, or not confirmed - * gone, 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. */ + * entry back -- false when the entry did not commit. The 8852C firmware + * ignores the addr_cam H2C, so on that die the host writes the same entry + * itself (HalKestrel::write_addr_cam_sta, the vendor's no-firmware path) and + * no H2C is sent. Post-bring-up only. Clear disconnects that station (8852B + * only) and clears the entry's VALID bit from the host (the 8852B firmware + * leaves a disconnected station's entry valid), and succeeds only once the + * entry has read back empty for 50 ms; 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. On the 8852C a true return is not a guarantee: some bring-ups + * read the entry back and answer no ACK for the whole session, more often + * the longer the adapter has run (docs/kestrel-ack-responder.md); nothing on + * the host was found to detect it. */ bool SetAckResponder(const devourer::MacAddr &mac) override; void ClearAckResponder() override; /* rxdemo's live-disarm measurement hook (DEVOURER_ACK_DISARM_AFTER_MS): the @@ -316,6 +321,20 @@ class RtlKestrelDevice : public IRtlRadio { * timeout, nullopt once gone. Caller holds _ack_mutex. */ std::optional disarm_ack_locked(std::span held); + /* The ACK responder's own macid and ADDR_CAM entry (the same number), past + * the injection context at macid 0 / entry 0 and the low slots peer + * registrations take by convention (RegisterPeerSta callers use 1, 2, ...). + * 127 on the 8852B, where the firmware commits and the RMAC answers there + * (measured); 63 on the 8852C, its last entry: the CAM reads back + * 0xdeadbeef from entry 64 up. */ + uint8_t ack_slot() const { + return _variant == kestrel::ChipVariant::C8852C ? 63 : 127; + } + /* The 8852C firmware ignores the addr_cam H2C, so the ACK responder's + * station entry is written (and cleared) by the host on that die. */ + bool host_written_ack_cam() const { + return _variant == kestrel::ChipVariant::C8852C; + } /* True (and logged) when a peer registration would take the ACK * responder's reserved macid or ADDR_CAM entry. */ bool peer_slot_reserved(const char *who, uint8_t macid, diff --git a/tests/kestrel_addr_cam_selftest.cpp b/tests/kestrel_addr_cam_selftest.cpp new file mode 100644 index 00000000..58ca9206 --- /dev/null +++ b/tests/kestrel_addr_cam_selftest.cpp @@ -0,0 +1,54 @@ +// Headless guard for kestrel::addr_cam_sta_entry, the station address-CAM +// encoder behind both the 8852B's addr_cam H2C and the 8852C's host write. +// The expected words are read-backs from live hardware, not hand-derived: +// what the 8852B firmware committed for its own H2C, and what an 8852C held +// after the host write. A layout change that breaks either fails here. +#include +#include + +#include "kestrel/KestrelAddrCam.h" + +namespace { + +int failures = 0; + +void expect(const char *what, uint32_t got, uint32_t want) { + if (got != want) { + std::printf("FAIL %s: got %08x want %08x\n", what, got, want); + ++failures; + } +} + +} // namespace + +int main() { + const uint8_t bssid[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; + const uint8_t infra = kestrel::reg::MAC_AX_NET_TYPE_INFRA; + + // 8852B, firmware-committed entry for register_infra_sta (macid 1, BSSID + // CAM 1), physical words 0..3 read through the indirect window. + { + const uint8_t own[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55}; + const auto w = kestrel::addr_cam_sta_entry(own, bssid, infra, 1, 1); + expect("8852B w0", w[0], 0x03130005u); + expect("8852B w1", w[1], 0x00000001u); + expect("8852B w2", w[2], 0x33221102u); + expect("8852B w3", w[3], 0x00025544u); + } + + // 8852C, host-written entry at macid 63 (BSSID CAM 1), words 0..4 read back. + { + const uint8_t own[6] = {0x02, 0x12, 0x34, 0x56, 0x78, 0x9a}; + const auto w = kestrel::addr_cam_sta_entry(own, bssid, infra, 1, 63); + expect("8852C w0", w[0], 0x03900005u); + expect("8852C w1", w[1], 0x00000001u); + expect("8852C w2", w[2], 0x56341202u); + expect("8852C w3", w[3], 0x00029a78u); + expect("8852C w4", w[4], 0x01000000u); + expect("8852C w6", w[6], 63u | (1u << 14)); + } + + if (failures == 0) + std::printf("kestrel_addr_cam: ok\n"); + return failures == 0 ? 0 : 1; +} From e40d7b1c78fe550e2855df84053bf9521b0de31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Thu, 15 Oct 2026 09:22:10 -0300 Subject: [PATCH 2/3] kestrel: review fixes for the 8852C ACK responder - ack_responder_ok is false on the 8852C: a session that answers no ACK reads its entry back like a good one, so the flag could not tell a caller the loop closes. SetAckResponder still arms it; true on the 8852B as before. - The host write of the ADDR_CAM entry reports a failed register transfer and stops before setting VALID again, and the 8852C arm is confirmed by reading the whole entry back (addr_cam_sta_holds), not only VALID + SMA. Dword 5 reads 0xdeadbeef on the 8852C whatever is written (measured), so it is skipped. Re-measured: 3 sessions armed, 99.1 / 98.6 / 98.4% ACKed. - RegisterPeerSta / RegisterBeamformee refuse an ADDR_CAM index past the die's CAM (64 entries on the 8852C, 128 on the 8852B) instead of sending an entry that never lands. --- docs/kestrel-ack-responder.md | 10 ++++--- src/AdapterCaps.h | 10 +++++-- src/kestrel/HalKestrel.h | 47 ++++++++++++++++++++++++-------- src/kestrel/RtlKestrelDevice.cpp | 44 ++++++++++++++++++++---------- src/kestrel/RtlKestrelDevice.h | 15 ++++++++-- 5 files changed, 89 insertions(+), 37 deletions(-) diff --git a/docs/kestrel-ack-responder.md b/docs/kestrel-ack-responder.md index 2732a44c..efeb2bd6 100644 --- a/docs/kestrel-ack-responder.md +++ b/docs/kestrel-ack-responder.md @@ -121,7 +121,9 @@ recovered or failed partway. Measured in failed sessions: slot. What decides it is not found. The 8852B shows the same shape far more rarely -(about one bring-up in 55). A caller that needs the ACK on the 8852C should -treat a soliciting peer's retries as the tell and bring the adapter up again; -when that keeps failing, letting the adapter cool down is what brought it back -here. +(about one bring-up in 55). Because a dead session reads back like a good one, +`GetAdapterCaps` reports `ack_responder_ok` false on the 8852C (true on the +8852B), while `SetAckResponder` still arms it. A caller that needs the ACK on +the 8852C should treat a soliciting peer's retries as the tell and bring the +adapter up again; when that keeps failing, letting the adapter cool down is +what brought it back here. diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 571fa210..373d66d1 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -207,7 +207,11 @@ struct AdapterCaps { * old address, 0/6062 never armed, 0/5965 after a live disarm; some * bring-ups answer no ACK at all, more often the longer the adapter has * run: 3 of the 12 armed sessions in that matrix, 0 of the 12 run before it - * straight after a long power-off). + * straight after a long power-off). The 8852C is measured but reports + * FALSE: a dead session reads its entry back like a good one, so the flag + * could not tell a caller the loop closes; SetAckResponder still arms it, + * and a caller that wants it there judges by the soliciting peer's + * retries. * * The `on`, `retarget`, and legacy `off` rows establish arming, retargeting, * and a never-armed control. A backend-owned `disarmed` row supports only @@ -232,8 +236,8 @@ struct AdapterCaps { * generation — the AP-mode work proved the gate where it was measured. * False-as-unmeasured (the * vht_2g4_ok reading: unmeasured, not incapable): the 8821C — it shares - * the recipe but no 8821CU/CE cell has run. FALSE on Kestrel: - * SetAckResponder is not implemented on the AX generation. + * the recipe but no 8821CU/CE cell has run. FALSE on the Kestrel 8852C + * (measured, see above). * tx_retry_limit_ok: DEVOURER_TX_RETRY_LIMIT drives hardware autonomous * retransmission (measured 12/0/12 A/B: 8821AU, 8812BU, 8822CU; the 8733B * by airtime dose-response instead, 0/3/12 -> 1.00/4.00/12.32–12.33 airings per diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index c6db5920..39cf4d83 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -312,14 +312,15 @@ class HalKestrel { } /* Write one dword of the MAC's internal table SRAM (the write half of - * read_mac_mem32). */ - void write_mac_mem32(uint32_t internal_addr, uint32_t value) { - _device.rtw_write32(reg::R_AX_FILTER_MODEL_ADDR, internal_addr); - _device.rtw_write32_wide(reg::R_AX_INDIR_ACCESS_ENTRY, value); + * read_mac_mem32). False when either register transfer failed. */ + bool write_mac_mem32(uint32_t internal_addr, uint32_t value) { + return _device.rtw_write32(reg::R_AX_FILTER_MODEL_ADDR, internal_addr) && + _device.rtw_write32_wide(reg::R_AX_INDIR_ACCESS_ENTRY, value); } /* Clear VALID on address-CAM entry `idx`. */ - void clear_addr_cam_entry(uint8_t idx) { - write_mac_mem32(reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE, 0u); + bool clear_addr_cam_entry(uint8_t idx) { + return write_mac_mem32( + reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE, 0u); } /* Write the station entry register_infra_sta asks the firmware for straight @@ -330,9 +331,11 @@ class HalKestrel { * `bssid_cam_idx`, SMA=own, TMA=bssid, `macid` plus the vendor's bit 14. * Only the address CAM: the vendor path also writes the BSSID CAM entry, * but host-writing it changed nothing measurable (the response rate, and - * the bring-ups that answer no ACKs, both stayed). Synchronous; callers - * verify with addr_cam_holds(). */ - void write_addr_cam_sta(uint8_t idx, const uint8_t own[6], + * the bring-ups that answer no ACKs, both stayed). Synchronous. False on + * the first failed transfer, VALID then not set again (a failure after the + * VALID-off write leaves the entry invalid, not half-written and live); + * callers verify with addr_cam_sta_holds(). */ + bool write_addr_cam_sta(uint8_t idx, const uint8_t own[6], const uint8_t bssid[6], uint8_t macid, uint8_t bssid_cam_idx) { const auto w = addr_cam_sta_entry(own, bssid, reg::MAC_AX_NET_TYPE_INFRA, @@ -340,10 +343,30 @@ class HalKestrel { const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; /* VALID off first (a retarget rewrites a live entry), then the body, then * VALID last, so the RMAC never matches a half-written entry. */ - write_mac_mem32(base, 0u); + if (!write_mac_mem32(base, 0u)) + return false; for (int i = 7; i >= 1; --i) - write_mac_mem32(base + 4 * i, w[i]); - write_mac_mem32(base, w[0]); + if (!write_mac_mem32(base + 4 * i, w[i])) + return false; + return write_mac_mem32(base, w[0]); + } + + /* Whether the physical address-CAM entry `idx` reads back as exactly the + * station entry write_addr_cam_sta writes for these arguments -- the check + * for the host-written path, where nothing but the read says the whole + * entry landed (addr_cam_holds only checks VALID + SMA). Every dword but + * dword 5, which the 8852C does not back: it reads 0xdeadbeef whatever was + * written (measured; the other seven read back as written). */ + bool addr_cam_sta_holds(uint8_t idx, const uint8_t own[6], + const uint8_t bssid[6], uint8_t macid, + uint8_t bssid_cam_idx) { + const auto w = addr_cam_sta_entry(own, bssid, reg::MAC_AX_NET_TYPE_INFRA, + bssid_cam_idx, macid); + const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; + for (int i = 0; i < 8; ++i) + if (i != 5 && read_mac_mem32(base + 4 * i) != w[i]) + return false; + return true; } /* One dword of the MAC's internal table SRAM (rtw89 mac_mem_read): the diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index fe0257b1..7f3d3c4d 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -576,21 +576,26 @@ bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { /* The 8852B firmware commits the station entry from H2C; the 8852C * firmware ignores the addr_cam H2C, so on that die the host writes it. */ const bool host_cam = host_written_ack_cam(); - bool sent = true; - if (host_cam) - _hal.write_addr_cam_sta(ack_slot(), m, bssid, ack_slot(), - kAckBssidCamIdx); - else - sent = _hal.register_infra_sta(m, bssid, ack_slot(), ack_slot(), - kAckBssidCamIdx); + const bool sent = + host_cam ? _hal.write_addr_cam_sta(ack_slot(), m, bssid, ack_slot(), + kAckBssidCamIdx) + : _hal.register_infra_sta(m, bssid, ack_slot(), ack_slot(), + kAckBssidCamIdx); _device.rtw_write32(r::R_AX_TRXPTCL_RESP_0, resp0 & ~r::B_AX_RSP_CHK_ALL); /* Neither the H2Cs going out nor the host write says what the RMAC will * match: read the physical CAM entry back. The firmware applies its queue - * asynchronously, so poll for a bounded time rather than read once. */ + * asynchronously, so poll for a bounded time rather than read once. The + * host-written entry is compared whole (the host knows every dword it + * wrote); the firmware-committed one by VALID + address. */ + const auto holds = [&] { + return host_cam ? _hal.addr_cam_sta_holds(ack_slot(), m, bssid, + ack_slot(), kAckBssidCamIdx) + : _hal.addr_cam_holds(ack_slot(), m); + }; const auto t0 = std::chrono::steady_clock::now(); bool committed = false; int polls = 0; - while (sent && !(committed = _hal.addr_cam_holds(ack_slot(), m)) && + while (sent && !(committed = holds()) && std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(100)) { ++polls; std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -602,8 +607,9 @@ bool RtlKestrelDevice::SetAckResponder(const devourer::MacAddr &mac) { waited_us, polls); if (!sent || !committed) { if (host_cam) - _logger->error("Kestrel: ACK responder for {} did not arm: the host-" - "written ADDR_CAM entry does not read back", mac_text(m)); + _logger->error("Kestrel: ACK responder for {} did not arm: the host " + "write of the ADDR_CAM entry {}", mac_text(m), + sent ? "does not read back" : "failed"); else _logger->error("Kestrel: ACK responder for {} did not arm: the firmware " "{} the ADDR_CAM entry (H2C {})", @@ -1098,6 +1104,12 @@ bool RtlKestrelDevice::ConfigureUlOfdma(const devourer::UlOfdmaConfig &cfg) { bool RtlKestrelDevice::peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const { + if (addr_cam_idx >= addr_cam_entries()) { + _logger->error("Kestrel: {} refused: ADDR_CAM entry {} is past this die's " + "last entry ({})", who, addr_cam_idx, + addr_cam_entries() - 1); + return true; + } if (macid != ack_slot() && addr_cam_idx != ack_slot()) return false; _logger->error("Kestrel: {} refused: macid {} / ADDR_CAM entry {} overlaps " @@ -1159,15 +1171,17 @@ devourer::AdapterCaps RtlKestrelDevice::GetAdapterCaps() { /* Hardware ARQ: SetAckResponder arms on both dies, verified by CAM read-back * -- the 8852B through a firmware-committed INFRA station entry * (HalKestrel::register_infra_sta), the 8852C through a host-written one - * (HalKestrel::write_addr_cam_sta), its firmware ignoring the H2C. True on - * the 8852C means the arm works and was measured, not that every bring-up - * answers: some answer no ACK (see SetAckResponder). The retry knob IS wired + * (HalKestrel::write_addr_cam_sta), its firmware ignoring the H2C. The flag + * is the 8852B's only: on the 8852C some bring-ups arm, read the entry back + * and answer no ACK for the whole session, and nothing on the host tells + * those apart (SetAckResponder), so a caller cannot rely on the loop + * closing there. SetAckResponder still arms the 8852C. 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 = true; + c.ack_responder_ok = _variant != kestrel::ChipVariant::C8852C; 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 0e716a9a..2428c764 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -108,7 +108,8 @@ class RtlKestrelDevice : public IRtlRadio { * serialized. On the 8852C a true return is not a guarantee: some bring-ups * read the entry back and answer no ACK for the whole session, more often * the longer the adapter has run (docs/kestrel-ack-responder.md); nothing on - * the host was found to detect it. */ + * the host was found to detect it, so GetAdapterCaps reports + * ack_responder_ok false on that die. */ bool SetAckResponder(const devourer::MacAddr &mac) override; void ClearAckResponder() override; /* rxdemo's live-disarm measurement hook (DEVOURER_ACK_DISARM_AFTER_MS): the @@ -222,7 +223,9 @@ class RtlKestrelDevice : public IRtlRadio { /* Register an associated peer STA (macid/addr-cam) so a Trigger's per-user * grant scores against it — the AP-side companion to SendTrigger for the - * end-to-end UL path. */ + * end-to-end UL path. Refuses the ACK responder's slot (ack_slot()) and an + * `addr_cam_idx` past the die's CAM (64 entries on the 8852C), as does + * RegisterBeamformee. */ bool RegisterPeerSta(const uint8_t peer_mac[6], uint8_t macid, uint8_t addr_cam_idx) override; @@ -330,13 +333,19 @@ class RtlKestrelDevice : public IRtlRadio { uint8_t ack_slot() const { return _variant == kestrel::ChipVariant::C8852C ? 63 : 127; } + /* Address-CAM entries that hold what is written to them: 64 on the 8852C + * (0xdeadbeef from entry 64 up), 128 on the 8852B. */ + unsigned addr_cam_entries() const { + return _variant == kestrel::ChipVariant::C8852C ? 64u : 128u; + } /* The 8852C firmware ignores the addr_cam H2C, so the ACK responder's * station entry is written (and cleared) by the host on that die. */ bool host_written_ack_cam() const { return _variant == kestrel::ChipVariant::C8852C; } /* True (and logged) when a peer registration would take the ACK - * responder's reserved macid or ADDR_CAM entry. */ + * responder's reserved macid or ADDR_CAM entry, or an ADDR_CAM entry past + * the die's last one (addr_cam_entries()). */ bool peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const; std::optional _ack_disarm_after_ms; From 45720c9b76cc6ff0103553b296144ba8a7e96453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Gonz=C3=A1lez?= Date: Thu, 15 Oct 2026 10:23:51 -0300 Subject: [PATCH 3/3] kestrel: per-die address-CAM stride, 128 entries on the 8852C The 8852C lays its address CAM out at the SHORT entry (0x20), not the 8852B's LONG one (0x40): the vendor's get_addr_cam_size returns LONG only for the 8852A/8852B/8851B/8852BT, and mac_upd_addr_cam writes entry N at N * len. HalKestrel used 0x40 on both, so the 8852C "entry 63" was hardware entry 126 and "0xdeadbeef from entry 64 up" was the end of a 128-entry CAM. addr_cam_entry_addr() now picks the stride per die for the host write, the read-backs and the clear; the 8852B is unchanged. - Measured on the 8852C: the entry reads back at the 0x20 offset (dword 5 still reads 0xdeadbeef and stays skipped), the 0x40 offset holds power-up garbage, and the responder answers. - 128 entries on both dies; the peer-registration bound refuses 128..255. - ack_responder_ok == C8852B, so a future variant does not inherit true. - Live disarm re-measured with the solicitor running across the disarm in one session: 8852C 3812/3837 then 0/1616, 3846/3848 then 0/1623; 8852B 3842/3848 then 0/1773. - Dead 8852C bring-ups at hardware entry 63: 6 of 15, interleaved with good sessions. The doc no longer reads them as thermal/uptime and states both benches' observations; the slot is not ruled out. --- docs/kestrel-ack-responder.md | 52 ++++++++++++++++++++------------ src/AdapterCaps.h | 21 ++++++++----- src/kestrel/HalKestrel.h | 20 +++++++++--- src/kestrel/MacRegAx.h | 3 +- src/kestrel/RtlKestrelDevice.cpp | 9 +++--- src/kestrel/RtlKestrelDevice.h | 20 +++++------- 6 files changed, 75 insertions(+), 50 deletions(-) diff --git a/docs/kestrel-ack-responder.md b/docs/kestrel-ack-responder.md index efeb2bd6..0b559ffc 100644 --- a/docs/kestrel-ack-responder.md +++ b/docs/kestrel-ack-responder.md @@ -27,9 +27,14 @@ was measured to get there. dropped to 82.4% single-shot; three with it stayed at >= 99.6%. On a quiet ch161 it made no difference. - **Own slot.** The slot itself is documented at `ack_slot()`. Measured: the - 8852B firmware commits entry 127 in the same ~2.2 ms as entry 1 or 0; the - 8852C's CAM reads back 0xdeadbeef from entry 64 up, which is why its slot is - 63. With the station at entry 1 during development, entry 0 read back + 8852B firmware commits entry 127 in the same ~2.2 ms as entry 1 or 0. The + CAM has 128 entries on both dies, laid out at a per-die stride (the vendor's + get_addr_cam_size): 0x40 on the 8852B, 0x20 on the 8852C. An earlier + revision used the 8852B's stride on both, so its 8852C "entry 63" was + hardware entry 126 and the 0xdeadbeef it read "from entry 64 up" was the + end of the 128-entry CAM. Dword 5 of an 8852C entry reads 0xdeadbeef + whatever is written (the other seven read back as written), so the host's + read-back skips it. With the station at entry 1 during development, entry 0 read back byte-identical before the arm, after it and after the clear. Sharing entry 0 (the previous layout) had no measurable cost: a witness counted the same >= 2900 of 3000 injected frames armed and unarmed, on both layouts. @@ -82,23 +87,30 @@ read-backs). Measured on its own: them cleared. - **The lookup hits the entry.** The long RX descriptor (dword 5: ADDR_CAM_VLD, addr_cam index, macid) shows every frame addressed to the - responder matching the host-written entry (macid 63; the index reads 126, - the entry counted in 32-byte units). -- **Clear** reads back empty and the soliciting peer then gets no ACK (the - live-disarm row in `AdapterCaps.h`). + responder matching the host-written entry (macid 63). That was measured on + the earlier layout, where the index read 126: the entry the 0x40-stride + write had landed in, counted at the 8852C's 0x20 stride. +- **Clear** reads back empty and the soliciting peer then gets no ACK. On + this die a 0 after a disarm only counts when the same session answered + before it (a dead bring-up also reads 0), so the live-disarm row in + `AdapterCaps.h` solicits across the disarm. ## 8852C: bring-ups that answer no ACKs Some 8852C bring-ups answer no ACK at all: the responder arms, the entry reads back, and the adapter answers nothing for the whole session, where a good -session answers >= 97.5%. How often tracks how long the adapter has been -running. Over a day of back-to-back sessions it rose from about one in ten to -three in four (10 of 83 sessions overall); after the adapter was unplugged for -a long while, the first 12 sessions in a row answered 97.5-99.4%, and 3 of the -next 12 armed sessions (the `AdapterCaps.h` matrix) came up dead. The RF -thermal meter read delta 2 rising to 4, too coarse to say more. So it looks -thermal or uptime-related, though temperature was not varied on its own. It is -a property of the bring-up, not of the arm: in one failed session three +session answers >= 97.5%. How often varies and no cause is established. On +the first bench it looked tied to uptime: over a day of back-to-back sessions +it rose from about one in ten to three in four (10 of 83 overall), and after a +long power-off the first 12 sessions answered 97.5-99.4% before 3 of the next +12 came up dead. A second bench (two TX50UH, an 8812CU soliciting) does not +support that reading: dead sessions came interleaved with good ones +(dead/good/dead/good/dead in one series), one came on the second session after +a 3 s VBUS cut, a VBUS cold cycle before every session still left 1 of 13 +dead, and the RF thermal meter sat at delta 2-3 throughout. Back on the first +bench, with the entry at hardware entry 63, 6 of 15 sessions were dead, in +runs (3 dead, 3 good, 2 dead, 4 good). It is a property of the bring-up, not +of the arm: in one failed session three separate injector processes in a row all saw 0, and no session has ever recovered or failed partway. Measured in failed sessions: @@ -117,13 +129,15 @@ recovered or failed partway. Measured in failed sessions: - Ruled out by measurement: the response NAV checks (`R_AX_RSP_CHK_SIG` bits 19-21 cleared, still fails), the BSSID CAM (all zero in good and failed sessions alike; writing entry 1 valid, still fails), the injector, the - host's per-frame RX cost, the arm's timing relative to the RX loop, and the - slot. + host's per-frame RX cost, and the arm's timing relative to the RX loop. + The slot is not ruled out: development tested hardware entry 126 only, and + on a second bench entry 126 gave more dead sessions than entry 63 (8 of 21 + against 1 of 16), but entry 63 gave 6 of 15 on the first, so the slot does + not explain it. What decides it is not found. The 8852B shows the same shape far more rarely (about one bring-up in 55). Because a dead session reads back like a good one, `GetAdapterCaps` reports `ack_responder_ok` false on the 8852C (true on the 8852B), while `SetAckResponder` still arms it. A caller that needs the ACK on the 8852C should treat a soliciting peer's retries as the tell and bring the -adapter up again; when that keeps failing, letting the adapter cool down is -what brought it back here. +adapter up again. diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 373d66d1..93f88424 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -197,17 +197,22 @@ struct AdapterCaps { * ch161, 3 runs per row, responder on its own macid 127 / ADDR_CAM entry * 127: 14414/14422 armed at Init, 14376/14417 armed at InitWrite, * 14419/14432 retargeted, 0/6597 at the old address after the retarget, - * 0/6575 never armed, 0/6561 after a live disarm; one cell in about 55 + * 0/6575 never armed, 0/6561 after a live disarm, re-run with the + * solicitor across the disarm: 3842/3848 then 0/1773; one cell in about 55 * across the development runs, at entry 1, read the entry back committed * and answered no ACKs, not reproduced since), 8852C (the same bench with * the roles swapped, the 8852B soliciting; the entry is host-written at - * macid / ADDR_CAM entry 63, docs/kestrel-ack-responder.md; 3 runs per row: - * 13831/14041 armed at Init, 14239/14408 retargeted, armed at InitWrite - * 98.9% and 97.0% in two runs with the third a dead session, 0/5993 at the - * old address, 0/6062 never armed, 0/5965 after a live disarm; some - * bring-ups answer no ACK at all, more often the longer the adapter has - * run: 3 of the 12 armed sessions in that matrix, 0 of the 12 run before it - * straight after a long power-off). The 8852C is measured but reports + * macid / ADDR_CAM entry 63, docs/kestrel-ack-responder.md. 3 runs per row + * with the entry at hardware entry 126, an earlier layout that used the + * 8852B's CAM stride: 13831/14041 armed at Init, 14239/14408 retargeted, + * armed at InitWrite 98.9% and 97.0% in two runs with the third a dead + * session, 0/5993 at the old address, 0/6062 never armed. At hardware + * entry 63, the solicitor running across a live disarm in one session: + * 3812/3837 then 0/1616, 3846/3848 then 0/1623 (a third cell was a dead + * session and says nothing); 15 armed sessions back to back, 9 answering + * 97.5-99.9% and 6 dead. Some bring-ups answer no ACK at all, at any + * entry tried: 3 of 12 and 6 of 15 here, 8 of 21 and 1 of 16 on a second + * bench, interleaved with good sessions). The 8852C is measured but reports * FALSE: a dead session reads its entry back like a good one, so the flag * could not tell a caller the loop closes; SetAckResponder still arms it, * and a caller that wants it there judges by the soliciting peer's diff --git a/src/kestrel/HalKestrel.h b/src/kestrel/HalKestrel.h index 39cf4d83..a5cd8fd2 100644 --- a/src/kestrel/HalKestrel.h +++ b/src/kestrel/HalKestrel.h @@ -311,6 +311,17 @@ class HalKestrel { return ok; } + /* Internal address of address-CAM entry `idx`. The stride is per die, as + * the vendor's get_addr_cam_size + mac_upd_addr_cam lay it out (entry N at + * N * len): the LONG entry (0x40) on the 8852B, the SHORT one (0x20) on the + * 8852C, 128 entries on both (ADDRCAM_NUM). */ + uint32_t addr_cam_entry_addr(uint8_t idx) const { + const uint32_t stride = _variant == ChipVariant::C8852C + ? reg::ADDR_CAM_ENT_SHORT_SIZE + : reg::ADDR_CAM_ENT_LONG_SIZE; + return reg::ADDR_CAM_MEM_BASE + idx * stride; + } + /* Write one dword of the MAC's internal table SRAM (the write half of * read_mac_mem32). False when either register transfer failed. */ bool write_mac_mem32(uint32_t internal_addr, uint32_t value) { @@ -319,8 +330,7 @@ class HalKestrel { } /* Clear VALID on address-CAM entry `idx`. */ bool clear_addr_cam_entry(uint8_t idx) { - return write_mac_mem32( - reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE, 0u); + return write_mac_mem32(addr_cam_entry_addr(idx), 0u); } /* Write the station entry register_infra_sta asks the firmware for straight @@ -340,7 +350,7 @@ class HalKestrel { uint8_t bssid_cam_idx) { const auto w = addr_cam_sta_entry(own, bssid, reg::MAC_AX_NET_TYPE_INFRA, bssid_cam_idx, macid); - const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; + const uint32_t base = addr_cam_entry_addr(idx); /* VALID off first (a retarget rewrites a live entry), then the body, then * VALID last, so the RMAC never matches a half-written entry. */ if (!write_mac_mem32(base, 0u)) @@ -362,7 +372,7 @@ class HalKestrel { uint8_t bssid_cam_idx) { const auto w = addr_cam_sta_entry(own, bssid, reg::MAC_AX_NET_TYPE_INFRA, bssid_cam_idx, macid); - const uint32_t base = reg::ADDR_CAM_MEM_BASE + idx * reg::ADDR_CAM_ENT_LONG_SIZE; + const uint32_t base = addr_cam_entry_addr(idx); for (int i = 0; i < 8; ++i) if (i != 5 && read_mac_mem32(base + 4 * i) != w[i]) return false; @@ -383,7 +393,7 @@ class HalKestrel { * 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; + const uint32_t base = addr_cam_entry_addr(idx); if (!(read_mac_mem32(base) & 0x1u)) return false; const uint32_t w2 = read_mac_mem32(base + 8); diff --git a/src/kestrel/MacRegAx.h b/src/kestrel/MacRegAx.h index 79b4aea9..8d374be0 100644 --- a/src/kestrel/MacRegAx.h +++ b/src/kestrel/MacRegAx.h @@ -683,7 +683,8 @@ constexpr uint8_t FWCMD_H2C_CL_FR_EXCHG = 0x5; constexpr uint8_t FWCMD_H2C_FUNC_CCTLINFO_UD = 0x2; constexpr uint8_t FWCMD_H2C_FUNC_DCTLINFO_UD = 0x1; -/* addr_cam entry geometry (addr_cam.h): 8852B uses the LONG entry (0x40). */ +/* addr_cam entry geometry (addr_cam.h get_addr_cam_size): the 8852B uses the + * LONG entry (0x40) as its CAM stride, the 8852C the SHORT one (0x20). */ 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; diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 7f3d3c4d..59d39eba 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -1104,10 +1104,9 @@ bool RtlKestrelDevice::ConfigureUlOfdma(const devourer::UlOfdmaConfig &cfg) { bool RtlKestrelDevice::peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const { - if (addr_cam_idx >= addr_cam_entries()) { - _logger->error("Kestrel: {} refused: ADDR_CAM entry {} is past this die's " - "last entry ({})", who, addr_cam_idx, - addr_cam_entries() - 1); + if (addr_cam_idx >= kAddrCamEntries) { + _logger->error("Kestrel: {} refused: ADDR_CAM entry {} is past the CAM's " + "last entry ({})", who, addr_cam_idx, kAddrCamEntries - 1); return true; } if (macid != ack_slot() && addr_cam_idx != ack_slot()) @@ -1181,7 +1180,7 @@ devourer::AdapterCaps RtlKestrelDevice::GetAdapterCaps() { * 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::C8852C; + 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 2428c764..5eacc702 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -106,8 +106,8 @@ class RtlKestrelDevice : public IRtlRadio { * clear (or a failed retarget, which checks the old address too) stays * recorded, so a later ClearAckResponder retries it. Arm and clear are * serialized. On the 8852C a true return is not a guarantee: some bring-ups - * read the entry back and answer no ACK for the whole session, more often - * the longer the adapter has run (docs/kestrel-ack-responder.md); nothing on + * read the entry back and answer no ACK for the whole session + * (docs/kestrel-ack-responder.md); nothing on * the host was found to detect it, so GetAdapterCaps reports * ack_responder_ok false on that die. */ bool SetAckResponder(const devourer::MacAddr &mac) override; @@ -224,8 +224,7 @@ class RtlKestrelDevice : public IRtlRadio { /* Register an associated peer STA (macid/addr-cam) so a Trigger's per-user * grant scores against it — the AP-side companion to SendTrigger for the * end-to-end UL path. Refuses the ACK responder's slot (ack_slot()) and an - * `addr_cam_idx` past the die's CAM (64 entries on the 8852C), as does - * RegisterBeamformee. */ + * `addr_cam_idx` past the 128-entry CAM, as does RegisterBeamformee. */ bool RegisterPeerSta(const uint8_t peer_mac[6], uint8_t macid, uint8_t addr_cam_idx) override; @@ -328,16 +327,13 @@ class RtlKestrelDevice : public IRtlRadio { * the injection context at macid 0 / entry 0 and the low slots peer * registrations take by convention (RegisterPeerSta callers use 1, 2, ...). * 127 on the 8852B, where the firmware commits and the RMAC answers there - * (measured); 63 on the 8852C, its last entry: the CAM reads back - * 0xdeadbeef from entry 64 up. */ + * (measured); 63 on the 8852C, where the host writes it and the RMAC + * answers there (measured). Both dies have 128 entries. */ uint8_t ack_slot() const { return _variant == kestrel::ChipVariant::C8852C ? 63 : 127; } - /* Address-CAM entries that hold what is written to them: 64 on the 8852C - * (0xdeadbeef from entry 64 up), 128 on the 8852B. */ - unsigned addr_cam_entries() const { - return _variant == kestrel::ChipVariant::C8852C ? 64u : 128u; - } + /* Address-CAM entries (vendor ADDRCAM_NUM): 128 on both dies. */ + static constexpr unsigned kAddrCamEntries = 128; /* The 8852C firmware ignores the addr_cam H2C, so the ACK responder's * station entry is written (and cleared) by the host on that die. */ bool host_written_ack_cam() const { @@ -345,7 +341,7 @@ class RtlKestrelDevice : public IRtlRadio { } /* True (and logged) when a peer registration would take the ACK * responder's reserved macid or ADDR_CAM entry, or an ADDR_CAM entry past - * the die's last one (addr_cam_entries()). */ + * the CAM (kAddrCamEntries). */ bool peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const; std::optional _ack_disarm_after_ms;