Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/experiments/8822c-ht40-tx-stall.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 6 additions & 1 deletion src/DeviceConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 13 additions & 1 deletion src/jaguar3/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/jaguar3/FrameParserJaguar3.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(TXDESC_SIZE_8822C));
SET_TX_DESC_LS_8822C(d, 1);
Expand All @@ -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);
Expand Down
27 changes: 24 additions & 3 deletions src/jaguar3/PhydmRuntimeJaguar3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions src/jaguar3/PhydmRuntimeJaguar3.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
Loading