diff --git a/docs/scheduled-mac.md b/docs/scheduled-mac.md index 96c07a0..2658dd2 100644 --- a/docs/scheduled-mac.md +++ b/docs/scheduled-mac.md @@ -190,6 +190,17 @@ worst case doubles, then stops growing). Prefer **16** on an ARQ link, or **8 + a light FEC floor** where airtime is precious; the per-run residual gap analysis is `tests/arq_fec_dimension.py`. +Retries also change RATE by default: every generation's inject path leaves +the firmware fallback ladder enabled, and retried frames step down (measured +on the 8812CU: MCS3 → 54M → 24M → 18M → 9M → 6M, ~10% of retried frames +finishing below the original rate). `DEVOURER_TX_RETRY_FALLBACK=off` pins +every retry at the descriptor rate (measured: 1,200/1,200) — for +constant-rate links where a 6M re-air of an MCS3 frame costs ~4× the +airtime. There is deliberately no floor form: bounding the ladder via +DATA_RTY_LOWEST_RATE was measured anomalous (the fw reinterprets the bound +inside the RA-group rate space — retries wandered into VHT rates with a 20× +retry inflation), see the `RetryFallback` note in `src/DeviceConfig.h`. + Responder-side capability (same setup, J3 TX as the reference soliciting station): **8814AU** closes the loop at retries ~0.1 (the bench responder of choice); **8812AU** works but degraded (97% delivery at ~7 mean retries — diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 6fca6dc..e8c9fb7 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -115,6 +115,19 @@ devourer::DeviceConfig devourer_config_from_env() { } if (env_long("DEVOURER_TX_RETRY_LIMIT", &v)) cfg.tx.retry_limit = static_cast(v < 0 ? 0 : (v > 63 ? 63 : v)); + if (const char *e = env_str("DEVOURER_TX_RETRY_FALLBACK")) { + if (str_ieq(e, "off")) { + cfg.tx.retry_fallback = devourer::RetryFallback::Off; + } else { + /* A floor form was measured and rejected — the fw reinterprets + * DATA_RTY_LOWEST_RATE inside the RA-group rate space (see the + * RetryFallback enum note in DeviceConfig.h). */ + std::fprintf(stderr, + "devourer [W] DEVOURER_TX_RETRY_FALLBACK='%s' unsupported " + "(only \"off\") — keeping the firmware ladder\n", e); + std::fflush(stderr); /* the diagnostic plane is per-line flushed */ + } + } /* ---- bf ---- */ if (const char *snd = env_str("DEVOURER_BF_ARM_SOUNDER")) { diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 23fa626..e4ac8df 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -79,6 +79,19 @@ enum class RxMode : uint8_t { Decoupled, /* naive worker-thread hand-off (known-bad A/B control) */ }; +/* Hardware retry rate-fallback control for injected frames. Every generation + * defaults to the firmware's fallback ladder (measured on the 8812CU: + * retried frames step MCS3 → 54M → 24M → 18M → 9M → 6M, the fw's own 5 GHz + * floor). 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 low-rate retry costs + * multiples of the frame's airtime. A DATA_RTY_LOWEST_RATE floor form was + * measured and REJECTED: the fw reinterprets the bound inside the RA-group + * rate space (inject runs RA-group 9), wandering retries into VHT rates + * (final_rate 45 = VHT1SS_MCS1 on an HT frame) with a 20x retry inflation — + * the field is not a plain DESC_RATE bound on this fw. */ +enum class RetryFallback : uint8_t { Default, Off }; + /* What the spsc-fat ring does when its buffer pool runs dry (consumer behind * under sustained overload). The choice decides which side of the hardware-ARQ * contract survives overload — the chip ACKs on FIFO admission, so anything @@ -205,6 +218,12 @@ struct DeviceConfig { * (firmware-level retry) and on the 8814A die (vendor DATA_RETRY_LIMIT=0 * carve-out kept). */ int retry_limit = 0; + /* 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. */ + 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 * degrades to a per-frame loop and every TX path is byte-identical to diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index a55da29..c028aae 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -1175,6 +1175,13 @@ size_t RtlJaguarDevice::build_tx_block(const uint8_t *packet, size_t length, ? 1 : 0); SET_TX_DESC_RETRY_LIMIT_ENABLE_8812(usb_frame, 1); + /* Retry rate-fallback control (DEVOURER_TX_RETRY_FALLBACK): the data path + * leaves DISABLE_FB at 0 (the fw ladder) by default — only the NDPA branch + * disables it. Off pins retries at the descriptor rate; there is no floor + * form (the RetryFallback note in DeviceConfig.h has the measurement). + * Dword3, inside the checksummed 32 bytes. */ + if (_cfg.tx.retry_fallback == devourer::RetryFallback::Off) + SET_TX_DESC_DISABLE_FB_8812(usb_frame, 1); if (!is_8814a) { /* 88XXau leaves DATA_RETRY_LIMIT=0 for monitor injection on 8814A * (RETRY_LIMIT_ENABLE stays set to 1 in both). diff --git a/src/jaguar2/FrameParserJaguar2.h b/src/jaguar2/FrameParserJaguar2.h index fd31e22..b0b15d3 100644 --- a/src/jaguar2/FrameParserJaguar2.h +++ b/src/jaguar2/FrameParserJaguar2.h @@ -39,6 +39,11 @@ constexpr size_t RXDESC_SIZE_8822B = 24; /* RX_DESC_SIZE_88XX */ #define SET_TX_DESC_CHK_EN_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 14, 1, v) #define SET_TX_DESC_DISDATAFB_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 10, 1, v) #define SET_TX_DESC_DATARATE_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x10, 0, 7, v) +/* DATA_RTY_LOWEST_RATE (0x10 [12:8]): measured NOT a plain DESC_RATE bound on + * the sibling 8822C fw (RA-group reinterpretation — RetryFallback note in + * DeviceConfig.h). Field documented, deliberately unwritten. */ +#define SET_TX_DESC_DATA_RTY_LOWEST_RATE_8822B(d, v) \ + SET_BITS_TO_LE_4BYTE((d) + 0x10, 8, 5, v) #define SET_TX_DESC_DATA_SC_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 0, 4, v) #define SET_TX_DESC_DATA_SHORT_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 4, 1, v) #define SET_TX_DESC_DATA_BW_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 5, 2, v) diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 6646e7f..7608eaa 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -1539,6 +1539,12 @@ size_t RtlJaguar2Device::build_tx_block(const uint8_t *packet, size_t length, * half-duplex link. Both fields sit inside the checksummed span. */ SET_TX_DESC_RTY_LMT_EN_8822B(out, 1); SET_TX_DESC_RTS_DATA_RTY_LMT_8822B(out, _cfg.tx.retry_limit); + /* Retry rate-fallback control (DEVOURER_TX_RETRY_FALLBACK): default leaves + * the builder's DISDATAFB=0 (the fw ladder); Off pins retries at the + * descriptor rate. No floor form (the RetryFallback note in DeviceConfig.h + * has the measurement). Same checksummed span as the retry limit. */ + if (_cfg.tx.retry_fallback == devourer::RetryFallback::Off) + SET_TX_DESC_DISDATAFB_8822B(out, 1); jaguar2::cal_txdesc_chksum_8822b(out); const devourer::AmpduMode am = _ampdu; /* one lock-free load */ if (am.enabled || _cfg.debug.tx_qsel || _cfg.debug.tx_ampdu_max) { diff --git a/src/jaguar3/FrameParserJaguar3.h b/src/jaguar3/FrameParserJaguar3.h index 44d4baa..433bfdd 100644 --- a/src/jaguar3/FrameParserJaguar3.h +++ b/src/jaguar3/FrameParserJaguar3.h @@ -42,6 +42,12 @@ constexpr size_t RXDESC_SIZE_8822C = 24; /* RX_DESC_SIZE_88XX */ #define SET_TX_DESC_NAVUSEHDR_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 15, 1, v) #define SET_TX_DESC_NDPA_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 22, 2, v) #define SET_TX_DESC_DATARATE_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x10, 0, 7, v) +/* DATA_RTY_LOWEST_RATE (0x10 [12:8]): measured NOT a plain DESC_RATE bound — + * the fw reinterprets it inside the RA-group rate space (see the + * RetryFallback note in DeviceConfig.h). Field documented, deliberately + * unwritten. */ +#define SET_TX_DESC_DATA_RTY_LOWEST_RATE_8822C(d, v) \ + SET_BITS_TO_LE_4BYTE((d) + 0x10, 8, 5, v) #define SET_TX_DESC_DATA_SC_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 0, 4, v) #define SET_TX_DESC_DATA_SHORT_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 4, 1, v) #define SET_TX_DESC_DATA_BW_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 5, 2, v) diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index b07516e..dee1c90 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -2017,6 +2017,13 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, * half-duplex link. Both fields sit inside the checksummed span. */ SET_TX_DESC_RTY_LMT_EN_8822C(out, 1); SET_TX_DESC_RTS_DATA_RTY_LMT_8822C(out, _cfg.tx.retry_limit); + /* Retry rate-fallback control (DEVOURER_TX_RETRY_FALLBACK): default leaves + * the builder's DISDATAFB=0 (the fw ladder — measured stepping toward 6M + * on deep retries); Off pins retries at the descriptor rate. No floor form + * (the RetryFallback note in DeviceConfig.h has the measurement). Same + * checksummed span as the retry limit. */ + if (_cfg.tx.retry_fallback == devourer::RetryFallback::Off) + SET_TX_DESC_DISDATAFB_8822C(out, 1); jaguar3::cal_txdesc_chksum_8822c(out); const devourer::AmpduMode am = _ampdu; /* one lock-free load */ if (am.enabled || _cfg.debug.tx_qsel || _cfg.debug.tx_ampdu_max) { diff --git a/tests/arq_e2e_delivery.sh b/tests/arq_e2e_delivery.sh index 080dd86..196da70 100644 --- a/tests/arq_e2e_delivery.sh +++ b/tests/arq_e2e_delivery.sh @@ -44,6 +44,7 @@ MAC1=${MAC1:-02:12:34:56:78:9a} # DUT responder identity = drone RA TX_SA=${TX_SA:-02:aa:bb:cc:dd:01} # drone TA (unicast — the I/G footgun) RETRY_LIMIT=${RETRY_LIMIT:-3} # field report used 3 DRONE_REPORT_N=${DRONE_REPORT_N:-1} # CCX sampling divisor (1 = every frame) +DRONE_FALLBACK=${DRONE_FALLBACK:-} # retry rate fallback: off | "" (=fw ladder) RECEIPT_MS=${RECEIPT_MS:-} # windowed RX receipts cadence (off="") DRONE_RATE=${DRONE_RATE:-MCS3} DRONE_PAYLOAD=${DRONE_PAYLOAD:-512} # >= 30 so the pctr stamp fits @@ -167,6 +168,7 @@ env DEVOURER_VID="$DRONE_VID" DEVOURER_PID="$DRONE_PID" DEVOURER_CHANNEL="$CH" \ DEVOURER_TX_RATE="$DRONE_RATE" DEVOURER_TX_PAYLOAD_BYTES="$DRONE_PAYLOAD" \ DEVOURER_TX_GAP_US="$DRONE_GAP_US" \ DEVOURER_TX_REPORT="$DRONE_REPORT_N" DEVOURER_TX_RETRY_LIMIT="$RETRY_LIMIT" \ + ${DRONE_FALLBACK:+DEVOURER_TX_RETRY_FALLBACK="$DRONE_FALLBACK"} \ ${RECEIPT_MS:+DEVOURER_TX_RECEIPTS=1 DEVOURER_TX_WITH_RX=thread} \ DEVOURER_DIS_CCA="$DRONE_DIS_CCA" \ DEVOURER_TX_PWR_OFFSET_QDB="$PWR_QDB" \