diff --git a/CLAUDE.md b/CLAUDE.md index adf60d8..611f297 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,12 +21,25 @@ 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). 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 + 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..ebd316e 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,68 @@ 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). 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); + /* 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 +775,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 +792,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 +1277,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 +1310,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 +1509,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..bab63a6 --- /dev/null +++ b/tests/narrowband_cross_rx.sh @@ -0,0 +1,113 @@ +#!/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. +# +# 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")/.." + +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 + + # 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 '"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" +} + +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 ]