From 71a2708af3b80112bedb3f735679c49650482191 Mon Sep 17 00:00:00 2001 From: Gilang Date: Sun, 11 Oct 2026 22:16:06 +0700 Subject: [PATCH 1/6] =?UTF-8?q?jaguar3:=208822C=2040=20MHz=20inject=20?= =?UTF-8?q?=E2=80=94=20DISDATAFB=3D1=20and=20an=20IGI=20floor=20on=20the?= =?UTF-8?q?=20C=20die?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RTL8812CU could not air a single 40 MHz frame on most channels while the same card does HT40 under mainline rtw88. Two faults, both gated on the 8822C so the 8822E's descriptor and DIG stay byte-identical: - The inject descriptor carried DISDATAFB=0 (the Jaguar1 mirror). With USE_RATE=1 the MAC's per-frame rate-fallback lookup consults the fw's per-MACID entry, which an injecting MACID lacks; on the 8822C that wedges the first 40 MHz frame and the TX FIFO fills. The device now passes dis_data_fb=1 on the C die (the vendor's fixed-rate path does). - On a quiet channel the receiver parks below IGI 0x24 in a gain state where the secondary 20 MHz never reads idle, so the MAC holds every 40 MHz frame; busy channels self-heal because received frames re-run the AGC. DIG_MIN_8822C_WIDE 0x26 is pushed into the phydm runtime after every tune (Init/InitWrite/SetMonitorChannel/FastRetune/ FastSetBandwidth) at 40/80 MHz and honoured by every DIG pass. Costs ~6 dB of idle-channel RX sensitivity at those widths on the CU. Measured on 144(-)/52(+)/149(+)/36(+): ~20.5k frames per 50 s run, 0 failures, all received at MCS3 bw 40; HT20 unchanged. Record: docs/experiments/8822c-ht40-tx-stall.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PbfYcAbwocHjWbXn9q8tzB --- docs/experiments/8822c-ht40-tx-stall.md | 83 +++++++++++++++++++++++++ src/jaguar3/CLAUDE.md | 15 ++++- src/jaguar3/FrameParserJaguar3.h | 10 ++- src/jaguar3/PhydmRuntimeJaguar3.cpp | 10 +++ src/jaguar3/PhydmRuntimeJaguar3.h | 16 +++++ src/jaguar3/RtlJaguar3Device.cpp | 17 ++++- src/jaguar3/RtlJaguar3Device.h | 4 ++ 7 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 docs/experiments/8822c-ht40-tx-stall.md diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md new file mode 100644 index 00000000..7a9ac19d --- /dev/null +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -0,0 +1,83 @@ +# RTL8812CU (8822C) 40 MHz injection stall + +Why devourer's 8822C could not transmit a single 40 MHz frame on most +channels while the same card does HT40 under mainline rtw88, and the two +fixes that came out of it (`src/jaguar3/FrameParserJaguar3.h` DISDATAFB, +`PhydmRuntimeJaguar3::set_igi_floor`). Bench: txdemo on the CU (host), +rxdemo on an 8812EU ground-station card at the same channel/width, +1 kB QoS-data frames, MCS3/40/LDPC, 50 s per run; "aired" = frames the +EU received with CRC ok and `bw:1`. + +## Symptom + +HT20: 20 k frames in 50 s, every channel. HT40 on 144(-), 36(+), 149(+): +257 frames accepted by bulk-OUT, then rc=-7 and nothing on air — at the +abort the TX FIFO is full (`LQ 64/0 PUB 1745/19`, 2048 x 128 B pages), i.e. +the MAC never completed the FIRST 40 MHz frame. The vendor 88x2cu driver +in monitor mode trickles (273/7500 at 7-15 fps); mainline `rtw88_8822cu` +as a hostapd HT40 AP with a phone client sent 5.2 k MCS15/40 data frames +in 45 s on the same card — the chip is fine. + +## Dead ends (all measured, 12+ runs) + +- Firmware RA entry for the inject MACID — H2C 0x40 RA_INFO in every + layout variant (macid 0/1, MEDIA_STATUS_RPT first, dis_ra, rate_id + 5/1/10+VHT, no_update_bw, init_lv, plus the 0x1D channel-switch H2C): + the desc path stalls identically. The fw-driven path (USE_RATE=0) + drains but at 20 MHz, and its own C2H RA report (0x0c, `curr_bw` byte 6) + says it CHOSE 20 regardless of `ra_bw_mode`. The fw only honours a + bandwidth through the vendor's `phydm_fw_fix_rate` mailbox — MAC 0x450 = + `0x60<<24 | macid<<16 | bw<<8 | rate` — which does give a clean 40 MHz + fw-driven drain on a quiet 149 (4446/0 failed) but is a per-MACID fixed + rate, not devourer's per-packet design. +- Firmware version: the vendor blob is v9.17, rtw88 loads v9.9 — devourer + built with v9.9 behaves the same. +- IQK (`DEVOURER_SKIP_IQK`), TX power (index 16), USB port, channel pair, + `DEVOURER_DIS_CCA`, 0x878 bw-indication, AGC table index, the RF 0x3f + mode-table write, DFS (52 — DFS, low band — also hung; see below). + +## Fault 1 — DISDATAFB + +devourer's Jaguar3 inject descriptor mirrored the Jaguar1 path with +DISDATAFB=0. With USE_RATE=1 the MAC's per-frame rate-fallback lookup +still consults the fw's per-MACID rate entry, which an injecting MACID +lacks; on the 8822C that collision wedges a 40 MHz frame outright. With +DISDATAFB=1 (what the vendor's fixed-rate xmit branch writes) the hard +hang is gone — but only on some channels. + +## Fault 2 — receiver gain on a quiet channel + +With DISDATAFB=1: 36(+) perfect (20.7 k frames; a neighbour's RTS/CTS/BA +at ~200 fps on it), 149(+) works with 500 ms pauses that track the gaps +in a neighbour AP's traffic, 52/100/120/144 hang after 0-130 frames — +the SILENT channels. A 40 MHz TX needs the secondary 20 MHz to read +idle; the FA/CCA counters show nothing unusual, but the idle-vs-live +register diff shows the receiver parked in a different gain state +(RF 0x08/0x58, CCK/AGC status 0x1a30-0x1a68). Two proofs: + +- the GS's second card sending MCS0/20 at 50 fps on silent 144 turns the + immediate hang into 20.4 k frames / 0 failures; +- pinning IGI (0x1d70) on silent 144: 0x35, 0x2c, 0x26, 0x24 → ~20.6 k + frames / 0 failures; 0x22 → 22 frames then hang. devourer's DIG floors + were 0x1e (coverage) / 0x20 (DFS). The AGC lower bound 0x828[7:3] + (vendor measures it per table; devourer uses 0xd) does not substitute + (0x10/0x14: hang, 0x18: 139 frames). + +Fix: `DIG_MIN_8822C_WIDE 0x26` (one step over the threshold) applied on +the C die at 40/80 MHz after every tune and honoured by DIG. Cost: ~6 dB +of idle-channel RX sensitivity at those widths on that die, nothing at +20 MHz, nothing on the 8822E (its descriptor and DIG are byte-identical +to before — not re-flown after this change). + +## Result (no knobs) + +| channel | width | submitted | failed | aired (EU, bw) | +|---|---|---|---|---| +| 144(-) | 40 | 20552 | 0 | 20500 MCS3 bw:1 | +| 52(+) | 40 | 20525 | 0 | 20425 MCS3 bw:1 | +| 149(+) | 40 | 20590 | 0 | 20477 MCS3 bw:1 | +| 36(+) | 40 | 20483 | 0 | 19723 MCS3 bw:1 | +| 144(-) | 20 | 20679 | 0 | 20629 MCS3 bw:0 (IGI stays 0x20) | + +Open: 80 MHz inherits the floor untested; 2.4 GHz HT40 (ch 6) drained on +the TX side but the EU received none of it — unexplained. diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index e99e4ac5..f9b6aae5 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -23,6 +23,16 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` dividers (vs the `0x8ac` block the Jaguar1/2 chips share). 80 MHz works, incl. a 40-in-80 frame via TX-descriptor DATA_SC. See `docs/narrowband.md`. - halrf calibration: DACK/IQK/TXGAPK/thermal tracking. +- **8822C 40 MHz inject needs two things the 8822E does not** (both gated + on the C die, `docs/experiments/8822c-ht40-tx-stall.md`): the inject + descriptor carries DISDATAFB=1 (with USE_RATE=1 the MAC's per-frame + fallback lookup hits the fw's per-MACID entry, which an injecting MACID + lacks — the first 40 MHz frame wedges and the TX FIFO fills), and DIG is + floored at `DIG_MIN_8822C_WIDE 0x26` at 40/80 MHz (`set_igi_floor`, pushed + after every tune): on a quiet channel below IGI 0x24 the receiver never + reads the secondary 20 MHz idle and the MAC holds every 40 MHz frame — + busy channels self-heal because received frames re-run the AGC. The floor + costs ~6 dB of idle-channel RX sensitivity on the CU at those widths. - **Firmware channel switch** (H2C 0x1D via `fastretune_fw`, both dies): the fw-switch H2C must ride `HalJaguar3::send_h2c_raw`'s HMEBOX box counter — the coex runtime thread shares it (both callers hold `_reg_mu`); a second @@ -169,7 +179,10 @@ the JGR3 register map (CLM period is the low half of `0x1e40`, trigger This is the generation where `nhm_env` works as intended, because `PhydmRuntimeJaguar3.cpp` clamps DIG to `DIG_MIN_COVERAGE 0x1e` … `DIG_MAX_OF_MIN_COVERAGE 0x22` — four steps — so the gain reference barely -moves and the histogram mass is free to march up under an interferer. Against a +moves and the histogram mass is free to march up under an interferer (the +numbers below are 20 MHz; on the 8822C at 40/80 MHz the clamp sits at +`DIG_MIN_8822C_WIDE 0x26` instead, 4 steps higher, and no `nhm_env` figure +has been taken there). Against a 5 MHz non-802.11 carrier on a traffic-free channel: 0 frames decoded, `clm` 6, `nhm_env` 56, against a quiet 0/0/0; 802.11 traffic at MCS1 read 606 frames / 15 / 15. The discriminator is the ratio — `nhm_env`/`clm` ≈ 1 under 802.11, ≈ 9 diff --git a/src/jaguar3/FrameParserJaguar3.h b/src/jaguar3/FrameParserJaguar3.h index ee717418..088265ea 100644 --- a/src/jaguar3/FrameParserJaguar3.h +++ b/src/jaguar3/FrameParserJaguar3.h @@ -146,7 +146,8 @@ inline void fill_data_tx_desc_8822c(uint8_t *d, uint16_t pkt_size, bool bmc = false, bool ndpa = false, uint8_t data_sc = 0, uint8_t pwr_ofset_type = 0, - uint8_t pkt_offset = 0) { + uint8_t pkt_offset = 0, + bool dis_data_fb = false) { SET_TX_DESC_TXPKTSIZE_8822C(d, pkt_size); SET_TX_DESC_OFFSET_8822C(d, static_cast(TXDESC_SIZE_8822C)); SET_TX_DESC_LS_8822C(d, 1); @@ -172,7 +173,12 @@ inline void fill_data_tx_desc_8822c(uint8_t *d, uint16_t pkt_size, * the legacy 54..6M chain (tests/retry_ladder_probe.sh). */ SET_TX_DESC_RATE_ID_8822C(d, rate_id); SET_TX_DESC_USE_RATE_8822C(d, 1); - SET_TX_DESC_DISDATAFB_8822C(d, 0); + /* DISDATAFB: with USE_RATE=1 the MAC's per-frame rate-fallback lookup still + * consults the fw's per-MACID rate entry, which no injecting MACID has. The + * 8822C wedges on that collision for a 40 MHz frame (the first one never + * airs and the TX FIFO fills), so its caller passes dis_data_fb=1 — what the + * vendor driver's own fixed-rate path writes; the 8822E keeps 0. */ + SET_TX_DESC_DISDATAFB_8822C(d, dis_data_fb ? 1 : 0); SET_TX_DESC_SW_DEFINE_8822C(d, 1); SET_TX_DESC_DATARATE_8822C(d, rate_hw); SET_TX_DESC_DATA_BW_8822C(d, bw); diff --git a/src/jaguar3/PhydmRuntimeJaguar3.cpp b/src/jaguar3/PhydmRuntimeJaguar3.cpp index 69288bcf..e0ff5770 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.cpp +++ b/src/jaguar3/PhydmRuntimeJaguar3.cpp @@ -28,6 +28,12 @@ void PhydmRuntimeJaguar3::set_igi(uint8_t igi) { _device.phy_set_bb_reg(0x1d70, 0x00007f00, igi); } +void PhydmRuntimeJaguar3::set_igi_floor(uint8_t floor) { + _igi_floor = floor; + if (floor != 0 && get_igi() < floor) + set_igi(floor); +} + /* phydm_fa_cnt_statistics_jgr3 + phydm_false_alarm_counter_reg_reset: read * the per-window false-alarm and CCA counters, then clear and re-arm them so * the next tick sees a fresh window. */ @@ -85,6 +91,10 @@ void PhydmRuntimeJaguar3::dig(const FaStats &fa, bool is_dfs) { th0 = 250; th1 = 1000; th2 = 2000; lo = DIG_MIN_DFS; hi = DIG_MIN_DFS; } + if (lo < _igi_floor) + lo = _igi_floor; + if (hi < _igi_floor) + hi = _igi_floor; if (fa.fa_all > th2) igi += 2; diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index 1d75749f..cf091b9a 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -8,6 +8,17 @@ namespace jaguar3 { +/* 8822C IGI floor at 40/80 MHz. A wide-channel TX needs the secondary 20 MHz + * to read idle, and below IGI 0x24 the 8822C receiver parks, on a QUIET + * channel, in a gain state where the secondary never does: the MAC holds + * every 40 MHz frame (measured on 144/52/100/120 — the first frame never + * airs and the TX FIFO fills; 0x24 clears it, 0x22 does not; busy channels + * self-heal because each received frame re-runs the AGC). One step of margin + * on top. Costs ~6 dB of idle-channel RX sensitivity against DIG_MIN_DFS at + * those widths only; 20 MHz and the 8822E are untouched. 80 MHz inherits the + * floor untested. */ +constexpr uint8_t DIG_MIN_8822C_WIDE = 0x26; + /* Port of the phydm dynamic mechanisms the vendor kernel runs from its ~2 s * watchdog in a no-link (monitor) session — the RX-side adaptation loop: * @@ -40,6 +51,10 @@ class PhydmRuntimeJaguar3 { * re-applied when they change). */ void on_channel_change() { _cck_lv_valid = false; } + /* Lower bound for DIG: applied at once (IGI raised to it if below) and + * honoured by every later tick. 0 = none. See DIG_MIN_8822C_WIDE. */ + void set_igi_floor(uint8_t floor); + private: /* --- false-alarm / CCA statistics (this tick's window) --- */ struct FaStats { @@ -62,6 +77,7 @@ class PhydmRuntimeJaguar3 { /* DIG state */ int _last_igi_logged = -1; + uint8_t _igi_floor = 0; /* CCK-PD state (phydm_cckpd_type4) */ bool _cck_tbl_valid = false; diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index a6372ba8..062f7681 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -130,6 +130,7 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); _hal.enable_rx_path(); /* RF into RX mode (IGI toggle) — must follow channel set */ + apply_igi_floor_locked(channel.ChannelWidth); _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ _hal.coex_wlan_only_init(); /* lock antenna to WLAN (disable BT/LTE coex) */ @@ -954,6 +955,7 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { channel.ChannelWidth); if (want_rx) _hal.enable_rx_path(); /* RF into RX mode — same slot as the Init path */ + apply_igi_floor_locked(channel.ChannelWidth); _hal.dpk_force_bypass_8822e(); /* 8822e rfe 21/22: kernel bypasses DPK (after IQK) */ _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins (PA enable) */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ @@ -1518,6 +1520,7 @@ void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { _channel = channel; _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); + apply_igi_floor_locked(channel.ChannelWidth); /* Stored after the retune. rxsc 0 is resolved against the width configured * when the frame is PARSED, as the vendor does (phydm_rxsc_2_bw reads the * current dm->band_width): a frame delivered while the retune runs uses the @@ -1575,6 +1578,7 @@ void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { _channel.Channel = channel; _radioManagement.set_channel_bwmode(channel, _channel.ChannelOffset, _channel.ChannelWidth); + apply_igi_floor_locked(_channel.ChannelWidth); } void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { @@ -1598,6 +1602,7 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { _radioManagement.fast_set_bandwidth(bw)) { _channel.ChannelWidth = bw; _rx_bw_code.store(channel_width_to_bw_code(bw), std::memory_order_relaxed); + apply_igi_floor_locked(bw); return; } /* Fast path declined (40/80 endpoint, cold radio) — full channel set, under @@ -1606,6 +1611,14 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { bw); _channel.ChannelWidth = bw; _rx_bw_code.store(channel_width_to_bw_code(bw), std::memory_order_relaxed); + apply_igi_floor_locked(bw); +} + +void RtlJaguar3Device::apply_igi_floor_locked(ChannelWidth_t width) { + const bool wide = width == CHANNEL_WIDTH_40 || width == CHANNEL_WIDTH_80; + _phydm.set_igi_floor((_variant == jaguar3::ChipVariant::C8822C && wide) + ? jaguar3::DIG_MIN_8822C_WIDE + : 0); } /* Re-program TXAGC from the current knob state (see header). Both TXAGC @@ -2732,10 +2745,12 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, uint8_t pwr_type = _txpkt_dflt_type.load(std::memory_order_relaxed); if (radiotap_pkt_pwr_db != INT_MIN) pwr_type = txpkt_type_for_idx(txpkt_idx_for_qdb(radiotap_pkt_pwr_db * 4)); + /* 8822C only: DISDATAFB=1 — see fill_data_tx_desc_8822c. The 8822E keeps + * the byte-identical descriptor it flies 40 MHz with. */ jaguar3::fill_data_tx_desc_8822c( out, static_cast(frame_len), MRateToHwRate(fixed_rate), rate_id, bw_desc, sgi != 0, ldpc != 0, stbc, bmc, ndpa, data_sc, pwr_type, - pkt_offset); + pkt_offset, /*dis_data_fb=*/_variant == jaguar3::ChipVariant::C8822C); /* A DATA FRAME BELONGS IN A DATA QUEUE. fill_data_tx_desc_8822c stamps * QSEL 0x12 (MGNT) on every descriptor, and init_trx_cfg maps MG to the * 64-page HIGH queue that management frames and the beacon share. So DATA diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 15de9c39..c0491d3c 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -346,6 +346,10 @@ class RtlJaguar3Device : public IRtlRadio { /* phydm dynamic mechanisms (FA/DIG/CCK-PD/EDCCA), ticked from the coex * thread every ~2 s like the vendor watchdog. */ jaguar3::PhydmRuntimeJaguar3 _phydm; + /* DIG_MIN_8822C_WIDE for `width` on this die, pushed into _phydm right + * after every tune so the first 40 MHz frame is not held until the next + * 2 s tick. Caller holds _reg_mu (or is pre-coex bring-up). */ + void apply_igi_floor_locked(ChannelWidth_t width); SelectedChannel _channel{}; /* Mirrors _channel.ChannelWidth as a devourer bw code (0/1/2 = 20/40/80 MHz) * so the RX completion handler can read the currently-tuned width without From 4b1e6609f1e9e66323c184719adcba872de33aa8 Mon Sep 17 00:00:00 2001 From: Gilang Date: Sun, 11 Oct 2026 23:47:16 +0700 Subject: [PATCH 2/6] jaguar3: arm the 8822C wide-channel IGI floor on the first wide frame sent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor costs ~6 dB of idle-channel RX sensitivity, which a device that only receives at 40 MHz — a CU ground station, whose own control frames are 20 MHz — pays for nothing. Arm it from build_tx_block on the first >= 40 MHz descriptor (one _reg_mu take, the FastRetune precedent), keep it across tunes while the width stays wide, drop it at a narrow width. Bench, ch 144 silent, no knobs: HT40 TX 9091 frames / 0 failed with IGI 0x26 after arming; 20 MHz frames into the 40 MHz tune 9928 / 0 at IGI 0x20; RX-only at 40 MHz stays at 0x20. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PbfYcAbwocHjWbXn9q8tzB --- docs/experiments/8822c-ht40-tx-stall.md | 13 ++++++++----- src/jaguar3/CLAUDE.md | 12 +++++++----- src/jaguar3/PhydmRuntimeJaguar3.h | 6 ++++-- src/jaguar3/RtlJaguar3Device.cpp | 23 ++++++++++++++++++++--- src/jaguar3/RtlJaguar3Device.h | 10 +++++++--- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md index 7a9ac19d..4ba6d5e1 100644 --- a/docs/experiments/8822c-ht40-tx-stall.md +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -63,11 +63,14 @@ register diff shows the receiver parked in a different gain state (vendor measures it per table; devourer uses 0xd) does not substitute (0x10/0x14: hang, 0x18: 139 frames). -Fix: `DIG_MIN_8822C_WIDE 0x26` (one step over the threshold) applied on -the C die at 40/80 MHz after every tune and honoured by DIG. Cost: ~6 dB -of idle-channel RX sensitivity at those widths on that die, nothing at -20 MHz, nothing on the 8822E (its descriptor and DIG are byte-identical -to before — not re-flown after this change). +Fix: `DIG_MIN_8822C_WIDE 0x26` (one step over the threshold) on the C die +at 40/80 MHz, armed by the first wide frame the device transmits and +re-applied after every tune while wide, honoured by DIG. Cost: ~6 dB of +idle-channel RX sensitivity at those widths on that die — only on a device +that transmits wide frames; a CU ground station (40 MHz receive, 20 MHz +control frames) never arms it. Nothing changes at 20 MHz or on the 8822E +(its descriptor and DIG are byte-identical to before — not re-flown after +this change). ## Result (no knobs) diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index f9b6aae5..40bd9955 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -28,11 +28,13 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` descriptor carries DISDATAFB=1 (with USE_RATE=1 the MAC's per-frame fallback lookup hits the fw's per-MACID entry, which an injecting MACID lacks — the first 40 MHz frame wedges and the TX FIFO fills), and DIG is - floored at `DIG_MIN_8822C_WIDE 0x26` at 40/80 MHz (`set_igi_floor`, pushed - after every tune): on a quiet channel below IGI 0x24 the receiver never - reads the secondary 20 MHz idle and the MAC holds every 40 MHz frame — - busy channels self-heal because received frames re-run the AGC. The floor - costs ~6 dB of idle-channel RX sensitivity on the CU at those widths. + floored at `DIG_MIN_8822C_WIDE 0x26` at 40/80 MHz (`set_igi_floor`, armed + by the device's first >= 40 MHz frame and re-applied after every tune while + wide): on a quiet channel below IGI 0x24 the receiver never reads the + secondary 20 MHz idle and the MAC holds every 40 MHz frame — busy channels + self-heal because received frames re-run the AGC. The floor costs ~6 dB of + idle-channel RX sensitivity on the CU at those widths, so a CU that only + receives at 40 MHz (a ground station; its RCFs are 20 MHz) never arms it. - **Firmware channel switch** (H2C 0x1D via `fastretune_fw`, both dies): the fw-switch H2C must ride `HalJaguar3::send_h2c_raw`'s HMEBOX box counter — the coex runtime thread shares it (both callers hold `_reg_mu`); a second diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index cf091b9a..165f7e08 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -15,8 +15,10 @@ namespace jaguar3 { * airs and the TX FIFO fills; 0x24 clears it, 0x22 does not; busy channels * self-heal because each received frame re-runs the AGC). One step of margin * on top. Costs ~6 dB of idle-channel RX sensitivity against DIG_MIN_DFS at - * those widths only; 20 MHz and the 8822E are untouched. 80 MHz inherits the - * floor untested. */ + * those widths only, and only once the device has transmitted a wide frame + * (RtlJaguar3Device::_wide_tx_armed): a 40 MHz receiver-only session or a + * ground station sending 20 MHz control frames keeps the normal floor. 20 MHz + * and the 8822E are untouched. 80 MHz inherits the floor untested. */ constexpr uint8_t DIG_MIN_8822C_WIDE = 0x26; /* Port of the phydm dynamic mechanisms the vendor kernel runs from its ~2 s diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 062f7681..a0d81942 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -1616,9 +1616,13 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { void RtlJaguar3Device::apply_igi_floor_locked(ChannelWidth_t width) { const bool wide = width == CHANNEL_WIDTH_40 || width == CHANNEL_WIDTH_80; - _phydm.set_igi_floor((_variant == jaguar3::ChipVariant::C8822C && wide) - ? jaguar3::DIG_MIN_8822C_WIDE - : 0); + if (!wide) + _wide_tx_armed.store(false, std::memory_order_relaxed); + const bool armed = _wide_tx_armed.load(std::memory_order_relaxed); + _phydm.set_igi_floor( + (_variant == jaguar3::ChipVariant::C8822C && wide && armed) + ? jaguar3::DIG_MIN_8822C_WIDE + : 0); } /* Re-program TXAGC from the current knob state (see header). Both TXAGC @@ -2745,6 +2749,19 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, uint8_t pwr_type = _txpkt_dflt_type.load(std::memory_order_relaxed); if (radiotap_pkt_pwr_db != INT_MIN) pwr_type = txpkt_type_for_idx(txpkt_idx_for_qdb(radiotap_pkt_pwr_db * 4)); + /* 8822C: the first >= 40 MHz frame arms the IGI floor (DIG_MIN_8822C_WIDE) + * before it is queued — on a quiet channel the MAC would otherwise hold it + * forever. One _reg_mu take per arming, never again on the hot path; the + * same precedent as the FastRetune above. */ + if (_variant == jaguar3::ChipVariant::C8822C && + (bwidth == CHANNEL_WIDTH_40 || bwidth == CHANNEL_WIDTH_80) && + !_wide_tx_armed.load(std::memory_order_relaxed)) { + std::lock_guard lk(_reg_mu); + if (!_wide_tx_armed.load(std::memory_order_relaxed)) { + _wide_tx_armed.store(true, std::memory_order_relaxed); + apply_igi_floor_locked(_channel.ChannelWidth); + } + } /* 8822C only: DISDATAFB=1 — see fill_data_tx_desc_8822c. The 8822E keeps * the byte-identical descriptor it flies 40 MHz with. */ jaguar3::fill_data_tx_desc_8822c( diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index c0491d3c..03688e72 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -346,10 +346,14 @@ class RtlJaguar3Device : public IRtlRadio { /* phydm dynamic mechanisms (FA/DIG/CCK-PD/EDCCA), ticked from the coex * thread every ~2 s like the vendor watchdog. */ jaguar3::PhydmRuntimeJaguar3 _phydm; - /* DIG_MIN_8822C_WIDE for `width` on this die, pushed into _phydm right - * after every tune so the first 40 MHz frame is not held until the next - * 2 s tick. Caller holds _reg_mu (or is pre-coex bring-up). */ + /* DIG_MIN_8822C_WIDE policy. The floor is armed by the first >= 40 MHz + * frame this device transmits (build_tx_block) and re-applied after every + * tune while the width stays wide; a session that only receives at 40 MHz + * — or only transmits 20 MHz frames into a 40 MHz channel, as a ground + * station does — never arms it and keeps its full RX sensitivity. A + * narrow width disarms it. Caller holds _reg_mu (or is pre-coex bring-up). */ void apply_igi_floor_locked(ChannelWidth_t width); + std::atomic _wide_tx_armed{false}; SelectedChannel _channel{}; /* Mirrors _channel.ChannelWidth as a devourer bw code (0/1/2 = 20/40/80 MHz) * so the RX completion handler can read the currently-tuned width without From 74f48a5d6074f91d8b34be0eaa7a0ec76002539b Mon Sep 17 00:00:00 2001 From: Gilang Date: Sun, 11 Oct 2026 23:53:55 +0700 Subject: [PATCH 3/6] jaguar3: 8822C wide IGI floor - arm only into a wide tune; state the cost as an estimate Review follow-up to the 8822C HT40 inject fix. - A 40 MHz frame into a 20 MHz tune armed the floor and apply_igi_floor_locked disarmed it again at the narrow width, so every such frame re-took _reg_mu on the send path (txdemo hits it with DEVOURER_TX_RATE=.../40 and no DEVOURER_HOP_BW). Arming now requires the tune to be >= 40 MHz too. - Stop() clears the arm, so a later Init on the same device starts without the floor. - The RX-sensitivity cost was quoted as "~6 dB". It is arithmetic from the 1 dB IGI step, not a measurement, and 6 dB holds only over the DFS floor; on a non-DFS channel (36 and 149 in the result table) it is 4-8 dB over the coverage window. Header, experiment doc and CLAUDE.md now say so. - src/jaguar3/CLAUDE.md keeps the fact and points at the headers and the experiment doc instead of restating them. No hardware re-run: the arming change only removes a narrow-tune case that never applied the floor anyway. ctest 81/81. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/experiments/8822c-ht40-tx-stall.md | 12 +++++++----- src/jaguar3/CLAUDE.md | 20 ++++++++------------ src/jaguar3/PhydmRuntimeJaguar3.h | 6 ++++-- src/jaguar3/RtlJaguar3Device.cpp | 15 +++++++++++---- src/jaguar3/RtlJaguar3Device.h | 11 ++++++----- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md index 4ba6d5e1..f1d8bc9c 100644 --- a/docs/experiments/8822c-ht40-tx-stall.md +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -64,11 +64,13 @@ register diff shows the receiver parked in a different gain state (0x10/0x14: hang, 0x18: 139 frames). Fix: `DIG_MIN_8822C_WIDE 0x26` (one step over the threshold) on the C die -at 40/80 MHz, armed by the first wide frame the device transmits and -re-applied after every tune while wide, honoured by DIG. Cost: ~6 dB of -idle-channel RX sensitivity at those widths on that die — only on a device -that transmits wide frames; a CU ground station (40 MHz receive, 20 MHz -control frames) never arms it. Nothing changes at 20 MHz or on the 8822E +at 40/80 MHz, armed by the first wide frame the device transmits into a +wide tune and re-applied after every tune while wide, honoured by DIG. +Cost: idle-channel RX sensitivity at those widths on that die — estimated +from the 1 dB IGI step, **not measured**: 6 dB over the DFS floor 0x20 +(144, 52), 4-8 dB over the 0x1e..0x22 coverage window on a non-DFS channel +(36, 149). Only on a device that transmits wide frames; a CU ground station +(40 MHz receive, 20 MHz control frames) never arms it. Nothing changes at 20 MHz or on the 8822E (its descriptor and DIG are byte-identical to before — not re-flown after this change). diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index 40bd9955..c108c8ce 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -23,18 +23,14 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` dividers (vs the `0x8ac` block the Jaguar1/2 chips share). 80 MHz works, incl. a 40-in-80 frame via TX-descriptor DATA_SC. See `docs/narrowband.md`. - halrf calibration: DACK/IQK/TXGAPK/thermal tracking. -- **8822C 40 MHz inject needs two things the 8822E does not** (both gated - on the C die, `docs/experiments/8822c-ht40-tx-stall.md`): the inject - descriptor carries DISDATAFB=1 (with USE_RATE=1 the MAC's per-frame - fallback lookup hits the fw's per-MACID entry, which an injecting MACID - lacks — the first 40 MHz frame wedges and the TX FIFO fills), and DIG is - floored at `DIG_MIN_8822C_WIDE 0x26` at 40/80 MHz (`set_igi_floor`, armed - by the device's first >= 40 MHz frame and re-applied after every tune while - wide): on a quiet channel below IGI 0x24 the receiver never reads the - secondary 20 MHz idle and the MAC holds every 40 MHz frame — busy channels - self-heal because received frames re-run the AGC. The floor costs ~6 dB of - idle-channel RX sensitivity on the CU at those widths, so a CU that only - receives at 40 MHz (a ground station; its RCFs are 20 MHz) never arms it. +- **8822C 40 MHz inject needs two things the 8822E does not**, both gated on + the C die: DISDATAFB=1 in the inject descriptor (`fill_data_tx_desc_8822c`) + and a DIG floor at 40/80 MHz (`DIG_MIN_8822C_WIDE`, armed only by the + device's own wide TX). Missing either one, the first 40 MHz frame never airs + and the TX FIFO fills — the floor half only on quiet channels, which is + why a busy bench channel hides it. The floor's RX-sensitivity cost is + estimated, not measured; 80 MHz and 2.4 GHz HT40 are open. Mechanism and + the dead ends: `docs/experiments/8822c-ht40-tx-stall.md`. - **Firmware channel switch** (H2C 0x1D via `fastretune_fw`, both dies): the fw-switch H2C must ride `HalJaguar3::send_h2c_raw`'s HMEBOX box counter — the coex runtime thread shares it (both callers hold `_reg_mu`); a second diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index 165f7e08..6941cf67 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -14,8 +14,10 @@ namespace jaguar3 { * every 40 MHz frame (measured on 144/52/100/120 — the first frame never * airs and the TX FIFO fills; 0x24 clears it, 0x22 does not; busy channels * self-heal because each received frame re-runs the AGC). One step of margin - * on top. Costs ~6 dB of idle-channel RX sensitivity against DIG_MIN_DFS at - * those widths only, and only once the device has transmitted a wide frame + * on top. Costs idle-channel RX sensitivity at those widths only — estimated + * from the 1 dB IGI step, NOT measured: 6 dB over DIG_MIN_DFS 0x20, 4-8 dB over + * the 0x1e..0x22 coverage window on a non-DFS channel — and only once the + * device has transmitted a wide frame into a wide tune * (RtlJaguar3Device::_wide_tx_armed): a 40 MHz receiver-only session or a * ground station sending 20 MHz control frames keeps the normal floor. 20 MHz * and the 8822E are untouched. 80 MHz inherits the floor untested. */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index a0d81942..ecbe0f8a 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -867,6 +867,9 @@ void RtlJaguar3Device::Stop() { if (_station.armed()) (void)_station.clear(_device, _logger, "Jaguar3"); } + /* The 8822C wide IGI floor is earned by this session's TX; a later Init on + * the same device must not inherit it. */ + _wide_tx_armed.store(false, std::memory_order_relaxed); try { _hal.rtw_hal_deinit(); } catch (...) { @@ -2749,12 +2752,16 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, uint8_t pwr_type = _txpkt_dflt_type.load(std::memory_order_relaxed); if (radiotap_pkt_pwr_db != INT_MIN) pwr_type = txpkt_type_for_idx(txpkt_idx_for_qdb(radiotap_pkt_pwr_db * 4)); - /* 8822C: the first >= 40 MHz frame arms the IGI floor (DIG_MIN_8822C_WIDE) - * before it is queued — on a quiet channel the MAC would otherwise hold it - * forever. One _reg_mu take per arming, never again on the hot path; the - * same precedent as the FastRetune above. */ + /* 8822C: the first >= 40 MHz frame into a >= 40 MHz tune arms the IGI floor + * (DIG_MIN_8822C_WIDE) before it is queued — on a quiet channel the MAC + * would otherwise hold it forever. One _reg_mu take per arming, never again + * on the hot path; the same precedent as the FastRetune above. The tune must + * be wide too: a narrow width disarms (apply_igi_floor_locked), so a 40 MHz + * frame into a 20 MHz tune would re-arm and take the lock on every frame. */ + const ChannelWidth_t tune_bw = _channel.ChannelWidth; if (_variant == jaguar3::ChipVariant::C8822C && (bwidth == CHANNEL_WIDTH_40 || bwidth == CHANNEL_WIDTH_80) && + (tune_bw == CHANNEL_WIDTH_40 || tune_bw == CHANNEL_WIDTH_80) && !_wide_tx_armed.load(std::memory_order_relaxed)) { std::lock_guard lk(_reg_mu); if (!_wide_tx_armed.load(std::memory_order_relaxed)) { diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 03688e72..81d3e4c8 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -347,11 +347,12 @@ class RtlJaguar3Device : public IRtlRadio { * thread every ~2 s like the vendor watchdog. */ jaguar3::PhydmRuntimeJaguar3 _phydm; /* DIG_MIN_8822C_WIDE policy. The floor is armed by the first >= 40 MHz - * frame this device transmits (build_tx_block) and re-applied after every - * tune while the width stays wide; a session that only receives at 40 MHz - * — or only transmits 20 MHz frames into a 40 MHz channel, as a ground - * station does — never arms it and keeps its full RX sensitivity. A - * narrow width disarms it. Caller holds _reg_mu (or is pre-coex bring-up). */ + * frame this device transmits into a >= 40 MHz tune (build_tx_block) and + * re-applied after every tune while the width stays wide; a session that + * only receives at 40 MHz — or only transmits 20 MHz frames into a 40 MHz + * channel, as a ground station does — never arms it and keeps its full RX + * sensitivity. A narrow width or Stop() disarms it. Caller holds _reg_mu + * (or is pre-coex bring-up). */ void apply_igi_floor_locked(ChannelWidth_t width); std::atomic _wide_tx_armed{false}; SelectedChannel _channel{}; From 9a1c801e9fe35744d85ae6f10c08533564279c14 Mon Sep 17 00:00:00 2001 From: Gilang Date: Mon, 12 Oct 2026 00:13:03 +0700 Subject: [PATCH 4/6] docs: 8822C HT40 re-test after the arming fix, 8822E re-flown Five 30 s cells on silent 144(-)/149(+), both adapters local: - CU HT40: 0 failed, witness 99.6 % at MCS3 bw:1, floor armed at 0x26. - 40 MHz frames into a 20 MHz tune leave the floor unarmed (0x20). - HT20 control: unchanged. - 8822E HT40: 0 failed, 99.1 % at bw:1, IGI 0x20, as is the RX-only CU witness. Near-field single runs. The send rate is gap-limited, so this does not measure the removed per-frame lock. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/experiments/8822c-ht40-tx-stall.md | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md index f1d8bc9c..932c24de 100644 --- a/docs/experiments/8822c-ht40-tx-stall.md +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -70,9 +70,9 @@ Cost: idle-channel RX sensitivity at those widths on that die — estimated from the 1 dB IGI step, **not measured**: 6 dB over the DFS floor 0x20 (144, 52), 4-8 dB over the 0x1e..0x22 coverage window on a non-DFS channel (36, 149). Only on a device that transmits wide frames; a CU ground station -(40 MHz receive, 20 MHz control frames) never arms it. Nothing changes at 20 MHz or on the 8822E -(its descriptor and DIG are byte-identical to before — not re-flown after -this change). +(40 MHz receive, 20 MHz control frames) never arms it. Nothing changes at +20 MHz or on the 8822E (its descriptor and DIG are byte-identical to before; +re-flown below). ## Result (no knobs) @@ -84,5 +84,33 @@ this change). | 36(+) | 40 | 20483 | 0 | 19723 MCS3 bw:1 | | 144(-) | 20 | 20679 | 0 | 20629 MCS3 bw:0 (IGI stays 0x20) | +## Re-test after the arming review fix + +Final code (arming needs a wide frame AND a wide tune; `Stop()` disarms). +Both adapters on one host, 1 kB QoS-data, run-unique SA, txdemo default +2 ms gap, 30 s per cell. Witness = the other local card. Before the run, an +8 s witness scan of 36/52/100/120/144/149/157 at 40 MHz heard 0 frames. + +| cell | DUT -> witness | tune | frames | submitted | failed | witness | IGI | +|---|---|---|---|---|---|---|---| +| 144(-) | CU -> EU | 40 | MCS3/40 | 14161 | 0 | 14109 MCS3 bw:1 | 0x26 | +| 149(+) | CU -> EU | 40 | MCS3/40 | 14146 | 0 | 14094 MCS3 bw:1 | 0x26 | +| 144 | CU -> EU | **20** | MCS3/40 | 14282 | 0 | 0 (expected) | 0x20 | +| 144 | CU -> EU | 20 | MCS3/20 | 14198 | 0 | 14142 MCS3 bw:0 | 0x20 | +| 144(-) | **EU** -> CU | 40 | MCS3/40 | 14455 | 0 | 14321 MCS3 bw:1 | 0x20 | + +The third row is the review fix: 40 MHz frames into a 20 MHz tune leave the +floor unarmed. The fourth is the 20 MHz control. In the fifth, the 8822E +transmitter stays at 0x20, and so does the CU witness, which receives at +40 MHz and never transmits. + +What this does not show: +- The witness is near-field, one desk away. These are pass/fail results, + not range numbers. +- Every cell is a single run. +- The send rate is gap-limited (~460 fps), so a per-frame lock cost would + not show in `submitted`. The third row checks that the floor stays + unarmed, not that the lock is no longer taken. + Open: 80 MHz inherits the floor untested; 2.4 GHz HT40 (ch 6) drained on the TX side but the EU received none of it — unexplained. From f87521b7b6545633e749d668285fb9718bbd946b Mon Sep 17 00:00:00 2001 From: Gilang Date: Mon, 12 Oct 2026 00:32:19 +0700 Subject: [PATCH 5/6] jaguar3: #484 review - DISDATAFB only on 8822C wide frames, session-scoped floor arm - DISDATAFB=1 was written on every 8822C frame, which silently turned the fw retry ladder (#362/#371, RetryFallback::Default) off at 20 MHz too. It now covers 40/80 MHz frames only. Measured on the CU, unACKed unicast MCS3 limit 8: - 20 MHz: MCS3 x4 > MCS2 > 6M restored, 1223/1224. - 40 MHz: pinned at MCS3, 1208/1209. - 20 MHz frames with DISDATAFB=0 on a 40 MHz tune: 14163 / 0 failed, so the wedge keys on frame width. DeviceConfig.h's RetryFallback note says the 8822C's wide frames have no ladder at either setting. - The floor arm is now "this session has sent a wide frame" (_wide_tx_seen): - Set once, published after the floor is applied, cleared by Init, InitWrite and Stop. - The floor follows the tuned width, read under _reg_mu on both sides. - A retune racing the first wide frame can no longer skip it, and a re-Init no longer inherits it. - A 40 MHz frame on a 20 MHz tune still takes the lock once per session. - Dropping the floor clamps IGI back into the channel's DIG window at once, instead of leaving 0x26 until the next tick. Keeping it at 0 touches no register, so the 8822E path is unchanged. Not exercised on air: the drop clamp and the race. ctest 78/78. HT40 CU and EU and HT20 cells re-run, 0 failed (experiment doc). Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/experiments/8822c-ht40-tx-stall.md | 55 ++++++++++++++--- src/DeviceConfig.h | 7 ++- src/jaguar3/CLAUDE.md | 7 ++- src/jaguar3/FrameParserJaguar3.h | 6 +- src/jaguar3/PhydmRuntimeJaguar3.cpp | 23 ++++++-- src/jaguar3/PhydmRuntimeJaguar3.h | 17 +++--- src/jaguar3/RtlJaguar3Device.cpp | 78 +++++++++++++++---------- src/jaguar3/RtlJaguar3Device.h | 25 +++++--- 8 files changed, 149 insertions(+), 69 deletions(-) diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md index 932c24de..702f1daa 100644 --- a/docs/experiments/8822c-ht40-tx-stall.md +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -38,12 +38,18 @@ in 45 s on the same card — the chip is fine. ## Fault 1 — DISDATAFB -devourer's Jaguar3 inject descriptor mirrored the Jaguar1 path with -DISDATAFB=0. With USE_RATE=1 the MAC's per-frame rate-fallback lookup -still consults the fw's per-MACID rate entry, which an injecting MACID -lacks; on the 8822C that collision wedges a 40 MHz frame outright. With -DISDATAFB=1 (what the vendor's fixed-rate xmit branch writes) the hard -hang is gone — but only on some channels. +devourer's Jaguar3 inject descriptor writes DISDATAFB=0 on purpose: it +keeps the firmware's per-retry rate-fallback ladder +(`DEVOURER_TX_RETRY_FALLBACK`, #362/#371). With USE_RATE=1 the MAC's +per-frame rate-fallback lookup still consults the fw's per-MACID rate +entry, which an injecting MACID lacks; on the 8822C that collision wedges +a 40 MHz frame outright. With DISDATAFB=1 (what the vendor's fixed-rate +xmit branch writes) the hard hang is gone — but only on some channels. + +The fix sets DISDATAFB=1 on the 8822C's 40/80 MHz frames only, so they +have no retry ladder at either knob setting. 20 MHz frames keep +DISDATAFB=0 and the ladder: 20 MHz with 0 is the configuration #362 +measured, and the HT20 rows below. ## Fault 2 — receiver gain on a quiet channel @@ -64,8 +70,11 @@ register diff shows the receiver parked in a different gain state (0x10/0x14: hang, 0x18: 139 frames). Fix: `DIG_MIN_8822C_WIDE 0x26` (one step over the threshold) on the C die -at 40/80 MHz, armed by the first wide frame the device transmits into a -wide tune and re-applied after every tune while wide, honoured by DIG. +at 40/80 MHz, honoured by DIG. It is armed for the session by the first +wide frame the device transmits, and from then on follows the tuned +width: applied while tuned to 40/80 MHz, dropped at a narrower width, with +IGI clamped straight back into the normal window. `Init`, `InitWrite` and +`Stop()` clear the arm. Cost: idle-channel RX sensitivity at those widths on that die — estimated from the 1 dB IGI step, **not measured**: 6 dB over the DFS floor 0x20 (144, 52), 4-8 dB over the 0x1e..0x22 coverage window on a non-DFS channel @@ -86,7 +95,8 @@ re-flown below). ## Re-test after the arming review fix -Final code (arming needs a wide frame AND a wide tune; `Stop()` disarms). +Code at the time (arming needed a wide frame AND a wide tune; `Stop()` +disarmed). Both adapters on one host, 1 kB QoS-data, run-unique SA, txdemo default 2 ms gap, 30 s per cell. Witness = the other local card. Before the run, an 8 s witness scan of 36/52/100/120/144/149/157 at 40 MHz heard 0 frames. @@ -112,5 +122,32 @@ What this does not show: not show in `submitted`. The third row checks that the floor stays unarmed, not that the lock is no longer taken. +## Re-test after the #484 review + +The code now sets DISDATAFB=1 on 40/80 MHz frames only, arms per session, +and clamps IGI when the floor drops. The bench is the same, on silent 144: + +| cell | tune | frames | submitted | failed | witness | IGI | +|---|---|---|---|---|---|---| +| CU -> EU, 144(-) | 40 | MCS3/40 | 14117 | 0 | 14067 MCS3 bw:1 | 0x26 | +| CU -> EU, 144(-) | 40 | **MCS3/20** (DISDATAFB=0) | 14163 | 0 | 14111 MCS3 bw:0 | 0x20 | +| CU -> EU, 144 | 20 | MCS3/20 | 14285 | 0 | 14232 MCS3 bw:0 | 0x20 | +| EU -> CU, 144(-) | 40 | MCS3/40 | 14456 | 0 | 14406 MCS3 bw:1 | 0x20 | + +The second row is the new combination: a 20 MHz frame with DISDATAFB=0 on +a 40 MHz tune does not wedge. The fault keys on the frame's width, not the +tune's. + +Retry ladder, `tests/retry_ladder_probe.sh` shape: unACKed unicast, MCS3, +limit 8, CU -> EU on 144. +- **20 MHz:** 1223/1224 frames flew MCS3 x4 > MCS2 > 6M x4, the ladder + #371 documents, restored. +- **40 MHz:** 1208/1209 frames flew MCS3 x9, pinned by DISDATAFB=1, as + documented. + +Not exercised on air: the IGI clamp when the floor drops (no demo changes +width mid-session), and the retune/first-wide-frame race, which is a +lock-ordering argument, not a measurement. + Open: 80 MHz inherits the floor untested; 2.4 GHz HT40 (ch 6) drained on the TX side but the EU received none of it — unexplained. diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 17a7ad9d..9c87e835 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -87,6 +87,11 @@ enum class RxMode : uint8_t { * 8822C MCS3 x4 -> MCS2 -> 6M floor (CCK floor at 2.4 GHz; VHT steps * M7 -> M4 -> M1 -> M0 -> 6M). Jaguar1's fw does not step at all — every * retry at the descriptor rate regardless of this knob (8821AU-measured). + * The 8822C's 40/80 MHz frames have no ladder at either setting: their + * descriptor carries DISDATAFB=1 unconditionally, because with 0 the first + * 40 MHz injected frame wedges the TX FIFO + * (docs/experiments/8822c-ht40-tx-stall.md). Its 20 MHz frames keep the + * ladder above. * Off pins every retry at the descriptor DATARATE (DISDATAFB / DISABLE_FB = * 1; measured: 1,200/1,200 retried frames re-aired at the original rate) — * for constant-rate links where a step-down retry costs extra airtime. A @@ -282,7 +287,7 @@ struct DeviceConfig { int ack_timeout_us = 128; /* env: DEVOURER_TX_RETRY_FALLBACK — "off" | unset. Unset = the firmware * fallback ladder with its own floor (the current behaviour, descriptors - * byte-identical). "off" disables per-retry rate fallback (DISDATAFB / + * byte-identical; not on an 8822C 40/80 MHz frame — see the enum). "off" disables per-retry rate fallback (DISDATAFB / * DISABLE_FB = 1): retries re-air at the descriptor rate. See the * RetryFallback enum for why there is no floor form. */ RetryFallback retry_fallback = RetryFallback::Default; diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index c108c8ce..3b153046 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -24,9 +24,10 @@ narrowband dividers, RF18 encoding), strategy interfaces `Jaguar3Calibration` incl. a 40-in-80 frame via TX-descriptor DATA_SC. See `docs/narrowband.md`. - halrf calibration: DACK/IQK/TXGAPK/thermal tracking. - **8822C 40 MHz inject needs two things the 8822E does not**, both gated on - the C die: DISDATAFB=1 in the inject descriptor (`fill_data_tx_desc_8822c`) - and a DIG floor at 40/80 MHz (`DIG_MIN_8822C_WIDE`, armed only by the - device's own wide TX). Missing either one, the first 40 MHz frame never airs + the C die: DISDATAFB=1 on 40/80 MHz inject descriptors + (`fill_data_tx_desc_8822c`, so those frames have no fw retry ladder; 20 MHz + frames keep it) and a DIG floor at 40/80 MHz (`DIG_MIN_8822C_WIDE`, armed + only by the session's own wide TX). Missing either one, the first 40 MHz frame never airs and the TX FIFO fills — the floor half only on quiet channels, which is why a busy bench channel hides it. The floor's RX-sensitivity cost is estimated, not measured; 80 MHz and 2.4 GHz HT40 are open. Mechanism and diff --git a/src/jaguar3/FrameParserJaguar3.h b/src/jaguar3/FrameParserJaguar3.h index 088265ea..daba817f 100644 --- a/src/jaguar3/FrameParserJaguar3.h +++ b/src/jaguar3/FrameParserJaguar3.h @@ -176,8 +176,10 @@ inline void fill_data_tx_desc_8822c(uint8_t *d, uint16_t pkt_size, /* DISDATAFB: with USE_RATE=1 the MAC's per-frame rate-fallback lookup still * consults the fw's per-MACID rate entry, which no injecting MACID has. The * 8822C wedges on that collision for a 40 MHz frame (the first one never - * airs and the TX FIFO fills), so its caller passes dis_data_fb=1 — what the - * vendor driver's own fixed-rate path writes; the 8822E keeps 0. */ + * airs and the TX FIFO fills), so its caller passes dis_data_fb=1 for a + * 40/80 MHz frame — what the vendor driver's own fixed-rate path writes. A + * 20 MHz frame and every 8822E frame keep 0, and with it the fw retry ladder + * (DeviceConfig.h RetryFallback). */ SET_TX_DESC_DISDATAFB_8822C(d, dis_data_fb ? 1 : 0); SET_TX_DESC_SW_DEFINE_8822C(d, 1); SET_TX_DESC_DATARATE_8822C(d, rate_hw); diff --git a/src/jaguar3/PhydmRuntimeJaguar3.cpp b/src/jaguar3/PhydmRuntimeJaguar3.cpp index e0ff5770..3ed71344 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.cpp +++ b/src/jaguar3/PhydmRuntimeJaguar3.cpp @@ -12,6 +12,10 @@ namespace { constexpr uint8_t DIG_MIN_COVERAGE = 0x1e; constexpr uint8_t DIG_MAX_OF_MIN_COVERAGE = 0x22; constexpr uint8_t DIG_MIN_DFS = 0x20; +/* DFS-band decision as the vendor driver's rtw_is_dfs_ch: 5 GHz ch 52-144. */ +bool is_dfs_channel(uint8_t channel) { + return channel > 14 && channel >= 52 && channel <= 144; +} /* phydm_adaptivity.h normal-mode EDCCA constants. */ constexpr int TH_L2H_DIFF_IGI = 8; constexpr int EDCCA_TH_L2H_LB = 48; @@ -28,10 +32,19 @@ void PhydmRuntimeJaguar3::set_igi(uint8_t igi) { _device.phy_set_bb_reg(0x1d70, 0x00007f00, igi); } -void PhydmRuntimeJaguar3::set_igi_floor(uint8_t floor) { +void PhydmRuntimeJaguar3::set_igi_floor(uint8_t floor, uint8_t channel) { + const uint8_t prev = _igi_floor; _igi_floor = floor; - if (floor != 0 && get_igi() < floor) - set_igi(floor); + if (floor != 0) { + if (get_igi() < floor) + set_igi(floor); + } else if (prev != 0) { + /* The top of the window dig() would clamp to on its next tick. */ + const uint8_t hi = + is_dfs_channel(channel) ? DIG_MIN_DFS : DIG_MAX_OF_MIN_COVERAGE; + if (get_igi() > hi) + set_igi(hi); + } } /* phydm_fa_cnt_statistics_jgr3 + phydm_false_alarm_counter_reg_reset: read @@ -215,9 +228,7 @@ void PhydmRuntimeJaguar3::edcca(uint8_t igi) { void PhydmRuntimeJaguar3::tick(uint8_t channel, bool edcca_track) { const FaStats fa = fa_statistics_and_reset(); - /* DFS-band decision as the vendor driver's rtw_is_dfs_ch: 5 GHz ch 52-144. */ - const bool is_dfs = channel > 14 && channel >= 52 && channel <= 144; - dig(fa, is_dfs); + dig(fa, is_dfs_channel(channel)); if (fa.cck_enabled) cck_pd(fa); if (edcca_track) diff --git a/src/jaguar3/PhydmRuntimeJaguar3.h b/src/jaguar3/PhydmRuntimeJaguar3.h index 6941cf67..6190c253 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -17,10 +17,10 @@ namespace jaguar3 { * on top. Costs idle-channel RX sensitivity at those widths only — estimated * from the 1 dB IGI step, NOT measured: 6 dB over DIG_MIN_DFS 0x20, 4-8 dB over * the 0x1e..0x22 coverage window on a non-DFS channel — and only once the - * device has transmitted a wide frame into a wide tune - * (RtlJaguar3Device::_wide_tx_armed): a 40 MHz receiver-only session or a - * ground station sending 20 MHz control frames keeps the normal floor. 20 MHz - * and the 8822E are untouched. 80 MHz inherits the floor untested. */ + * session has transmitted a wide frame (RtlJaguar3Device::_wide_tx_seen): a + * 40 MHz receiver-only session or a ground station sending 20 MHz control + * frames keeps the normal floor. 20 MHz and the 8822E are untouched. 80 MHz + * inherits the floor untested. */ constexpr uint8_t DIG_MIN_8822C_WIDE = 0x26; /* Port of the phydm dynamic mechanisms the vendor kernel runs from its ~2 s @@ -55,9 +55,12 @@ class PhydmRuntimeJaguar3 { * re-applied when they change). */ void on_channel_change() { _cck_lv_valid = false; } - /* Lower bound for DIG: applied at once (IGI raised to it if below) and - * honoured by every later tick. 0 = none. See DIG_MIN_8822C_WIDE. */ - void set_igi_floor(uint8_t floor); + /* Lower bound for DIG, honoured by every later tick. 0 = none. Applied at + * once both ways: setting one raises IGI to it, and dropping one clamps IGI + * back into `channel`'s normal DIG window instead of leaving the raised gain + * for up to a tick. Keeping it at 0 touches no register (the 8822E path). See + * DIG_MIN_8822C_WIDE. */ + void set_igi_floor(uint8_t floor, uint8_t channel); private: /* --- false-alarm / CCA statistics (this tick's window) --- */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index ecbe0f8a..75036192 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -83,6 +83,9 @@ struct WriteBatchScope { void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, SelectedChannel channel) { + /* A new session has sent no wide frame: a re-bring-up must not inherit the + * previous session's 8822C wide IGI floor (_wide_tx_seen). */ + _wide_tx_seen.store(false, std::memory_order_relaxed); /* A window armed before a (re-)bring-up describes a chip state that no * longer exists; leaving it armed would make the next unrelated * GetChannelBusy() take the armed branch and report a stale period. */ @@ -130,7 +133,7 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); _hal.enable_rx_path(); /* RF into RX mode (IGI toggle) — must follow channel set */ - apply_igi_floor_locked(channel.ChannelWidth); + apply_igi_floor_locked(channel.Channel, channel.ChannelWidth); _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ _hal.coex_wlan_only_init(); /* lock antenna to WLAN (disable BT/LTE coex) */ @@ -868,8 +871,9 @@ void RtlJaguar3Device::Stop() { (void)_station.clear(_device, _logger, "Jaguar3"); } /* The 8822C wide IGI floor is earned by this session's TX; a later Init on - * the same device must not inherit it. */ - _wide_tx_armed.store(false, std::memory_order_relaxed); + * the same device must not inherit it (Init/InitWrite clear it too, for a + * re-bring-up without a Stop). */ + _wide_tx_seen.store(false, std::memory_order_relaxed); try { _hal.rtw_hal_deinit(); } catch (...) { @@ -878,6 +882,9 @@ void RtlJaguar3Device::Stop() { } void RtlJaguar3Device::InitWrite(SelectedChannel channel) { + /* A new session has sent no wide frame: a re-bring-up must not inherit the + * previous session's 8822C wide IGI floor (_wide_tx_seen). */ + _wide_tx_seen.store(false, std::memory_order_relaxed); /* A window armed before a (re-)bring-up describes a chip state that no * longer exists; leaving it armed would make the next unrelated * GetChannelBusy() take the armed branch and report a stale period. */ @@ -958,7 +965,7 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { channel.ChannelWidth); if (want_rx) _hal.enable_rx_path(); /* RF into RX mode — same slot as the Init path */ - apply_igi_floor_locked(channel.ChannelWidth); + apply_igi_floor_locked(channel.Channel, channel.ChannelWidth); _hal.dpk_force_bypass_8822e(); /* 8822e rfe 21/22: kernel bypasses DPK (after IQK) */ _hal.config_rfe(channel.Channel); /* 8822e RFE/PAPE antenna-switch pins (PA enable) */ _hal.config_channel_8822e(channel.Channel); /* 8822e band TX scaling/backoff + shaping */ @@ -1523,7 +1530,7 @@ void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { _channel = channel; _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, channel.ChannelWidth); - apply_igi_floor_locked(channel.ChannelWidth); + apply_igi_floor_locked(channel.Channel, channel.ChannelWidth); /* Stored after the retune. rxsc 0 is resolved against the width configured * when the frame is PARSED, as the vendor does (phydm_rxsc_2_bw reads the * current dm->band_width): a frame delivered while the retune runs uses the @@ -1581,7 +1588,7 @@ void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { _channel.Channel = channel; _radioManagement.set_channel_bwmode(channel, _channel.ChannelOffset, _channel.ChannelWidth); - apply_igi_floor_locked(_channel.ChannelWidth); + apply_igi_floor_locked(channel, _channel.ChannelWidth); } void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { @@ -1605,7 +1612,7 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { _radioManagement.fast_set_bandwidth(bw)) { _channel.ChannelWidth = bw; _rx_bw_code.store(channel_width_to_bw_code(bw), std::memory_order_relaxed); - apply_igi_floor_locked(bw); + apply_igi_floor_locked(_channel.Channel, bw); return; } /* Fast path declined (40/80 endpoint, cold radio) — full channel set, under @@ -1614,18 +1621,18 @@ void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { bw); _channel.ChannelWidth = bw; _rx_bw_code.store(channel_width_to_bw_code(bw), std::memory_order_relaxed); - apply_igi_floor_locked(bw); + apply_igi_floor_locked(_channel.Channel, bw); } -void RtlJaguar3Device::apply_igi_floor_locked(ChannelWidth_t width) { +void RtlJaguar3Device::apply_igi_floor_locked(uint8_t channel, + ChannelWidth_t width, + bool wide_tx_seen) { const bool wide = width == CHANNEL_WIDTH_40 || width == CHANNEL_WIDTH_80; - if (!wide) - _wide_tx_armed.store(false, std::memory_order_relaxed); - const bool armed = _wide_tx_armed.load(std::memory_order_relaxed); _phydm.set_igi_floor( - (_variant == jaguar3::ChipVariant::C8822C && wide && armed) + (_variant == jaguar3::ChipVariant::C8822C && wide && wide_tx_seen) ? jaguar3::DIG_MIN_8822C_WIDE - : 0); + : 0, + channel); } /* Re-program TXAGC from the current knob state (see header). Both TXAGC @@ -2752,29 +2759,35 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, uint8_t pwr_type = _txpkt_dflt_type.load(std::memory_order_relaxed); if (radiotap_pkt_pwr_db != INT_MIN) pwr_type = txpkt_type_for_idx(txpkt_idx_for_qdb(radiotap_pkt_pwr_db * 4)); - /* 8822C: the first >= 40 MHz frame into a >= 40 MHz tune arms the IGI floor - * (DIG_MIN_8822C_WIDE) before it is queued — on a quiet channel the MAC - * would otherwise hold it forever. One _reg_mu take per arming, never again - * on the hot path; the same precedent as the FastRetune above. The tune must - * be wide too: a narrow width disarms (apply_igi_floor_locked), so a 40 MHz - * frame into a 20 MHz tune would re-arm and take the lock on every frame. */ - const ChannelWidth_t tune_bw = _channel.ChannelWidth; - if (_variant == jaguar3::ChipVariant::C8822C && - (bwidth == CHANNEL_WIDTH_40 || bwidth == CHANNEL_WIDTH_80) && - (tune_bw == CHANNEL_WIDTH_40 || tune_bw == CHANNEL_WIDTH_80) && - !_wide_tx_armed.load(std::memory_order_relaxed)) { + /* 8822C, the session's first >= 40 MHz frame: record it (_wide_tx_seen) and + * apply the IGI floor (DIG_MIN_8822C_WIDE) for the CURRENT tune before the + * frame is queued — on a quiet channel the MAC would otherwise hold it + * forever. Whether the floor is on follows the tuned width, which is read + * under _reg_mu here and by every retune (apply_igi_floor_locked), so a + * retune racing this frame either runs first and is seen here, or runs + * after and sees the flag. The flag is published only after the floor is + * applied, so a concurrent sender cannot skip ahead of it. One _reg_mu take + * per session, whatever the tuned width — the hot path stays lock-free. */ + const bool wide_frame = + bwidth == CHANNEL_WIDTH_40 || bwidth == CHANNEL_WIDTH_80; + const bool c_die = _variant == jaguar3::ChipVariant::C8822C; + if (c_die && wide_frame && + !_wide_tx_seen.load(std::memory_order_acquire)) { std::lock_guard lk(_reg_mu); - if (!_wide_tx_armed.load(std::memory_order_relaxed)) { - _wide_tx_armed.store(true, std::memory_order_relaxed); - apply_igi_floor_locked(_channel.ChannelWidth); + if (!_wide_tx_seen.load(std::memory_order_relaxed)) { + apply_igi_floor_locked(_channel.Channel, _channel.ChannelWidth, + /*wide_tx_seen=*/true); + _wide_tx_seen.store(true, std::memory_order_release); } } - /* 8822C only: DISDATAFB=1 — see fill_data_tx_desc_8822c. The 8822E keeps - * the byte-identical descriptor it flies 40 MHz with. */ + /* 8822C, 40/80 MHz frames only: DISDATAFB=1 — see fill_data_tx_desc_8822c. + * A 20 MHz frame keeps DISDATAFB=0 and with it the fw retry ladder + * (DEVOURER_TX_RETRY_FALLBACK); the 8822E keeps the byte-identical + * descriptor it flies 40 MHz with. */ jaguar3::fill_data_tx_desc_8822c( out, static_cast(frame_len), MRateToHwRate(fixed_rate), rate_id, bw_desc, sgi != 0, ldpc != 0, stbc, bmc, ndpa, data_sc, pwr_type, - pkt_offset, /*dis_data_fb=*/_variant == jaguar3::ChipVariant::C8822C); + pkt_offset, /*dis_data_fb=*/c_die && wide_frame); /* A DATA FRAME BELONGS IN A DATA QUEUE. fill_data_tx_desc_8822c stamps * QSEL 0x12 (MGNT) on every descriptor, and init_trx_cfg maps MG to the * 64-page HIGH queue that management frames and the beacon share. So DATA @@ -2821,7 +2834,8 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, if (_cfg.debug.tx_rateid) SET_TX_DESC_RATE_ID_8822C(out, *_cfg.debug.tx_rateid); /* Retry rate-fallback control (DEVOURER_TX_RETRY_FALLBACK): default leaves - * the builder's DISDATAFB=0 — the fw ladder, MCS-native now that the RA + * the builder's DISDATAFB (0, except an 8822C 40/80 MHz frame, which the + * builder forces to 1 — no ladder there either way) — the fw ladder, MCS-native now that the RA * group is family-correct (MCSx -> MCS(x-1) -> 6M floor; CCK floor at * 2.4 GHz); Off pins retries at the descriptor rate. No floor form (the * RetryFallback note in DeviceConfig.h has the measurement). Same diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 81d3e4c8..69b81ad0 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -346,15 +346,22 @@ class RtlJaguar3Device : public IRtlRadio { /* phydm dynamic mechanisms (FA/DIG/CCK-PD/EDCCA), ticked from the coex * thread every ~2 s like the vendor watchdog. */ jaguar3::PhydmRuntimeJaguar3 _phydm; - /* DIG_MIN_8822C_WIDE policy. The floor is armed by the first >= 40 MHz - * frame this device transmits into a >= 40 MHz tune (build_tx_block) and - * re-applied after every tune while the width stays wide; a session that - * only receives at 40 MHz — or only transmits 20 MHz frames into a 40 MHz - * channel, as a ground station does — never arms it and keeps its full RX - * sensitivity. A narrow width or Stop() disarms it. Caller holds _reg_mu - * (or is pre-coex bring-up). */ - void apply_igi_floor_locked(ChannelWidth_t width); - std::atomic _wide_tx_armed{false}; + /* DIG_MIN_8822C_WIDE policy. _wide_tx_seen records that this session has + * transmitted a >= 40 MHz frame (build_tx_block, set once); it is cleared + * by Init, InitWrite and Stop, and nothing else. While it is set the floor + * follows the TUNED width: on at 40/80 MHz, off (IGI clamped back into the + * normal window) at any narrower width. A session that only receives at + * 40 MHz, or only transmits 20 MHz frames into a 40 MHz channel as a ground + * station does, never sets it and keeps its full RX sensitivity. 8822C + * only; on the 8822E the floor stays 0. Caller holds _reg_mu (or is + * pre-coex bring-up). */ + void apply_igi_floor_locked(uint8_t channel, ChannelWidth_t width, + bool wide_tx_seen); + void apply_igi_floor_locked(uint8_t channel, ChannelWidth_t width) { + apply_igi_floor_locked(channel, width, + _wide_tx_seen.load(std::memory_order_relaxed)); + } + std::atomic _wide_tx_seen{false}; SelectedChannel _channel{}; /* Mirrors _channel.ChannelWidth as a devourer bw code (0/1/2 = 20/40/80 MHz) * so the RX completion handler can read the currently-tuned width without From c59a3a32a573a0a9c25e773858ad2242d873fef8 Mon Sep 17 00:00:00 2001 From: Gilang Date: Mon, 12 Oct 2026 00:35:17 +0700 Subject: [PATCH 6/6] docs: 8822C HT40 experiment - one Results section for the final code Folds the original table and both re-test sections into one: GS and local benches in one table, superseded intermediate rows dropped, the retry ladder and the limits stated once. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/experiments/8822c-ht40-tx-stall.md | 130 +++++++++++------------- 1 file changed, 60 insertions(+), 70 deletions(-) diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md index 702f1daa..dc9692ab 100644 --- a/docs/experiments/8822c-ht40-tx-stall.md +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -3,10 +3,9 @@ Why devourer's 8822C could not transmit a single 40 MHz frame on most channels while the same card does HT40 under mainline rtw88, and the two fixes that came out of it (`src/jaguar3/FrameParserJaguar3.h` DISDATAFB, -`PhydmRuntimeJaguar3::set_igi_floor`). Bench: txdemo on the CU (host), -rxdemo on an 8812EU ground-station card at the same channel/width, -1 kB QoS-data frames, MCS3/40/LDPC, 50 s per run; "aired" = frames the -EU received with CRC ok and `bw:1`. +`PhydmRuntimeJaguar3::set_igi_floor`). Bench: txdemo on the CU, rxdemo on +an 8812EU witness at the same channel/width, 1 kB QoS-data frames, +MCS3/LDPC. The two witness setups are under Results. ## Symptom @@ -49,7 +48,7 @@ xmit branch writes) the hard hang is gone — but only on some channels. The fix sets DISDATAFB=1 on the 8822C's 40/80 MHz frames only, so they have no retry ladder at either knob setting. 20 MHz frames keep DISDATAFB=0 and the ladder: 20 MHz with 0 is the configuration #362 -measured, and the HT20 rows below. +measured (see Results for the ladder on the final code). ## Fault 2 — receiver gain on a quiet channel @@ -80,74 +79,65 @@ from the 1 dB IGI step, **not measured**: 6 dB over the DFS floor 0x20 (144, 52), 4-8 dB over the 0x1e..0x22 coverage window on a non-DFS channel (36, 149). Only on a device that transmits wide frames; a CU ground station (40 MHz receive, 20 MHz control frames) never arms it. Nothing changes at -20 MHz or on the 8822E (its descriptor and DIG are byte-identical to before; -re-flown below). - -## Result (no knobs) - -| channel | width | submitted | failed | aired (EU, bw) | -|---|---|---|---|---| -| 144(-) | 40 | 20552 | 0 | 20500 MCS3 bw:1 | -| 52(+) | 40 | 20525 | 0 | 20425 MCS3 bw:1 | -| 149(+) | 40 | 20590 | 0 | 20477 MCS3 bw:1 | -| 36(+) | 40 | 20483 | 0 | 19723 MCS3 bw:1 | -| 144(-) | 20 | 20679 | 0 | 20629 MCS3 bw:0 (IGI stays 0x20) | - -## Re-test after the arming review fix - -Code at the time (arming needed a wide frame AND a wide tune; `Stop()` -disarmed). -Both adapters on one host, 1 kB QoS-data, run-unique SA, txdemo default -2 ms gap, 30 s per cell. Witness = the other local card. Before the run, an -8 s witness scan of 36/52/100/120/144/149/157 at 40 MHz heard 0 frames. - -| cell | DUT -> witness | tune | frames | submitted | failed | witness | IGI | -|---|---|---|---|---|---|---|---| -| 144(-) | CU -> EU | 40 | MCS3/40 | 14161 | 0 | 14109 MCS3 bw:1 | 0x26 | -| 149(+) | CU -> EU | 40 | MCS3/40 | 14146 | 0 | 14094 MCS3 bw:1 | 0x26 | -| 144 | CU -> EU | **20** | MCS3/40 | 14282 | 0 | 0 (expected) | 0x20 | -| 144 | CU -> EU | 20 | MCS3/20 | 14198 | 0 | 14142 MCS3 bw:0 | 0x20 | -| 144(-) | **EU** -> CU | 40 | MCS3/40 | 14455 | 0 | 14321 MCS3 bw:1 | 0x20 | - -The third row is the review fix: 40 MHz frames into a 20 MHz tune leave the -floor unarmed. The fourth is the 20 MHz control. In the fifth, the 8822E -transmitter stays at 0x20, and so does the CU witness, which receives at -40 MHz and never transmits. +20 MHz or on the 8822E (its descriptor and DIG are byte-identical to +before; re-flown in Results). + +## Results + +Two benches, both 1 kB QoS-data at MCS3/LDPC with no other knobs. +"Decoded" means CRC-ok frames the witness heard at the expected rate and +width. +- **GS:** the CU on the host, witnessed by an 8812EU on a separate ground + station. 50 s per run, on the first version of the fix (DISDATAFB=1 on + every C-die frame). For 40 MHz frames on a 40 MHz tune, that version and + the final code behave the same. +- **Local:** both adapters on one host, each witnessing the other + near-field, with a run-unique SA. txdemo's default 2 ms gap, 30 s per + cell, on the final code unless marked †. Beforehand, an 8 s witness scan + of 36/52/100/120/144/149/157 at 40 MHz heard 0 frames. + +| bench | channel | DUT -> witness | tune | frames | submitted | failed | decoded | IGI | +|---|---|---|---|---|---|---|---|---| +| GS | 144(-) | CU -> EU | 40 | MCS3/40 | 20552 | 0 | 20500, bw:1 | | +| GS | 52(+) | CU -> EU | 40 | MCS3/40 | 20525 | 0 | 20425, bw:1 | | +| GS | 149(+) | CU -> EU | 40 | MCS3/40 | 20590 | 0 | 20477, bw:1 | | +| GS | 36(+) | CU -> EU | 40 | MCS3/40 | 20483 | 0 | 19723, bw:1 | | +| local | 144(-) | CU -> EU | 40 | MCS3/40 | 14117 | 0 | 14067, bw:1 | 0x26 | +| local | 144(-) | CU -> EU | 40 | MCS3/20 | 14163 | 0 | 14111, bw:0 | 0x20 | +| local | 144 | CU -> EU | 20 | MCS3/20 | 14285 | 0 | 14232, bw:0 | 0x20 | +| local † | 144 | CU -> EU | 20 | MCS3/40 | 14282 | 0 | 0 (expected) | 0x20 | +| local | 144(-) | EU -> CU | 40 | MCS3/40 | 14456 | 0 | 14406, bw:1 | 0x20 | + +Before the fix, every 40 MHz row aired nothing. + +What the local rows show: +- **40 MHz frames on a 40 MHz tune** arm the floor (0x26). +- **20 MHz frames on a 40 MHz tune** carry DISDATAFB=0 and do not wedge, + so the fault keys on the frame's width, not the tune's. The floor stays + unarmed. +- **40 MHz frames on a 20 MHz tune** leave the floor off. † is from one + revision earlier, which then required a wide tune to arm. The final code + arms the session but applies no floor at 20 MHz, so the result is the + same by construction; this case was not re-run. +- **8822E transmitting HT40** stays at 0x20, and so does the CU witness, + which receives at 40 MHz and never transmits. + +Retry ladder, final code, in the `tests/retry_ladder_probe.sh` shape: +unACKed unicast, MCS3, retry limit 8, CU -> EU on 144. +- **20 MHz:** 1223/1224 frames flew MCS3 x4 > MCS2 > 6M x4, the ladder + #371 documents. +- **40 MHz:** 1208/1209 frames flew MCS3 x9, pinned by DISDATAFB=1. What this does not show: -- The witness is near-field, one desk away. These are pass/fail results, - not range numbers. -- Every cell is a single run. +- Every row is a single run. The local witness is near-field, so those + rows are pass/fail, not range numbers. +- The 36(+) GS row's lower decode count (96.3 % vs ~99.5 % elsewhere) was + not repeated. - The send rate is gap-limited (~460 fps), so a per-frame lock cost would - not show in `submitted`. The third row checks that the floor stays - unarmed, not that the lock is no longer taken. - -## Re-test after the #484 review - -The code now sets DISDATAFB=1 on 40/80 MHz frames only, arms per session, -and clamps IGI when the floor drops. The bench is the same, on silent 144: - -| cell | tune | frames | submitted | failed | witness | IGI | -|---|---|---|---|---|---|---| -| CU -> EU, 144(-) | 40 | MCS3/40 | 14117 | 0 | 14067 MCS3 bw:1 | 0x26 | -| CU -> EU, 144(-) | 40 | **MCS3/20** (DISDATAFB=0) | 14163 | 0 | 14111 MCS3 bw:0 | 0x20 | -| CU -> EU, 144 | 20 | MCS3/20 | 14285 | 0 | 14232 MCS3 bw:0 | 0x20 | -| EU -> CU, 144(-) | 40 | MCS3/40 | 14456 | 0 | 14406 MCS3 bw:1 | 0x20 | - -The second row is the new combination: a 20 MHz frame with DISDATAFB=0 on -a 40 MHz tune does not wedge. The fault keys on the frame's width, not the -tune's. - -Retry ladder, `tests/retry_ladder_probe.sh` shape: unACKed unicast, MCS3, -limit 8, CU -> EU on 144. -- **20 MHz:** 1223/1224 frames flew MCS3 x4 > MCS2 > 6M x4, the ladder - #371 documents, restored. -- **40 MHz:** 1208/1209 frames flew MCS3 x9, pinned by DISDATAFB=1, as - documented. - -Not exercised on air: the IGI clamp when the floor drops (no demo changes -width mid-session), and the retune/first-wide-frame race, which is a -lock-ordering argument, not a measurement. + not show in `submitted`. +- Not exercised on air: the IGI clamp when the floor drops (no demo + changes width mid-session), and the retune/first-wide-frame race, which + rests on lock ordering. Open: 80 MHz inherits the floor untested; 2.4 GHz HT40 (ch 6) drained on the TX side but the EU received none of it — unexplained.