Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
120 changes: 103 additions & 17 deletions docs/kestrel-ack-responder.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,32 +26,118 @@ 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
VALID from the host takes, and re-read for 2 s it stays clear. Because those
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.
2 changes: 1 addition & 1 deletion examples/rx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
27 changes: 21 additions & 6 deletions src/AdapterCaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
77 changes: 68 additions & 9 deletions src/kestrel/HalKestrel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
Loading
Loading