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
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,17 @@ add_executable(txdemo
target_link_libraries(txdemo PUBLIC devourer PRIVATE PkgConfig::libusb)
target_include_directories(txdemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)

# dwell-1 A/B injection over the firmware channel switch (issue #272 data
# plane): one admitted data-frame opportunity per wall-clock slot, with
# bounded admission so a frame for context A can never air on B.
add_executable(dwelltx
examples/dwelltx/main.cpp
examples/common/env_config.cpp
examples/common/usb_select.cpp
)
target_link_libraries(dwelltx PUBLIC devourer PRIVATE PkgConfig::libusb)
target_include_directories(dwelltx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)

# Pre-modulator subcarrier PoC: transmits PSDU bytes shaped by
# tools/precoder/encode_subcarriers.py so chosen OFDM data subcarriers carry
# chosen bits. Single-stream BPSK / HT MCS0 / BCC on 8812/8821/8811.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,11 @@ one `IRtlDevice` interface covers all four generations.
channel-switch fast path (`DEVOURER_FASTRETUNE_FW`).
- [Kernel channel-switch baseline](docs/kernel-channel-switch-baseline.md) +
[firmware offload](docs/kernel-channel-switch-offload.md) +
[MCC/FCS](docs/mcc-fcs-investigation.md) — how the standard Linux/Realtek
drivers retune measured against devourer, and where the chip firmware's own
H2C 0x1D switch beats them.
[MCC/FCS](docs/mcc-fcs-investigation.md) +
[dwell-1 A/B injection](docs/dwell1-ab-injection.md) — how the standard
Linux/Realtek drivers retune measured against devourer, where the chip
firmware's own H2C 0x1D switch beats them, and a two-context per-slot data
plane with zero wrong-channel over 100 k slots.
- [FHSS](docs/fhss.md) — the anti-jam design article: keyed SipHash hop
schedules, slot-locked lockstep RX, and
[jammer resilience](docs/jammer-resilience.md) — measured delivery against
Expand Down
136 changes: 136 additions & 0 deletions docs/dwell1-ab-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Dwell-1 A/B injection over the channel switch

A two-context A/B hopping **data plane**: the radio spends one wall-clock slot
per channel and each slot carries exactly one admitted data-frame
opportunity — "dwell-1". This is the fourth standard-driver FHSS experiment
(after the [baseline](kernel-channel-switch-baseline.md), the
[firmware offload](kernel-channel-switch-offload.md), and the rejected
[MCC/FCS scheduler](mcc-fcs-investigation.md)), and it answers the question
MCC could not: can firmware-grade channel time-slicing carry real per-slot
traffic with deterministic queue ownership?

It can — with the **synchronous** channel switch. The result also draws a
sharp line between devourer's two channel-switch primitives: the
fire-and-confirm-later firmware switch (H2C 0x1D) that wins continuous
per-packet hopping is the *wrong* primitive for tight slot placement, and the
synchronous software `FastRetune` is the right one. They have opposite sweet
spots.

## What dwell-1 means here

One dwell = one admitted devourer data-frame opportunity, placed inside a
bounded window that guarantees it finishes airing before the slot flips:

```
slot start slot end
|── switch ──|── settle ──|═══ admit window ═══|── guard ──|
^ ^ ^
FastRetune admit_open admit_close
returns = start+settle = end-guard-airtime
```

A frame is admitted only inside `[admit_open, admit_close]`. Past
`admit_close` it is **dropped** with a structured reason, never transmitted —
so a frame built for context A can never air on B. This bounded admission is
pure caller policy (`examples/dwelltx`); the library is unchanged. It is the
data-plane discipline the per-packet hop (radiotap `CHANNEL`, ~1 ms retune)
lacks on its own.

Each per-slot frame carries a `HopSyncMarker` (`src/HopSchedule.h`) whose
`slot` field, run through the shared public/keyed schedule, tells any receiver
which channel the slot belongs to. An oracle pinned to channel A that decodes
a frame whose slot maps to B is therefore a self-evident **wrong-channel**
event, and the unique per-slot marker doubles as the duplicate key — no extra
tag, and attribution needs no cross-clock alignment.

Harness: `examples/dwelltx` (the admission scheduler) + `tests/dwell1_ab.py`
(two host oracles on A/B, per-slot accounting, fault injection). DUT
RTL8822BU; oracles 8821AU (ch A) + 8812CU (ch B).

## Result — the synchronous switch carries dwell-1 cleanly

36↔40, 20 MHz, 20 ms slots, software `FastRetune`:

| run | slots | delivered on correct channel | wrong-channel | dup | late-drop |
|---|---|---|---|---|---|
| primary | 5 020 | 97.2 % | **0** | 0 | 0 |
| reliability | 100 013 | 96.2 % | **0** | **0** | 7 (0.007 %) |

Admission is tight: the in-slot placement of the admitted frame has a
p99−p1 jitter of **132 µs**, and the switch itself costs ~2.1 ms median
(p99 2.3 ms). Across **100 013** slots not one frame aired on the wrong
channel and not one was a duplicate re-air; the ~4 % shortfall is
first-decode/oracle loss, not mis-delivery. That is the acceptance
criterion — ≥ 100 k slots, zero wrong-channel, a quantified 0.007 %
late-drop rate — met.

**Fault — late enqueue.** Delaying the host enqueue to 18 ms into a 20 ms
slot (past `admit_close`): of 516 slots, 511 were dropped-late and 5 admitted,
with **zero wrong-channel**. The overload path is deterministic and
observable: a frame that cannot make its slot is dropped, never aired stale.

## The two switch primitives have opposite sweet spots

Running the identical dwell-1 loop over the **firmware** switch
(`DEVOURER_FASTRETUNE_FW=1`) instead inverts the schedule at 20 ms slots —
frames land on the previous slot's channel. The firmware switch is
fire-and-confirm-later: `FastRetune` returns after submitting the H2C, and in
the *sparse* dwell-1 call pattern (one retune per 20 ms, then idle) the RF
completes far later than the ~1 ms it shows under continuous per-packet
hopping. A post-switch settle sweep (20 ms slots) shows how late:

| settle before admit | delivery (correct channel) |
|---|---|
| 2 ms | 3 % |
| 6 ms | 36 % |
| 9 ms | 63 % |
| 12 ms | 82 % |

Even 12 ms of settle inside a 20 ms slot does not fully clear it; a 40 ms slot
with 15 ms settle reaches 97 %. So the firmware switch *can* carry dwell-1,
but only with a guard so large it erases the point of a tight slot. The
synchronous software `FastRetune` — RF programmed by the time the call
returns — places the frame deterministically with a ~2 ms settle and is the
correct dwell-1 primitive.

This does not contradict experiments 2–3: there the firmware switch halves
the dark time of *continuous* hopping, where frames flow immediately after the
switch and its late completion is masked. Dwell-1 exposes that latency. The
two results compose into one rule: **firmware switch for throughput hopping,
synchronous switch for deterministic slot placement.**

## Lockstep receive

A single receiver hopping the same A/B schedule (`rxdemo` lockstep mode —
`src/HopSchedule.h` `HopSyncMarker` tracking, anchor phase-correction) acquires
the schedule from the per-slot markers alone: 889 tracked `hop.rx` decodes over
a 12 s window against the dwell-1 transmitter. A cold restart of the receiver
re-acquires from scratch (861 decodes in the next 12 s) with no persistent
phase inversion — the marker carries the absolute slot, so acquisition needs no
prior state and survives a full restart.

## Go / no-go

**Go — advance to experiment 5.** Firmware-grade channel time-slicing carries
a real per-slot data plane: over 100 013 A/B slots, every admitted frame that
was delivered landed on the correct channel (zero wrong-channel, zero
duplicate), overload is deterministic (late frames dropped, never mis-aired),
and a single receiver acquires and recovers the schedule. This is the
capability MCC/FCS could not provide (it never ran a schedule from the
association path, and its slots are TU-coarse and two-context-bound —
[mcc-fcs-investigation.md](mcc-fcs-investigation.md)); the caller-side
admission scheduler over the ordinary channel switch does.

The load-bearing design choice for experiment 5: use the **synchronous**
`FastRetune`, not the firmware switch, as the slot-boundary primitive. The
firmware switch (`DEVOURER_FASTRETUNE_FW`) remains the right tool for
continuous per-packet hopping (experiments 2–3), but its fire-and-confirm-later
completion is too late and too variable for tight dwell-1 placement. The two
primitives are complementary, not interchangeable, and the data plane must pick
the synchronous one.

Open items experiment 5 inherits: the ~4 % first-decode/settle loss (a frame
occasionally airs before the switch fully settles — recoverable by a slightly
larger settle at a small duty cost), and bidirectional/ACK turnaround, which
this experiment deferred to keep the admission contract unidirectional and
clean.
Loading
Loading