Repository navigation
kestrel: hardware ACK responder on the 8852B - #487
Conversation
## The problem `SetAckResponder` was not implemented on the AX generation, and `ack_responder_ok` read false with the note "matrix-measured 0% closure". The Jaguar recipe has nothing to port: the AX MAC has no port identity register to retarget. Its RMAC answers for an address only when the firmware has committed a valid ADDR_CAM entry carrying it as SMA. Reading the physical CAM back (MAC table SRAM at 0x18850000, through `R_AX_FILTER_MODEL_ADDR` / `R_AX_INDIR_ACCESS_ENTRY`) shows why it was 0%: the entry `add_self_sta` sends for the NO_LINK self role is accepted over H2C and never committed. It reads valid=0. ## The change - **What commits the entry.** A USBPcap capture of the vendor Windows driver associating the adapter shows the sequence a real association uses. Registering the responder address as an INFRA station that way makes the firmware commit it. Two commands, in the vendor's order: `join_info(INFRA, connect)`, then the station form of the addr_cam (SMA = responder address, TMA = a BSSID, the SHORT entry, BSSID CAM at index 1). New: `KestrelFw::fw_join_info`, `KestrelFw::fw_upd_addr_cam_sta`, `HalKestrel::register_infra_sta`. - **Only what was measured to matter is sent.** The vendor sends role CLIENT, a macid unpause, and the CMAC and DMAC tables around those two. Each was dropped on the 8852B with no change in the commit or the ACK rate, and so was setting REC_ACK/DONE_ACK in the H2C header. Without `join_info` the entry never commits. - **`SetAckResponder` verifies by reading the CAM back** (`addr_cam_holds`) and returns false when the entry did not commit. The commit is asynchronous: typically read back about 2.2 ms after the H2Cs, once 19.5 ms, and a one-shot read refused healthy arms during development. So it polls for up to 100 ms. - **It clears the response-abort CCA gates** (`R_AX_TRXPTCL_RESP_0` bits 23-28, new `B_AX_RSP_CHK_*`). On ch149, shared with a neighbour AP, one of three runs without the clear fell to 82.4% single-shot, while three with it stayed at >= 99.6%. On a quiet ch161 it made no difference. - **It refuses a group address or a call before bring-up.** `DEVOURER_ACK_RESPONDER` arms at the end of `Init` and of `InitWrite` and throws if refused, as on Jaguar3. - **`ClearAckResponder` puts macid 0 back to the bring-up NO_LINK entry** and restores the gates, then confirms the CAM no longer holds the address. rxdemo's `DEVOURER_ACK_DISARM_AFTER_MS` now accepts Kestrel through a `ScheduleAckResponderDisarmForTest` hook like the RTL8733B's; `Stop()` cancels a pending one. - **`ack_responder_ok` is true on the 8852B only.** The 8852C firmware accepts the same two commands and leaves the entry invalid, so the arm refuses there. The vendor's 8852C association sends much more before the addr_cam, and which of it the C needs is open. The change is additive. Nothing that ran before this calls the new code unless `SetAckResponder` or `DEVOURER_ACK_RESPONDER` is used. ## Measured There is no CCX `tx.report` on Kestrel, so the soliciting TX is an RTL8832CU (TP-Link TX50UH) counting the ACK frames addressed to its own TA, one attempt per frame. The responder is an RTL8852BU (TP-Link TX20U Nano) running rxdemo, or txdemo for the `InitWrite` row. Setup: ch161, MCS1, 20 MHz, ~480 frames/s for 10 s, both dongles on one desk with the injector at -15 dB, Windows 11. Three runs per row: | row | ACKed / sent | |---|---| | armed at `Init` (rxdemo) | 14230 / 14430 (98.6%) | | armed at `InitWrite` (txdemo) | 14391 / 14431 (99.7%) | | retargeted to 02:12:34:56:78:9a | 14347 / 14425 (99.5%) | | same arm, TX aimed at the adapter's own MAC | 0 / 6440 | | never armed | 0 / 6194 | | armed, then live disarm before TX | 0 / 6603 | The 8852C refuses the arm with "the firmware accepted but did not commit the ADDR_CAM entry". Full build and `ctest` pass on MSVC (78 tests, 4 skipped as on master). What this does not show: - **Near field only.** These numbers are a near-field bench, not a link budget. - **Host-counted ACKs, not the TX's own ARQ verdict.** A separate run with the soliciting TMAC retrying and its ACK window narrowed found the responder's timing indistinguishable from a commercial AP's (both pass a 24 us window and fail at 20 us). - **The responder's own transmissions are outside this matrix.** With this die's TX carrier-sense default (gates cleared), a responder that also sends unicast loses some ACKs to its own queued frames, and the response scheduler abort counter (0xCC18[23:16]) moves. That is noted in `docs/kestrel-ack-responder.md` and is not addressed here.
|
/review |
Code Review by Qodo
1.
|
Review follow-ups on the 8852B ACK responder: - ClearAckResponder forgot the armed address before the NO_LINK rewrite was verified, so a clear that did not take could never be retried. The address is now dropped only once the CAM read-back confirms it is gone. - A failed retarget (A armed, arming B fails) recorded B and rolled back checking only B, while A could still be committed. The rollback now verifies both and keeps whichever is still held on record. - SetAckResponder and ClearAckResponder share a mutex: the DEVOURER_ACK_DISARM_AFTER_MS test disarm runs on its own thread. - The destructor joins that thread first, before quiescing the transport, for callers that destroy the device without Stop(). - The header documents that the responder shares macid 0 / ADDR_CAM entry 0 with injection, and docs/kestrel-ack-responder.md keeps the evidence and measurements but points at the header for the contract instead of restating it. TX20U Nano (8852B), rxdemo on ch149, DEVOURER_ACK_DISARM_AFTER_MS=2000: armed (read back after 2.2 ms), disarmed from the timer thread with the read-back confirming the entry no longer holds the address, no errors. The retry and retarget-rollback branches need a CAM write that fails to take and were not reproduced on hardware; the destructor ordering was not exercised (rxdemo exits through Stop()).
josephnef
left a comment
There was a problem hiding this comment.
Approving, with one hardware caveat stated up front: there is no RTL8852BU on this bench today (two TX50UH / RTL8832CU only), so I could not reproduce the positive ACK rows. What I could do:
8852C refusal path (the die the PR says must refuse) — rxdemo on a TX50UH, ch6, DEVOURER_ACK_RESPONDER=02:12:34:56:78:9a, PR build 808d384:
Kestrel: join_info (macid=0 op=0 net_type=2 self=0 wifi=1 port=0 band=0) -> sent
Kestrel: upd_addr_cam_sta (... sma=02:12:34:56:78:9a tma=02:00:00:00:00:01) -> sent
[E] Kestrel: ACK responder for 02:12:34:56:78:9a did not arm: the firmware accepted but did not commit the ADDR_CAM entry (H2C sent)
Kestrel: fw_role_maintain ... / upd_addr_cam (net_type=0 sma=57:42:75:05:d6:00) / upd_cctl ...
Kestrel: hardware ACK responder disarmed (ADDR_CAM entry 0 no longer holds 02:12:34:56:78:9a)
terminate called after throwing an instance of 'std::runtime_error'
what(): Kestrel: configured ACK responder could not be armed
Exactly the sequence described: H2Cs sent, 100 ms CAM poll never sees valid, NO_LINK rollback verified by read-back, then the configured-arm throw. No wedge; the adapter came straight back up on the next run.
No regression on the untouched path — same adapter, ch6, 12 s, master 74c6b37 vs this branch, two reps each: 5 / 5 / 5 / 5 rx.pkt (quiet 2.4 GHz lab), and the two Kestrel coex: LTE interface not ready errors are present on master too.
Build/CI — Linux Release build + ctest 82/82; full CI matrix green on 808d384.
Code — the second commit covers the qodo threads I would have raised (retryable clear, retarget rollback checking both addresses, mutex, destructor join order). Two small observations, neither blocking:
- The throw from
arm_configured_ack_responderis the Jaguar1/Jaguar3 convention, so consistent — but on the 8852C it is the guaranteed outcome of setting the env var, and inrxdemoit surfaces asterminate+ core dump rather than a logged exit. If that is ever softened it should be softened for all three at once, not here. - The failure
errorline could carrywaited_us/pollsthe way the successinfoline does; thedebugline that has them is compiled out in Release (DEVOURER_LOG_MAX_LEVELNDEBUG-derived), so a Release user on the C die sees no poll duration at all.
The 8852B rows stand on the author's measurement; when a TX20U Nano is back on this bench I'll run tests/ack_txreport_matrix.sh with the 8812CU as the CCX-reporting TX against it.
Follow-up to #487, review point 5 ("Arming changes injected TX context"). ## The problem The 8852B ACK responder registered its station at macid 0 / ADDR_CAM entry 0, which is the context `InitWrite` programs for injection and that every injected frame's WD points at. Arming rewrote that context and clearing re-ran the bring-up registration over it. In practice this had no measurable cost: a witness counted the same share of injected frames with the responder armed and unarmed. So this PR is about keeping the injection context intact, not about fixing delivery. ## The change - The responder's station now lives at macid 127 / ADDR_CAM entry 127 (BSSID CAM 1), the last entry of the 0x7f ADDR_CAM search range. That keeps it apart from the injection context at macid 0 / entry 0 and from the low slots peer registrations take by convention (`tests/ul_trigger_ap.cpp` and `kestrelprobe trig` register their peer at macid 1 / entry 1). `RegisterPeerSta` and `RegisterBeamformee` refuse slot 127 (logged), so a peer and the responder cannot replace each other, and clearing the responder cannot invalidate a peer's entry. - Clearing changed with it. The 8852B firmware leaves a disconnected station's entry VALID: `join_info(disconnect)` plus a NO_LINK addr_cam, a role REMOVE, and role CREATE plus NO_LINK all left the entry valid with the responder's address. The old layout never showed this, because the bring-up re-registration rewrote entry 0 wholesale. Clear now disconnects the station in the firmware and clears VALID from the host, the way the vendor's no-firmware `mac_upd_addr_cam` path writes the CAM. It repeats the clear until the entry has read back empty for 25 ms, and only that counts as disarmed: on a timeout the clear is reported as not taken and the last address the entry held stays recorded for a retry. - `HalKestrel`: `register_infra_sta` takes the macid and CAM indices; new `unregister_infra_sta`, `write_mac_mem32` and `clear_addr_cam_entry`. - The header doc-comment, `docs/kestrel-ack-responder.md` and the `AdapterCaps.h` evidence are updated. The 8852C path is unchanged: its firmware still does not commit the entry, and the arm still refuses. ## Measured TX20U Nano (RTL8852BU) as responder, TX50UH (RTL8832CU) injecting and counting the ACKs it receives, ch161, MCS1, one attempt per frame, 10 s per cell, 3 runs per row, on the final version (slot 127): | row | ACKs | per run | |---|---|---| | armed at Init (rxdemo) | 14414/14422 | 100.0 / 99.9 / 99.9 | | armed at InitWrite (txdemo) | 14376/14417 | 99.7 / 99.7 / 99.7 | | retargeted (non-EFUSE MAC) | 14419/14432 | 100.0 / 99.8 / 99.9 | | old address after the retarget | 0/6597 | 0 / 0 / 0 | | never armed | 0/6575 | 0 / 0 / 0 | | after a live disarm | 0/6561 | 0 / 0 / 0 | | responder transmitting (txdemo armed, carrier sense off) | 14308/14437 | 99.0 / 99.0 / 99.3 | Arm A, retarget to B, clear, re-arm A in one session, 3 runs: A 99.9-100% armed, then A 0% and B 99.8-99.9% after the retarget, both 0% after the clear, and A 99.9-100% (B 0%) after the re-arm. Injection delivery, TX20U txdemo injecting 3000 frames to a TX50UH witness, armed and unarmed: at least 2900/3000 in every cell on this branch and on master. Full MSVC build and `ctest` pass (78 tests, 4 skipped as on master). What this does not show: - One armed cell in about 55 across development (station at entry 1) read the entry back committed and answered no ACKs. It has not reproduced since; master's layout had 0 such cells in about 40. I don't have an explanation for it yet. - One unarmed delivery cell heard none of the 3179 frames txdemo sent at the witness. No arm runs in that cell, so this change's code is not on its path; three re-runs read at least 2900/3000. - In the earlier run of the matrix at entry 1, the responder-transmitting row read 80.6% once, with the injector's own USB sends stalling (up to 1 s per send, 7 failures). - One bench and one pair of dongles, near field.
Brings in OpenIPC#459-OpenIPC#487: station client series (SetStationIdentity, src/sta, station_mode_ok), RTL8852CE PCIe on the Kestrel HAL, stream timing (addr1/addr3 per-frame bytes, CCX join), svctx --live, duplex bring-up order, mt7612u no-TX-timeout, Kestrel us-delays on Windows and the 8852B hardware ACK responder. Conflicts were all upstream OpenIPC#484, the squash-merge of this fork's 8822C HT40 work, against the fork's earlier iteration of the same series (the cu-ht40-tx merge). Resolved to upstream's final reviewed form: the wide-TX IGI floor applies on both dies and is DeviceConfig::Tuning:: wide_tx_igi_floor (default 0x35); the fork's fixed DIG_MIN_8822C_WIDE 0x26 constant is removed (git auto-kept it as a fork-only addition, dead after resolution). Keeps fork-only work: NHM busy window (_nhm_busy_ready resets), scout, batched control transfers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The problem
SetAckResponderwas not implemented on the AX generation, andack_responder_okread false with the note "matrix-measured 0% closure". The Jaguar recipe has nothing to port: the AX MAC has no port identity register to retarget. Its RMAC answers for an address only when the firmware has committed a valid ADDR_CAM entry carrying it as SMA.Reading the physical CAM back (MAC table SRAM at 0x18850000, through
R_AX_FILTER_MODEL_ADDR/R_AX_INDIR_ACCESS_ENTRY) shows why it was 0%: the entryadd_self_stasends for the NO_LINK self role is accepted over H2C and never committed. It reads valid=0.The change
join_info(INFRA, connect), then the station form of the addr_cam (SMA = responder address, TMA = a BSSID, the SHORT entry, BSSID CAM at index 1). New:KestrelFw::fw_join_info,KestrelFw::fw_upd_addr_cam_sta,HalKestrel::register_infra_sta.join_infothe entry never commits.SetAckResponderverifies by reading the CAM back (addr_cam_holds) and returns false when the entry did not commit. The commit is asynchronous: typically read back about 2.2 ms after the H2Cs, once 19.5 ms, and a one-shot read refused healthy arms during development. So it polls for up to 100 ms.R_AX_TRXPTCL_RESP_0bits 23-28, newB_AX_RSP_CHK_*). On ch149, shared with a neighbour AP, one of three runs without the clear fell to 82.4% single-shot, while three with it stayed at >= 99.6%. On a quiet ch161 it made no difference.DEVOURER_ACK_RESPONDERarms at the end ofInitand ofInitWriteand throws if refused, as on Jaguar3.ClearAckResponderputs macid 0 back to the bring-up NO_LINK entry and restores the gates, then confirms the CAM no longer holds the address. rxdemo'sDEVOURER_ACK_DISARM_AFTER_MSnow accepts Kestrel through aScheduleAckResponderDisarmForTesthook like the RTL8733B's;Stop()cancels a pending one.ack_responder_okis true on the 8852B only. The 8852C firmware accepts the same two commands and leaves the entry invalid, so the arm refuses there. The vendor's 8852C association sends much more before the addr_cam, and which of it the C needs is open.The change is additive. Nothing that ran before this calls the new code unless
SetAckResponderorDEVOURER_ACK_RESPONDERis used.Measured
There is no CCX
tx.reporton Kestrel, so the soliciting TX is an RTL8832CU (TP-Link TX50UH) counting the ACK frames addressed to its own TA, one attempt per frame. The responder is an RTL8852BU (TP-Link TX20U Nano) running rxdemo, or txdemo for theInitWriterow. Setup: ch161, MCS1, 20 MHz, ~480 frames/s for 10 s, both dongles on one desk with the injector at -15 dB, Windows 11. Three runs per row:Init(rxdemo)The 8852C refuses the arm with "the firmware accepted but did not commit the ADDR_CAM entry". Full build and
ctestpass on MSVC (78 tests, 4 skipped as on master).What this does not show:
docs/kestrel-ack-responder.mdand is not addressed here.