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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ Common to both demos:
then restores the chip. `DEVOURER_CW_TONE_GAIN=0..31` sets the RF gain index
(`RF 0x00[4:0]`, default 0 = lowest). A controllable narrowband interferer / MP
tone source; SDR-validate with `tests/cw_tone_sdr.sh` (+ `tests/cw_tone_probe.py`).
- `DEVOURER_CONT_TX=1` — the modulated sibling of the CW tone: a true 100%-duty
modulated OFDM carrier (Realtek's MP hardware continuous-TX mode) on all three
generations, instead of a bare tone. A full-channel active stimulus for
spectral / power / thermal characterisation and the active-link probe. See
[`docs/adaptive-link-building-blocks.md`](docs/adaptive-link-building-blocks.md).

On-air TX throughput vs wfb-ng (SDR-verified parity; how to reproduce) is
documented in [`docs/wfb-ng-tuning.md`](docs/wfb-ng-tuning.md).
Expand Down
142 changes: 142 additions & 0 deletions docs/adaptive-link-building-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Adaptive-link building blocks

The [energy-minimizing adaptive link](adaptive-link.md) sets out *what* to
optimize — the fewest Joules per delivered bit under a video-quality floor, ridden
by pushing the highest modulation the link bears and spending only the power that
clears it. This document is the companion: *what* devourer actually exposes to
build that loop with. It catalogs the concrete primitives — the **levers** an
adaptive controller can pull, the **sensors** it can read, the **active stimuli**
it can emit to probe the link, and how they compose into a decision.

Devourer is the **mechanism, not the policy.** It gives a ground station a clean
view of the link and a drone a set of runtime-changeable transmit parameters; the
closed loop that scores the link and picks the operating point lives upstream (in
wfb-ng / OpenIPC / an adaptive sidecar). Everything below is a building block that
loop consumes or drives.

## The loop, in one line

**Sense → decide → act.** The ground senses link quality, a policy decides the
operating point, the drone acts on its transmit parameters (with local safety
overrides). The sections map onto that: *sensors* feed the sense step, *levers*
are the act step, and *active stimuli / probes* let the controller measure a
candidate operating point **before** committing video to it — turning a reactive
loop (discover the ceiling by dropping frames) into a proactive one.

## Levers — what the link can change at runtime

All of these move without a re-init; most move per-packet or within a few
milliseconds, so a controller can retune between frames.

| Lever | Effect on the link | Effect on energy/bit | How it moves |
|---|---|---|---|
| **Modulation / MCS** (time-on-air) | strong | **strong** — less airtime, fewer Joules/bit | per-packet radiotap rate, or the device TX-mode default (`DEVOURER_TX_RATE`); immediate |
| **FEC strength** | strong | strong | application-layer (the outer code / per-layer ladder), not a PHY register |
| **Channel / bandwidth** | strong | moderate | per-packet retune (~1–2 ms intra-band fast retune, longer on a band change); the channel-agility lever |
| **Transmit power** | **strongest** | **weak** — the always-on baseline draw dominates | runtime TXAGC override, re-applied without a channel switch |
| **Active receive chains** | conditional | **conditional** — pays only when the antennas decorrelate (motion) | RX-path enable mask; a fade-state lever, not a range lever |
| **Duty cycle** | — | direct | inter-frame gap; back-to-back for maximum airtime, idle to save it |

The energy asymmetry is the crux the design leans on: modulation and airtime are
strong energy levers, transmit power is a strong *link* lever but a weak *energy*
lever. So the reflex is to ride the fastest modulation the link tolerates and
spend the minimum power that clears it — which is precisely a **boundary search**,
and the active probes below exist to find that boundary cheaply.

## Sensors — how the link is measured

Two classes, differing in whether a frame has to arrive.

**Frame-driven** (ride ambient traffic — a received frame carries them):

- **per-chain RSSI / SNR / EVM** — link-quality scalars averaged over the channel,
per receive chain. The primary "how good is the link right now" signal, but only
as fast as frames arrive.

**Frame-free** (no received frame required — the receiver reads them off the
baseband directly; see [`rx-spectrum-sensing.md`](rx-spectrum-sensing.md)):

- **false-alarm (FA) + CCA counters** — in-band energy and channel-busy, read as a
delta over a poll interval. Spike when a carrier appears.
- **DIG initial gain (IGI)** — a noise-floor proxy.
- **NHM noise histogram** — a 12-bucket, IGI-referenced in-band **power
distribution**. Its mass shifts into higher buckets under a rising interferer —
a coarse spectrum-free "how much energy, and how high" without a sweep.
- **per-tone interference localizer** — from a self-sounded beamforming report,
per-subcarrier SNR / phase-variance that locates a narrowband interferer to a
fraction of the channel. The finest frequency-resolution sensor the silicon
offers (no raw per-subcarrier CSI is exported to the host).

**Thermal** (a local safety input, not a link sensor):

- **thermal meter + baseline** — the PA's relative temperature and its trend. The
drone's *local safety override* (thermal back-off) reads this; it also bounds
how much power/duty the controller may request.

## Active stimuli — probing the link on purpose

Passive sensing waits for the link to reveal itself. An active stimulus makes the
link reveal itself on demand.

- **CW tone** (`DEVOURER_CW_TONE`) — a bare, unmodulated RF carrier at the channel
center. A controllable **narrowband** probe / interferer: park it on a channel
and a second adapter's energy sensor detects it. Useful for reciprocity checks
and for injecting a known interferer to validate the sensors — but it occupies a
single frequency and doesn't stress the amplifier the way real traffic does.
- **Modulated continuous TX** (`DEVOURER_CONT_TX`) — the modulated sibling: a true
100%-duty **full-channel** OFDM carrier at a real rate, on all three chip
generations (Realtek's MP hardware continuous-TX mode). Because it fills the
whole 20/40/80 MHz and loads the PA like real traffic, it is the *realistic*
stimulus — what you want for spectral-occupancy, power, and thermal-duty
characterisation. It idle-holds the carrier until stopped, then restores the
chip. (SDR spectrum-shape check: `tests/sdr_spectrum.py` distinguishes a
full-channel modulated block from a bare tone by occupied bandwidth.)

The two are complementary: the tone probes *one frequency* narrowly; the modulated
carrier probes *the whole channel* realistically.

## Active probing — turning a stimulus into a decision

The **active link-probe** (`tests/link_probe.sh` + `tests/link_probe.py`) composes
a stimulus and a sensor into an operating-point recommendation. One adapter emits
a modulated feed and **sweeps a lever** in steps (marking each step); the ground
station reads its per-step SNR and NHM; the analyzer aligns the two by time and
reports the **margin-vs-lever curve** plus the operating point that meets a target.

- **Power↔margin** — sweep transmit power, read the ground SNR at each level, and
pick the *minimum power that clears the margin*. This is the energy-min reflex
made measurable: rather than guess the power or discover it by degrading video,
measure the cheapest power that holds the link. (Sweep the noise-limited,
lower-power regime for a clean monotonic curve; very high power into a strong
link just saturates the receiver.)
- **MCS-headroom** — the same harness with the rate as the swept axis: does the
next modulation still clear the SNR/EVM floor? A proactive rate-adaptation input.

This is the concrete building block an energy-min controller uses to place the
operating point *before* committing the video stream to it.

## Composing the blocks into the energy-min loop

The design's decisions map onto the blocks above:

- **Ride the highest MCS, spend the least power that clears it** — the
power↔margin and MCS-headroom probes measure that boundary; the rate and power
levers act on it.
- **Pick a clean channel / bandwidth** — sweep the frame-free energy sensor across
candidate channels (a coarse spectrum map), or localize an interferer per-tone,
then retune the channel lever there. A modulated continuous burst on a candidate
channel lets the far end confirm it carries the target rate.
- **Adapt receive chains to the fade state** — combine more chains under motion
(decorrelated antennas fill fades), collapse to one on a still, strong link; the
RX-path lever, driven by the per-chain sensors.
- **Hold the thermal / regulatory ceiling** — the thermal telemetry bounds the
power/duty the controller may request; the continuous-TX stimulus is the
worst-case duty for characterising that ceiling.
- **Re-find each other when feedback drops** — a modulated continuous (or periodic)
carrier as the ground's cheap re-find beacon, detected by the drone's low-duty
energy sensor — the asymmetric-duty rendezvous the design describes.

None of these is a policy in itself; each is a lever to pull or a number to read.
The controller that weighs them against the energy objective and the per-layer
quality floor is the subject of [`adaptive-link.md`](adaptive-link.md), and it
rides upstream of devourer.
45 changes: 45 additions & 0 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,51 @@ void RtlJaguarDevice::StopCwTone() {
_logger->info("CW single-tone stopped — chip restored");
}

/* Modulated continuous TX — vendor mpt_StartOfdmContTx (hal_mp.c). Unlike the
* CW tone this leaves the OFDM modulator + scrambler running and drives the
* normal TX pipeline; the caller feeds frames with send_packet and the chip
* holds a continuous modulated carrier. Rate comes from `mode` (SetTxMode);
* per-rate TXAGC/power is the normal path (SetTxPowerOverride/efuse). */
void RtlJaguarDevice::StartContinuousTx(const devourer::TxMode &mode) {
if (_cont_active)
return;
SetTxMode(mode);

/* OFDM block on. */
if (!(_radioManagement->phy_query_bb_reg_public(rFPGA0_RFMOD, bOFDMEn)))
_device.phy_set_bb_reg(rFPGA0_RFMOD, bOFDMEn, 1);
/* CCK test mode off, scrambler on. */
_device.phy_set_bb_reg(rCCK0_System, bCCKBBMode, 0);
_device.phy_set_bb_reg(rCCK0_System, bCCKScramble, 1);
/* Continuous-TX mode bits 0x914[18:16] = OFDM_ContinuousTx (1). */
_device.phy_set_bb_reg(rSingleTone_ContTx_Jaguar, BIT18 | BIT17 | BIT16, 0x1);
/* HSSI params the vendor sets for the continuous stream. */
_device.phy_set_bb_reg(rFPGA0_XA_HSSIParameter1, bMaskDWord, 0x01000500);
_device.phy_set_bb_reg(rFPGA0_XB_HSSIParameter1, bMaskDWord, 0x01000500);

_cont_active = true;
_logger->info("Modulated continuous TX armed @ ch{} (feed frames via "
"send_packet; StopContinuousTx to end)",
_channel.Channel);
}

/* Mirror of the vendor mpt_StopOfdmContTx: clear the continuous-TX mode bits,
* settle, pulse a BB reset, restore the HSSI params. */
void RtlJaguarDevice::StopContinuousTx() {
if (!_cont_active)
return;
_device.phy_set_bb_reg(rSingleTone_ContTx_Jaguar, BIT18 | BIT17 | BIT16, 0x0);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
/* BB reset pulse. */
_device.phy_set_bb_reg(rPMAC_Reset, bBBResetB, 0x0);
_device.phy_set_bb_reg(rPMAC_Reset, bBBResetB, 0x1);
_device.phy_set_bb_reg(rFPGA0_XA_HSSIParameter1, bMaskDWord, 0x01000100);
_device.phy_set_bb_reg(rFPGA0_XB_HSSIParameter1, bMaskDWord, 0x01000100);

_cont_active = false;
_logger->info("Modulated continuous TX stopped — chip restored");
}

/* Frame-free RX energy snapshot for the AC (Jaguar-1) BB. Reads the phydm
* OFDM/CCK false-alarm (0xF48/0xA5C) + CCA (0xF08) counters and the DIG IGI
* (0xC50), then resets the counters (phydm_false_alarm_counter_reg_reset AC:
Expand Down
17 changes: 17 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class RtlJaguarDevice : public IRtlDevice {
uint32_t _cw_rf00 = 0;
uint32_t _cw_bb[4] = {0, 0, 0, 0};

/* Modulated continuous TX (StartContinuousTx/StopContinuousTx) guard. */
bool _cont_active = false;

public:
RtlJaguarDevice(RtlUsbAdapter device, Logger_t logger);
~RtlJaguarDevice() override;
Expand Down Expand Up @@ -102,6 +105,20 @@ class RtlJaguarDevice : public IRtlDevice {
void StartCwTone(uint8_t gain);
void StopCwTone();

/* Realtek MP modulated continuous TX — the sibling of StartCwTone. Where the
* CW tone radiates a bare unmodulated LO carrier, this streams a *real*
* OFDM/HT/VHT waveform back-to-back at `mode`'s rate (the vendor
* mpt_StartOfdmContTx path: OFDM block on, scrambler on, continuous-TX mode
* bits 0x914[18:16]=1). It applies `mode` via SetTxMode; the caller primes a
* few frames to load a PPDU, then the chip holds a 100%-duty modulated carrier
* (idle-hold — no continuous feed needed). Full-channel, full-MCS occupancy —
* the active stimulus for spectral / power / thermal characterisation.
* StopContinuousTx clears the mode bits, pulses a BB reset, and restores.
* Idempotent via _cont_active. TXAGC/power is the normal per-rate path
* (SetTxPowerOverride), not a bare RF gain like the CW tone. */
void StartContinuousTx(const devourer::TxMode& mode);
void StopContinuousTx();

/* Frame-free RX energy / channel-busy snapshot (see RxSense.h) — reads the
* phydm OFDM/CCK false-alarm + CCA counters (0xF48/0xA5C/0xF08) and the DIG
* IGI noise-floor (0xC50), then resets the counters so the next call is a
Expand Down
52 changes: 52 additions & 0 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,58 @@ void RtlJaguar2Device::StopCwTone() {
_logger->info("CW single-tone stopped — chip restored");
}

/* Modulated continuous TX. On Jaguar2 the vendor 0x914 continuous-TX register
* mode wedges the USB TX FIFO (bulk-OUT NAKs) rather than holding a carrier, so
* the hardware-continuous path is NOT engaged here (unlike Jaguar1). Instead we
* apply the rate and let the caller's back-to-back send_packet loop
* (DEVOURER_TX_GAP_US=0) supply the modulated stimulus at beacon duty — enough
* for the link probe's per-frame SNR/EVM read. True 100%-duty HW continuous on
* Jaguar2 is a documented follow-up (needs the FIFO-safe MP setup). */
void RtlJaguar2Device::StartContinuousTx(const devourer::TxMode &mode) {
if (_cont_active)
return;
SetTxMode(mode);

/* Jaguar2 HW continuous TX. Setting 0x914[18:16]=1 alone wedges the USB TX
* FIFO; the missing piece is rCCAonSec (0x838)=0x6d (the vendor sets it before
* continuous mode, ioctl_mp.c). With it, the 0x914 continuous mode radiates a
* 100%-duty modulated carrier from BB state (SDR-verified: flat ~18 MHz OFDM
* block) — no send_packet feed required (the demo idle-holds). */
constexpr uint16_t REG_RFMOD = 0x800; /* bOFDMEn = bit25 */
constexpr uint16_t REG_CCK0_SYSTEM = 0xa00; /* [1:0]=BBmode, [3]=scramble */
constexpr uint16_t REG_CCAonSec = 0x838;
constexpr uint16_t REG_CONT_TX = 0x914; /* [18:16] = OFDM_TX_MODE */

_cont_cca838 = _device.rtw_read32(REG_CCAonSec);
_device.phy_set_bb_reg(REG_CCAonSec, 0xff, 0x6d);
if (!(_device.rtw_read32(REG_RFMOD) & 0x2000000u))
_device.phy_set_bb_reg(REG_RFMOD, 0x2000000u, 1); /* OFDM block on */
_device.phy_set_bb_reg(REG_CCK0_SYSTEM, 0x3u, 0); /* CCK test mode off */
_device.phy_set_bb_reg(REG_CCK0_SYSTEM, 0x8u, 1); /* scrambler on */
_device.phy_set_bb_reg(REG_CONT_TX, (7u << 16), 0x1); /* OFDM_ContinuousTx */

_cont_active = true;
_logger->info("Modulated continuous TX armed @ ch{} (Jaguar2 HW 100%%-duty "
"carrier; idle-hold, StopContinuousTx to end)",
_channel.Channel);
}

void RtlJaguar2Device::StopContinuousTx() {
if (!_cont_active)
return;
constexpr uint16_t REG_CONT_TX = 0x914;
constexpr uint16_t REG_PMAC_RESET = 0x100; /* bBBResetB = bit8 */
constexpr uint16_t REG_CCAonSec = 0x838;

_device.phy_set_bb_reg(REG_CONT_TX, (7u << 16), 0x0);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_device.phy_set_bb_reg(REG_PMAC_RESET, 0x100u, 0x0);
_device.phy_set_bb_reg(REG_PMAC_RESET, 0x100u, 0x1);
_device.phy_set_bb_reg(REG_CCAonSec, 0xff, _cont_cca838 & 0xff);
_cont_active = false;
_logger->info("Modulated continuous TX stopped — chip restored");
}

void RtlJaguar2Device::SetMonitorChannel(SelectedChannel channel) {
_channel = channel;
/* Retune the RF/BB to the new channel. set_channel_bw is a pure tune (RF18 +
Expand Down
14 changes: 14 additions & 0 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ class RtlJaguar2Device : public IRtlDevice {
void StartCwTone(uint8_t gain);
void StopCwTone();

/* Modulated continuous TX — sibling of StartCwTone. Streams a 100%-duty
* modulated OFDM carrier via the vendor 0x914 continuous mode. The 0x914 bit
* alone wedges the USB TX FIFO; the fix is rCCAonSec (0x838)=0x6d first, after
* which the carrier self-radiates from BB state (no send_packet feed needed —
* the demo idle-holds). SDR-verified as a flat ~18 MHz OFDM block.
* StopContinuousTx clears the mode, pulses a BB reset, restores 0x838. */
void StartContinuousTx(const devourer::TxMode& mode);
void StopContinuousTx();

/* Frame-free RX energy snapshot (see RxSense.h) — the FA/CCA/IGI values
* dig_step samples over its ~100 ms window, plus a fresh NHM power histogram.
* The read side of the CW tone. */
Expand Down Expand Up @@ -93,6 +102,11 @@ class RtlJaguar2Device : public IRtlDevice {
uint32_t _cw_rf00 = 0;
uint32_t _cw_bb[4] = {0, 0, 0, 0};

/* Modulated continuous TX (StartContinuousTx/StopContinuousTx) guard + saved
* pre-continuous rCCAonSec (0x838) for a clean restore. */
bool _cont_active = false;
uint32_t _cont_cca838 = 0;

/* DIG (dynamic initial gain) background thread — periodically runs
* HalJaguar2::dig_step so IGI tracks the false-alarm rate for weak-signal RX. */
std::thread _dig_thread;
Expand Down
Loading
Loading