Skip to content
Merged
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
13 changes: 11 additions & 2 deletions docs/aggregation.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ CCX reports: responder ON = 100% delivered at mean 0.4 retries (67%
first-try); OFF = 0% delivered, every frame pinned at the 12-retry limit. The
retry distribution is the per-frame TX-side link-quality sensor.

The same responder is a hardware **BlockAck** responder: the MAC's
On the adapter combinations measured by `tests/ampdu_ba_check.sh`, the same
responder is a hardware **BlockAck** responder: the MAC's
immediate-response engine generates a SIFS-timed BlockAck for a received
A-MPDU addressed to its MACID, on the same MACID + net_type gate. So
reliable-unicast **ACKed A-MPDU** works end to end — the TX runs `SetAmpduMode`
Expand All @@ -212,7 +213,15 @@ aggregates deliver at 100% / mean 0.1 retries and ~27× the throughput of the
responder-off case (where every aggregate re-airs to the retry limit). The
`no_ack = true` default is the broadcast/FEC flavor (OpenIPC wfb — no
responder, no re-air storm); `false` is the reliable-unicast flavor against a
BA responder.
BA responder. RTL8733B is also established as the **responder** by the
CCX-independent `tests/rtl8733b_blockack_onair.sh`: Jaguar2 `0bda:b812` TX,
RTL8733B `0bda:f72b` responder, and Jaguar1 `0bda:8812` passive witness at
ch36/MCS3. Armed, 128,702 unique aggregated payloads measured 1.001 witnessed
copies/frame and the witness decoded 14,402 addressed `0x94` BlockAck frames,
all with nonzero bitmaps. Active but unarmed, 1,605 payloads measured 12.720
copies/frame at retry limit 12 and zero matching BlockAcks. Both arms had
`paggr >= 0.665` and aggregate bursts of 9. RTL8733B's own A-MPDU **TX** path
remains unported.

Every MAC address in the loop must be **unicast** (I/G bit clear): the
responder `mac` (an ACK/BlockAck cannot target a group address) and the TX
Expand Down
1 change: 1 addition & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
| `rx.corrupt` | RX (`DEVOURER_RX_DUMP_ALL`) | len, crc, icv, rate, bw, stbc, ldpc, sgi, rssi[2], evm[2], snr[2] |
| `rx.txhit` | RX, TX | hits, total_rx, len, seq, paggr, ppdu, rate, bw, stbc, ldpc, ppdu_type — canonical-SA (57:42:75:05:d6:00) matcher; rate/ldpc prove what encoding was decoded (8814A reports ldpc=0 always — no HW indicator); ppdu_type is the AX RXD format nibble (7=HE_SU, 8=HE_ERSU; 255 on pre-AX chips) |
| `rx.seq` | RX, duplex (`DEVOURER_RX_PCTR`) | pctr, tsfl, seq, crc, paggr, ppdu, rate (hw rate index of this copy — retransmissions of one pctr can air at different rates, so per-copy rate + tsfl order reconstructs the fw fallback ladder on air, `tests/retry_ladder_probe.sh`) — the ground-truth per-frame delivery sequence for the RX-ring loss study: pctr is the u32 the txdemo QoS-Data path stamps at MPDU offset 26, so gaps in it are per-frame loss; paggr/ppdu carry the aggregate structure the host-vs-RF discriminator keys on. Lean by design (no body hex) so the emit can't perturb the pump thread. SA gate follows `DEVOURER_RX_AGG_SA` (required in duplex, whose canonical-SA `rx.frame` stream is a different transmitter), else canonical SA |
| `rx.blockack` | RX (`DEVOURER_RX_CONTROL`) | crc, rate, ctrl, start_seq, ra hex, ta hex, bitmap hex — one event per received 0x94 BlockAck control frame (bitmap carries the first 64 bits); narrow metadata-only stream used by `tests/rtl8733b_blockack_onair.sh` to prove the response subtype/address/bitmap without mirroring every A-MPDU data body |
| `rx.parse_abort` | RX (every generation, always on) | t, off, buf_len, remaining, frame_len, drvinfo, shift, total (cumulative) — the RX descriptor walk abandoned the rest of a bulk-IN aggregate; semantics doc-commented at `src/RxParseAbort.h`. No event in a session = the walk never aborted |
| `rx.ring` | L (`DEVOURER_RX_RING_MS`) | t, mode ("async"/"sync"/"reorder-pool"/"spsc-fat"), n_urbs, armed (URBs posted to the HCD and awaiting a frame — the depth that starves under a slow inline consumer), min_armed (low-water mark since the last emit), cb_max_us (worst inline-consume latency in the window), resubmit_fail, completions (cumulative URB callbacks), empties (cumulative callbacks that left the ring with zero posted URBs), pool_free (−1 = no host pool), qdepth (spsc-fat consumer-queue depth; 0 in the other ring modes), pool_dropped (cumulative received frames discarded at spsc-fat pool exhaustion under the `drop` policy — pool exhausted, or a failed re-arm, the latter also ticking resubmit_fail; each was already chip-ACKed, so a hardware-ARQ peer counts it delivered), pool_stalls (the `backpressure`-policy counterpart, cumulative URB park events: the payload still reaches the consumer, the ring shrinks and the chip declines further ACKs, so overload loss stays ARQ-visible — `DEVOURER_RX_POOL_EXHAUST`, default backpressure; `tests/arq_e2e_delivery.sh` measures both). Sync mode emits a reduced line (pool_free pinned at −1; no qdepth/pool_dropped/pool_stalls/completions/empties). The mechanism-proof telemetry: empties/completions is the host-starvation rate — near-0 under RF loss (the ring stays armed because frames don't arrive), high under host starvation (frames out-race resubmit and drain the ring). Counted in the callback, so robust to the pump-thread starvation that makes the periodic emit sparse — but blind while the pump itself is frozen: a stalled consumer drops frames these counters never see, which the per-frame `rx.seq` ledger exists to catch |
| `rx.count` | TX (its RX thread) | total, len |
Expand Down
78 changes: 37 additions & 41 deletions docs/rtl8733b.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,35 +372,34 @@ a request a caller may be making only through an inherited environment.

## Hardware ARQ

Measured against an RTL8812AU peer on the validation unit.
Both directions of a reliable-unicast link now work on this die.
Measured on the `f72b` validation unit with an RTL8812AU peer and an
independent RTL8812CU witness. Both directions of a reliable-unicast link work
in these cells; the result is not a multi-sample or multi-peer qualification.

| knob | state | evidence |
| --- | --- | --- |
| `tx.ack_timeout_us` | honoured | REG_ACKTO read back at bring-up: `<unset>/128/33/200 -> 128/128/33/200` |
| `SetAckResponder` | ported, `ack_responder_ok = true` (normal-ACK singles; BlockAck untested) | 8733B as responder: armed 1736/1736 ACKed, retries_mean 0.00; re-armed on a different MAC 1736/1736; disarmed 0.00 with retries pinned at 12 |
| `tx.retry_limit` | live, `tx_retry_limit_ok = true` | airtime dose-response 0/3/12 -> 0.93/3.93/12.27 airings per frame (expected 1 + N), repeatable over a 0/3/12/0/12 ladder |
| `tx.report` (CCX) | **not** ported — firmware emits nothing | see Known gaps below |
| `tx.ack_timeout_us` | honoured for CCK and non-CCK | 0x640 and CCK 0x639 both read back 33/200; an 11M dead-peer run submitted 1513/1129 frames in 8 s respectively (expected direction, not a timing calibration) |
| `SetAckResponder` | ported, `ack_responder_ok = true` (normal ACK and BlockAck response) | singles: on 1725/1725 ACKed, retarget 1728/1728, off 0/1723 with retries pinned at 12; A-MPDU armed/active-unarmed: 1.001/12.720 witnessed copies per payload and 14402/0 addressed BlockAck frames, with aggregation proven in both arms |
| `tx.retry_limit` | live, `tx_retry_limit_ok = true` | dead-peer dose response 0/3/12 -> 1.00/4.00/12.32–12.33 witnessed copies per frame (expected 1 + N), repeated over a 0/3/12/0/12 ladder |
| soliciting TX | normal ACK recognition measured | MCS3 responder on/off: 1.032/12.948 copies per observed frame; 11M CCK: 1.002/11.908, with 92.3–100% witness coverage |
| `tx.report` (CCX) | **not** ported — no reports observed | missing H2C/MEDIA_STATUS_RPT path remains the leading explanation; see Known gaps below |

Implementation detail, the register recipe and the full CCX bench narrative:
`src/rtl8733b/CLAUDE.md` "Hardware ARQ". The cross-generation ARQ matrix this
die now joins is `docs/scheduled-mac.md`.

The retry measurement could not use the 12/0/12 CCX A/B the Jaguars are judged
by, because this die has no `tx.report` to judge itself with; it counts airings
at a passive monitor instead (`tests/rtl8733b_retry_limit_onair.sh`), and takes
three dose levels rather than an on/off pair so that a straight line — not a
single delta that ambient conditions could fake — carries the claim. Its
counterparts: one physical unit and one peer generation, like every other
on-air claim in this section — note the document as a whole rests on two units,
but these ARQ cells ran on the `f72b` sample only; the monitor can only lose
airings, never invent them, so the measured ratio is a floor. Two different things account for the shortfalls, and only the smaller one is an
artefact: the `rx.txhit` readout quantizes to at most 99 airings (the event
fires on the first 10 hits then every 100th), which is <=0.066/frame at 1500
frames and covers the 0 and 3 arms entirely. It does NOT cover the 12 arm —
12.27 against 13 is 0.73/frame, about 1095 airings, an order of magnitude past
that bound. That residue is monitor loss (or genuinely fewer airings), which is
why the ratio is reported as a floor and not a point estimate.
die is compared in is `docs/scheduled-mac.md`.

The retry measurement counts airings at a passive monitor
(`tests/rtl8733b_retry_limit_onair.sh`) because this die has no `tx.report`.
It takes three dose levels rather than an on/off pair so that a straight line —
not a single delta that ambient conditions could fake — carries the claim. Its
counterparts: one physical RTL8733B unit, an RTL8812AU peer, and an RTL8812CU
passive witness. The document as a whole rests on two units, but these ARQ
cells ran on the `f72b` sample only. The harness uses a run-specific unicast SA
and counts every clean payload-counter event rather than the sampled and
quantized `rx.txhit` stream. At retry 12 the witness saw 12.32–12.33 rather
than the ideal 13 copies/frame; passive-monitor loss (or genuinely fewer
airings) keeps that number a conservative observation rather than an exact
retry count.

## Known gaps and deferred validation

Expand Down Expand Up @@ -435,24 +434,21 @@ These results have **not** been claimed:
the normal NIC image.
- SGI remains disabled: a descriptor with its short-GI bit set submitted
successfully, but an independent RTL8812AU decoded both broadcast and
unicast probes as long GI. STBC and LDPC remain rejected. A-MPDU and
throughput were not independently validated and remain unadvertised or
unsupported by this backend. Normal-ACK response IS ported and measured
(Hardware ARQ above); BlockAck response to an aggregate remains untested, as
does A-MPDU itself.
- No CCX path, so no `tx.report` events — and unlike every other gap here the
cause is the firmware, not a missing port. Everything under the backend's
control was verified correct on air (descriptor SPE_RPT/SW_DEFINE set at the
vendor's own bit positions, C2H decoded at the vendor's dword2[28], bulk-IN
delivery as the vendor uses, and the fw-offload C2H format this firmware
speaks per its own dispatch), yet with an RX loop live and 3160 frames
received the firmware returned **zero** C2H packets in any format — with and
without net_type armed, peer ACKing and silent. The outstanding lead is the
halmac H2C queue + a MEDIA_STATUS_RPT registering the descriptor MACID; this
backend has no H2C transport at all. The knob warns at bring-up instead of
stamping descriptors that buy nothing. Consequence: an 8733BU can be either
end of a hardware-ARQ link but cannot see per-frame delivery, so detecting a
departed peer needs an application-level timeout
unicast probes as long GI. STBC and LDPC remain rejected. RTL8733B A-MPDU
**TX** and throughput remain unvalidated and unported. Its responder side is
separate and measured: `tests/rtl8733b_blockack_onair.sh` proved BlockAck
response to incoming Jaguar2 A-MPDUs by an independent air-side retry A/B
(Hardware ARQ above).
- No CCX path, so no `tx.report` events. The descriptor request bits,
receive-side C2H marker, USB delivery path, and expected fw-offload format
were checked, yet a live RX loop saw zero C2H packets across 3160 received
frames. That does **not** isolate a firmware defect: this backend also lacks
the halmac H2C queue and MEDIA_STATUS_RPT needed to register the descriptor
MACID, which is the leading prerequisite to implement and test. The knob
warns at bring-up instead of implying reports will arrive. Consequence: the
measured `f72b` can be either end of a normal-ACK hardware-ARQ link, but it
cannot see per-frame delivery, so detecting a departed peer needs an
application-level timeout
(`src/rtl8733b/CLAUDE.md` "Hardware ARQ").
- Fast retune is now ported and independently witnessed (intra-band,
same-width; `src/rtl8733b/CLAUDE.md` has the measured contract and its
Expand Down
27 changes: 17 additions & 10 deletions docs/scheduled-mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,18 @@ scheduled MAC runs TX+RX anyway, so this is the relevant session shape.
| Jaguar2 8812BU | 0.91 / 2.1 (run-to-run 0.12–0.91) | 0.64 / 5.3 | yes (12) | 0.86 | 0 |
| Jaguar3 8822CU | 1.00 / 0.24 | 1.00 / 0.13 | yes (12) | 0.96 | 0 |

The RTL8733B is deliberately absent from this table: it is the one die that
runs closed-loop hardware ARQ with **no CCX report at all**, so none of these
columns can be filled for it as the soliciting TX. Its retry knob is measured
from the air instead (airings per submitted frame, 0/3/12 -> 0.93/3.93/12.27)
and its responder side is measured with this same harness pointed the other
way — `docs/rtl8733b.md` "Hardware ARQ".
The RTL8733B is deliberately absent from this CCX table: no `tx.report` events
arrive on that backend, so these columns cannot be filled for it. Independent
airtime evidence does cover its soliciting side: a passive RTL8812CU witness
measured 1.032 copies/frame with an RTL8812AU responder armed versus 12.948
with it off at MCS3; at 11M CCK the corresponding values were 1.002/11.908.
Its dead-peer retry dose response was 0/3/12 -> 1.00/4.00/12.32–12.33 copies/frame.
The same unit's BlockAck responder was judged without CCX: a Jaguar2
aggregating TX plus Jaguar1 passive witness measured armed/active-unarmed
1.001/12.720 copies per payload, with A-MPDU structure present in both arms;
the armed/active-unarmed control-frame counts were 14402/0 addressed
BlockAcks (`tests/rtl8733b_blockack_onair.sh`). All are one-RTL8733B bench
results; details are in `docs/rtl8733b.md`.

The OFF-phase pin is set by `DEVOURER_TX_RETRY_LIMIT` (the matrix runs 12,
the value the descriptors used to hardcode) — the knob, not a descriptor
Expand Down Expand Up @@ -258,7 +264,7 @@ nonzero limit for absolute numbers; 8821AU row re-measured ch6):
| 8821AU | 62% | 0% | works (94% closed-loop at retry 8) |
| 8812EU | 98% | 0% | works |
| 8812CU | 69% | 0% | works |
| 8733B | unmeasured | 0% | works (closed-loop 1736/1736 at retry 12; single-shot cell never run) |
| 8733B | unmeasured | 0% | works (closed-loop 1725/1725 at retry 12; single-shot cell never run) |
| 8852CU (Kestrel) | 0% | 0% | not implemented on the AX generation |

Unmeasured for lack of plugged hardware: 8821CU / PCIe 8821CE (recipe-shared
Expand Down Expand Up @@ -309,9 +315,10 @@ carry this table per die.
stalled spsc-fat pool drained ~3 k frames in one receipt interval; the
8192 default clears that bench worst case ~2.7×).
2. **Closed-loop hardware ACK + autonomous retry is GO on Jaguar1, Jaguar3 and
the RTL8733B** (100% delivery, retries ≈ 0.2–0.3; the 8733B closes the loop
as responder at 1736/1736 but has no CCX report of its own, so its TX-side
retry evidence is airtime rather than `tx.report` — `docs/rtl8733b.md`)
the RTL8733B** (the CCX delivery/retry figures apply to the Jaguars; the
one-sample 8733B closes the loop as responder at 1725/1725 and as soliciting
TX collapses from 12.948 to 1.032 witnessed copies/frame at MCS3, but has no
per-frame report of its own — `docs/rtl8733b.md`)
including retargeting an
arbitrary UE MAC mid-session (re-arm `SetAckResponder`, change the
descriptor RA — both fully dynamic). Requires a nonzero
Expand Down
6 changes: 5 additions & 1 deletion examples/chanmig/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,11 @@ int main(int argc, char **argv) {
/* link-derived unicast for the ACK responder */
devourer::MacAddr am{{0x57, 0x42, 0x75, static_cast<uint8_t>(g_link >> 8),
static_cast<uint8_t>(g_link), 0x01}};
dev->SetAckResponder(am);
if (!dev->SetAckResponder(am)) {
logger->error("chanmig: ACK responder arm was refused; aborting drone "
"bring-up instead of running with a silent responder");
return 1;
}

std::thread rx([&] { dev->StartRxLoop(drone_rx); });
/* synthetic video pump */
Expand Down
25 changes: 25 additions & 0 deletions examples/rx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,31 @@ static void packetProcessor(const Packet &packet) {
}
}

/* DEVOURER_RX_CONTROL=1 — emit addressed BlockAck frames for
* air-side responder qualification. This is deliberately a narrow event,
* not DEVOURER_STREAM_OUT=1: the latter mirrors every data body and would
* make a saturated A-MPDU witness log unnecessarily huge. BlockAck control
* frames have FC subtype 0x94, RA at 4, TA at 10, BA control/start-sequence
* at 16/18 and the first 64 bitmap bits at 20. The CRC flag remains in
* the event so a harness can refuse corrupt control observations. */
static const bool rx_control =
std::getenv("DEVOURER_RX_CONTROL") != nullptr;
if (rx_control && packet.Data.size() >= 28 &&
(packet.Data[0] & 0xfcu) == 0x94u) {
const uint16_t ba_ctrl = static_cast<uint16_t>(packet.Data[16]) |
(static_cast<uint16_t>(packet.Data[17]) << 8);
const uint16_t start_seq = static_cast<uint16_t>(packet.Data[18]) |
(static_cast<uint16_t>(packet.Data[19]) << 8);
devourer::Ev(*g_ev, "rx.blockack")
.f("crc", packet.RxAtrib.crc_err ? 1 : 0)
.f("rate", packet.RxAtrib.data_rate)
.f("ctrl", ba_ctrl)
.f("start_seq", start_seq)
.hex("ra", packet.Data.data() + 4, 6)
.hex("ta", packet.Data.data() + 10, 6)
.hex("bitmap", packet.Data.data() + 20, 8);
}

if (g_rx_count == 1) {
devourer::Ev(*g_ev, "init.timing")
.f("stage", "demo.first_rx_frame")
Expand Down
Loading
Loading