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..0b559ffc 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,23 @@ 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 + 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. - **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 +50,94 @@ 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). 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 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: + +- 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, 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. 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..93f88424 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -197,11 +197,26 @@ 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). 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 + * 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 + * retries. * * The `on`, `retarget`, and legacy `off` rows establish arming, retargeting, * and a never-armed control. A backend-owned `disarmed` row supports only @@ -226,8 +241,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 d02cd0f5..a5cd8fd2 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, @@ -309,15 +311,72 @@ 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). */ - 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(addr_cam_entry_addr(idx), 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. 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, + bssid_cam_idx, macid); + 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)) + return false; + for (int i = 7; i >= 1; --i) + 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 = 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; + return true; } /* One dword of the MAC's internal table SRAM (rtw89 mac_mem_read): the @@ -334,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/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/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 cab9eb20..59d39eba 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,29 @@ 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(); + 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); - /* 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. 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(kAckAddrCamIdx, 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)); @@ -600,11 +606,16 @@ 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 " + "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 {})", + 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 +628,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 +647,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 +666,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 +681,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 +701,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 +1104,16 @@ 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 (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()) 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,15 +1167,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. 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 = _variant == kestrel::ChipVariant::C8852B; c.bw_mask = devourer::bw_mask_for_generation(c.generation); diff --git a/src/kestrel/RtlKestrelDevice.h b/src/kestrel/RtlKestrelDevice.h index 95ae505d..5eacc702 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -91,19 +91,25 @@ 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 + * (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; void ClearAckResponder() override; /* rxdemo's live-disarm measurement hook (DEVOURER_ACK_DISARM_AFTER_MS): the @@ -217,7 +223,8 @@ 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 128-entry CAM, as does RegisterBeamformee. */ bool RegisterPeerSta(const uint8_t peer_mac[6], uint8_t macid, uint8_t addr_cam_idx) override; @@ -316,8 +323,25 @@ 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, 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 (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 { + 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 CAM (kAddrCamEntries). */ bool peer_slot_reserved(const char *who, uint8_t macid, uint8_t addr_cam_idx) const; std::optional _ack_disarm_after_ms; 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; +}