diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md new file mode 100644 index 00000000..dc9692ab --- /dev/null +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -0,0 +1,143 @@ +# 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, 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 + +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 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 (see Results for the ladder on the final code). + +## 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) on the C die +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 +(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 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: +- 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`. +- 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. 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 e99e4ac5..3b153046 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -23,6 +23,15 @@ 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: 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 + 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 @@ -169,7 +178,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..daba817f 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,14 @@ 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 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); SET_TX_DESC_DATA_BW_8822C(d, bw); diff --git a/src/jaguar3/PhydmRuntimeJaguar3.cpp b/src/jaguar3/PhydmRuntimeJaguar3.cpp index 69288bcf..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,6 +32,21 @@ void PhydmRuntimeJaguar3::set_igi(uint8_t igi) { _device.phy_set_bb_reg(0x1d70, 0x00007f00, igi); } +void PhydmRuntimeJaguar3::set_igi_floor(uint8_t floor, uint8_t channel) { + const uint8_t prev = _igi_floor; + _igi_floor = 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 * the per-window false-alarm and CCA counters, then clear and re-arm them so * the next tick sees a fresh window. */ @@ -85,6 +104,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; @@ -205,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 1d75749f..6190c253 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -8,6 +8,21 @@ 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 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 + * 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 * watchdog in a no-link (monitor) session — the RX-side adaptation loop: * @@ -40,6 +55,13 @@ class PhydmRuntimeJaguar3 { * re-applied when they change). */ void on_channel_change() { _cck_lv_valid = false; } + /* 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) --- */ struct FaStats { @@ -62,6 +84,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..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,6 +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.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) */ @@ -866,6 +870,10 @@ 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 (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 (...) { @@ -874,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. */ @@ -954,6 +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.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 */ @@ -1518,6 +1530,7 @@ void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { _channel = channel; _radioManagement.set_channel_bwmode(channel.Channel, channel.ChannelOffset, 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 @@ -1575,6 +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, _channel.ChannelWidth); } void RtlJaguar3Device::FastSetBandwidth(ChannelWidth_t bw) { @@ -1598,6 +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(_channel.Channel, bw); return; } /* Fast path declined (40/80 endpoint, cold radio) — full channel set, under @@ -1606,6 +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(_channel.Channel, bw); +} + +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; + _phydm.set_igi_floor( + (_variant == jaguar3::ChipVariant::C8822C && wide && wide_tx_seen) + ? jaguar3::DIG_MIN_8822C_WIDE + : 0, + channel); } /* Re-program TXAGC from the current knob state (see header). Both TXAGC @@ -2732,10 +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 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_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, 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); + 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 @@ -2782,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 15de9c39..69b81ad0 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -346,6 +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. _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