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
142 changes: 96 additions & 46 deletions docs/kestrel-ack-responder.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,49 +95,99 @@ read-backs). Measured on its own:
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.
## 8852C: bring-ups that answered no ACKs

**In short.** Some 8852C bring-ups armed, read the entry back, and answered no
ACK for the whole session. The chip did build the ACK, but every frame also
carries a selection of which transmit paths to use, and the selection the MAC
attached to the ACK was garbage in those bring-ups: it enabled a path this
2T2R die does not have, or mapped path A off the signal, so no antenna sent the
ACK. That selection comes from MAC state nothing initialises, which is why it
changed from one bring-up to the next. The fix makes the baseband take the
path selection from its own registers (P-MAC) instead of from the MAC.

**How it was found.** The TX-info debug port (`R_AX_WMAC_TX_INFO_DEBUG_SEL`
0xCAE4 select 0, read at 0xCAE8 with `R_AX_TCR1` bit 26 set) returns the TX
info the MAC hands the baseband, in the baseband's `INTF_TXINFO_*` layout
(`halbb_cr_info_8852c.h`, 0x1800): PATH_EN is bits [15:12], PATH_MAP_A..D bits
[23:16]. It reads 0 until the first response and then holds one value for the
session:

| TX-info word | paths enabled | path A mapped to | sessions without the fix |
|---|---|---|---|
| 0x10e41012 | A | 0 | all answered |
| 0x10??f012, map A = 3 | A, B, C, D | 3 | all answered |
| 0x10??f012 / 0x10??b012 / 0x10??7012, map A = 1 | several | 1 | 10 of 10 dead |
| 0x10914012 (second bench) | C only | 1 | dead |

The selection is not taken from the per-macid CMAC table (searched for in all
128 entries, every session), the MAC TX power tables (a good session's tables
written before every arm still gave dead sessions), the firmware version (the
Windows driver's 0.29.123.4 gave dead sessions too), the slot, temperature, or
the 5 GHz EDCCA level (#491). Where the MAC takes it from is still not found.
Comment thread
agustinglez marked this conversation as resolved.
The two experiments #490 proposed were run after the cause was found:

- **Firmware RAM is not it.** The firmware's data memory reads back through
the indirect window at the CPU's own addresses (0x20100000 up: the code
section reads back byte-identical to the image). Its writable parts,
0x20161000-0x20180000, 0x2020c000-0x20278000 and 0x20300000-0x20308000
(about 590 KB), were dumped 8 s after the arm in 26 sessions (2026-10-15,
21:18-21:49, UTC-3) and grouped by the TX-info word (that build had the
fix, so every session answered): 15 sessions with 0x10e41012 against 11
with the 0x10??f012 family. No bit is constant within each group and
different between them (the same test with shuffled labels also finds
none), and the 160 words the firmware keeps with only 2-4 values across
sessions take each value in sessions of both groups. Only one session had
the map-A-1 word, and it shares the common values. The path selection is
not something the firmware's memory records.
- **The vendor driver does not show it.** The same TX50UH on the Realtek
Windows driver (5001.19.128.1, firmware 0.29.123.4), associated as a
station to an AP on ch161, was restarted 45 times (`pnputil
/restart-device`, a full driver bring-up each time; 2026-10-16
00:15-00:33) and solicited for 8 s after each reconnect by the TX20U Nano
(MCS1, one attempt per frame): 0 dead of 45, every one 88.6-98.3%
(associated, it also carries the AP's traffic). Without the fix, devourer
on the same unit had 4 dead of 30 earlier that day. So the vendor
bring-up initialises the response's path selection where devourer's does
not; which write does it is still not found.

The attribution rests on the TX-info word predicting every outcome seen
(the map-A-1 selection dead 10 of 10 times, every other value alive) and on
the fix removing the dead sessions, including those with that selection.

**The fix.** Right after the T-MAC path routing at bring-up, the halbb glue
calls `halbb_ctrl_tx_path_pmac_8852c(RF_PATH_AB)`: 0x09a4 bits [4:2] select
P-MAC as the source of the path enable and the path A/B maps, set to paths A+B
with A on IFFT 0 and B on IFFT 1, plus the vendor's per-path TSSI reset pulse.
It applies to every PPDU, not only responses.

**Measured.** This bench: TX50UH responder, TX20U Nano soliciting, ch161,
MCS1, 8 s per session, all on 2026-10-15 (local time, UTC-3), since the dead
rate drifts through a day (#490):

| when | build | dead sessions |
|---|---|---|
| 10:12-10:19 | without the fix | 6 of 15 |
| 12:51-14:45 | without the fix | 3 of 21, 3 of 15, 2 of 25, 1 of 31, 1 of 12 |
| 14:47-16:04 | with the fix (register-level, then the PoC) | 0 of 93 |
| 16:15-16:31 | without the fix (master with #491) | 4 of 30 |
| 17:27-17:43 | with the fix (this commit) | 0 of 30 |

With the fix, 15 sessions had the map-A-1 selection; every session answered
96.8-100% and the selection registers held the P-MAC values at the end of every
session. The 4-of-30 series without the fix ran between series with it, so the
difference is not the time of day. An earlier day's matrix had 3 of 12 dead
(time not recorded). Second bench (two TX50UH, an 8812CU soliciting): up to
about 85% dead in the afternoon, then 0 dead of 20 on ch6 and ch161, both
units, an hour later with the fix. Injection from the 8852C, compared with the
same build without the change: unchanged for one stream (MCS1 and 6M
~1900/2000 to the 8852B on ch6 and on ch161 with carrier sense on). That is a
before/after check of this change only, not an absolute 5 GHz TX figure; the
vendor-driver injection A/B that #490 asks for before trusting 5 GHz Kestrel TX
numbers was not run. Two-stream injection from the 8852C does not decode with or without
the fix (MCS12, VHT2SS_MCS1: 0/2000, while the 8852B's are received by the
8852C), so what the fix does to two-stream TX is not measured.

The 8852B showed the same shape far more rarely (about one bring-up in 55, not
reproduced since); it routes TX paths per station through its CMAC antenna
model and is not changed.
13 changes: 12 additions & 1 deletion hal/halbb/g6/kestrel_halbb_glue.c
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,19 @@ void kestrel_halbb_ctrl_tx_path(struct kestrel_halbb_ctx *ctx) {
/* 8852C-only: the 8852B routes TX antennas per-STA via the CMAC antenna
* model (TXWD/cctl), there is no path-com block to program. */
#ifdef BB_8852C_SUPPORT
if (ctx && ctx->bb.ic_type == BB_RTL8852C)
if (ctx && ctx->bb.ic_type == BB_RTL8852C) {
halbb_ctrl_tx_path_tmac_8852c(&ctx->bb, RF_PATH_A, false);
/* Then take the TX path enable and the path A/B maps from the BB's P-MAC
* registers (0x09a4[4:2]) instead of the TX info the MAC sends with each
* PPDU: paths A+B, A on IFFT 0, B on IFFT 1, with the vendor's per-path
* TSSI reset pulse. The path selection the MAC attaches to a hardware
* response (ACK) comes from state nothing initialises on this die: in
* some bring-ups it enables only a path the die does not have, or maps
* path A off the signal, and the response is composed but never radiated
* (docs/kestrel-ack-responder.md). The switch applies to every PPDU;
* injection was measured unchanged for 1SS only. */
halbb_ctrl_tx_path_pmac_8852c(&ctx->bb, RF_PATH_AB, false);
}
#else
(void)ctx;
#endif
Expand Down
6 changes: 4 additions & 2 deletions hal/halbb/g6/kestrel_halbb_glue.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ void kestrel_halbb_ctrl_bw_ch(struct kestrel_halbb_ctx *ctx, unsigned char pri_c
/* T-MAC TX path-com routing (halbb_ctrl_tx_path_tmac_8852c): the 0xD800..
* 0xD820 path-com block (per-cut, via the MAC pwr-reg plane) + the 0x9a4 path
* enable + both-path TSSI reset. Connects the BB IFFT output to the TX chain.
* 8852C-only — the 8852B selects its TX antenna per-STA via the CMAC antenna
* model, so this is a no-op there. Single-PHY, RF_PATH_A. */
* Then the path enable / path A-B maps from P-MAC (halbb_ctrl_tx_path_pmac_
* 8852c, RF_PATH_AB), so a response's path selection does not come from the
* MAC (see the definition). 8852C-only — the 8852B selects its TX antenna
* per-STA via the CMAC antenna model, so this is a no-op there. Single-PHY. */
void kestrel_halbb_ctrl_tx_path(struct kestrel_halbb_ctx *ctx);
/* 6 GHz EDCCA threshold after a 6 GHz tune (the vendor's unlinked
* EDCCA_NORMAL_MODE level — see the definition); bw is the halbb
Expand Down
22 changes: 12 additions & 10 deletions src/AdapterCaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,17 @@ struct AdapterCaps {
* 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.
* session and says nothing). Some bring-ups used to answer no ACK at all:
* the TX path selection the MAC attached to the ACK was uninitialised. The
* dead rate drifts through a day, so with times (2026-10-15, UTC-3):
* without the fix 6 of 15 at 10:12-10:19, 10 of 104 at 12:51-14:45 and
* 4 of 30 at 16:15-16:31 (plus 3 of 12 on an earlier day, time not
* recorded; up to ~85% on a second bench in the afternoon); with the path selection taken from the BB's
* P-MAC registers (docs/kestrel-ack-responder.md) 0 dead of 93 at
* 14:47-16:04 and 0 of 30 at 17:27-17:43, every session 96.8-100%, and 0
* of 20 on the second bench an hour after its ~85% series; solicitor
* across a live disarm 3791/3845 then 0/1602 and 3829/3843 then 0/1639,
* never armed 0/1591, at 17:44-17:46).
*
* The `on`, `retarget`, and legacy `off` rows establish arming, retargeting,
* and a never-armed control. A backend-owned `disarmed` row supports only
Expand All @@ -241,8 +244,7 @@ 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 the Kestrel 8852C
* (measured, see above).
* the recipe but no 8821CU/CE cell has run.
* 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
3 changes: 2 additions & 1 deletion src/kestrel/HalKestrel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2281,7 +2281,8 @@ void HalKestrel::vnd_bb_set_gain(uint8_t channel, uint8_t band_type) {
void HalKestrel::vnd_bb_ctrl_tx_path() {
if (_halbb_ctx && _variant == ChipVariant::C8852C) {
kestrel_halbb_ctrl_tx_path(_halbb_ctx);
_logger->info("Kestrel PHY: T-MAC TX path-com routed (vendored, 8852C)");
_logger->info("Kestrel PHY: T-MAC TX path-com routed (vendored, 8852C), "
"TX path enable/map from P-MAC (A+B)");
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/kestrel/RtlKestrelDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1170,17 +1170,17 @@ devourer::AdapterCaps RtlKestrelDevice::GetAdapterCaps() {
/* Hardware ARQ: SetAckResponder arms on both dies, verified by CAM read-back
* -- the 8852B through a firmware-committed INFRA station entry
* (HalKestrel::register_infra_sta), the 8852C through a host-written one
* (HalKestrel::write_addr_cam_sta), its firmware ignoring the H2C. 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
* (HalKestrel::write_addr_cam_sta), its firmware ignoring the H2C. Both dies
* are measured; the 8852C has answered in every measured session since the
* bring-up takes the TX path selection from P-MAC
* (kestrel_halbb_ctrl_tx_path). Any other variant does not inherit it. The retry knob IS wired
* (WD DATA_TXCNT_LMT per frame, attempts-semantics folded to the N-retries
* contract in send_packet; witness-measured on the 8832CU: limits {0,2} ->
* modal {1,3} on-air copies exactly, limit 8 -> an 8/9 near-tie consistent
* with ~90% witness capture of a 9-copy truth — obedient, no wedge). */
c.tx_retry_limit_ok = true;
c.ack_responder_ok = _variant == kestrel::ChipVariant::C8852B;
c.ack_responder_ok = _variant == kestrel::ChipVariant::C8852B ||
_variant == kestrel::ChipVariant::C8852C;
c.bw_mask = devourer::bw_mask_for_generation(c.generation);
if (_variant == kestrel::ChipVariant::C8852C)
c.bw_mask |= devourer::kBw160; /* 8852C-only (vendor bw_sup BW_CAP_160M) */
Expand Down
9 changes: 4 additions & 5 deletions src/kestrel/RtlKestrelDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,10 @@ class RtlKestrelDevice : public IRtlRadio {
* 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. */
* serialized. On the 8852C the response also depends on the bring-up
* taking the TX path selection from the BB's P-MAC registers
* (kestrel_halbb_ctrl_tx_path); without it some bring-ups armed and
* answered no ACK (docs/kestrel-ack-responder.md). */
bool SetAckResponder(const devourer::MacAddr &mac) override;
void ClearAckResponder() override;
/* rxdemo's live-disarm measurement hook (DEVOURER_ACK_DISARM_AFTER_MS): the
Expand Down
Loading