From be8d1cf299755673f42f50fe02c39223b523ca4c Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:43:38 +0300 Subject: [PATCH 1/3] =?UTF-8?q?Jaguar2=205/10=20MHz=20narrowband=20?= =?UTF-8?q?=E2=80=94=208821C=20validated,=208822B=20gated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the vendor narrowband re-clock (config_phydm_switch_bandwidth_8822b/ _8821c: 0x8ac small-BW [7:6] + ADC/DAC clock fields, 0x8c4[30]=0, 0x8c8[31]=1, RF18 kept at the 20 MHz encoding) to the Jaguar2 HAL, plus the three pieces the vendor register recipe alone does not give, each hardware-bisected on the bench: - BB reset after the re-clock (MAC 0x0 BIT16 toggle) so the DAC/DFE relatch at the new sample rate — same lesson the Jaguar3 port hardware-taught. - Narrowband applied as an END-of-bring-up retune (kernel-flow parity): the vendor NB switch only ever runs on an initialized interface; re-clocking mid-bring-up (before IQK / TRX re-assert) leaves the chip deaf. - The config_phydm_switch_band CCK-block trio (5G: 0x808[28]=0, 0x454[7]=1, 0xa80[18]=1) the 8822B path had never ported — a CCK engine left enabled under the NB re-clock breaks the OFDM demod. Also fixed en route: the 8821C RX-DFIR 0xc20[31] conditional was wrong for bw=5/6, and apply_tx_power read raw ChannelWidth 5/6 as HT40 and missed the txpwr_lmt lookup entirely (unclamped 63) — narrowband now folds to the 20 MHz power column, mirroring Jaguar3. 8821C (RTL8811CU/8821CU/8821CE) is fully validated: SDR occupied-bandwidth 16.71/8.55/4.41 MHz (ratios 1.95 / 3.79), cross-generation RX+TX against Jaguar3 at 20 MHz-control parity for both widths, NB survives FastRetune, 2.4 GHz + efuse-power fold path verified. 8822B stays gated (narrowband_ok=false, bw_mask clears kBw5|kBw10): with the same recipe it syncs ~10% of NB frames and airs no NB TX, while the OpenHD kernel module decodes 5000/5000 on the same dongle with a byte-identical firmware blob and identical page-8 BB state (write_reg reverse-bisect). The kernel's own 20->10 MHz delta shows an RF-internal RXBB LPF retune (RF 0x82/0xf2) driven by a runtime FW interaction devourer does not reproduce yet — documented in the NB branch comment; the DEVOURER_NB_ADC/DEVOURER_NB_DAC divider-override knobs remain as the debugging levers for that follow-up. Jaguar1 has no vendor narrowband implementation anywhere (enum-only dead code) — documented out of scope. New tests: tests/jaguar2_narrowband_sdr.sh (differential-PSD occupied-BW, the Jaguar3 script's twin) and tests/narrowband_cross_rx.sh (both-ends re-clocked demod proof across generations, with DLFW-flake retry). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 20 +++-- README.md | 10 ++- docs/frequency-hopping.md | 5 +- docs/rx-spectrum-sensing.md | 5 +- examples/common/env_config.cpp | 2 + examples/rx/main.cpp | 3 +- examples/tx/main.cpp | 3 +- src/AdapterCaps.h | 18 ++-- src/DeviceConfig.h | 10 ++- src/jaguar2/HalJaguar2.cpp | 116 +++++++++++++++++++++++-- src/jaguar2/HalJaguar2.h | 29 +++++-- src/jaguar2/HalmacJaguar2MacInit.cpp | 13 ++- src/jaguar2/RtlJaguar2Device.cpp | 32 ++++++- tests/adapter_caps_selftest.cpp | 5 +- tests/jaguar2_narrowband_sdr.sh | 122 +++++++++++++++++++++++++++ tests/narrowband_cross_rx.sh | 101 ++++++++++++++++++++++ 16 files changed, 447 insertions(+), 47 deletions(-) create mode 100755 tests/jaguar2_narrowband_sdr.sh create mode 100755 tests/narrowband_cross_rx.sh diff --git a/CLAUDE.md b/CLAUDE.md index adf60d8..919990b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,12 +21,22 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**): RTL8811CU / RTL8821CU (chip 8821C, chip-id `0x09`). A hybrid: HalMAC FW download / MAC init / power sequencing like Jaguar3, phydm `check_positive` register tables like Jaguar1 (shared `PhyTableLoader`). RX + TX on 2.4/5 GHz - at 20/40/80 MHz, per-rate bandwidth-aware efuse TX power clamped to generated - `txpwr_lmt` tables. + at 20/40/80 MHz, plus **5/10 MHz narrowband on the 8821C variant** (a + baseband ADC/DAC re-clock packed into BB `0x8ac`; the RF stays in 20 MHz + mode; applied as an end-of-bring-up retune, kernel-flow parity). The 8822B + carries the same NB register recipe but is gated off (`narrowband_ok=false`): + its NB RX syncs ~10% and NB TX airs nothing, while the OpenHD kernel module + does full-rate NB on the same dongle with the same firmware — the kernel's + NB switch retunes the RF RXBB LPF via a runtime FW interaction not yet + ported (see the `set_channel_bw` NB branch comment). Per-rate + bandwidth-aware efuse TX power clamped to generated `txpwr_lmt` tables + (narrowband folds to the 20 MHz column). - **Jaguar3** (`src/jaguar3/`): rtl8822c (RTL8812CU/8822CU, chip-id `0x13`) and - rtl8822e (RTL8812EU/8822EU, chip-id `0x17`). Adds **5/10 MHz narrowband** - the Jaguar1 silicon lacks, 80 MHz (incl. a 40-in-80 frame via TX-descriptor - DATA_SC), and halrf calibration (DACK/IQK/TXGAPK/thermal tracking). + 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. + 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** (`RtlJaguar3Device::coex_runtime_loop`, started in `InitWrite`) — without its ~2 s WiFi-only coex re-apply + FW heartbeats, the combo chip's coex firmware diff --git a/README.md b/README.md index ee57aed..25ce243 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ long-range digital video links. 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 - newest chips — half/quarter the bandwidth, more range from the same power. + Jaguar3 chips and the Jaguar2 8821C — half/quarter the bandwidth, more + range from the same power. - **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, @@ -54,9 +55,10 @@ Bandwidth cells are devourer's measured on-air TX throughput (Mbps, HT MCS7, | **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`) | | **RTL8812BU** | 1T1R + BT | — | — | — | 1T1R cut of 8822B silicon; rides the 8822BU code path. Not benchmarked | -| **RTL8811CU** | 1T1R + BT | 36 | 29 | 28 | COMFAST CF-811AC (`0bda:c811`) | -| **RTL8812CU** | 2T2R | 65 | 60 | 60 | LB-LINK WDN1300H (`0bda:c812`) | -| **RTL8822CU** | 2T2R + BT | — | — | — | not benchmarked (`0bda:c82c`) | +| **RTL8811CU** | 1T1R + BT | 36 | 29 | 28 | COMFAST CF-811AC (`0bda:c811`). 5/10 MHz capable | +| **RTL8821CU** | 1T1R + BT | — | — | — | rides the 8811CU (8821C) code path. 5/10 MHz capable | +| **RTL8812CU** | 2T2R | 65 | 60 | 60 | LB-LINK WDN1300H (`0bda:c812`). 5/10 MHz capable | +| **RTL8822CU** | 2T2R + BT | — | — | — | not benchmarked (`0bda:c82c`). 5/10 MHz capable | | **RTL8812EU** | 2T2R | 8 | 51 | 47 | LB-LINK BL-M8812EU2 (`0bda:a81a`); bare 5 GHz FPV module. 5/10 MHz capable | | **RTL8822EU** | 2T2R + BT | — | — | — | not benchmarked. 5/10 MHz capable | | **RTL8821CE** (PCIe) | 1T1R + BT | — | — | — | Radxa X4 onboard Wi-Fi (`10ec:c821`); not benchmarked | diff --git a/docs/frequency-hopping.md b/docs/frequency-hopping.md index d724529..9246286 100644 --- a/docs/frequency-hopping.md +++ b/docs/frequency-hopping.md @@ -337,7 +337,10 @@ rounds (three of them on the 8821C's band/channel/BW split) — the 8821CU hop i a **single LSSI write**; AGC index, CFO fc, the 8822B RF 0xBE VCO band / ch144 RF 0xDF flag / 2G spur registers and the 8821C 2G CCK filter are composed writes on bucket change. RFE pins and the 8821C switch-band/RF-set block are -band-keyed and stay untouched. +band-keyed and stay untouched. 5/10 MHz narrowband (8821C) survives fast hops +for free: the re-clock state lives in the bandwidth-keyed +`0x8ac`/`0x8c4`/`0x8c8` block the hop never touches, and the NB RF18 BW bits +equal the 20 MHz encoding, so the cached RF18 write is already correct. **Jaguar3** (`RadioManagementJaguar3::fast_retune`, both variants): the RF18 window write inside its 3-wire bracket + force-anapar + BB reset every hop — diff --git a/docs/rx-spectrum-sensing.md b/docs/rx-spectrum-sensing.md index 712aa95..170194c 100644 --- a/docs/rx-spectrum-sensing.md +++ b/docs/rx-spectrum-sensing.md @@ -28,7 +28,8 @@ What *is* available is **scalar, channel-wide** energy: the whole channel, available only on frames that arrive. To turn scalar energy into a coarse *spectrum*, sweep the channel/bandwidth and -sample the energy per bin (narrowband down to 5 MHz on Jaguar3). Per-tone +sample the energy per bin (narrowband down to 5 MHz on Jaguar3 and the +Jaguar2 8821C). Per-tone interference localisation is possible through a different mechanism entirely — the self-sounding beamforming report (see `docs/beamforming-self-sounding.md`), whose per-tone SNR / V-angle variance localises an interferer to ~1 MHz. @@ -126,7 +127,7 @@ The aggregate is drained at each dwell start, so retune-transient frames never leak into a bin. The resolution is the channel grid: 20 MHz on the 2.4/5 GHz plan, and down to -~5 MHz on Jaguar3 (`DEVOURER_NB_BW=5` — the 2.4 GHz channels are 5 MHz apart, so +~5 MHz on Jaguar3 and the Jaguar2 8821C (`DEVOURER_NB_BW=5` — the 2.4 GHz channels are 5 MHz apart, so stepping them at 5 MHz bandwidth gives 5 MHz bins; fast dwells preserve the narrowband dividers, so an NB sweep never re-runs the re-clock recipe). This is a scalar-energy spectrum, not an FFT — there is no sub-channel structure within diff --git a/examples/common/env_config.cpp b/examples/common/env_config.cpp index 543379d..511810b 100644 --- a/examples/common/env_config.cpp +++ b/examples/common/env_config.cpp @@ -104,6 +104,8 @@ devourer::DeviceConfig devourer_config_from_env() { cfg.tuning.rfe_type = static_cast(v); if (env_long("DEVOURER_NB_DAC", &v)) cfg.tuning.nb_dac = static_cast(v & 0x7); + if (env_long("DEVOURER_NB_ADC", &v)) + cfg.tuning.nb_adc = static_cast(v & 0x7); if (const char *e = env_str("DEVOURER_REGULATION")) { if (str_ieq(e, "ETSI")) cfg.tuning.regulation = devourer::Regulation::ETSI; diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index d322e15..8a5d042 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -948,7 +948,8 @@ int main() { /* RX bandwidth: 20 MHz by default. DEVOURER_BW=40|80 selects a wide monitor * channel (for receiving HT40 / VHT80 frames); DEVOURER_CHOFFSET picks the * secondary half (1 = secondary above the primary, 2 = secondary below). - * DEVOURER_NB_BW=5|10 re-clocks the baseband to narrowband (Jaguar3 only). */ + * DEVOURER_NB_BW=5|10 re-clocks the baseband to narrowband (Jaguar2/3; + * check the adapter.caps narrowband_ok flag). */ ChannelWidth_t width = CHANNEL_WIDTH_20; uint8_t ch_offset = 0; if (const char *bw_env = std::getenv("DEVOURER_BW")) { diff --git a/examples/tx/main.cpp b/examples/tx/main.cpp index 15a6206..e15ae40 100644 --- a/examples/tx/main.cpp +++ b/examples/tx/main.cpp @@ -490,7 +490,8 @@ int main(int argc, char **argv) { /* Bandwidth for init + hopping. DEVOURER_HOP_BW = 20|40|80 (default 20), * DEVOURER_HOP_OFFSET = primary-channel offset (0=DONT_CARE, 1=LOWER/HT40+, * 2=UPPER/HT40-) for 40/80. FastRetune reuses the device's bandwidth. - * DEVOURER_NB_BW = 5|10 re-clocks the baseband to narrowband (Jaguar3 only). */ + * DEVOURER_NB_BW = 5|10 re-clocks the baseband to narrowband (Jaguar2/3; + * check the adapter.caps narrowband_ok flag). */ ChannelWidth_t init_width = CHANNEL_WIDTH_20; uint8_t init_offset = 0; if (const char *e = std::getenv("DEVOURER_HOP_BW")) { diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 070baa1..c60a661 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -49,21 +49,25 @@ inline const char *generation_name(ChipGeneration g) { } /* Supported channel widths, one bit per width (MHz). A mask, not a max, because - * the set is not contiguous per family: Jaguar3 adds 5/10 MHz narrowband BELOW - * the 20/40/80 the AC families do. */ + * the set is not contiguous per family: Jaguar2/Jaguar3 add 5/10 MHz narrowband + * BELOW the 20/40/80 all AC families do. */ constexpr uint8_t kBw5 = 1u << 0; constexpr uint8_t kBw10 = 1u << 1; constexpr uint8_t kBw20 = 1u << 2; constexpr uint8_t kBw40 = 1u << 3; constexpr uint8_t kBw80 = 1u << 4; -/* J1/J2 do 20/40/80; J3 adds the 5/10 MHz narrowband re-clock. Pure; - * unit-tested in tests/adapter_caps_selftest.cpp. */ +/* J1 does 20/40/80; J2 and J3 add the 5/10 MHz narrowband re-clock (J2 packs + * the ADC/DAC clock word into 0x8ac, J3 into 0x9b0/0x9b4 — same RF-stays-20MHz + * concept). Within J2 only the 8821C variant has working narrowband — the + * 8822B device fill clears kBw5|kBw10 (see RtlJaguar2Device::GetAdapterCaps). + * J1 has no vendor narrowband reference (the rtl8812au trees carry only dead + * enum values). Pure; unit-tested in tests/adapter_caps_selftest.cpp. */ inline uint8_t bw_mask_for_generation(ChipGeneration g) { const uint8_t ac = kBw20 | kBw40 | kBw80; - return g == ChipGeneration::Jaguar3 ? (ac | kBw5 | kBw10) + return g == ChipGeneration::Jaguar1 ? ac : g == ChipGeneration::Unknown ? 0 - : ac; + : (ac | kBw5 | kBw10); } /* A tunable / characterized frequency span (MHz). valid=false = band absent. */ @@ -110,7 +114,7 @@ struct AdapterCaps { /* --- feature flags --- */ bool per_packet_txpower = false; /* Jaguar2 descriptor TXPWR_OFSET LUT only */ - bool narrowband_ok = false; /* 5/10 MHz re-clock (Jaguar3) */ + bool narrowband_ok = false; /* 5/10 MHz re-clock (Jaguar2 8821C, Jaguar3) */ bool fastretune_ok = false; /* lean FastRetune override exists */ bool per_chain_rssi = false; /* frame parser fills per-chain rssi (>=2ch) */ }; diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index 0e43fc9..798700d 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -157,9 +157,15 @@ struct DeviceConfig { /* env: DEVOURER_RFE — Jaguar2 RFE type override (antenna/LNA switch * variant; unset = efuse, blank efuse falls back per vendor). */ std::optional rfe_type; - /* env: DEVOURER_NB_DAC — Jaguar3 5/10 MHz: force the 3-bit DAC-divider - * code (divider-mapping experiments only). */ + /* env: DEVOURER_NB_DAC — 5/10 MHz divider-mapping experiments only. + * Jaguar3: force the 3-bit DAC-divider code (0x9b4[10:8]). Jaguar2: + * force the 0x8ac DAC clock field — bits [1:0] -> 0x8ac[21:20], + * bit 2 -> 0x8ac[28]. */ std::optional nb_dac; + /* env: DEVOURER_NB_ADC — Jaguar2 5/10 MHz: force the 0x8ac ADC clock + * field — bits [1:0] -> 0x8ac[9:8], bit 2 -> 0x8ac[16] (divider-mapping + * experiments only). */ + std::optional nb_adc; /* env: DEVOURER_REGULATION — Jaguar1 regulatory domain override for the * TX-power limit tables (unset = efuse). */ std::optional regulation; diff --git a/src/jaguar2/HalJaguar2.cpp b/src/jaguar2/HalJaguar2.cpp index edac53d..6625d2e 100644 --- a/src/jaguar2/HalJaguar2.cpp +++ b/src/jaguar2/HalJaguar2.cpp @@ -514,6 +514,14 @@ void HalJaguar2::igi_toggle() { _device.phy_set_bb_reg(0x0e50, 0x7f, igi); } +void HalJaguar2::bb_reset() { + /* MAC 0x0 BIT16 (SYS_FUNC_EN FEN_BBRSTB) 0->1 — the _iqk_bb_reset_8822b / + * _8821c mechanism; relatches the BB DAC/DFE clock tree. */ + uint32_t r0 = _device.rtw_read32(0x0); + _device.rtw_write32(0x0, r0 & ~(1u << 16)); + _device.rtw_write32(0x0, r0 | (1u << 16)); +} + /* Central channel of the wide channel; `channel` is the primary 20 MHz channel * and primary_ch_idx its position. 20 MHz: central = primary. 40 MHz: * primary_ch_idx 1(lower)/2(upper) -> ±2. 80 MHz: 1..4 -> +6/+2/-2/-6. (The @@ -626,7 +634,7 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, rf18 |= (1u << 17); } - /* --- config_phydm_switch_bandwidth_8822b (20/40/80 MHz) --- */ + /* --- config_phydm_switch_bandwidth_8822b (20/40/80 + 5/10 MHz) --- */ uint32_t v8ac = _device.rtw_read32(0x08ac); const uint8_t sub = static_cast((primary_ch_idx & 0xf) << 2); if (bw == 1) { /* 40 MHz */ @@ -648,6 +656,61 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _device.phy_set_bb_reg(0x0840, 0x0000f000, 0x6); _device.phy_set_bb_reg(0x08c8, (1u << 10), 0x1); } + } else if (bw == 5 || bw == 6) { + /* CHANNEL_WIDTH_5 / _10 — narrowband baseband re-clock: small-BW + * 0x8ac[7:6] = 1/2, rf mode [1:0] = 20M, ADC clock [9:8]+[16], DAC clock + * [21:20]+[28]; the RF synth stays in its 20 MHz mode (vendor + * config_phydm_switch_bandwidth_8822b CHANNEL_WIDTH_5/10 — verbatim, + * incl. its masks keeping the ADC/DAC fields from the previous BW set). + * + * EXPERIMENTAL on the 8822B (caps report narrowband_ok=false): with this + * recipe the chip syncs only ~10% of NB frames on RX and airs no valid NB + * TX, while the OpenHD kernel module on the same dongle does full-rate NB + * with the SAME firmware blob and the same page-8 BB state (hardware + * A/B'd via reference/rtl88x2bu/88x2bu_ohd.ko + write_reg bisect). The + * kernel's NB switch additionally retunes the RF RXBB LPF (RF 0x82/0xf2 + * move with bandwidth with no driver register write — a runtime FW + * interaction devourer does not yet reproduce). The 8821C variant has no + * such gap and is fully validated. */ + const bool is5 = (bw == 5); + v8ac &= is5 ? 0xEFEEFE00 : 0xEFFEFF00; + v8ac |= is5 ? (1u << 6) : (1u << 7); + /* DEVOURER_NB_ADC / DEVOURER_NB_DAC — divider-mapping experiment knobs: + * force the ADC ([9:8] + bit16) / DAC ([21:20] + bit28) clock fields the + * vendor comments describe but its code never writes. */ + if (_cfg.tuning.nb_adc) { + v8ac &= ~((0x3u << 8) | (1u << 16)); + v8ac |= (static_cast(*_cfg.tuning.nb_adc & 0x3) << 8) | + ((*_cfg.tuning.nb_adc & 0x4) ? (1u << 16) : 0u); + _logger->info("Jaguar2: tuning.nb_adc override — ADC code {:#x}", + *_cfg.tuning.nb_adc); + } + if (_cfg.tuning.nb_dac) { + v8ac &= ~((0x3u << 20) | (1u << 28)); + v8ac |= (static_cast(*_cfg.tuning.nb_dac & 0x3) << 20) | + ((*_cfg.tuning.nb_dac & 0x4) ? (1u << 28) : 0u); + _logger->info("Jaguar2: tuning.nb_dac override — DAC code {:#x}", + *_cfg.tuning.nb_dac); + } + _device.phy_set_bb_reg(0x08ac, 0xffffffff, v8ac); + _device.phy_set_bb_reg(0x08c4, (1u << 30), 0x0); /* ADC buffer clock */ + _device.phy_set_bb_reg(0x08c8, (1u << 31), 0x1); + rf18 |= (1u << 11) | (1u << 10); /* RF stays 20M */ + /* config_phydm_switch_band_8822b CCK-block trio (the 8821C path has it in + * its band block; the 8822B port never carried it). At 5G the CCK block + * must be OFF — the table-apply POST bracket leaves it on, harmless at + * 20 MHz but under the NB re-clock the mis-clocked CCK engine breaks the + * demod. NB-gated pending 20/40/80 regression of the full band block. */ + if (g2) { + _device.phy_set_bb_reg(0x0808, (1u << 28), 0x1); /* CCK block on */ + _device.phy_set_bb_reg(0x0454, (1u << 7), 0x0); /* MAC CCK check off */ + _device.phy_set_bb_reg(0x0a80, (1u << 18), 0x0); /* BB CCK check off */ + } else { + _device.phy_set_bb_reg(0x0a80, (1u << 18), 0x1); + _device.phy_set_bb_reg(0x0454, (1u << 7), 0x1); + _device.phy_set_bb_reg(0x0808, (1u << 28), 0x0); /* CCK block off */ + _device.phy_set_bb_reg(0x0814, 0x0000FC00, 34); /* CCA mask 13.6us */ + } } else { /* 20 MHz */ v8ac &= 0xFFCFFC00; v8ac |= 0x0u; /* CHANNEL_WIDTH_20 = 0 */ @@ -705,6 +768,13 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _device.phy_set_bb_reg(0x0808, 0xff, rx_ant | (rx_ant << 4)); igi_toggle(); + /* 5/10 MHz: BB reset (MAC 0x0 BIT16 toggle — the same _iqk_bb_reset_8822b + * mechanism) so the DAC/DFE relatch at the new sample rate. Hardware-taught + * on Jaguar3 (RadioManagementJaguar3::set_bandwidth_dividers): without it + * the re-clock doesn't take. */ + if (bw == 5 || bw == 6) + bb_reset(); + _last_tuned_ch = channel; _logger->info("Jaguar2: channel set ch={} bw={} (rf18=0x{:05x})", channel, (int)bw, rf18); @@ -715,14 +785,15 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, void HalJaguar2::DumpCanary() { /* Channel/BW-relevant set (both variants; the control run in the parity * script classifies natural run-variance): RX path (0x808), CCA thresholds - * (0x82c/0x830/0x838), fc (0x860), BW block (0x8ac/0x8c4/0x8f0), RX DFIR + * (0x82c/0x830/0x838), fc (0x860), BW block (0x8ac/0x8c4/0x8c8/0x8f0 — + * 0x8c8[31] is narrowband ADC-buffer state, set on 5/10 MHz), RX DFIR * (0x948/0x94c/0xc20/0xe20), AGC index (0x958 8822B / 0xc1c 8821C), CCK pri * (0xa00), spur / CCK filter (0xa24/0xa28/0xaac), 8821C band block * (0xa80/0xa84/0x814), RFE pins (0xcb0/0xcb4/0xcb8/0xca0/0xeb0/0xeb4/0xea0). * MAC: CCK check (0x454). RF via the direct read window: 0x18 channel, * 0xbe VCO band, 0xdf, 0xb8. IGI (0xc50/0xe50) excluded — live. */ static const uint16_t bb_canary[] = { - 0x808, 0x814, 0x82c, 0x830, 0x838, 0x860, 0x8ac, 0x8c4, 0x8f0, + 0x808, 0x814, 0x82c, 0x830, 0x838, 0x860, 0x8ac, 0x8c4, 0x8c8, 0x8f0, 0x948, 0x94c, 0x958, 0xa00, 0xa24, 0xa28, 0xaac, 0xa80, 0xa84, 0xc1c, 0xc20, 0xe20, 0xca0, 0xcb0, 0xcb4, 0xcb8, 0xea0, 0xeb0, 0xeb4}; static const uint16_t mac_canary[] = {0x454}; @@ -1199,6 +1270,26 @@ void HalJaguar2::set_channel_bw_8821c(uint8_t channel, uint8_t bw, _device.phy_set_bb_reg(0x08c4, (1u << 30), 0x1); rf18 &= ~((1u << 11) | (1u << 10)); rf18 |= (1u << 10); + } else if (bw == 5) { /* CHANNEL_WIDTH_5 — narrowband baseband re-clock: + * small-BW 0x8ac[7:6]=1, ADC clock 40M ([9:8]=0x2, [16]=0), DAC field + * ([21:20]=0x2, [28]=0); RF synth stays in 20 MHz mode (vendor + * config_phydm_switch_bandwidth_8821c CHANNEL_WIDTH_5). */ + uint32_t v8ac = _device.rtw_read32(0x08ac); + v8ac &= 0xefcefc00; + v8ac |= (0x2u << 20) | (0x2u << 8) | (1u << 6); + _device.phy_set_bb_reg(0x08ac, 0xffffffff, v8ac); + _device.phy_set_bb_reg(0x08c4, (1u << 30), 0x0); /* ADC buffer clock */ + _device.phy_set_bb_reg(0x08c8, (1u << 31), 0x1); + rf18 |= (1u << 11) | (1u << 10); /* RF stays 20M */ + } else if (bw == 6) { /* CHANNEL_WIDTH_10 — small-BW=2, ADC 80M ([9:8]=0x3), + * DAC field 0x3 */ + uint32_t v8ac = _device.rtw_read32(0x08ac); + v8ac &= 0xefcefc00; + v8ac |= (0x3u << 20) | (0x3u << 8) | (1u << 7); + _device.phy_set_bb_reg(0x08ac, 0xffffffff, v8ac); + _device.phy_set_bb_reg(0x08c4, (1u << 30), 0x0); /* ADC buffer clock */ + _device.phy_set_bb_reg(0x08c8, (1u << 31), 0x1); + rf18 |= (1u << 11) | (1u << 10); /* RF stays 20M */ } else { /* 20 MHz */ uint32_t v8ac = _device.rtw_read32(0x08ac); v8ac &= 0xffcffc00; @@ -1212,14 +1303,22 @@ void HalJaguar2::set_channel_bw_8821c(uint8_t channel, uint8_t bw, /* phydm_rxdfirpar_by_bw_8821c: the RX digital filter must match the bandwidth * or the OFDM demod never completes a PSDU (energy detected, nothing reaches * the MAC RX FIFO) — the 8821C "first RX" piece. 1T1R: path A regs only - * (0x948/0x94c/0xc20/0x8f0), distinct from the 8822B set. */ + * (0x948/0x94c/0xc20/0x8f0), distinct from the 8822B set. 5/10 MHz share the + * BW20 filter values (the narrowing happens in the ADC/DAC re-clock, the + * demod still runs a 20 MHz signal shape). */ + const bool nb = (bw == 5 || bw == 6); _device.phy_set_bb_reg(0x0948, (1u << 29) | (1u << 28), 0x2); _device.phy_set_bb_reg(0x094c, (1u << 29) | (1u << 28), bw == 2 ? 0x1u : 0x2u); - _device.phy_set_bb_reg(0x0c20, (1u << 31), bw == 0 ? 0x1u : 0x0u); + _device.phy_set_bb_reg(0x0c20, (1u << 31), (bw == 0 || nb) ? 0x1u : 0x0u); _device.phy_set_bb_reg(0x08f0, (1u << 31), bw == 2 ? 0x1u : 0x0u); igi_toggle(); + + /* 5/10 MHz: BB reset so the DAC/DFE relatch at the new sample rate (see the + * 8822B path). */ + if (nb) + bb_reset(); _logger->info("Jaguar2/8821C: channel set ch={} bw={} cch={} {} (rf18={:05x})", channel, (int)bw, cch, btg ? "BTG" : "WLG", rf18); } @@ -1403,6 +1502,13 @@ void HalJaguar2::apply_tx_power(uint8_t channel, uint8_t bw, uint8_t rfe_type, if (channel == 0) return; + /* 5/10 MHz narrowband (CHANNEL_WIDTH_5=5 / _10=6) folds to the 20 MHz power + * column: the RF runs in its 20 MHz mode and the txpwr_lmt tables carry no + * narrowband rows (a raw 5/6 here would read as HT40 and miss the regulatory + * lookup entirely -> unclamped 63). Mirrors the Jaguar3 fold. */ + if (bw >= 5) + bw = 0; + /* Fresh rail-hit snapshot for this apply (SetTxPowerOffsetQdb's "knob out * of travel" signal). */ _txpwr_sat_low = false; diff --git a/src/jaguar2/HalJaguar2.h b/src/jaguar2/HalJaguar2.h index bd727ef..a6fbf82 100644 --- a/src/jaguar2/HalJaguar2.h +++ b/src/jaguar2/HalJaguar2.h @@ -53,7 +53,9 @@ class HalJaguar2 { void read_efuse_logical_map(uint8_t *map, uint16_t map_size, bool dump); /* Program the per-rate TXAGC (0x1d00 path A / 0x1d80 path B) from the EFUSE - * power-by-rate calibration for `channel` at bandwidth `bw` (0=20/1=40/2=80) — + * power-by-rate calibration for `channel` at bandwidth `bw` (0=20/1=40/2=80; + * 5/6 = 5/10 MHz narrowband, folded to the 20 MHz column — the RF runs in + * 20 MHz mode and the regulatory tables have no narrowband rows) — * the efuse-calibrated level the kernel uses. Without it the TXAGC sits at the * hot BB-table default which overdrives high-order QAM (MCS5/7) into PA * compression. Ports phy_get_pg_txpwr_idx (base + per-BW/Nss diff) + @@ -111,10 +113,14 @@ class HalJaguar2 { /* Set RF channel + bandwidth (config_phydm_switch_channel_8822b + * config_phydm_switch_bandwidth_8822b): RF18 tune, band AGC/fc/CCK-filter, - * RFE antenna pins, RX-path + IGI toggle. bw: 0=20/1=40/2=80 MHz. - * primary_ch_idx = sub-channel index for 40/80 (the vendor primary_ch_idx; - * from SelectedChannel.ChannelOffset). rfe_type selects the RFE-pin table - * and the BW80 extra writes; rf_2t2r drives path-B writes. */ + * RFE antenna pins, RX-path + IGI toggle. bw: 0=20/1=40/2=80 MHz, 5/6 = + * 5/10 MHz narrowband (raw ChannelWidth_t values) — a baseband ADC/DAC + * re-clock packed into the 0x8ac dword (small-BW [7:6] = 1/2) plus + * 0x8c4[30]=0 / 0x8c8[31]=1; the RF synth stays in its 20 MHz mode, so the + * RF18 BW bits equal the 20 MHz encoding. primary_ch_idx = sub-channel index + * for 40/80 (the vendor primary_ch_idx; from SelectedChannel.ChannelOffset). + * rfe_type selects the RFE-pin table and the BW80 extra writes; rf_2t2r + * drives path-B writes. */ void set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, uint8_t primary_ch_idx = 0); @@ -130,9 +136,11 @@ class HalJaguar2 { * hop does not need it (hardware-measured on both variants, both * directions: identical hopping-RX catch rate and hopping-TX delivery with * and without, no decay over repeated kickless retunes). Everything - * bandwidth-keyed (0x8ac/0x8c4 block, RX DFIR, CCA thresholds) and - * band-keyed (RFE pins, 8821C switch-band/RF-set block) stays untouched — - * set by the last full set at this BW/band. Returns false (chip untouched) + * bandwidth-keyed (0x8ac/0x8c4/0x8c8 block — including the 5/10 MHz + * narrowband re-clock state, whose RF18 BW bits equal the 20 MHz encoding, + * so NB survives fast hops with no divider re-cache — RX DFIR, CCA + * thresholds) and band-keyed (RFE pins, 8821C switch-band/RF-set block) + * stays untouched — set by the last full set at this BW/band. Returns false (chip untouched) * on a band change or when the radio was never tuned; the caller falls back * to the full set_channel_bw. */ bool fast_retune(uint8_t channel, uint8_t bw, uint8_t primary_ch_idx, @@ -232,6 +240,11 @@ class HalJaguar2 { void rfe_ifem(uint8_t channel); /* phydm_igi_toggle_8822b: toggle 0xc50/0xe50 IGI to enter RX mode. */ void igi_toggle(); + + /* BB reset (MAC 0x0 BIT16 = FEN_BBRSTB toggle, the _iqk_bb_reset_8822b + * mechanism) — relatches the BB clock tree; required after the 5/10 MHz + * ADC/DAC re-clock (as on Jaguar3). */ + void bb_reset(); /* Central channel of the wide channel (shared full/fast paths): 20 MHz -> * primary; 40 MHz -> ±2 by primary_ch_idx; 80 MHz -> +6/+2/-2/-6. */ static uint8_t central_ch(uint8_t channel, uint8_t bw, uint8_t primary_ch_idx); diff --git a/src/jaguar2/HalmacJaguar2MacInit.cpp b/src/jaguar2/HalmacJaguar2MacInit.cpp index 4c0a62f..6600727 100644 --- a/src/jaguar2/HalmacJaguar2MacInit.cpp +++ b/src/jaguar2/HalmacJaguar2MacInit.cpp @@ -235,12 +235,6 @@ constexpr FifoParams fifo_params(ChipVariant v, bool is_usb = true) { PG_HQ, PG_NQ, PG_LQ, PG_EXQ, PG_GAP}; } -bool is_5m(ChannelWidth_t bw) { - return bw == ChannelWidth_t::CHANNEL_WIDTH_5; -} -bool is_10m(ChannelWidth_t bw) { - return bw == ChannelWidth_t::CHANNEL_WIDTH_10; -} } /* namespace */ HalmacJaguar2MacInit::HalmacJaguar2MacInit(RtlAdapter device, Logger_t logger, @@ -294,9 +288,12 @@ void HalmacJaguar2MacInit::pre_init_system_cfg() { } void HalmacJaguar2MacInit::init_system_cfg(ChannelWidth_t bw, uint8_t cut) { + /* bw unused by design: halmac cfg_bw_88xx treats HALMAC_BW_5/10 identically + * to 20 MHz at the MAC (REG_WMAC_TRXPTCL_CTL bits 7|8 cleared), so 5/10 MHz + * narrowband needs no MAC delta for monitor/injection use — the re-clock is + * pure PHY (set_channel_bw). Vendor SIFS scaling / CCK strip is AP/STA-only, + * same policy as Jaguar3. */ (void)bw; - (void)is_5m; - (void)is_10m; (void)WLAN_PHY_REQ_DELAY; /* NB: init_system_cfg_8822b differs from _8822c — it sets ONLY * BIT_WL_PLATFORM_RST in REG_CPU_DMEM_CON (NOT BIT_DDMA_EN), and does NOT diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index ea75014..530613f 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -44,7 +44,15 @@ void RtlJaguar2Device::bring_up(SelectedChannel channel) { * chip-version -> init_system_cfg -> firmware DLFW -> post-DLFW MAC cfg + * USB RX-DMA + BB/RF enable -> BB/AGC/RF phydm tables -> TRX mode -> channel * -> LCK -> IQK -> coex WL grant -> enable RX/TX MAC engine. */ - const uint8_t bw = static_cast(channel.ChannelWidth); + /* 5/10 MHz narrowband: bring the chip up at 20 MHz and re-clock at the END + * (kernel-flow parity — the vendor NB switch only ever runs as a retune on + * an initialized interface via iw / the OpenHD monitor_chan_override; with + * the NB re-clock applied mid-bring-up, IQK and the TRX re-assert run + * against the divided BB clock and the chip comes up deaf both directions — + * hardware-bisected on the T3U with a known-good Jaguar3 NB peer). */ + const uint8_t bw_final = static_cast(channel.ChannelWidth); + const bool nb = (bw_final == 5 || bw_final == 6); + const uint8_t bw = nb ? 0 : bw_final; /* DLFW download BEFORE trx/queue config (HalMAC order): running init_trx first * over-allocates the FIFOPAGE queues and wedges the DLFW bcn-valid. * @@ -154,6 +162,14 @@ void RtlJaguar2Device::bring_up(SelectedChannel channel) { else _logger->info("Jaguar2: coex WL grant SKIPPED (tuning.skip_coex)"); _hal.enable_rx(); /* CR MACTX|MACRX + RCR + IGI — enables both TX and RX */ + if (nb) { + /* The end-of-bring-up narrowband re-clock (see the top of this function). + * A pure re-tune: RF18 comes out identical to the 20 MHz set (NB keeps + * the 20 MHz RF encoding), only the 0x8ac/0x8c4/0x8c8 clock state and the + * BB reset differ. */ + _hal.set_channel_bw(static_cast(channel.Channel), bw_final, _rfe, + channel.ChannelOffset); + } _brought_up = true; } @@ -673,6 +689,20 @@ devourer::AdapterCaps RtlJaguar2Device::GetAdapterCaps() { c.rx_chains = chains; c.per_chain_rssi = chains >= 2; c.bw_mask = devourer::bw_mask_for_generation(c.generation); + /* 5/10 MHz baseband re-clock via the 0x8ac small-BW/clock word. 8821C only: + * hardware-validated at both widths cross-generation against Jaguar3. The + * 8822B path carries the same (vendor-parity) register recipe but the chip + * comes up with ~10% RX sync rate and dead NB TX — the OpenHD kernel module + * on the same dongle does full-rate NB with the SAME firmware blob and the + * same BB register state, so the missing piece is a runtime FW interaction + * (the kernel's NB switch retunes the RF RXBB LPF — RF 0x82/0xf2 move with + * bandwidth without any driver register write). Until that is ported the + * 8822B does not advertise narrowband. */ + if (_variant == jaguar2::ChipVariant::C8821C) { + c.narrowband_ok = true; + } else { + c.bw_mask &= static_cast(~(devourer::kBw5 | devourer::kBw10)); + } c.fastretune_ok = true; c.per_packet_txpower = true; /* TX descriptor TXPWR_OFSET LUT — Jaguar2 only */ devourer::set_standard_freq_ranges(c); diff --git a/tests/adapter_caps_selftest.cpp b/tests/adapter_caps_selftest.cpp index 91ff9fe..ceaca8c 100644 --- a/tests/adapter_caps_selftest.cpp +++ b/tests/adapter_caps_selftest.cpp @@ -26,8 +26,9 @@ int main() { const uint8_t ac = kBw20 | kBw40 | kBw80; expect("J1 bw = 20/40/80", bw_mask_for_generation(ChipGeneration::Jaguar1) == ac); - expect("J2 bw = 20/40/80", - bw_mask_for_generation(ChipGeneration::Jaguar2) == ac); + expect("J2 bw adds 5/10", + bw_mask_for_generation(ChipGeneration::Jaguar2) == + (ac | kBw5 | kBw10)); expect("J3 bw adds 5/10", bw_mask_for_generation(ChipGeneration::Jaguar3) == (ac | kBw5 | kBw10)); diff --git a/tests/jaguar2_narrowband_sdr.sh b/tests/jaguar2_narrowband_sdr.sh new file mode 100755 index 0000000..9f4b8ea --- /dev/null +++ b/tests/jaguar2_narrowband_sdr.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# jaguar2_narrowband_sdr.sh — 5/10 MHz narrowband validation for Jaguar2 +# (RTL8811CU/8821C default — the validated variant; CHIP=8822b selects the +# RTL8822BU, whose NB is EXPERIMENTAL/gated: partial RX, no NB TX — see the +# set_channel_bw NB comment in HalJaguar2.cpp). The Jaguar2 twin of +# tests/jaguar3_narrowband_sdr.sh: radiotap stays 20 MHz in narrowband mode, so +# the ONLY witness is an SDR. Measures the OCCUPIED BANDWIDTH of devourer's +# continuous TX at 20 / 10 / 5 MHz with the USRP B210 and confirms it halves / +# quarters — the on-air proof of the 0x8ac ADC/DAC re-clock (small-BW [7:6] + +# 0x8c4[30]/0x8c8[31]). +# +# Ambient isolation: DIFFERENTIAL PSD (TX PSD - silent-baseline PSD); the +# -10 dB-about-peak width of that difference spectrum is the discriminator. +# +# sudo tests/jaguar2_narrowband_sdr.sh # 8811CU (0bda:c811) +# CHIP=8822b sudo tests/jaguar2_narrowband_sdr.sh # 8822BU (T3U 2357:012d) +set -u +cd "$(dirname "$0")/.." + +CHIP=${CHIP:-8821c} +if [ "$CHIP" = 8821c ]; then + VID=0x0bda; PID=0xc811; DRV=rtw88_8821cu + # 8821C worldwide-min txpwr_lmt clamps UNII-3 to 0 — force a flat TXAGC + # (occupied bandwidth is independent of power level). + TX_PWR=${TX_PWR:-0x2d} +else + VID=0x2357; PID=0x012d; DRV=rtw88_8822bu + TX_PWR=${TX_PWR:-} +fi + +# CHANNEL/FREQ must agree (ch44=5220e6). ch44 default — ch36 frequently has a +# strong ambient AP that swamps the differential PSD. +CHANNEL=${CHANNEL:-44} +FREQ=${FREQ:-5220e6} +RATE=46.08e6 # wide enough to see the full 20 MHz channel + skirts +OUT=/tmp/j2_nb_${CHIP} +rm -rf "$OUT"; mkdir -p "$OUT" + +cleanup() { + sudo pkill -x txdemo 2>/dev/null + sudo modprobe "$DRV" 2>/dev/null +} +trap cleanup EXIT + +probe() { # label psd_out + sudo python3 tests/sdr_tx_probe.py --freq "$FREQ" --rate "$RATE" --gain 50 \ + --nsamps 6e6 --label "$1" --psd-out "$2" 2>&1 | grep '\[sdr' || true +} + +echo "=== baseline: $CHIP silent (ambient PSD) ===" +sudo modprobe -r "$DRV" 2>/dev/null; sleep 1 +probe baseline "$OUT/baseline.npy" + +for BW in 20 10 5; do + echo "=== TX at ${BW} MHz: devourer continuous, capture PSD ===" + # DLFW is flaky on the 8821C; retry the TX bring-up until it floods. + for try in 1 2 3 4; do + sudo modprobe -r "$DRV" 2>/dev/null; sleep 1 + timeout 26 sudo env DEVOURER_VID=$VID DEVOURER_PID=$PID \ + DEVOURER_CHANNEL="$CHANNEL" DEVOURER_NB_BW="$BW" \ + DEVOURER_TX_PWR="$TX_PWR" DEVOURER_TX_GAP_US=0 \ + ./build/txdemo >"$OUT/dev_${BW}.log" 2>&1 & + sleep 11 # power-on -> DLFW -> init -> TX flooding + if grep -q 'ready for TX' "$OUT/dev_${BW}.log"; then break; fi + echo " (try $try: bring-up failed, retrying)" + sudo pkill -x txdemo 2>/dev/null; wait 2>/dev/null + done + probe "tx${BW}" "$OUT/tx_${BW}.npy" + sudo pkill -x txdemo 2>/dev/null; wait 2>/dev/null; sleep 2 +done + +echo "=== differential-PSD occupied bandwidth ===" +python3 - "$OUT" "$RATE" <<'PY' +import sys, numpy as np +out, rate = sys.argv[1], float(sys.argv[2]) +base = np.load(f"{out}/baseline.npy"); bf, bp = base[0], base[1] + +def occ_bw(label): + a = np.load(f"{out}/{label}.npy"); f, p = a[0], a[1] + diff = p - bp # isolate devourer's emission + diff = np.clip(diff, 0, None) + if diff.max() <= 0: + return None, 0.0 + k = 33 + sm = np.convolve(diff, np.ones(k)/k, mode='same') + tot = sm.sum() + csum = np.cumsum(sm) + lo = np.searchsorted(csum, 0.005*tot) + hi = np.searchsorted(csum, 0.995*tot) + bw = f[min(hi, len(f)-1)] - f[max(lo,0)] + # -10 dB width about the peak — CONTIGUOUS run containing the peak (a + # first/last-above-threshold scan bridges the DUT lobe with ambient + # bursts elsewhere in the span and inflates the width). + pk = sm.max(); thr = pk/10.0 + ipk = int(np.argmax(sm)) + lo_i = ipk + while lo_i > 0 and sm[lo_i-1] >= thr: + lo_i -= 1 + hi_i = ipk + while hi_i < len(sm)-1 and sm[hi_i+1] >= thr: + hi_i += 1 + bw10 = f[hi_i] - f[lo_i] + return bw, bw10 + +print(f"{'bw':>6} {'-10dB(MHz)':>12} {'occ99ref(MHz)':>14}") +res = {} +for bw in (20, 10, 5): + o99, o10 = occ_bw(f"tx_{bw}") + res[bw] = o10 + s99 = f"{o99/1e6:.2f}" if o99 else "n/a" + print(f"{bw:>5}M {o10/1e6:>12.2f} {s99:>14}") +if res.get(20) and res.get(10): + print(f"\nratio 20M/10M = {res[20]/res[10]:.2f} (expect ~2.0)") +if res.get(20) and res.get(5): + print(f"ratio 20M/5M = {res[20]/res[5]:.2f} (expect ~4.0)") +ok = (res.get(20) and res.get(10) and res.get(5) + and 1.6 <= res[20]/res[10] <= 2.5 + and 3.0 <= res[20]/res[5] <= 5.0) +print("\nNARROWBAND CONFIRMED (10 MHz ~ half, 5 MHz ~ quarter of 20 MHz " + "occupied BW)" if ok + else "\nINCONCLUSIVE — bandwidth ratio not as expected (see PSDs in "+out+")") +PY diff --git a/tests/narrowband_cross_rx.sh b/tests/narrowband_cross_rx.sh new file mode 100755 index 0000000..f713e7f --- /dev/null +++ b/tests/narrowband_cross_rx.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# narrowband_cross_rx.sh — does 5/10 MHz narrowband actually DEMODULATE across +# chip generations, not just narrow the TX lobe? Both ends re-clock to the same +# narrowband width; the RX end's frame count is the witness that the ADC +# re-clock receives a real NB signal (an SDR only proves the TX side). +# +# Cells: a 20 MHz control brackets the bench, then 10 and 5 MHz with both ends +# re-clocked. Defaults pair Jaguar3 TX (8812CU, whose narrowband is already +# SDR-validated) with Jaguar2 8821C RX (8811CU) — run the reverse direction +# too. The 8822BU (2357:012d) can be substituted but its NB is +# EXPERIMENTAL/gated (partial RX, no NB TX): +# +# sudo tests/narrowband_cross_rx.sh # J3 c812 -> 8811CU +# sudo tests/narrowband_cross_rx.sh 0bda:c811 0bda:c812 # 8811CU -> J3 c812 +# +# The 8821C TX at UNII needs a flat TXAGC (worldwide-min limit table clamps +# it to 0): TX_PWR=0x2d is applied automatically for a c811 TX. +# +# Usage: sudo tests/narrowband_cross_rx.sh [TX_VID:PID] [RX_VID:PID] [DUR] [CH] +set -u +cd "$(dirname "$0")/.." + +TX_SPEC=${1:-0bda:c812} +RX_SPEC=${2:-0bda:c811} +DUR=${3:-15} +CH=${4:-44} + +TX_VID=${TX_SPEC%:*}; TX_PID=${TX_SPEC#*:} +RX_VID=${RX_SPEC%:*}; RX_PID=${RX_SPEC#*:} + +LOGDIR=/tmp/devourer-narrowband-cross-rx +rm -rf "$LOGDIR"; mkdir -p "$LOGDIR" + +cleanup() { + pkill -INT -x txdemo 2>/dev/null + pkill -INT -x rxdemo 2>/dev/null + sleep 1 + pkill -KILL -x txdemo 2>/dev/null + pkill -KILL -x rxdemo 2>/dev/null +} +trap cleanup EXIT INT TERM + +# Keep the in-tree rtw88 auto-probe off both DUTs (best effort; harness rigs +# usually blacklist these already). +for m in rtw88_8822bu rtw88_8821cu rtw88_8822cu rtw88_8822eu; do + sudo modprobe -r "$m" 2>/dev/null +done + +# run_cell +run_cell() { + local name=$1 nb=$2 + local rxlog="$LOGDIR/rx-$name.log" txlog="$LOGDIR/tx-$name.log" + local nb_env=() + [ -n "$nb" ] && nb_env=(DEVOURER_NB_BW="$nb") + + local tx_pwr=() + [ "$TX_PID" = "c811" ] && tx_pwr=(DEVOURER_TX_PWR=0x2d) + env DEVOURER_VID="0x$TX_VID" DEVOURER_PID="0x$TX_PID" DEVOURER_CHANNEL=$CH \ + "${nb_env[@]}" "${tx_pwr[@]}" \ + timeout -s INT -k 5 $((DUR * 3 + 20)) ./build/txdemo >"$txlog" 2>&1 & # covers RX bring-up retries + local txpid=$! + sleep 7 # TX bring-up (DLFW + re-clock) before the RX window opens + + # 8821C DLFW is flaky — retry the RX bring-up until the RX loop starts. + local try + for try in 1 2 3; do + env DEVOURER_VID="0x$RX_VID" DEVOURER_PID="0x$RX_PID" DEVOURER_CHANNEL=$CH \ + "${nb_env[@]}" \ + timeout -s INT -k 5 "$DUR" ./build/rxdemo >"$rxlog" 2>&1 + grep -q "entering RX loop" "$rxlog" && break + echo " ($name: RX bring-up failed, retry $try)" + done + wait "$txpid" 2>/dev/null + sleep 3 + + local hits + hits=$(grep -cF '"ev":"rx.txhit"' "$rxlog") + echo "$hits" >"$LOGDIR/$name.count" + printf " %-6s (%s MHz): %s hits\n" "$name" "${nb:-20}" "$hits" +} + +echo "narrowband cross-RX: TX $TX_SPEC -> RX $RX_SPEC, ch$CH, ${DUR}s cells" +run_cell bw20 "" +run_cell bw10 10 +run_cell bw5 5 + +C20=$(cat "$LOGDIR/bw20.count") +C10=$(cat "$LOGDIR/bw10.count") +C5=$(cat "$LOGDIR/bw5.count") +echo +if [ "$C20" -lt 20 ]; then + echo "VERDICT: bench broken (20 MHz control $C20 hits) — fix before judging NB" + exit 1 +fi +# NB thresholds are deliberately loose: the ratio to the control absorbs +# bench-to-bench delivery variance; near-zero is the failure signature. +if [ "$C10" -ge $((C20 / 10)) ] && [ "$C10" -ge 10 ]; then V10=OK; else V10=FAIL; fi +if [ "$C5" -ge $((C20 / 10)) ] && [ "$C5" -ge 10 ]; then V5=OK; else V5=FAIL; fi +echo "VERDICT: 20M=$C20 (control), 10M=$C10 [$V10], 5M=$C5 [$V5]" +echo "logs: $LOGDIR" +[ "$V10" = OK ] && [ "$V5" = OK ] From f37e54dda08481d8bd7a6ecf5518dd37f4eae403 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:06:59 +0300 Subject: [PATCH 2/3] Narrowband follow-up: fix throttled txhit metric, refute RF 0x82/0xf2 lever, document 5 MHz CFO limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systematic comparison against the vendored OpenHD rtl88x2bu (issue #184 pattern — devourer as an incomplete port): - tests/narrowband_cross_rx.sh counted rx.txhit EVENTS, but the demo throttles them (first 10, then every 100th) — cells reading "57 hits" were really ~4700 frames. Count the `hits` field of the last event. Re-validated with true numbers: 8821C 10 MHz at 2100 vs 4700-frame 20 MHz control (the remaining factor is NB frame airtime), 5 MHz at 7000+ per 10 s on 2.4 GHz. - Time-resolved kernel dumps show the working driver CONVERGES RF 0x82/0xf2 (RXBB) to narrowband values over ~3 s after the switch (0x44 -> 0x50 -> 0x52) via its dynamic loop. Forcing those converged values statically from devourer's NB branch does NOT recover 8822B RX (hardware-tested, reverted) — the 8822B gap note now records the refutation; the operative delta is the dynamic mechanism itself or a still-unobserved FW interplay. - 5 MHz at 5 GHz is CFO-limited physics, not a driver bug: quarter clock shrinks subcarrier spacing 4x, and a far-offset TX/RX crystal pair is bimodal per bring-up (9100 hits one run, 0 the next, while a closer-crystal peer catches 10200 from the same TX); the same pair is stable at 2.4 GHz. Documented in the test header + CLAUDE.md. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 5 ++++- src/jaguar2/HalJaguar2.cpp | 10 ++++++---- tests/narrowband_cross_rx.sh | 14 +++++++++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 919990b..611f297 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,10 @@ construction from the `SYS_CFG2` chip-id (see **Architecture**): register tables like Jaguar1 (shared `PhyTableLoader`). RX + TX on 2.4/5 GHz at 20/40/80 MHz, plus **5/10 MHz narrowband on the 8821C variant** (a baseband ADC/DAC re-clock packed into BB `0x8ac`; the RF stays in 20 MHz - mode; applied as an end-of-bring-up retune, kernel-flow parity). The 8822B + mode; applied as an end-of-bring-up retune, kernel-flow parity). 5 MHz at + 5 GHz is CFO-limited: subcarrier spacing shrinks 4× and a far-offset + TX/RX crystal pair syncs bimodally per bring-up — at 2.4 GHz the same + pair is stable (`tests/narrowband_cross_rx.sh` header). The 8822B carries the same NB register recipe but is gated off (`narrowband_ok=false`): its NB RX syncs ~10% and NB TX airs nothing, while the OpenHD kernel module does full-rate NB on the same dongle with the same firmware — the kernel's diff --git a/src/jaguar2/HalJaguar2.cpp b/src/jaguar2/HalJaguar2.cpp index 6625d2e..e324a3e 100644 --- a/src/jaguar2/HalJaguar2.cpp +++ b/src/jaguar2/HalJaguar2.cpp @@ -668,10 +668,12 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, * TX, while the OpenHD kernel module on the same dongle does full-rate NB * with the SAME firmware blob and the same page-8 BB state (hardware * A/B'd via reference/rtl88x2bu/88x2bu_ohd.ko + write_reg bisect). The - * kernel's NB switch additionally retunes the RF RXBB LPF (RF 0x82/0xf2 - * move with bandwidth with no driver register write — a runtime FW - * interaction devourer does not yet reproduce). The 8821C variant has no - * such gap and is fully validated. */ + * kernel additionally converges RF 0x82/0xf2 (RXBB) to NB values within + * ~3 s of the switch via a dynamic loop devourer doesn't run — but + * forcing those converged values statically does NOT recover devourer's + * RX (hardware-tested), so the operative delta is the dynamic mechanism + * itself or something still unobserved (FW H2C interplay, per-frame AGC). + * The 8821C variant has no such gap and is fully validated. */ const bool is5 = (bw == 5); v8ac &= is5 ? 0xEFEEFE00 : 0xEFFEFF00; v8ac |= is5 ? (1u << 6) : (1u << 7); diff --git a/tests/narrowband_cross_rx.sh b/tests/narrowband_cross_rx.sh index f713e7f..bab63a6 100755 --- a/tests/narrowband_cross_rx.sh +++ b/tests/narrowband_cross_rx.sh @@ -16,6 +16,14 @@ # The 8821C TX at UNII needs a flat TXAGC (worldwide-min limit table clamps # it to 0): TX_PWR=0x2d is applied automatically for a c811 TX. # +# 5 MHz at 5 GHz is CFO-limited: the quarter clock shrinks the subcarrier +# spacing 4x, so a TX/RX crystal-offset pair near the sync tolerance is +# BIMODAL per bring-up (measured: c812->c811 ch44 5M reads 9100 hits on one +# bring-up and 0 on the next, while a closer-crystal peer catches 10200 from +# the same TX; at 2.4 GHz — half the absolute offset — the same pair is +# stable at 7000+). A 0 in the 5M cell at 5 GHz means "retry / try 2.4 GHz" +# before it means "broken". +# # Usage: sudo tests/narrowband_cross_rx.sh [TX_VID:PID] [RX_VID:PID] [DUR] [CH] set -u cd "$(dirname "$0")/.." @@ -73,8 +81,12 @@ run_cell() { wait "$txpid" 2>/dev/null sleep 3 + # rx.txhit events are throttled (first 10, then every 100th) — the true + # count is the `hits` field of the LAST event, not the event count. local hits - hits=$(grep -cF '"ev":"rx.txhit"' "$rxlog") + hits=$(grep '"ev":"rx.txhit"' "$rxlog" | tail -1 | + grep -oE '"hits":[0-9]+' | cut -d: -f2) + hits=${hits:-0} echo "$hits" >"$LOGDIR/$name.count" printf " %-6s (%s MHz): %s hits\n" "$name" "${nb:-20}" "$hits" } From be3ef9a8dbc9edb3cf7182df56839810e300dfac Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:10:53 +0300 Subject: [PATCH 3/3] 8822B NB gap: usbmon closes the host-side hypothesis space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit usbmon capture of the working kernel's 20->10 MHz switch + the 5 s convergence window: 189 vendor control writes, zero H2C box writes, and only three 3-wire RF writes (RF18 x2 for the switch, RF 0x42 thermal trigger from the 2.1 s watchdog). The switch-time MAC writes are the halmac cfg_bw defaults (0x668 + the 80 MHz MAC-clock trio 0x24/0x55c/ 0x638), spur/CSI block, and DIG — all benign or already matched. Meaning: the kernel's RF 0x82/0xf2 RXBB retune at narrowband is done by the on-chip firmware autonomously, with no host trigger — and the FW does it under the kernel but not under devourer, whose FW never receives the interface-up H2C state (media status / macid / RA) that arms the FW dynamic engine. Since statically forcing the converged RF values does not recover RX, the FW's runtime assist itself is the operative delta. Comment updated to record the closed hypothesis space; porting the interface-up H2C bring-up is the follow-up (same machinery gap as the issue #184 thermal-tracking request). Co-Authored-By: Claude Opus 4.8 --- src/jaguar2/HalJaguar2.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/jaguar2/HalJaguar2.cpp b/src/jaguar2/HalJaguar2.cpp index e324a3e..ebd316e 100644 --- a/src/jaguar2/HalJaguar2.cpp +++ b/src/jaguar2/HalJaguar2.cpp @@ -667,13 +667,18 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, * recipe the chip syncs only ~10% of NB frames on RX and airs no valid NB * TX, while the OpenHD kernel module on the same dongle does full-rate NB * with the SAME firmware blob and the same page-8 BB state (hardware - * A/B'd via reference/rtl88x2bu/88x2bu_ohd.ko + write_reg bisect). The - * kernel additionally converges RF 0x82/0xf2 (RXBB) to NB values within - * ~3 s of the switch via a dynamic loop devourer doesn't run — but - * forcing those converged values statically does NOT recover devourer's - * RX (hardware-tested), so the operative delta is the dynamic mechanism - * itself or something still unobserved (FW H2C interplay, per-frame AGC). - * The 8821C variant has no such gap and is fully validated. */ + * A/B'd via reference/rtl88x2bu/88x2bu_ohd.ko + write_reg bisect). A + * usbmon capture of the kernel's 20->10 MHz switch plus the following + * 5 s shows NO H2C and NO host RF write beyond RF18 — yet the kernel's + * RF 0x82/0xf2 (RXBB) converge to NB values within ~3 s (0x44 -> 0x50 -> + * 0x52), i.e. the on-chip FIRMWARE retunes the RX front-end for small-BW + * autonomously — and it does so under the kernel but not under devourer, + * whose FW never receives the interface-up H2C state (media status / + * macid / RA) that arms the FW dynamic engine. Forcing the converged RF + * values statically does NOT recover RX (hardware-tested), so the FW's + * runtime assist — not the final register values — is the operative + * delta. The 8821C variant has no such dependence and is fully + * validated. */ const bool is5 = (bw == 5); v8ac &= is5 ? 0xEFEEFE00 : 0xEFFEFF00; v8ac |= is5 ? (1u << 6) : (1u << 7);