From a82f6a44432337483beb731d4a7f859f7d085a2e Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:57:04 +0300 Subject: [PATCH 1/4] SPIKE: 5/10 MHz narrowband on Jaguar1 (8812AU/8821AU) via the shared 0x8ac clock block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jaguar1 was believed to have no narrowband path ("no vendor reference, dead enum values only"). The Jaguar2/3 work revealed the mechanism is a baseband ADC/DAC clock divide with the RF left in 20 MHz mode — and the 8812A drives the SAME 0x8ac block: phy_PostSetBwMode8812 writes the bandwidth via 0x8ac under mask 0x003003C3 (bits [1:0] rf-mode, [7:6] small-BW, [9:8] ADC clock, [21:20] DAC clock), and phy_FixSpur_8812A already pokes 0x8ac[9:8] calling it "ADC clock 160M/80M". So the divider fields exist on 8812A silicon; the vendor just never programs small-BW. This spike adds CHANNEL_WIDTH_5/10 to the 8812/8821 bandwidth path (rtw_get_center_ch, phy_SetRegBW_8812 = MAC 20M, PHY_RF6052SetBandwidth = RF 20M, and a new 0x8ac branch that divides ADC/DAC one/two notches down + sets small-BW; phy_FixSpur skipped so it doesn't stomp the divided clock). Divide codes are sweepable via DEVOURER_NB_ADC/NB_DAC. Bench result (first working attempt) — the block reuse holds: - SDR occupied bandwidth, 8812AU TX ch44: 20M=16.8, 10M=8.2, 5M=4.2 MHz (2.05x / 4.02x — clean halving/quartering) - 8812AU 10M TX -> Jaguar3 RX: 15300 hits (decodable, not just narrow) - 8812AU 5M TX @2.4G -> Jaguar3 RX: 14800 (5M works; the 5G zero is the #217 CFO bimodality, not a divide error) - Jaguar3 10M TX -> 8812AU RX: 6900 (receives narrowband too) - 20 MHz on-air regression 9900; ctest 13/13 (wide paths byte-unchanged) Caps: narrowband_ok + kBw5/kBw10 on the 8812/8821 die only (8814A has a separate phy_PostSetBwMode8814A path, untouched). EXPERIMENTAL — divide codes not fully characterized, 8814/8821A-pair and 5M@5G still open. Co-Authored-By: Claude Opus 4.8 --- src/jaguar1/RadioManagementModule.cpp | 53 +++++++++++++++++++++++++-- src/jaguar1/RtlJaguarDevice.cpp | 9 +++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index c7c771a..619fe57 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -188,7 +188,11 @@ static uint8_t rtw_get_center_ch(uint8_t channel, ChannelWidth_t chnl_bw, center_ch = 7; } else if (chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_40) { center_ch = get_40mhz_center_channel(center_ch); - } else if (chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_20) { + } else if (chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_20 || + chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_5 || + chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_10) { + /* 5/10 MHz narrowband (SPIKE, 8812A): a 20 MHz-based baseband re-clock — + * the RF stays a 20 MHz tune, center == primary. */ center_ch = channel; } else { throw std::logic_error("not yet implemented"); @@ -1886,14 +1890,53 @@ void RadioManagementModule::phy_PostSetBwMode8812() { break; + case CHANNEL_WIDTH_5: + case CHANNEL_WIDTH_10: { + /* SPIKE — 5/10 MHz narrowband on Jaguar1 (no vendor reference). The + * 8812A shares the 8822B/8821C baseband clock block at 0x8ac: bits + * [9:8] are the ADC clock (phy_FixSpur_8812A calls [9:8]=3 "ADC 160M", + * [9:8]=2 "80M" — the normal 20 MHz value), [21:20] the DAC clock, + * [7:6] the (8822B) small-BW field. Hypothesis: divide the ADC/DAC + * clocks down and set small-BW to shrink the occupied bandwidth while + * the RF stays a 20 MHz tune — exactly the Jaguar2/3 trick. The divide + * codes are the unknown (8812A encoding differs from the 8822B's), so + * they are sweepable via DEVOURER_NB_ADC / DEVOURER_NB_DAC; the + * defaults step one notch down from the 20 MHz values (ADC 2->1/0, + * DAC 3->2/1) per octave of bandwidth. */ + const bool is5 = (_currentChannelBw == CHANNEL_WIDTH_5); + uint32_t adc = is5 ? 0u : 1u; /* 20M ADC / 40M ADC */ + uint32_t dac = is5 ? 1u : 2u; /* divide DAC one/two notches */ + const uint32_t smallbw = is5 ? 1u : 2u; + if (_tuning.nb_adc) + adc = *_tuning.nb_adc & 0x3; + if (_tuning.nb_dac) + dac = *_tuning.nb_dac & 0x3; + const uint32_t v8ac = + (dac << 20) | (adc << 8) | (smallbw << 6) | 0x0u /* rf mode 20M */; + _device.phy_set_bb_reg(rRFMOD_Jaguar, 0x003003C3, v8ac); + _device.phy_set_bb_reg(rADC_Buf_Clk_Jaguar, BIT30, 0); /* 0x8c4[30]=0 */ + if (_eepromManager->numTotalRfPath >= 2) + _device.phy_set_bb_reg(rL1PeakTH_Jaguar, 0x03C00000, 7); + else + _device.phy_set_bb_reg(rL1PeakTH_Jaguar, 0x03C00000, 8); + _logger->info("SPIKE Jaguar1 narrowband: {} MHz — 0x8ac={:#010x} " + "(adc={} dac={} smallbw={})", + is5 ? 5 : 10, v8ac, adc, dac, smallbw); + break; + } + default: _logger->error("phy_PostSetBWMode8812(): unknown Bandwidth: {}", (int)_currentChannelBw); break; } - /* <20121109, Kordan> A workaround for 8812A only. */ - phy_FixSpur_8812A(_currentChannelBw, _currentChannel); + /* <20121109, Kordan> A workaround for 8812A only. phy_FixSpur rewrites the + * 0x8ac[9:8] ADC clock field — skip it for narrowband so it doesn't stomp + * the divided clock. */ + if (_currentChannelBw != CHANNEL_WIDTH_5 && + _currentChannelBw != CHANNEL_WIDTH_10) + phy_FixSpur_8812A(_currentChannelBw, _currentChannel); /* RTW_INFO("phy_PostSetBwMode8812(): Reg483: %x\n", rtw_read8(adapterState, * 0x483)); */ @@ -1911,6 +1954,8 @@ void RadioManagementModule::phy_SetRegBW_8812(ChannelWidth_t CurrentBW) { RegRfMod_BW = _device.rtw_read16(REG_WMAC_TRXPTCL_CTL); switch (CurrentBW) { + case CHANNEL_WIDTH_5: /* narrowband: MAC stays 20 MHz (BIT7=BIT8=0) */ + case CHANNEL_WIDTH_10: case CHANNEL_WIDTH_20: _device.rtw_write16( REG_WMAC_TRXPTCL_CTL, @@ -1944,6 +1989,8 @@ void RadioManagementModule::PHY_RF6052SetBandwidth8812( * Apply to every populated RF path (4 paths on 8814AU, 2 on 8812AU). */ uint32_t bw_bits; switch (Bandwidth) { + case CHANNEL_WIDTH_5: /* narrowband: RF stays in its 20 MHz mode */ + case CHANNEL_WIDTH_10: case CHANNEL_WIDTH_20: bw_bits = 3; break; diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 99019b4..6be22ff 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -868,6 +868,15 @@ devourer::AdapterCaps RtlJaguarDevice::GetAdapterCaps() { c.rx_chains = chains; c.per_chain_rssi = chains >= 2; c.bw_mask = devourer::bw_mask_for_generation(c.generation); + /* SPIKE: 5/10 MHz narrowband on the 8812/8821 die (NOT 8814, which has its + * own phy_PostSetBwMode8814A path). The 8812A shares the 8822B/8821C 0x8ac + * baseband clock-divider block — bench-demonstrated both directions at both + * widths. Experimental (5 MHz@5 GHz is CFO-limited, divide codes not fully + * characterized). */ + if (_eepromManager->version_id.ICType != CHIP_8814A) { + c.bw_mask |= devourer::kBw5 | devourer::kBw10; + c.narrowband_ok = true; + } c.fastretune_ok = true; /* phy_SwChnl8812_fast (8812/8821) + full-path fallback */ devourer::set_standard_freq_ranges(c); From 44a88d3cb5d6f829b958b8f67884372093e5ba51 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:34:25 +0300 Subject: [PATCH 2/4] Jaguar1 narrowband: characterize the 8812AU divide codes; document 8814A gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Divide-code characterization (tests/jaguar1_nb_divide_sweep.sh — full SDR occupied-bandwidth grid + cross-RX vs a Jaguar3 peer): - The DAC code (0x8ac[21:20]) sets the emitted lobe width; the ADC code (0x8ac[9:8]) sets receive sensitivity. Octave step off the 20 MHz values (DAC 3 / ADC 2): 10 MHz -> DAC 2, ADC 1 (TX lobe 8.2 MHz; RX 4700 hits @ADC1 vs 1900 @ADC0) 5 MHz -> DAC 1, ADC 0 (TX lobe 4.1 MHz; DAC 0/2 read wide) - SDR grid: DAC>=2 yields the 10 MHz lobe; DAC=1 the 5 MHz lobe. The shipped defaults are the characterized optimum. TX cross-RX flat ~12500 across ADC (ADC doesn't shape TX); RX ADC sweep picks ADC 1. - Comment upgraded from hypothesis to measured; DEVOURER_NB_ADC/NB_DAC retained as overrides for other cuts. 8814A: documented as NOT ported — it uses a different BW mechanism (0x8ac[1:0] mode selector + phy_SetBwRegAgc_8814A, not the [9:8] divider), so the 8812AU divide does not transplant; caps already gate it off. 8814 narrowband is a separate research effort (host TX needs the 3081 MCU; RX flaky on the host rig). Co-Authored-By: Claude Opus 4.8 --- src/jaguar1/RadioManagementModule.cpp | 28 ++++++++++++------ tests/jaguar1_nb_divide_sweep.sh | 42 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100755 tests/jaguar1_nb_divide_sweep.sh diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index 619fe57..329fdff 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -1160,6 +1160,12 @@ void RadioManagementModule::InitRFEGpio8814A() { * per bandwidth; both bands write the same value here. */ void RadioManagementModule::phy_SetBwRegAdc_8814A(BandType Band, ChannelWidth_t bw) { + /* NB: the 8814A ADC-clock BW config is a DIFFERENT mechanism from the + * 8812/8821 0x8ac[9:8] divider — here 0x8ac[1:0] is a mode selector + * (20M=0/40M=1/80M=2) paired with phy_SetBwRegAgc_8814A. The 8812AU + * narrowband divide (5/10 MHz) does not transplant here; 8814 narrowband + * is a separate, unported research effort (caps gate it off — see + * RtlJaguarDevice::GetAdapterCaps). */ (void)Band; uint32_t val; switch (bw) { @@ -1896,16 +1902,20 @@ void RadioManagementModule::phy_PostSetBwMode8812() { * 8812A shares the 8822B/8821C baseband clock block at 0x8ac: bits * [9:8] are the ADC clock (phy_FixSpur_8812A calls [9:8]=3 "ADC 160M", * [9:8]=2 "80M" — the normal 20 MHz value), [21:20] the DAC clock, - * [7:6] the (8822B) small-BW field. Hypothesis: divide the ADC/DAC - * clocks down and set small-BW to shrink the occupied bandwidth while - * the RF stays a 20 MHz tune — exactly the Jaguar2/3 trick. The divide - * codes are the unknown (8812A encoding differs from the 8822B's), so - * they are sweepable via DEVOURER_NB_ADC / DEVOURER_NB_DAC; the - * defaults step one notch down from the 20 MHz values (ADC 2->1/0, - * DAC 3->2/1) per octave of bandwidth. */ + * [7:6] the (8822B) small-BW field. Divide the ADC/DAC clocks down and + * set small-BW to shrink the occupied bandwidth while the RF stays a + * 20 MHz tune — exactly the Jaguar2/3 trick. The divide codes are + * CHARACTERIZED on the 8812AU (SDR occupied-bandwidth grid + cross-RX vs + * a Jaguar3 peer — tests/jaguar1_nb_divide_sweep.sh): an octave step off + * the 20 MHz values (DAC 3 / ADC 2): + * 10 MHz -> DAC 2, ADC 1 (TX lobe 8.2 MHz; RX best at ADC 1: 4700 + * vs 1900 hits at ADC 0) + * 5 MHz -> DAC 1, ADC 0 (TX lobe 4.1 MHz) + * The DAC code sets the emitted lobe width, the ADC code sets receive + * sensitivity. Overridable via DEVOURER_NB_ADC / DEVOURER_NB_DAC. */ const bool is5 = (_currentChannelBw == CHANNEL_WIDTH_5); - uint32_t adc = is5 ? 0u : 1u; /* 20M ADC / 40M ADC */ - uint32_t dac = is5 ? 1u : 2u; /* divide DAC one/two notches */ + uint32_t adc = is5 ? 0u : 1u; + uint32_t dac = is5 ? 1u : 2u; const uint32_t smallbw = is5 ? 1u : 2u; if (_tuning.nb_adc) adc = *_tuning.nb_adc & 0x3; diff --git a/tests/jaguar1_nb_divide_sweep.sh b/tests/jaguar1_nb_divide_sweep.sh new file mode 100755 index 0000000..4b34ab6 --- /dev/null +++ b/tests/jaguar1_nb_divide_sweep.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# jaguar1_nb_divide_sweep.sh — characterize the 8812AU narrowband ADC/DAC +# clock-divide codes (0x8ac [9:8]/[21:20]) against the SDR. For a target +# width, sweep DEVOURER_NB_DAC 0..3 x DEVOURER_NB_ADC 0..3 and record the +# occupied bandwidth of each combo's continuous TX. The TX lobe width tracks +# the DAC clock; the goal code produces ~target MHz with a single clean lobe. +# +# sudo tests/jaguar1_nb_divide_sweep.sh <5|10> +set -uo pipefail +cd "$(dirname "$0")/.." +BW=${1:-10} +VID=0x0bda PID=0x8812 +CH=44 FREQ=5220e6 RATE=46.08e6 +OUT=/tmp/j1_nb_sweep_$BW +rm -rf "$OUT"; mkdir -p "$OUT" + +cleanup() { sudo pkill -x txdemo 2>/dev/null; } +trap cleanup EXIT INT TERM + +probe() { # freq label -> prints occ_bw MHz + sudo python3 tests/sdr_tx_probe.py --freq "$FREQ" --rate "$RATE" --gain 50 \ + --nsamps 5e6 --label "$1" 2>&1 | + grep -oE 'occ_bw=[0-9.]+MHz' | head -1 +} + +echo "=== 8812AU ${BW} MHz narrowband ADC/DAC divide-code grid (occ_bw) ===" +printf " %8s %8s %8s %8s (DAC->)\n" 0 1 2 3 +for adc in 0 1 2 3; do + printf "ADC=%d" "$adc" + for dac in 0 1 2 3; do + sudo env DEVOURER_VID=$VID DEVOURER_PID=$PID DEVOURER_CHANNEL=$CH \ + DEVOURER_NB_BW=$BW DEVOURER_NB_ADC=$adc DEVOURER_NB_DAC=$dac \ + DEVOURER_TX_GAP_US=0 timeout -s INT -k 5 24 ./build/txdemo \ + >"$OUT/tx_a${adc}d${dac}.log" 2>&1 & + sleep 9 + ob=$(probe "a${adc}d${dac}") + printf " %8s" "${ob#occ_bw=}" + sudo pkill -x txdemo 2>/dev/null; wait 2>/dev/null; sleep 1 + done + printf "\n" +done +echo "target ~${BW} MHz single lobe; baseline noise reads ~45 MHz (full window)" From 71aa73f37f1a38693f0d799b600cb7004b8916a5 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:48:30 +0300 Subject: [PATCH 3/4] =?UTF-8?q?Jaguar1=20narrowband:=20gate=20to=20the=208?= =?UTF-8?q?812=20die=20=E2=80=94=20the=208821A=20can't=20divide=20the=20DA?= =?UTF-8?q?C=20clock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bench characterization of the 8821AU (2357:0120): it is healthy at 20 MHz (TX 15200 hits, 0 USB failures; RX 11400) but narrowband destabilizes its TX — bulk-out submission failures (rc -2) scale with DAC-clock divide depth: dac=2 ~35% fail, dac=1 ~72%, vs 0% at full clock. Its 1T1R clock tree couples the DAC clock to the TX DMA/USB path, which the divide starves; the 8812AU (separate 2T2R baseband) is unaffected. - Caps: narrowband_ok + kBw5/kBw10 now gated to CHIP_8812 (8812AU/8811AU) only, was all-non-8814. - Path: the 0x8ac narrowband branch guards on CHIP_8812 and falls back to a plain 20 MHz baseband on other dies (verified: 8821AU forced-NB TX = 0 failures, received as 20 MHz). 8812AU narrowband unaffected (10M TX -> J3 RX 15800). ctest 13/13. Remaining known-open (not code bugs): 5 MHz @ 5 GHz is CFO-limited (issue #217, needs the XTAL-cap trim); 8814A narrowband is a separate effort against its different BW mechanism. Co-Authored-By: Claude Opus 4.8 --- src/jaguar1/RadioManagementModule.cpp | 10 ++++++++++ src/jaguar1/RtlJaguarDevice.cpp | 16 ++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index 329fdff..ad2633f 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -1913,6 +1913,16 @@ void RadioManagementModule::phy_PostSetBwMode8812() { * 5 MHz -> DAC 1, ADC 0 (TX lobe 4.1 MHz) * The DAC code sets the emitted lobe width, the ADC code sets receive * sensitivity. Overridable via DEVOURER_NB_ADC / DEVOURER_NB_DAC. */ + /* 8812 die only — the 8821A's clock tree starves TX when the DAC clock + * is divided (see RtlJaguarDevice::GetAdapterCaps). Fall back to a plain + * 20 MHz baseband there rather than wedge the TX path. */ + if (_eepromManager->version_id.ICType != CHIP_8812) { + _logger->warn("narrowband not supported on this Jaguar1 die — " + "applying 20 MHz baseband"); + _device.phy_set_bb_reg(rRFMOD_Jaguar, 0x003003C3, 0x00300200); + _device.phy_set_bb_reg(rADC_Buf_Clk_Jaguar, BIT30, 0); + break; + } const bool is5 = (_currentChannelBw == CHANNEL_WIDTH_5); uint32_t adc = is5 ? 0u : 1u; uint32_t dac = is5 ? 1u : 2u; diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 6be22ff..5647a09 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -868,12 +868,16 @@ devourer::AdapterCaps RtlJaguarDevice::GetAdapterCaps() { c.rx_chains = chains; c.per_chain_rssi = chains >= 2; c.bw_mask = devourer::bw_mask_for_generation(c.generation); - /* SPIKE: 5/10 MHz narrowband on the 8812/8821 die (NOT 8814, which has its - * own phy_PostSetBwMode8814A path). The 8812A shares the 8822B/8821C 0x8ac - * baseband clock-divider block — bench-demonstrated both directions at both - * widths. Experimental (5 MHz@5 GHz is CFO-limited, divide codes not fully - * characterized). */ - if (_eepromManager->version_id.ICType != CHIP_8814A) { + /* SPIKE: 5/10 MHz narrowband on the 8812 die only (8812AU/8811AU). The + * 8812A shares the 8822B/8821C 0x8ac baseband clock-divider block — the + * divide codes are bench-characterized (tests/jaguar1_nb_divide_sweep.sh), + * TX+RX both directions, both widths. EXCLUDED: the 8821A — its 1T1R clock + * tree couples the DAC clock to the TX DMA/USB path, so dividing it starves + * TX (bulk-out submission failures scale with divide depth: dac=2 ~35% fail, + * dac=1 ~72%, vs 0% at full clock; 20 MHz is unaffected). The 8814A has its + * own phy_PostSetBwMode8814A path (0x8ac[1:0] mode selector) that the + * divide does not transplant to. Experimental (5 MHz@5 GHz is CFO-limited). */ + if (_eepromManager->version_id.ICType == CHIP_8812) { c.bw_mask |= devourer::kBw5 | devourer::kBw10; c.narrowband_ok = true; } From 6f01e7b512279e848bfcc2994d7e3dacbd18562c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:00:26 +0300 Subject: [PATCH 4/4] docs: narrowband machinery + walls guide (docs/narrowband.md); wire README/CLAUDE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New docs/narrowband.md — the 5/10 MHz companion to docs/frequency-hopping.md: the underclock-the-baseband idea, the per-generation register machinery (Jaguar1 0x8ac shared block, Jaguar2 0x8ac + RF18 re-latch edge, Jaguar3 0x9b0/0x9b4), the chip-specific walls (8822B RF18 edge, 8821A DAC-clock starve, 5 MHz/5 GHz CFO limit, 8814A different mechanism), and the SDR-occupied-bandwidth + cross-RX validation methodology — so the next person who hits one of these walls finds it documented. README: link the doc, mark 8812AU/8811AU 5/10 MHz capable. CLAUDE.md: the Jaguar1 bullet now records the 8812-die narrowband and the 8821A/8814A exclusions; drop the stale "Jaguar1 has no narrowband" line. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 12 ++- README.md | 13 +-- docs/narrowband.md | 192 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 docs/narrowband.md diff --git a/CLAUDE.md b/CLAUDE.md index d40e8ec..1fd4dd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` @@ -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** diff --git a/README.md b/README.md index 7694508..bf2873a 100644 --- a/README.md +++ b/README.md @@ -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, @@ -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 | @@ -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 diff --git a/docs/narrowband.md b/docs/narrowband.md new file mode 100644 index 0000000..87cc842 --- /dev/null +++ b/docs/narrowband.md @@ -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).