From c8b7917b0a63bea2c5de3b93581d80c5c36a6d45 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:07:54 +0300 Subject: [PATCH 1/4] DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever, every generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MAC writes a frame off (and retries) when no ACK is counted within its response window, and round-trip propagation eats ~6.7 µs/km — so the window is what caps hardware-ARQ distance. It is one 8-bit µs register everywhere: REG_ACKTO 0x640 on the 11ac generations (J1/J2 default 0x80 — the vendor doubled its own 0x40 "for BCM IOT"; J3 halmac default 0x21, scaled to 0x3D/0x75 at 10/5 MHz narrowband), R_AX_RSP_CHK_SIG 0xCC00 byte0 on Kestrel (the field its vendor narrowband path scales). The knob (1..255 µs, 0 = per-chip default) applies at bring-up; on J3 it overrides the per-bandwidth default init_wmac_cfg writes, so an override must budget the slowest ACK duration in use. Bench (8812CU TX -> live 8812EU responder, MCS3, retry 8): default: ok=100% retries {0:891, 1:3} ACKTO=8: ok=0% retries pinned {8:907} — the window closes before the ACK's flight time, every frame written off despite the responder ACKing: the register provably gates the ARQ verdict ACKTO=255: ok=100% retries {0:897, 1:5} — opening the window costs nothing at bench range Register-proof on the other families: 0x640 reads back 0xfa on the 8821AU and 0xCC00 byte0 0xfa on the 8832CU with ACKTO=250. Also: the DeviceConfig retry_limit doc no longer claims the knob is inert on Kestrel (its WD attempts field carries it). Co-Authored-By: Claude Opus 4.8 --- docs/scheduled-mac.md | 5 +++++ examples/common/env_config.cpp | 2 ++ src/DeviceConfig.h | 31 +++++++++++++++++++++++-------- src/jaguar1/RtlJaguarDevice.cpp | 6 ++++++ src/jaguar2/RtlJaguar2Device.cpp | 4 ++++ src/jaguar3/RtlJaguar3Device.cpp | 4 ++++ src/kestrel/RtlKestrelDevice.cpp | 4 ++++ 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/docs/scheduled-mac.md b/docs/scheduled-mac.md index 22eb345..d3ec34a 100644 --- a/docs/scheduled-mac.md +++ b/docs/scheduled-mac.md @@ -186,6 +186,11 @@ witness capture of a 9-copy truth. Kestrel has no CCX `tx.report` path, so the witness copy-count is the retry ground truth there — the fw-level delivery outcome stays invisible until a receipts-tier consumer counts it. +The ACK window itself is a knob — `DEVOURER_ACK_TIMEOUT_US`, the +hardware-ARQ *range* lever (round-trip propagation eats ~6.7 µs/km; the +per-chip defaults and the bench proof live at the field doc in +`src/DeviceConfig.h`). + Choosing the limit (`tests/arq_retry_sweep.sh`, collision regime: a ~1 k fps retrying unicast flood into an 8812EU duplex ground station airing PixelPilot-shaped feedback bursts, near-field): retries are backoff-spaced, diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 7d98775..36e00cf 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -121,6 +121,8 @@ devourer::DeviceConfig devourer_config_from_env() { std::fflush(stderr); } } + if (env_long("DEVOURER_ACK_TIMEOUT_US", &v)) + cfg.tx.ack_timeout_us = static_cast(v < 1 ? 1 : (v > 255 ? 255 : v)); 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")) { diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index d90b4f0..2307554 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -213,15 +213,30 @@ struct DeviceConfig { * Runtime equivalent: StartCwTone/StopCwTone on the concrete device. */ bool cw_tone = false; uint8_t cw_tone_gain = 0; - /* env: DEVOURER_TX_RETRY_LIMIT — per-frame hardware retry limit (0..63). - * Maps to the TX descriptor DATA_RETRY_LIMIT / RTS_DATA_RTY_LMT field - * (Dword4 bits 18-23). 0 = no retries (WFB default: FEC provides - * reliability, not MAC retries). On a busy half-duplex link retries flood - * the air and blind the receiver. Hardware-ARQ (SetAckResponder + unicast - * TA, docs/scheduled-mac.md) needs a nonzero value. Inert on Kestrel - * (firmware-level retry) and on the 8814A die (vendor DATA_RETRY_LIMIT=0 - * carve-out kept). */ + /* env: DEVOURER_TX_RETRY_LIMIT — per-frame hardware retry limit (0..63; + * Kestrel ceiling 62 — its attempts-counting WD field folds +1). Maps to + * the TX descriptor DATA_RETRY_LIMIT / RTS_DATA_RTY_LMT field on the + * 11ac generations and wd_info DATA_TXCNT_LMT on Kestrel. 0 = no retries + * (WFB default: FEC provides reliability, not MAC retries). On a busy + * half-duplex link retries flood the air and blind the receiver. + * Hardware-ARQ (SetAckResponder + unicast TA, docs/scheduled-mac.md) + * needs a nonzero value. Inert on the 8814A die only (vendor + * DATA_RETRY_LIMIT=0 carve-out kept pending its bench). */ int retry_limit = 0; + /* env: DEVOURER_ACK_TIMEOUT_US — hardware ACK/CTS response window in µs + * (1..255; 0 = per-chip default), the hardware-ARQ RANGE lever: the MAC + * writes off a frame (and retries) when no ACK is counted within this + * window, and round-trip propagation eats ~6.7 µs per km, so a long + * link needs the window opened. One 8-bit register everywhere: + * REG_ACKTO 0x640 on the 11ac generations (defaults: 0x80 J1/J2's MAC + * reset value; 0x21 J3 halmac, scaled to 0x3D/0x75 at 10/5 MHz + * narrowband — the override REPLACES the per-bandwidth default, so + * budget the slowest ACK duration in use), R_AX_RSP_CHK_SIG 0xCC00 + * byte0 on Kestrel. Applied at bring-up. Bench: shrinking it below the + * ACK flight time (8 µs) pins retries at the limit with 0%% ok against + * a live responder — the register provably gates the ARQ verdict — + * while 255 behaves as the default at bench range. */ + int ack_timeout_us = 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 / diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 27013bf..776a7e5 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -80,6 +80,9 @@ void RtlJaguarDevice::InitWrite(SelectedChannel channel) { * DEVOURER_DIS_CCA. Always applied — the enable path is what programs * the BB EDCCA thresholds off their parked never-trigger table value. */ SetCcaMode(_cfg.tuning.disable_cca); + /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ + if (_cfg.tx.ack_timeout_us > 0) + _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) @@ -1333,6 +1336,9 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, * DEVOURER_DIS_CCA. Always applied — the enable path is what programs * the BB EDCCA thresholds off their parked never-trigger table value. */ SetCcaMode(_cfg.tuning.disable_cca); + /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ + if (_cfg.tx.ack_timeout_us > 0) + _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 5eb26f6..1c7f87c 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -249,6 +249,10 @@ void RtlJaguar2Device::bring_up(SelectedChannel channel) { if (_cfg.tuning.disable_cca) SetCcaMode(true); + /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ + if (_cfg.tx.ack_timeout_us > 0) + _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); + /* DEVOURER_XTAL_CAP — apply the crystal-cap trim once the AFE is up * (issue #217, the narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 89b1f9b..3abe32c 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -878,6 +878,10 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { else _txpkt_img.store(0, std::memory_order_relaxed); apply_dpdt_route_8822e(); /* 8822E DPDT/eFEM pin-mux (post-coex) */ + /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — overrides the + * halmac per-bandwidth REG_ACKTO default init_wmac_cfg just wrote. */ + if (_cfg.tx.ack_timeout_us > 0) + _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */ if (_cfg.debug.bb_dump) { /* Full MAC+BB dump (0x000..0x4ffc — MAC plane, then BB incl. the RF diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index a153416..c730085 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -254,6 +254,10 @@ void RtlKestrelDevice::InitWrite(SelectedChannel channel) { * re-asserts explicitly (idempotent) when the knob is set. */ if (_cfg.tuning.disable_cca) SetCcaMode(true); + /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — byte0 of + * R_AX_RSP_CHK_SIG (the field the vendor's narrowband path scales). */ + if (_cfg.tx.ack_timeout_us > 0) + _device.rtw_write8(0xCC00, static_cast(_cfg.tx.ack_timeout_us)); _tx_mgmt_ep = _device.nth_bulk_out_ep(0); /* B0MG -> BULKOUTID0 */ _tx_data_ep = _device.nth_bulk_out_ep(3); /* ACH0 -> BULKOUTID3 */ if (_tx_mgmt_ep == 0) { From 10820f3c4c95f82585c779a8ef2772f1083871a8 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:12:01 +0300 Subject: [PATCH 2/4] =?UTF-8?q?ACK=20window:=20pair=20the=20adversarial=20?= =?UTF-8?q?measurement=20=E2=80=94=20long=20windows=20slow=20write-offs=20?= =?UTF-8?q?1.8x=20under=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "255 behaves as the default" claim held only when ACKs arrive. The other half, measured (dead RA, retry 8, max duty on the 8812CU): every retry of a lost frame waits the full window, so write-off cadence drops 2719 -> 2015 -> 1507 frames/8 s at 33/128/255 µs (~1.8x slower at max). Size the window to the link (~6.7 µs/km + margin); the 0/vendor-faithful default exists precisely so nobody pays this tax without opting in. Co-Authored-By: Claude Opus 4.8 --- src/DeviceConfig.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 2307554..9659d08 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -235,7 +235,13 @@ struct DeviceConfig { * byte0 on Kestrel. Applied at bring-up. Bench: shrinking it below the * ACK flight time (8 µs) pins retries at the limit with 0%% ok against * a live responder — the register provably gates the ARQ verdict — - * while 255 behaves as the default at bench range. */ + * while 255 behaves as the default at bench range WHEN ACKS ARRIVE. + * The adversarial half: every retry of a LOST frame waits the full + * window, so a long window slows the write-off cadence under loss — + * measured (dead RA, retry 8, max duty): 33 µs default 2719 write-offs + * /8 s vs 128 µs 2015 vs 255 µs 1507 (~1.8x slower). Size the window + * to the link (~6.7 µs/km + ACK flight + margin), don't just max it — + * and that is why the default is 0/vendor-faithful, not 255. */ int ack_timeout_us = 0; /* env: DEVOURER_TX_RETRY_FALLBACK — "off" | unset. Unset = the firmware * fallback ladder with its own floor (the current behaviour, descriptors From 9421fbf7fbfd3cf55a663bbcbc8cd25d069cf7ef Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:14:07 +0300 Subject: [PATCH 3/4] Review round: explicit =0 keeps the default, apply-site clamp, ACK-only doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DEVOURER_ACK_TIMEOUT_US=0 no longer collapses to 1 µs (which would write off every frame) — 0 passes through as use-per-chip-default; verified live: =0 leaves REG_ACKTO at the halmac 0x21. - Programmatic ack_timeout_us > 255 clamps at every apply site instead of silently wrapping through the uint8_t cast. - The doc claims the ACK window only — the CTS window (REG_CTS2TO 0x641) is a separate register this knob does not touch. Co-Authored-By: Claude Opus 4.8 --- examples/common/env_config.cpp | 4 +++- src/DeviceConfig.h | 8 +++++--- src/jaguar1/RtlJaguarDevice.cpp | 6 ++++-- src/jaguar2/RtlJaguar2Device.cpp | 3 ++- src/jaguar3/RtlJaguar3Device.cpp | 3 ++- src/kestrel/RtlKestrelDevice.cpp | 3 ++- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 36e00cf..fbe2b41 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -122,7 +122,9 @@ devourer::DeviceConfig devourer_config_from_env() { } } if (env_long("DEVOURER_ACK_TIMEOUT_US", &v)) - cfg.tx.ack_timeout_us = static_cast(v < 1 ? 1 : (v > 255 ? 255 : v)); + /* 0 = per-chip default (no write); clamp only the top — an explicit 0 + * collapsing to 1 us would write off every frame. */ + cfg.tx.ack_timeout_us = static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); 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")) { diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 9659d08..ad230a2 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -223,8 +223,9 @@ struct DeviceConfig { * needs a nonzero value. Inert on the 8814A die only (vendor * DATA_RETRY_LIMIT=0 carve-out kept pending its bench). */ int retry_limit = 0; - /* env: DEVOURER_ACK_TIMEOUT_US — hardware ACK/CTS response window in µs - * (1..255; 0 = per-chip default), the hardware-ARQ RANGE lever: the MAC + /* env: DEVOURER_ACK_TIMEOUT_US — hardware ACK response window in µs + * (1..255, clamped; 0 = per-chip default), the hardware-ARQ RANGE + * lever: the MAC * writes off a frame (and retries) when no ACK is counted within this * window, and round-trip propagation eats ~6.7 µs per km, so a long * link needs the window opened. One 8-bit register everywhere: @@ -232,7 +233,8 @@ struct DeviceConfig { * reset value; 0x21 J3 halmac, scaled to 0x3D/0x75 at 10/5 MHz * narrowband — the override REPLACES the per-bandwidth default, so * budget the slowest ACK duration in use), R_AX_RSP_CHK_SIG 0xCC00 - * byte0 on Kestrel. Applied at bring-up. Bench: shrinking it below the + * byte0 on Kestrel. The neighbouring CTS window (REG_CTS2TO 0x641) is + * a separate register this knob does NOT touch. Applied at bring-up. Bench: shrinking it below the * ACK flight time (8 µs) pins retries at the limit with 0%% ok against * a live responder — the register provably gates the ARQ verdict — * while 255 behaves as the default at bench range WHEN ACKS ARRIVE. diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 776a7e5..78659f6 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -82,7 +82,8 @@ void RtlJaguarDevice::InitWrite(SelectedChannel channel) { SetCcaMode(_cfg.tuning.disable_cca); /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) @@ -1338,7 +1339,8 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, SetCcaMode(_cfg.tuning.disable_cca); /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 1c7f87c..822e5f6 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -251,7 +251,8 @@ void RtlJaguar2Device::bring_up(SelectedChannel channel) { /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — apply the crystal-cap trim once the AFE is up * (issue #217, the narrowband CFO lever). */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 3abe32c..b583c18 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -881,7 +881,8 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — overrides the * halmac per-bandwidth REG_ACKTO default init_wmac_cfg just wrote. */ if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast(_cfg.tx.ack_timeout_us)); + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */ if (_cfg.debug.bb_dump) { /* Full MAC+BB dump (0x000..0x4ffc — MAC plane, then BB incl. the RF diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index c730085..651e73a 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -257,7 +257,8 @@ void RtlKestrelDevice::InitWrite(SelectedChannel channel) { /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — byte0 of * R_AX_RSP_CHK_SIG (the field the vendor's narrowband path scales). */ if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0xCC00, static_cast(_cfg.tx.ack_timeout_us)); + _device.rtw_write8(0xCC00, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); _tx_mgmt_ep = _device.nth_bulk_out_ep(0); /* B0MG -> BULKOUTID0 */ _tx_data_ep = _device.nth_bulk_out_ep(3); /* ACH0 -> BULKOUTID3 */ if (_tx_mgmt_ep == 0) { From 549ead7f222d3a5bbecd10aed26aa422e62e9a18 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:25:05 +0300 Subject: [PATCH 4/4] =?UTF-8?q?ACK=20window:=20one=20library=20default=20(?= =?UTF-8?q?128=20=C2=B5s)=20on=20every=20generation,=20not=20a=20per-chip?= =?UTF-8?q?=20sentinel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0-means-vendor-default design exported four chips' defaults through one knob: the same configuration gave 128 µs of range budget on an 8812AU and 33 µs on an 8812CU — hardware-ARQ range silently depended on which die was plugged, the same per-chip-divergence disease the EDCCA silent no-op was (and the opposite of the retry-limit knob, whose +1 fold exists precisely so N means N everywhere). Now: ack_timeout_us defaults to 128 and is programmed identically on every generation at bring-up. 128 is the vendor's interop-blessed J1/J2 value and covers the slowest narrowband ACK in the tree (5 MHz vendor value 117 µs), so the per-bandwidth special cases collapse too; J3 gains ~12 km of round-trip range budget out of the box. Knob 1..255 clamped at parse AND at every apply site (a sub-1 value would write off every frame — the first cut of this rework proved it on air: a silently-failed default edit left the field 0, the <1 clamp wrote a 1 µs window, and the validation matrix caught 0% ok / retries pinned before it could ship). Validated live, no env: 0x640 reads 0x80 on the 8812CU and 8821AU, 0xCC00 byte0 0x80 on the 8832CU; the responder cell at the uniform default runs 100% ok, retries {0:895, 1:4}. ctest 49/49. Co-Authored-By: Claude Opus 4.8 --- examples/common/env_config.cpp | 8 +++--- src/DeviceConfig.h | 42 ++++++++++++++++---------------- src/jaguar1/RtlJaguarDevice.cpp | 20 +++++++++------ src/jaguar2/RtlJaguar2Device.cpp | 10 +++++--- src/jaguar3/RtlJaguar3Device.cpp | 13 ++++++---- src/kestrel/RtlKestrelDevice.cpp | 12 +++++---- 6 files changed, 58 insertions(+), 47 deletions(-) diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index fbe2b41..606d266 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -121,10 +121,10 @@ devourer::DeviceConfig devourer_config_from_env() { std::fflush(stderr); } } - if (env_long("DEVOURER_ACK_TIMEOUT_US", &v)) - /* 0 = per-chip default (no write); clamp only the top — an explicit 0 - * collapsing to 1 us would write off every frame. */ - cfg.tx.ack_timeout_us = static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); + if (env_long("DEVOURER_ACK_TIMEOUT_US", &v) && v >= 1) + /* 1..255; out-of-range low keeps the library default (a 0 collapsing + * to 1 us would write off every frame). */ + cfg.tx.ack_timeout_us = static_cast(v > 255 ? 255 : v); 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")) { diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index ad230a2..de8d8e3 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -224,27 +224,27 @@ struct DeviceConfig { * DATA_RETRY_LIMIT=0 carve-out kept pending its bench). */ int retry_limit = 0; /* env: DEVOURER_ACK_TIMEOUT_US — hardware ACK response window in µs - * (1..255, clamped; 0 = per-chip default), the hardware-ARQ RANGE - * lever: the MAC - * writes off a frame (and retries) when no ACK is counted within this - * window, and round-trip propagation eats ~6.7 µs per km, so a long - * link needs the window opened. One 8-bit register everywhere: - * REG_ACKTO 0x640 on the 11ac generations (defaults: 0x80 J1/J2's MAC - * reset value; 0x21 J3 halmac, scaled to 0x3D/0x75 at 10/5 MHz - * narrowband — the override REPLACES the per-bandwidth default, so - * budget the slowest ACK duration in use), R_AX_RSP_CHK_SIG 0xCC00 - * byte0 on Kestrel. The neighbouring CTS window (REG_CTS2TO 0x641) is - * a separate register this knob does NOT touch. Applied at bring-up. Bench: shrinking it below the - * ACK flight time (8 µs) pins retries at the limit with 0%% ok against - * a live responder — the register provably gates the ARQ verdict — - * while 255 behaves as the default at bench range WHEN ACKS ARRIVE. - * The adversarial half: every retry of a LOST frame waits the full - * window, so a long window slows the write-off cadence under loss — - * measured (dead RA, retry 8, max duty): 33 µs default 2719 write-offs - * /8 s vs 128 µs 2015 vs 255 µs 1507 (~1.8x slower). Size the window - * to the link (~6.7 µs/km + ACK flight + margin), don't just max it — - * and that is why the default is 0/vendor-faithful, not 255. */ - int ack_timeout_us = 0; + * (1..255, clamped), the hardware-ARQ RANGE lever: the MAC writes a + * frame off (and retries) when no ACK is counted within this window, + * and round-trip propagation eats ~6.7 µs per km. ONE default, 128 µs, + * programmed identically on every generation at bring-up — the same + * knob value means the same range budget (~15 km round trip) no matter + * which die is plugged. 128 is the vendor's interop-blessed J1/J2 + * value and covers the slowest narrowband ACK in the tree (the 5 MHz + * per-bandwidth vendor value is 117 µs), so it also replaces the + * per-chip / per-bandwidth vendor defaults (which ranged 33..128 µs + * and made hardware-ARQ range silently die-dependent). The register: + * REG_ACKTO 0x640 on the 11ac generations, R_AX_RSP_CHK_SIG 0xCC00 + * byte0 on Kestrel; the CTS window (REG_CTS2TO 0x641) is separate and + * untouched. Sizing: ~6.7 µs x round-trip km + ~50 µs ACK flight and + * detection margin; a longer window is NOT free — every retry of a + * LOST frame waits the full window, measured (dead RA, retry 8, max + * duty): 2719 write-offs/8 s at 33 µs vs 2015 at 128 vs 1507 at 255. + * Bench proof the register gates the ARQ verdict: at 8 µs (below the + * ACK's flight time) retries pin at the limit with 0% ok against a + * live responder; at 128/255 the responder cell runs 100% ok, + * retries ~0. */ + 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 / diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 78659f6..70829fa 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -80,10 +80,12 @@ void RtlJaguarDevice::InitWrite(SelectedChannel channel) { * DEVOURER_DIS_CCA. Always applied — the enable path is what programs * the BB EDCCA thresholds off their parked never-trigger table value. */ SetCcaMode(_cfg.tuning.disable_cca); - /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ - if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast( - _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); + /* ACK window (DEVOURER_ACK_TIMEOUT_US): one library default on every + * generation — see the DeviceConfig field doc. */ + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 + : _cfg.tx.ack_timeout_us < 1 ? 1 + : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) @@ -1337,10 +1339,12 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, * DEVOURER_DIS_CCA. Always applied — the enable path is what programs * the BB EDCCA thresholds off their parked never-trigger table value. */ SetCcaMode(_cfg.tuning.disable_cca); - /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ - if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast( - _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); + /* ACK window (DEVOURER_ACK_TIMEOUT_US): one library default on every + * generation — see the DeviceConfig field doc. */ + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 + : _cfg.tx.ack_timeout_us < 1 ? 1 + : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — crystal-cap trim (issue #217, narrowband CFO lever). */ if (_cfg.tuning.xtal_cap) diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 822e5f6..25706be 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -249,10 +249,12 @@ void RtlJaguar2Device::bring_up(SelectedChannel channel) { if (_cfg.tuning.disable_cca) SetCcaMode(true); - /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever (REG_ACKTO). */ - if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast( - _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); + /* ACK window (DEVOURER_ACK_TIMEOUT_US): one library default on every + * generation — see the DeviceConfig field doc. */ + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 + : _cfg.tx.ack_timeout_us < 1 ? 1 + : _cfg.tx.ack_timeout_us)); /* DEVOURER_XTAL_CAP — apply the crystal-cap trim once the AFE is up * (issue #217, the narrowband CFO lever). */ diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index b583c18..e443ff5 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -878,11 +878,14 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { else _txpkt_img.store(0, std::memory_order_relaxed); apply_dpdt_route_8822e(); /* 8822E DPDT/eFEM pin-mux (post-coex) */ - /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — overrides the - * halmac per-bandwidth REG_ACKTO default init_wmac_cfg just wrote. */ - if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0x0640, static_cast( - _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); + /* ACK window (DEVOURER_ACK_TIMEOUT_US): one library default on every + * generation, replacing the halmac per-bandwidth REG_ACKTO defaults + * init_wmac_cfg just wrote (the 128 default covers the slowest + * narrowband ACK) — see the DeviceConfig field doc. */ + _device.rtw_write8(0x0640, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 + : _cfg.tx.ack_timeout_us < 1 ? 1 + : _cfg.tx.ack_timeout_us)); apply_replay_wseq(); /* DEVOURER_REPLAY_WSEQ golden-init replay (debug) */ if (_cfg.debug.bb_dump) { /* Full MAC+BB dump (0x000..0x4ffc — MAC plane, then BB incl. the RF diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 651e73a..a4983e8 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -254,11 +254,13 @@ void RtlKestrelDevice::InitWrite(SelectedChannel channel) { * re-asserts explicitly (idempotent) when the knob is set. */ if (_cfg.tuning.disable_cca) SetCcaMode(true); - /* DEVOURER_ACK_TIMEOUT_US: the hardware-ARQ range lever — byte0 of - * R_AX_RSP_CHK_SIG (the field the vendor's narrowband path scales). */ - if (_cfg.tx.ack_timeout_us > 0) - _device.rtw_write8(0xCC00, static_cast( - _cfg.tx.ack_timeout_us > 255 ? 255 : _cfg.tx.ack_timeout_us)); + /* ACK window (DEVOURER_ACK_TIMEOUT_US): one library default on every + * generation — byte0 of R_AX_RSP_CHK_SIG (the field the vendor's + * narrowband path scales); see the DeviceConfig field doc. */ + _device.rtw_write8(0xCC00, static_cast( + _cfg.tx.ack_timeout_us > 255 ? 255 + : _cfg.tx.ack_timeout_us < 1 ? 1 + : _cfg.tx.ack_timeout_us)); _tx_mgmt_ep = _device.nth_bulk_out_ep(0); /* B0MG -> BULKOUTID0 */ _tx_data_ep = _device.nth_bulk_out_ep(3); /* ACH0 -> BULKOUTID3 */ if (_tx_mgmt_ep == 0) {