diff --git a/docs/kestrel-ack-responder.md b/docs/kestrel-ack-responder.md index 0b559ffc..d5c36c89 100644 --- a/docs/kestrel-ack-responder.md +++ b/docs/kestrel-ack-responder.md @@ -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. +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. diff --git a/hal/halbb/g6/kestrel_halbb_glue.c b/hal/halbb/g6/kestrel_halbb_glue.c index 36941e54..c4ef304b 100644 --- a/hal/halbb/g6/kestrel_halbb_glue.c +++ b/hal/halbb/g6/kestrel_halbb_glue.c @@ -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 diff --git a/hal/halbb/g6/kestrel_halbb_glue.h b/hal/halbb/g6/kestrel_halbb_glue.h index 4f6f5785..df85f30d 100644 --- a/hal/halbb/g6/kestrel_halbb_glue.h +++ b/hal/halbb/g6/kestrel_halbb_glue.h @@ -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 diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 93f88424..15f8087b 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -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 @@ -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 diff --git a/src/kestrel/HalKestrel.cpp b/src/kestrel/HalKestrel.cpp index 74d0d835..62fc34f7 100644 --- a/src/kestrel/HalKestrel.cpp +++ b/src/kestrel/HalKestrel.cpp @@ -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)"); } } diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 59d39eba..aa5655f8 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -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) */ diff --git a/src/kestrel/RtlKestrelDevice.h b/src/kestrel/RtlKestrelDevice.h index 5eacc702..e0bfefb2 100644 --- a/src/kestrel/RtlKestrelDevice.h +++ b/src/kestrel/RtlKestrelDevice.h @@ -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