Skip to content

kestrel: hardware ACK responder on the 8852B - #487

Merged
josephnef merged 3 commits into
OpenIPC:masterfrom
agustinglez:kestrel-ack-responder
Oct 14, 2026
Merged

josephnef merged 3 commits into
OpenIPC:masterfrom
agustinglez:kestrel-ack-responder

Conversation

@agustinglez

Copy link
Copy Markdown
Contributor

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, 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.

## 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.
@openipc-ai

Copy link
Copy Markdown

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Failed disarms cannot be retried ✓ Resolved
Description
ClearAckResponder() resets _ack_responder before submitting the NO_LINK rewrite or verifying
that the physical CAM changed. If either firmware command fails or the rewrite does not commit, the
method warns that the MAC may keep answering, but every subsequent clear returns immediately.
Code

src/kestrel/RtlKestrelDevice.cpp[608]

+  _ack_responder.reset();
Evidence
The reset precedes both best-effort firmware calls and the CAM poll; the failure branch explicitly
acknowledges that the MAC may still answer, while the entry guard makes a later retry a no-op.

src/kestrel/RtlKestrelDevice.cpp[603-623]
src/kestrel/HalKestrel.h[240-259]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed CAM rewrite leaves the responder potentially active, but clearing its saved address prevents another disarm attempt.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[603-627]
## Recommended Fix
Keep the armed address until CAM read-back confirms removal. On failure, preserve retryable state and report the unsuccessful rollback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. ACK docs duplicate the API contract ✓ Resolved
Description
docs/kestrel-ack-responder.md repeats the responder's ADDR_CAM commitment and arming sequence
already documented in RtlKestrelDevice.h. Readers must update both the standalone guide and the
header when this contract changes, allowing the two descriptions to drift.
Code

docs/kestrel-ack-responder.md[R9-12]

+The AX MAC has no Jaguar-style port identity register to retarget: the RMAC
+answers for an address only when the firmware has COMMITTED a valid
+ADDR_CAM entry carrying it as SMA. `add_self_sta`'s NO_LINK entry is accepted
+over H2C and never committed (the physical entry, read through
Evidence
The checklist requires documentation to point to authoritative header doc-comments instead of
copying their contracts. The new guide describes the same committed ADDR_CAM requirement and
association-based arming sequence that the new RtlKestrelDevice header comment documents.

CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims: CLAUDE.md: Avoid Duplicated Header Contracts and One-Sided Measurement Claims
docs/kestrel-ack-responder.md[9-21]
src/kestrel/RtlKestrelDevice.h[87-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Kestrel ACK responder guide duplicates the mechanism and commitment contract already documented in the public device header, creating two maintenance points that can diverge.
## Fix Focus Areas
- docs/kestrel-ack-responder.md[9-21]
- src/kestrel/RtlKestrelDevice.h[87-96]
## Recommended Fix
Remove the duplicated API-contract explanation from the guide and point readers to the authoritative header doc-comment. Keep only measured results, setup details, limitations, and other information that is not already part of the API contract.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A failed retarget can hide the old arm ✓ Resolved
Description
SetAckResponder() replaces _ack_responder with the requested MAC before invoking rollback, so
ClearAckResponder() checks only whether that new MAC is absent. If retargeting fails while the
previous CAM entry remains committed and the rollback rewrite also fails, the call returns false and
reports a successful clear without checking the address the hardware can still answer.
Code

src/kestrel/RtlKestrelDevice.cpp[R592-593]

+    _ack_responder = mac;
+    ClearAckResponder();
Evidence
The failure branch stores the new address, and clear copies that stored address for its sole CAM
comparison. Firmware submission and physical commitment are separate, so a failed retarget does not
establish that the old entry was removed.

src/kestrel/RtlKestrelDevice.cpp[573-600]
src/kestrel/RtlKestrelDevice.cpp[603-626]
src/kestrel/HalKestrel.h[269-290]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed retarget overwrites the previously armed address before rollback, so verification can miss an old CAM entry.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[586-594]
- src/kestrel/RtlKestrelDevice.cpp[603-627]
## Recommended Fix
Preserve the prior armed address through the retarget attempt. On failure, verify the physical CAM against both the old and requested addresses, and retain retryable state if rollback is not confirmed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Timed disarm can overwrite a new arm ✓ Resolved
Description
SetAckResponder() and ClearAckResponder() have no serialization around _ack_responder, the
saved response register, or the CAM rewrite. If a caller retargets the public responder while the
new timer thread clears it, their state changes and asynchronous firmware commands can interleave,
leaving the final arm different from the one either call verified.
Code

src/kestrel/RtlKestrelDevice.cpp[R255-257]

+      _logger->info("DEVOURER_ACK_DISARM_AFTER_MS: disarming Kestrel ACK "
+                    "responder {} ms after completed bring-up", delay_ms);
+      ClearAckResponder();
Evidence
The timer directly calls clear on another thread. Both methods update the same optional and register
and issue commands for the same CAM entry without a lock; clear resets its optional before its
asynchronous rewrite finishes.

src/kestrel/RtlKestrelDevice.cpp[247-258]
src/kestrel/RtlKestrelDevice.cpp[565-579]
src/kestrel/RtlKestrelDevice.cpp[603-619]
src/kestrel/RtlKestrelDevice.h[97-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The timer invokes clear on a separate thread while public arm calls can update the same state and CAM.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[545-627]
- src/kestrel/RtlKestrelDevice.h[297-302]
## Recommended Fix
Protect the entire arm and clear transactions, including CAM read-back and saved-register updates, with a device-level responder mutex. Ensure the timer follows the same locking path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
5. Arming changes injected TX context 🐞 Bug ≡ Correctness
Description
SetAckResponder() programs the responder address into CAM index 0 and MACID 0, replacing the
self-station context installed by InitWrite() for injected frames. When a TX-capable session arms
the responder, send_packet() still builds its management and data descriptors with MACID 0, so
injection no longer uses the self-station CAM context that bring-up established for it.
Code

src/kestrel/RtlKestrelDevice.cpp[568]

+  const bool sent = _hal.register_infra_sta(m, bssid);
Evidence
Bring-up sets CAM index 0 and MACID 0 for injected frames; the new arm overwrites those same
identifiers, while the transmit path continues to select MACID 0. The clear path restores the
injection context, confirming the shared ownership while armed.

src/kestrel/RtlKestrelDevice.cpp[327-336]
src/kestrel/HalKestrel.h[281-290]
src/kestrel/RtlKestrelDevice.cpp[568-568]
src/kestrel/RtlKestrelDevice.cpp[1383-1392]
src/kestrel/RtlKestrelDevice.cpp[609-613]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The responder reuses the CAM index and MACID that injected frames continue to reference.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[331-336]
- src/kestrel/RtlKestrelDevice.cpp[568-568]
- src/kestrel/HalKestrel.h[281-290]
- src/kestrel/RtlKestrelDevice.cpp[1383-1392]
## Recommended Fix
Allocate a distinct firmware station/CAM context for the responder where supported, leaving injection's MACID 0 context intact. If the hardware requires sharing, explicitly coordinate TX descriptors and document or enforce the resulting limitation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Destroying the device can race a pending test disarm ✓ Resolved
Description
~RtlKestrelDevice() runs quiesce_tx, stop_wp_drain, and pcie_quiesce before
_ack_disarm_thread is cancelled and joined by member destruction. If the device is destroyed
without Stop() as a DEVOURER_ACK_DISARM_AFTER_MS delay expires, the callback can call
ClearAckResponder() and send H2Cs or read CAM registers while transport teardown is underway,
including while PCIe ring indices are cleared.
Code

src/kestrel/RtlKestrelDevice.h[302]

+  std::jthread _ack_disarm_thread;
Evidence
The destructor body tears down the transport without touching _ack_disarm_thread; as a
std::jthread member, it is joined only after that body completes unless Stop() resets it first.
The thread callback calls ClearAckResponder(), whose register I/O and H2Cs use _hal and
_device, so it can access the transport during teardown.

src/kestrel/RtlKestrelDevice.cpp[95-103]
src/kestrel/RtlKestrelDevice.cpp[244-259]
src/kestrel/RtlKestrelDevice.h[69-72]
src/kestrel/RtlKestrelDevice.cpp[95-105]
src/kestrel/RtlKestrelDevice.cpp[247-258]
src/kestrel/RtlKestrelDevice.cpp[603-619]
src/kestrel/RtlKestrelDevice.h[68-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ACK-disarm callback can use the device while its destructor is quiescing the transport because the thread is not joined until member destruction.
## Fix Focus Areas
- src/kestrel/RtlKestrelDevice.cpp[95-105]
- src/kestrel/RtlKestrelDevice.cpp[247-258]
## Recommended Fix
Add `_ack_disarm_thread = {};` as the first statement of `~RtlKestrelDevice()` to request stop and join the thread before quiescing TX or PCIe. Keep the existing early join in `Stop()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/kestrel-ack-responder.md Outdated
Comment thread src/kestrel/RtlKestrelDevice.cpp Outdated
Comment thread src/kestrel/RtlKestrelDevice.cpp Outdated
Comment thread src/kestrel/RtlKestrelDevice.cpp
Comment thread src/kestrel/RtlKestrelDevice.cpp
Comment thread src/kestrel/RtlKestrelDevice.h
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 josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_responder is the Jaguar1/Jaguar3 convention, so consistent — but on the 8852C it is the guaranteed outcome of setting the env var, and in rxdemo it surfaces as terminate + 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 error line could carry waited_us/polls the way the success info line does; the debug line that has them is compiled out in Release (DEVOURER_LOG_MAX_LEVEL NDEBUG-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.

@josephnef
josephnef enabled auto-merge (squash) October 14, 2026 11:27
@josephnef
josephnef merged commit 036a860 into OpenIPC:master Oct 14, 2026
26 checks passed
josephnef pushed a commit that referenced this pull request Oct 14, 2026
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.
gilankpam added a commit to gilankpam/devourer that referenced this pull request Oct 14, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants