diff --git a/docs/experiments/8822c-ht40-tx-stall.md b/docs/experiments/8822c-ht40-tx-stall.md new file mode 100644 index 00000000..d904d3ed --- /dev/null +++ b/docs/experiments/8822c-ht40-tx-stall.md @@ -0,0 +1,242 @@ +# 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 on +the C die, and a wide-TX IGI floor on both Jaguar3 dies — +`DeviceConfig::Tuning::wide_tx_igi_floor`, applied by +`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 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). + +**Silence, not DFS, is the trigger.** Every silent channel above is a DFS +channel, where DIG pins IGI at 0x20, so the two were confounded until a +silent non-DFS channel was run: on 157(+) (an 8 s scan heard 0-4 frames) +the CU with the floor off hangs the same way at IGI 0x1e — 257 / 8 / 0, +and 335 / 12 / 74 on a second run — and 36(+), with a few 20 MHz frames a +second of ambient traffic, still wedges after 850. The floor therefore +applies on every channel, not only DFS ones. + +**The edge is per unit, and it is not C-die-only.** A second bench (the +#484 review: LB-LINK WDN1300H 8812CU, chip-id 0x13 cut 3, and an LB-LINK +BL-M8812EU2 8812EU, rfe 21) found the same stall on its CU with a higher +edge, and on its EU, where our EU never showed it — see Results. Four +units so far: + +| unit | 0x20 | 0x24 | 0x26 | 0x2c | 0x35 | +|---|---|---|---|---|---| +| 8812CU, this bench | hang (0x1e on 157 too) | clean | clean | clean | clean | +| 8812CU, second bench | hang | | hang | ~1.5 k then wedge | clean | +| 8812EU, this bench | clean (0x1e on 157 too) | | | | clean | +| 8812EU, second bench | hang | | | clean | clean | + +(silent 144(-), MCS3/40; blank = not run; "hang" = the 257-submitted / +0-aired shape; this bench's 0x22 also hung). The vendor phydm has no +bandwidth-raised DIG bound to lean on (`phydm_dig_abs_boundary_decision` +pins DFS at `DIG_MIN_DFS` at every width), so the number is devourer's own. + +The frame shape does not matter: the demo's default 148-byte management +frame (no `DEVOURER_TX_QOS_DATA`) stalls the same way on silent 144(-) at +40 MHz on both dies, master and the first floor revision alike (second +bench: 953 submitted / 8 failed / 0 witnessed). + +## The fix + +`wide_tx_igi_floor` (env `DEVOURER_WIDE_TX_IGI_FLOOR`, 0 = off), default +**0x35** — the lowest value measured to clear every unit above, not a +margin over a characterized edge: the second CU's edge sits somewhere in +0x2c..0x35 and was not bisected, and a fifth unit may need more. A unit +whose own edge has been measured can take a lower value. On both dies, +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, estimated from the +~1 dB IGI step, **not measured**: 0x35 is 21 steps over the DFS floor 0x20 +(144, 52) and 19-23 over the 0x1e..0x22 coverage window on a non-DFS +channel (36, 149). Only on a device that transmits wide frames; a ground +station (40 MHz receive, 20 MHz control frames) never arms it. An earlier +revision used 0x26 on the C die only — one step over this bench's CU, and +inert on the second bench's. + +DISDATAFB stays C-only: the second bench's EU flew 40 MHz with +DISDATAFB=0 and the floor. + +## Results + +Four bench sessions, all 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, floor 0x26 on the C die). For 40 MHz frames on a + 40 MHz tune, that version and the 0x26 revision 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 0x26 / C-die-only revision unless marked †. Beforehand, an + 8 s witness scan of 36/52/100/120/144/149/157 at 40 MHz heard 0 frames. +- **Final:** the local pair again, on the final code (default floor 0x35 + on both dies unless the floor column says otherwise, via + `DEVOURER_WIDE_TX_IGI_FLOOR`), 30 s of frames per cell bounded by + `DEVOURER_TX_FRAMES=13800` (submitted counts txdemo's own 50 lead + frames on top), run-unique SA, TX started only once the witness was in + its RX loop. The EU-TX cells ran at `DEVOURER_TX_PWR_OFFSET_QDB=-80` + (-20 dB): at full power the near-field CU witness read RSSI 88 / EVM -33, + the saturation signature. 8 s scans at 40 MHz: 144(-) and 157(+) 0 + frames, 36(+) 36, 44(+) 103, 149(+) 71 (all 20 MHz). +- **Second:** the #484 review's bench (units above), near-field, no rtw88 + loaded, 2 ms gap, 20 s per cell, single runs. 144(-) and 100(+) silent + (8 s witness scan at 40 MHz: 0 frames). "PR" = the 0x26 / C-die-only + revision; a floor column other than 0x26 on the CU, or any floor on the + EU, is that tree with the floor overridden or the C-die test removed. + +| bench | channel | DUT -> witness | tune | frames | floor | submitted | failed | decoded | +|---|---|---|---|---|---|---|---|---| +| GS | 144(-) | CU -> EU | 40 | MCS3/40 | 0x26 | 20552 | 0 | 20500, bw:1 | +| GS | 52(+) | CU -> EU | 40 | MCS3/40 | 0x26 | 20525 | 0 | 20425, bw:1 | +| GS | 149(+) | CU -> EU | 40 | MCS3/40 | 0x26 | 20590 | 0 | 20477, bw:1 | +| GS | 36(+) | CU -> EU | 40 | MCS3/40 | 0x26 | 20483 | 0 | 19723, bw:1 | +| local | 144(-) | CU -> EU | 40 | MCS3/40 | 0x26 | 14117 | 0 | 14067, bw:1 | +| local | 144(-) | CU -> EU | 40 | MCS3/20 | none (0x20) | 14163 | 0 | 14111, bw:0 | +| local | 144 | CU -> EU | 20 | MCS3/20 | none (0x20) | 14285 | 0 | 14232, bw:0 | +| local † | 144 | CU -> EU | 20 | MCS3/40 | none (0x20) | 14282 | 0 | 0 (expected) | +| local | 144(-) | EU -> CU | 40 | MCS3/40 | none (0x20) | 14456 | 0 | 14406, bw:1 | +| second, master | 144(-) | CU -> EU | 40 | MCS3/40 | – | 257 | 8 | 0 | +| second, PR | 144(-) | CU -> EU | 40 | MCS3/40 | 0x26 | 257 | 8 | 0 | +| second, PR + `DIS_CCA=1` | 144(-) | CU -> EU | 40 | MCS3/40 | 0x26 | 257 | 8 | 0 | +| second, PR | 100(+) | CU -> EU | 40 | MCS3/40 | 0x26 | 308 | 8 | 51 | +| second, PR | 36(+), busy | CU -> EU | 40 | MCS3/40 | 0x26 | 7842 | 0 | 7727 | +| second, PR + flooder | 144(-) | CU -> EU | 40 | MCS3/40 | 0x26 | 7321 | 8 | 7064 | +| second | 144(-) | CU -> EU | 40 | MCS3/40 | 0x2c | 1752 | 8 | 1495 | +| second | 144(-) | CU -> EU | 40 | MCS3/40 | 0x35 | 7851 | 0 | 7801 | +| second | 100(+) | CU -> EU | 40 | MCS3/40 | 0x35 | 7837 | 0 | 7787 | +| second, PR | 144(-) | EU -> CU | 40 | MCS3/40 | none (0x20) | 257 | 8 | 0 | +| second, PR | 100(+) | EU -> CU | 40 | MCS3/40 | none | 269 | 8 | 12 | +| second, PR + flooder | 144(-) | EU -> CU | 40 | MCS3/40 | none | 4811 | 343 | 4095 | +| second | 144(-) | EU -> CU | 40 | MCS3/40 | 0x2c | 7858 | 6 | 7794 | +| second | 144(-) | EU -> CU | 40 | MCS3/40 | 0x35 | 7787 | 4 | 7725 | +| second | 100(+) | EU -> CU | 40 | MCS3/40 | 0x35 | 7897 | 3 | 7844 | +| second, PR | 144(-) | CU -> EU | 40 | MCS3/20 | none (0x20) | 7857 | 0 | 7807, bw:0 | +| final | 144(-) | CU -> EU | 40 | MCS3/40 | off (0x20) | 257 | 8 | 0 | +| final | 144(-) | CU -> EU | 40 | MCS3/40 | 0x35 | 13850 | 0 | 13800, bw:1 | +| final | 157(+) | CU -> EU | 40 | MCS3/40 | off (0x1e) | 335 | 12 | 74 | +| final | 157(+) | CU -> EU | 40 | MCS3/40 | off (0x1e) | 257 | 8 | 0 | +| final | 157(+) | CU -> EU | 40 | MCS3/40 | 0x35 | 13850 | 0 | 13748, bw:1 | +| final | 36(+), light | CU -> EU | 40 | MCS3/40 | off (0x1e) | 850 | 8 | 786 | +| final | 144(-) | EU -> CU | 40 | MCS3/40 | off (0x20) | 13850 | 0 | 13800, bw:1 | +| final | 157(+) | EU -> CU | 40 | MCS3/40 | off (0x1e) | 13850 | 0 | 13800, bw:1 | +| final | 157(+) | EU -> CU | 40 | MCS3/40 | 0x35 | 13850 | 0 | 13800, bw:1 | + +Before the fix, every GS and local 40 MHz row aired nothing. "Flooder" = +an 8822BU sending MCS0/20 at 50 fps on the channel; in the CU flooder row +the 8 failures are the last 8 sends, after the flooder stopped ~2 s before +the DUT did — the hang resumes the moment the channel goes quiet. + +What the local rows show: +- **40 MHz frames on a 40 MHz tune** arm the floor. +- **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 (both benches). +- **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. Later 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. +- **This bench's 8822E transmitting HT40** needed no floor (it ran under + the C-die-only revision), and the CU witness, which receives at 40 MHz + and never transmits, stays at 0x20. The second bench's EU contradicts + the first half — which is why the floor now arms on both dies. + +Retry ladder, 0x26 revision (DISDATAFB is unchanged since), 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 0x35 default has been flown on this bench's two units (final + rows) and, forced by hand, on the second bench's; a fifth unit is + unknown. The EU-at-0x35 rows show only that arming it costs this EU + nothing near-field — this EU never needed it. +- Every row is a single run, so the 0x2c and the partial rows are + indicative; the 0-vs-7.8k rows are outside any noise band. Both local + benches are near-field, so the rows are pass/fail, not range numbers. +- The RX-sensitivity cost of the floor — the thing a lower default would + buy back — is estimated, not measured. +- 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. +- Only one silent non-DFS channel (157(+)) was available; 36(+) is the + only lightly-loaded one run. + +Open: 80 MHz is only partly working — the second bench's CU -> EU on +149(+), VHT1SS_MCS3/80 with floor 0x26 ran 470 submitted / 10 failed / +209 decoded, and 0x35 has not been tried there. 2.4 GHz HT40 (ch 6) +drained on the TX side but the EU received none of it — unexplained. diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index ca169f3a..21748e95 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -235,6 +235,9 @@ devourer::DeviceConfig devourer_config_from_env() { if (const char *e = env_str("DEVOURER_THERMAL_TRACK")) cfg.tuning.thermal_track = std::strcmp(e, "0") != 0; cfg.tuning.disable_cca = env_flag("DEVOURER_DIS_CCA"); + /* Jaguar3 40/80 MHz TX IGI floor; 0 = off, 7-bit IGI field. */ + if (env_long("DEVOURER_WIDE_TX_IGI_FLOOR", &v) && v >= 0 && v <= 0x7f) + cfg.tuning.wide_tx_igi_floor = static_cast(v); if (env_long("DEVOURER_FASTRETUNE_FW", &v) && v >= 0) cfg.tuning.fastretune_fw = static_cast(v); if (env_long("DEVOURER_KFR_OFLD", &v) && v >= 0) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 17a7ad9d..a5949432 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,9 +287,10 @@ 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 / - * DISABLE_FB = 1): retries re-air at the descriptor rate. See the - * RetryFallback enum for why there is no floor form. */ + * 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; /* env: DEVOURER_TX_USB_AGG — USB TX aggregation: max frames packed into * one bulk-OUT URB by send_packets (0 = off, the default: send_packets @@ -410,6 +416,31 @@ struct DeviceConfig { * Default-on on the streamtx FPV downlink (the link owns the * channel). */ bool disable_cca = false; + /* env: DEVOURER_WIDE_TX_IGI_FLOOR — Jaguar3 (8822C and 8822E) DIG lower + * bound at 40/80 MHz, raw IGI (0x1d70[14:8], 0..0x7f; 0 = off). Without + * it, on a QUIET channel the receiver parks in a gain state where the + * secondary 20 MHz never reads idle, so the MAC holds the first 40 MHz + * frame forever and the TX FIFO fills; any received traffic re-runs the + * AGC, which is why a busy channel hides it. Quiet is the trigger, not + * DFS: silent non-DFS 157(+) hangs the same way at IGI 0x1e, so the + * floor applies on every channel. Armed by the session's first + * >= 40 MHz transmitted frame, then applied while tuned to 40/80 MHz and + * dropped at a narrower width; a receive-only session, or one that sends + * only 20 MHz frames into a 40 MHz tune, keeps its normal DIG window. + * + * The edge is PER UNIT, measured on silent 144(-) at MCS3/40: one 8812CU + * clears at 0x24 (0x22 hangs); a second 8812CU still hangs at 0x26 and + * airs ~1.5 k frames then wedges at 0x2c, clean at 0x35; one 8812EU + * never stalls at its normal 0x20; a second 8812EU hangs at 0x20, clean + * at 0x2c and 0x35. The default is the lowest value measured to clear + * every unit so far, not a margin over a characterized edge — a further + * unit may need more. Cost: idle-channel RX sensitivity at 40/80 MHz on + * a wide-transmitting device, estimated from the ~1 dB IGI step and NOT + * measured: 0x35 is 21 steps over the DFS floor 0x20 and 19-23 over the + * 0x1e..0x22 non-DFS window. Lower it to a unit's own measured edge to + * buy that back. 80 MHz carries the same floor, only partially + * validated. docs/experiments/8822c-ht40-tx-stall.md. */ + uint8_t wide_tx_igi_floor = 0x35; /* env: DEVOURER_TXPKT_STEP_QDB — Jaguar3 per-packet power-bank step size * in quarter-dB: the dB weight of one 0x1e70 offset-index step * (SetTxPacketPowerOffsetQdb / radiotap DBM_TX_POWER). diff --git a/src/jaguar3/CLAUDE.md b/src/jaguar3/CLAUDE.md index e99e4ac5..ed8bc30c 100644 --- a/src/jaguar3/CLAUDE.md +++ b/src/jaguar3/CLAUDE.md @@ -23,6 +23,20 @@ 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. +- **40 MHz inject needs two things.** (1) 8822C only: 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 — the 8822E flies 40 MHz + with 0). (2) Both dies: a DIG floor at 40/80 MHz + (`DeviceConfig::Tuning::wide_tx_igi_floor`, default 0x35, 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 edge is **per unit** + (0x24 on one 8812CU, above 0x2c on another; one 8812EU never stalls, a + second needs it), so a passing unit says nothing about the next; the + default is the lowest value measured to clear all four, and its + RX-sensitivity cost (~21 dB over the DFS floor) is estimated, not measured. + 80 MHz and 2.4 GHz HT40 are open. Mechanism, per-unit table 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 +183,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; at 40/80 MHz on a device that has transmitted a +wide frame the clamp sits at `wide_tx_igi_floor`, default 0x35 — 19+ 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..d7e0b545 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 >= 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..31c6eef0 100644 --- a/src/jaguar3/PhydmRuntimeJaguar3.h +++ b/src/jaguar3/PhydmRuntimeJaguar3.h @@ -40,6 +40,14 @@ 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 wide-TX floor + * this carries, and why it exists: DeviceConfig::Tuning:: + * wide_tx_igi_floor. */ + void set_igi_floor(uint8_t floor, uint8_t channel); + private: /* --- false-alarm / CCA statistics (this tick's window) --- */ struct FaStats { @@ -62,6 +70,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..1adde54e 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 wide-TX 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,9 @@ 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 */ + /* _wide_tx_seen was just cleared, so this never arms a floor: it only drops + * one a previous session left in _phydm. */ + 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 +872,10 @@ void RtlJaguar3Device::Stop() { if (_station.armed()) (void)_station.clear(_device, _logger, "Jaguar3"); } + /* The wide-TX 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 +884,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 wide-TX 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 +967,8 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { channel.ChannelWidth); if (want_rx) _hal.enable_rx_path(); /* RF into RX mode — same slot as the Init path */ + /* Clear-only, as in Init: _wide_tx_seen was just reset. */ + 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 +1533,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 @@ -1568,6 +1584,11 @@ void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { if (band_change && _brought_up && (_tx_pwr_offset_steps != 0 || _tx_pwr_override >= 0)) apply_tx_power_current(/*full=*/true); + /* No apply_igi_floor_locked here, deliberately: the lean hop keeps the + * width and leaves IGI (0x1d70) where it found it, and the floor's value + * does not depend on the channel's DFS-ness — whatever floor the previous + * tune applied is still the right one. (dig() clamps to the floor on + * every tick regardless.) */ return; } /* Fast path declined (band change / never tuned) — full channel set at the @@ -1575,6 +1596,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 +1620,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 +1629,15 @@ 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( + (wide && wide_tx_seen) ? _cfg.tuning.wide_tx_igi_floor : 0, channel); } /* Re-program TXAGC from the current knob state (see header). Both TXAGC @@ -2732,10 +2764,36 @@ 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)); + /* Either die, the session's first >= 40 MHz frame: record it + * (_wide_tx_seen) and apply the IGI floor (DeviceConfig::Tuning:: + * wide_tx_igi_floor; 0 = off, no arm) 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 (wide_frame && _cfg.tuning.wide_tx_igi_floor != 0 && + !_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 +2840,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..5d77c6a5 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; + /* Wide-TX IGI floor policy (DeviceConfig::Tuning::wide_tx_igi_floor, both + * dies). _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. 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