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
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**):
- **Jaguar1** (`src/jaguar1/`): RTL8812AU (2T2R reference), RTL8811AU (1T1R cut,
rides the 8812 path), RTL8814AU (4T4R RF / 3-SS baseband — host-pushed TX
requires the on-chip 3081 MCU booted during FW download; a failed FW-boot
poll means dead TX while RX still works), RTL8821AU (1T1R + BT).
poll means dead TX while RX still works), RTL8821AU (1T1R + BT). **5/10 MHz
narrowband on the 8812 die** (8812AU/8811AU): the 8812A shares the Jaguar2
`0x8ac` ADC/DAC clock-divider block, so the same re-clock trick works even
though the vendor never wired it — TX+RX bench-characterized. The 8821A is
excluded (dividing its DAC clock starves the 1T1R TX DMA/USB path) and the
8814A uses a different BW block; both fall back to 20 MHz. See
`docs/narrowband.md`.
- **Jaguar2** (`src/jaguar2/`): RTL8822BU / RTL8812BU (chip-id `0x0a`) and
RTL8811CU / RTL8821CU (chip 8821C, chip-id `0x09`). A hybrid: HalMAC FW
download / MAC init / power sequencing like Jaguar3, phydm `check_positive`
Expand All @@ -35,8 +41,8 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**):
at the end of Init) is the debugging lever that found the RF18-edge bug.
- **Jaguar3** (`src/jaguar3/`): rtl8822c (RTL8812CU/8822CU, chip-id `0x13`) and
rtl8822e (RTL8812EU/8822EU, chip-id `0x17`). **5/10 MHz narrowband** (its
re-clock lives in the `0x9b0`/`0x9b4` dividers; Jaguar1 silicon has no
narrowband — the vendor drivers carry only dead enum values), 80 MHz (incl.
re-clock lives in the `0x9b0`/`0x9b4` dividers, vs the `0x8ac` block the
Jaguar1/2 chips share), 80 MHz (incl.
a 40-in-80 frame via TX-descriptor DATA_SC), and halrf calibration
(DACK/IQK/TXGAPK/thermal tracking).
Sustained 5 GHz TX needs the **coex runtime thread**
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ long-range digital video links.
- **Frequency hopping at FHSS speed.** A channel hop costs ~0.5–2.5 ms
depending on chip — fast enough to hop on every packet
([how](docs/frequency-hopping.md)).
- **Narrowband modes the kernel can't do.** 5 and 10 MHz channels on the
Jaguar2 and Jaguar3 chips — half/quarter the bandwidth, more range from
the same power.
- **Narrowband modes the kernel can't do.** 5 and 10 MHz channels — including
on the decade-old RTL8812AU the vendor never gave narrowband — half/quarter
the bandwidth, more range from the same power ([how](docs/narrowband.md)).
- **A radio lab in a dongle.** Channel sounding, per-antenna signal quality,
beamforming report capture (enough to do
[motion sensing](docs/beamforming-victim-sensing.md)), spectrum sweeps,
Expand All @@ -49,8 +49,8 @@ Bandwidth cells are devourer's measured on-air TX throughput (Mbps, HT MCS7,

| Part | RF / streams | 2.4 GHz (ch6) | UNII-1 (ch36) | UNII-2/3 (ch149) | Notes |
| ----------------------------- | ----------------- | ------------- | ------------- | ---------------- | ------------------------------------------- |
| **RTL8812AU** | 2T2R | 56 | 52 | 52 | [CHANEVE CHW50L](https://www.aliexpress.com/item/4000762461362.html) (`0bda:8812`) |
| **RTL8811AU** | 1T1R | — | — | — | 1T1R cut of 8812 silicon; rides the 8812 code path. Not benchmarked |
| **RTL8812AU** | 2T2R | 56 | 52 | 52 | [CHANEVE CHW50L](https://www.aliexpress.com/item/4000762461362.html) (`0bda:8812`). 5/10 MHz capable |
| **RTL8811AU** | 1T1R | — | — | — | 1T1R cut of 8812 silicon; rides the 8812 code path. Not benchmarked. 5/10 MHz capable |
| **RTL8814AU** | 4T4R, 3-SS max | 65 | †(32) | †(32) | `0bda:8813`; tested on COMFAST CF-938AC and CF-960AC — antenna builds differ in realised [RX diversity](docs/measuring-spatial-diversity.md) |
| **RTL8821AU** | 1T1R AC + BT | 54 | 32 | 28 | TP-Link Archer T2U Plus (`2357:0120`) |
| **RTL8822BU** | 2T2R + BT | 52 | 50 | 49 | TP-Link Archer T3U (`2357:012d`). 5/10 MHz capable |
Expand Down Expand Up @@ -167,6 +167,9 @@ one `IRtlDevice` interface covers all three generations.
per-layer PHY rates, corrupt-frame salvage, outer erasure code.
- [Frequency hopping](docs/frequency-hopping.md) — how per-packet hopping
works and what it costs on each chip.
- [Narrowband](docs/narrowband.md) — 5/10 MHz channels across all three
generations: the baseband re-clock, the per-chip register machinery, and the
walls (RF re-latch edges, per-die clock coupling, the 5 MHz/5 GHz CFO limit).
- [Startup time](docs/startup-time.md) — devourer vs. kernel driver,
measured on every supported chip.
- [Adapter doctor](docs/adapter-doctor.md) — dying-dongle triage: EFUSE
Expand Down
192 changes: 192 additions & 0 deletions docs/narrowband.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Narrowband (5 and 10 MHz) channels

This document describes how devourer runs a Realtek Jaguar adapter in 5 MHz and
10 MHz channel bandwidths — half and quarter of a standard 20 MHz channel — and
the machinery that makes it work across three chip generations. Narrowband
trades throughput for link budget: halving the bandwidth roughly halves the
noise power the receiver integrates, which is worth ~3 dB per octave of range
on a clean link. It doubles as a porting guide, because the underlying trick is
the same everywhere and the walls are chip-specific.

## The one idea: underclock the baseband, leave the RF alone

A narrowband channel is **not** a different RF tune. The RF synthesizer stays in
its ordinary 20 MHz mode — same LO, same channel, same 20 MHz analog filter
front end. What changes is the **baseband sample-clock rate**: the ADC and DAC
run at a divided-down clock, so the same OFDM machine emits and captures a
proportionally narrower signal. 10 MHz halves the ADC/DAC clocks; 5 MHz quarters
them.

Two consequences fall out of this and are worth internalizing before touching a
register:

- **Radiotap stays 20 MHz.** The modulation the frame carries is unchanged — it
is a 20 MHz-shaped waveform played out of a slower DAC. A monitor sniffer
reports 20 MHz; the *only* witness that anything narrowed is a wideband SDR
measuring occupied bandwidth, or another narrowband receiver decoding it. Any
validation that relies on a second Wi-Fi card's reported bandwidth is blind
here.
- **The MAC still thinks it is 20 MHz.** `REG_WMAC_TRXPTCL_CTL` (0x668) BW bits
stay cleared, exactly as for 20 MHz. Narrowband is a pure PHY re-clock for
monitor/injection use; the vendor drivers' AP/STA-mode narrowband additionally
scales SIFS and strips CCK, which devourer does not need for injection.

Because the RF is untouched, narrowband is applied as an **end-of-bring-up
retune** on an already-tuned channel: bring the chip up at 20 MHz, then re-clock.

## Per-generation register machinery

All three generations do the same thing — divide the ADC and DAC clocks and tell
the baseband it is "small BW" — but the register block differs.

### Jaguar1 (RTL8812AU / RTL8811AU) — the shared `0x8ac` block

The 8812A drives its bandwidth through BB register `0x8ac` (`rRFMOD_Jaguar`)
under mask `0x003003C3`, whose fields are:

| Field | Bits | Role |
|-------|------|------|
| rf mode | [1:0] | 0 = 20 MHz (kept for narrowband) |
| small BW | [7:6] | 0 normally; 1 = 5 MHz, 2 = 10 MHz |
| ADC clock | [9:8] | receive sample clock divider |
| DAC clock | [21:20] | transmit sample clock divider |

The `[9:8]` field is unambiguously the ADC clock: the vendor's own
`phy_FixSpur_8812A` pokes it and comments it "ADC clock 160M / 80M". Normal
20 MHz sits at ADC `[9:8]=2`, DAC `[21:20]=3`. Narrowband steps both down one
notch per octave:

| Bandwidth | DAC `[21:20]` | ADC `[9:8]` | small BW `[7:6]` | SDR lobe |
|-----------|---------------|-------------|------------------|----------|
| 20 MHz | 3 | 2 | 0 | 16.8 MHz |
| 10 MHz | 2 | 1 | 2 | 8.2 MHz |
| 5 MHz | 1 | 0 | 1 | 4.1 MHz |

`0x8c4[30]` (ADC buffer clock) is held at 0, and `phy_FixSpur_8812A` is skipped
for narrowband (it would overwrite `[9:8]`). Bench-characterized fact worth
knowing for porting: **the DAC code sets the transmitted lobe width, the ADC
code sets receive sensitivity** — sweeping ADC while transmitting does nothing
to the emission, while at 10 MHz an ADC of 1 receives roughly 2.5× better than 0.
The codes are overridable via `DEVOURER_NB_ADC` / `DEVOURER_NB_DAC` for
uncharacterized cuts.

### Jaguar2 (RTL8822BU / RTL8821CU) — `0x8ac` packed, plus an RF re-latch edge

Jaguar2 uses the same `0x8ac` register with the small-BW field at `[7:6]` and
the ADC/DAC clock fields; it also sets `0x8c4[30]=0` and `0x8c8[31]=1`, and the
RF18 bandwidth bits stay in their 20 MHz encoding. The bit values differ from
the 8812A's — the block is shared, the divide encoding is not.

The Jaguar2 wall: **the 8822B RF synthesizer only re-latches its internal
channel state on an RF18 *value edge*.** Narrowband is applied as an
end-of-bring-up retune, and the RF18 value it wants is the same 20 MHz-mode
value the chip already holds — so a plain rewrite is a no-op and the RF stays
latched to its pre-re-clock internals, leaving the receiver deaf and the
transmitter emitting nothing decodable (band-independent). The kernel driver
never trips this because its flow always tunes *through* a channel change (an
RF18 edge) around any bandwidth switch. The fix is to force the edge: write RF18
once with a substitute channel byte, then the real value. (The 8821C variant
does not share this dependence.)

### Jaguar3 (RTL8822CU / RTL8822EU) — the `0x9b0`/`0x9b4` dividers

Jaguar3 moved the block: small-BW lives in `0x9b0[7:6]`, the ADC/DAC dividers in
`0x9b4`, with the RF18 bandwidth bits again at the 20 MHz encoding. The 8822E
additionally needs a MAC-clock reconfiguration (`REG_AFE_CTRL1 0x24[21:20]` plus
the TSF/EDCA microsecond-tick dividers `0x55c`/`0x638`) and CFR/TX-shaping
tweaks, and a DAC-FIFO soft reset to suppress a 5 MHz image; the 8822C narrowband
works without them. The 8822E's vendor 5 MHz DAC divide code is dead on real
silicon — only the 8822C code emits the lobe, a fact the OpenHD 8812EU ecosystem
converged on independently.

### Common to all generations

- **A baseband reset relatches the divided clock tree.** Toggling MAC `0x0`
BIT16 (the `_iqk_bb_reset` mechanism) after the re-clock makes the DAC/DFE
pick up the new sample rate. Jaguar3 needs it; the Jaguar2 narrowband path
performs it; the 8812A happens not to require it.
- **TX power folds to the 20 MHz column.** The per-rate regulatory `txpwr_lmt`
tables carry no narrowband rows. A raw 5/10 bandwidth index reads as HT40 and
misses the table entirely — leaving TX power unclamped at the rail. Narrowband
must fold to the 20 MHz power column (the RF is a 20 MHz tune anyway).

## The walls (in case you hit the same one)

Narrowband looks like a two-register change and is mostly a collection of
chip-specific traps. The ones this port paid for, current-state:

1. **8822B RF18 re-latch edge** (above). Symptom: SDR shows a perfectly narrow
lobe, but nothing decodes and no valid frame airs — the classic "the TX side
works, the whole thing is dead" signature of a stale RF latch.

2. **8821A cannot divide its DAC clock.** The RTL8821AU is healthy at 20 MHz but
its 1T1R clock tree couples the DAC clock to the TX DMA/USB path. Dividing it
starves TX: bulk-out submissions fail (libusb `rc -2`) at a rate that scales
with divide depth — ~35 % failures at the 10 MHz DAC code, ~72 % at 5 MHz,
0 % at full clock. This is a per-die difference *within* Jaguar1: the 8812AU's
2T2R baseband is unaffected. devourer gates narrowband to the 8812 die and
falls the 8821A back to a 20 MHz baseband. The lesson generalizes: a shared
register block does not guarantee shared behavior; validate each die on
hardware.

3. **5 MHz at 5 GHz is CFO-limited.** Quartering the clock quarters the OFDM
subcarrier spacing (78.125 → ~19.5 kHz), so the carrier-frequency-offset
budget shrinks 4×, while the absolute crystal offset between a TX/RX pair is
~2.2× larger at 5.2 GHz than at 2.4 GHz for the same ppm. A far-offset crystal
pair is therefore **bimodal per bring-up** at 5 MHz/5 GHz — it syncs on one
power-up and is deaf on the next — while a closer-crystal peer decodes the
same transmitter and the same pair is stable at 2.4 GHz. This is physics, not
a driver bug; the durable fix is a crystal-cap trim lever (tracked
separately).

4. **8814A uses a different bandwidth mechanism.** The 4T4R 8814A configures
bandwidth through `0x8ac[1:0]` as a mode selector plus a separate ADC/AGC
function, not the `[9:8]` divider the 8812A exposes. The 8812AU divide does
not transplant; 8814 narrowband is an unported research effort.

## Validation methodology

Because radiotap lies about bandwidth, narrowband is validated two ways, and
both are needed — the first proves the emission narrowed, the second proves it
is still coherent.

### SDR occupied bandwidth

Transmit continuously and measure the occupied bandwidth of the emission with a
wideband SDR, isolating the DUT's footprint from ambient with a differential PSD
(TX PSD minus a silent-baseline PSD). A working narrowband TX halves (10 MHz) or
quarters (5 MHz) the −10 dB lobe width relative to 20 MHz. This is the only test
that catches a wrong divide code — but it *cannot* distinguish a clean narrow
lobe from a narrowed-but-garbage one (an aliased or image-corrupted spectrum can
read the right width and decode nothing).

### Cross-RX hit count

Run a narrowband transmitter on one adapter and a narrowband receiver on
another, counting delivered frames. This is the discriminator: it proves the
divided ADC path actually demodulates a real narrowband signal, not just that
the lobe is narrow. Pair a chip under test with a known-good narrowband peer of
another generation to isolate TX-side from RX-side faults. When a cross-RX cell
reads zero, an arbiter receiver (a third, closer-crystal adapter) separates a
real fault from CFO bimodality: if the arbiter decodes the same transmitter, the
zero is the crystal pair, not the code.

### The divide-code sweep

`DEVOURER_NB_ADC` / `DEVOURER_NB_DAC` expose the ADC/DAC clock fields for a grid
sweep against the SDR (TX lobe) and cross-RX (decode quality). This is how the
8812AU codes above were pinned rather than guessed, and how the 8821A DAC-starve
was characterized (the TX failure rate versus divide depth).

## Using it

`DEVOURER_NB_BW=5` or `=10` on the demos selects narrowband; the library exposes
it as `CHANNEL_WIDTH_5` / `CHANNEL_WIDTH_10` on `SelectedChannel`, and
`IRtlDevice::GetAdapterCaps().narrowband_ok` reports whether the running chip
supports it. Support today: **Jaguar2 (8822B/8821C) and Jaguar3 (8822C/8822E)**
fully; **Jaguar1 on the 8812 die (8812AU/8811AU)** as a characterized addition;
8821A and 8814A excluded (see the walls above).

Test scripts: `tests/jaguar2_narrowband_sdr.sh` and
`tests/jaguar1_nb_divide_sweep.sh` (SDR occupied-bandwidth), and
`tests/narrowband_cross_rx.sh` (cross-generation decode).
Loading
Loading