--- title: RF Test Bench description: Automated CW injection testing with NanoVNA, HMC472A digital attenuator, and SkyWalker-1 receiver. --- import { Aside, Badge, Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; The `rf_testbench.py` tool turns a NanoVNA, an HMC472A digital attenuator, and the SkyWalker-1 into an automated RF test bench. It injects CW signals at known frequencies and power levels, then measures the receiver's response — characterizing AGC linearity, IF band flatness, frequency accuracy, sensitivity, and BPSK mode 9 behavior. ## Hardware Setup 1. **NanoVNA CH0 output** (SMA) connects to a **DC blocker** (SMA inline, required) 2. **DC blocker output** connects to the **HMC472A RF IN** (SMA) 3. **HMC472A RF OUT** (SMA) connects via **SMA-to-F adapter** to the **SkyWalker-1 F-connector** 4. **HMC472A ESP32-S2 controller** connected to your network (WiFi) — reachable at `http://attenuator.local` 5. **NanoVNA** connected via USB (for mcnanovna automation) or operated via touchscreen (manual mode) ``` NanoVNA CH0 ──→ DC Blocker ──→ HMC472A (0-31.5 dB) ──→ SMA-to-F ──→ SkyWalker-1 (SMA) (SMA) REST API control adapter (F-type) http://attenuator.local ``` ### Components | Component | Purpose | Notes | |-----------|---------|-------| | NanoVNA-H (9 kHz-1.5 GHz) | CW signal source | Output ~-15 dBm at max power. Overlaps SkyWalker-1 IF band at 950-1500 MHz | | DC Blocker (SMA inline) | Protect NanoVNA from LNB voltage | Required — even though the tool disables LNB power, this prevents accidental damage | | HMC472A attenuator module | Precision level control | 0-31.5 dB in 0.5 dB steps, controlled via ESP32-S2 REST API | | SMA-to-F adapter | Connector transition | 50-to-75 ohm mismatch is ~0.2 dB — negligible | ### HMC472A Attenuator The [HMC472A digital attenuator](https://hmc472.l.zmesh.systems/) provides programmable signal level control via its ESP32-S2 REST API: - **Range**: 0 to 31.5 dB in 0.5 dB steps (64 discrete settings) - **Bandwidth**: DC to 3.8 GHz (covers the full SkyWalker-1 IF range) - **Insertion loss**: 1.4-1.9 dB typical - **Control**: HTTP REST — `POST /set {"attenuation_db": 10.5}` - **Switching speed**: 60 ns (faster than any measurement cycle) The tool communicates with the attenuator at `http://attenuator.local` by default. Override with `--attenuator http://10.0.0.50` if your device has a different address. ### NanoVNA Frequency Overlap The NanoVNA-H (HW3.7) covers 9 kHz to 1.5 GHz. The SkyWalker-1's IF range is 950-2150 MHz. The **overlapping usable range is 950-1500 MHz** — the lower portion of the IF band. This is sufficient for characterizing the tuner and AGC, and includes the 1420 MHz hydrogen line region. For testing above 1500 MHz, a different signal source (bladeRF, signal generator) would be needed. ## Calibration Before running quantitative tests, characterize the signal path loss: 1. Disconnect the SkyWalker-1 end of the cable 2. Connect: **NanoVNA CH0** → DC blocker → HMC472A (set to 0 dB) → cable → **NanoVNA CH1** 3. Run an S21 sweep from 950 to 1500 MHz using mcnanovna or the NanoVNA touchscreen 4. Export as CSV with columns `freq_mhz` and `s21_db` (or `frequency_hz` and `loss_db`) 5. Pass to `rf_testbench.py` with `--cal path_loss.csv` The tool interpolates the measured path loss at each test frequency and subtracts it from AGC readings. Without a calibration file, raw AGC values are still reported — useful for relative measurements but not calibrated to absolute power. ## Prerequisites - **SkyWalker-1** with [custom firmware v3.02+](/firmware/custom-v302/) (for `tune_monitor` command) - **HMC472A** attenuator with ESP32-S2 controller on the network - **NanoVNA-H** (manual mode works with any VNA; auto mode requires [mcnanovna](https://git.supported.systems/rf/mcnanovna)) - **Python 3.10+** with `pyusb` installed - **DC blocker** (SMA inline) - **SMA-to-F adapter** ## Test Descriptions ### AGC Power Linearity ```bash python tools/rf_testbench.py agc-linearity --freq 1200 ``` Injects CW at a fixed frequency while sweeping the HMC472A from 0 to 31.5 dB in 0.5 dB steps. At each attenuation level, the SkyWalker-1 reports AGC1, AGC2, and derived power. This maps the **AGC transfer function** — how the receiver's automatic gain control responds to known changes in input power. The output shows whether the AGC is linear, where it saturates, and its effective dynamic range. With 64 measurement points across 31.5 dB, the resolution is high enough to reveal nonlinearities in the BCM3440 tuner's gain control loop. ### IF Band Flatness ```bash python tools/rf_testbench.py band-flatness --start 950 --stop 1500 --step 10 ``` Sweeps the NanoVNA CW frequency across the IF band while keeping the HMC472A at a fixed attenuation (10 dB default). At each frequency, the SkyWalker-1 tunes and reads AGC power. The result reveals: - **Tuner gain slope**: the BCM3440 may have more gain at some frequencies than others - **Passband ripple**: resonances or nulls in the IF filter chain - **Cable/path frequency response**: if a calibration file is loaded, this is subtracted out ### Frequency Accuracy ```bash python tools/rf_testbench.py freq-accuracy --freqs 1000,1100,1200,1300,1400 ``` At each test frequency, the NanoVNA injects CW while the SkyWalker-1 runs a narrow spectrum sweep (+/- 5 MHz) around the expected frequency. The detected power peak is compared against the injected frequency. This characterizes the **BCM3440 tuner's frequency accuracy** — how much the actual tuned frequency differs from the commanded frequency. The error may be systematic (constant offset) or frequency-dependent. ### Minimum Detectable Signal ```bash python tools/rf_testbench.py mds --freq 1200 ``` First measures the noise floor with maximum attenuation (31.5 dB). Then injects CW and steps the HMC472A from 0 dB upward in 1 dB increments until the signal drops below 3-sigma above the noise floor. The attenuation level where the signal disappears, combined with the NanoVNA output power (~-15 dBm), gives an approximate **minimum detectable signal level** in dBm. ### BPSK Mode 9 CW Probe ```bash python tools/rf_testbench.py bpsk-probe --freq 1200 ``` An exploratory test that tunes the SkyWalker-1 in **BPSK mode (index 9)** — the same Viterbi rate 1/2 K=7 inner FEC used by GOES LRIT. A CW carrier has no modulation, so the demodulator shouldn't acquire lock, but the AGC and carrier recovery behavior is informative. Tests several symbol rates (293,883 sps matching LRIT, plus 500K, 1M, and 5M) and compares against QPSK mode 0 at the same frequency. This establishes a baseline for what mode 9 reports with an unmodulated carrier — useful context for future modulated-signal experiments with a bladeRF. ## Options | Flag | Default | Description | |------|---------|-------------| | `--attenuator` | `http://attenuator.local` | HMC472A REST API base URL | | `--nanovna` | `auto` | NanoVNA control: `auto` (mcnanovna) or `manual` (prompted) | | `--cal` | — | Path loss calibration CSV file | | `--settle` | 200 | Settle time in ms after changing attenuation | | `--output` / `-o` | — | CSV output file | | `--verbose` / `-v` | — | Show raw USB traffic | ### Per-Test Options | Test | Flag | Default | Description | |------|------|---------|-------------| | `agc-linearity` | `--freq` | 1200 | Test frequency in MHz | | `band-flatness` | `--start` | 950 | Start frequency in MHz | | `band-flatness` | `--stop` | 1500 | Stop frequency in MHz | | `band-flatness` | `--step` | 10 | Frequency step in MHz | | `freq-accuracy` | `--freqs` | 1000,1100,1200,1300,1400 | Comma-separated test frequencies | | `mds` | `--freq` | 1200 | Test frequency in MHz | | `bpsk-probe` | `--freq` | 1200 | Test frequency in MHz | ## CSV Output Format All tests write the same CSV format when `--output` is specified: | Column | Description | |--------|-------------| | `timestamp` | ISO 8601 UTC timestamp | | `test_name` | Test identifier (agc_linearity, band_flatness, freq_accuracy, mds, bpsk_probe) | | `freq_mhz` | Frequency in MHz | | `atten_db` | HMC472A attenuation setting in dB | | `agc1` | BCM3440 AGC1 register value | | `agc2` | BCM3440 AGC2 register value | | `power_db` | Derived power estimate in dB (relative) | | `snr_raw` | Raw SNR register value | | `snr_db` | SNR in dB | | `locked` | Demodulator lock status | | `lock_raw` | Raw lock status byte | | `status` | Status byte | | `notes` | Test-specific metadata | ## Interpreting Results ### AGC Linearity Curves A well-behaved AGC should show a roughly linear relationship between attenuation (dB) and AGC register value. Look for: - **Linear region**: Where AGC tracks input power changes 1:1 in dB — this is the useful measurement range - **Saturation**: Where adding more signal doesn't change AGC — the tuner's front end is compressing - **Noise floor**: Where reducing signal doesn't change AGC — the receiver's internal noise dominates ### Band Flatness Ideal response is flat across the band. In practice: - **1-3 dB variation** across 950-1500 MHz is typical for a consumer-grade tuner - **Sharp dips** may indicate cable resonances or connector issues - **Systematic slope** (gain increasing or decreasing with frequency) is common and can be corrected in post-processing ### Frequency Error Consumer satellite tuners typically have **50-200 kHz frequency accuracy**. A consistent offset suggests LO error in the BCM3440. Frequency-dependent error suggests tuning nonlinearity. ## Mock Mode Run with `SKYWALKER_MOCK=1` for testing without hardware: ```bash SKYWALKER_MOCK=1 python tools/rf_testbench.py agc-linearity --freq 1200 --nanovna manual ``` Mock mode uses built-in simulated responses for the SkyWalker-1 and HMC472A. The NanoVNA prompts are skipped. Useful for verifying command structure and CSV output format. ## See Also - [Spectrum Analysis](/tools/spectrum-analysis/) — frequency sweep techniques - [Hydrogen 21 cm](/tools/h21cm/) — direct L-band input mode (same RF path concept) - [Signal Monitoring](/bcm4500/signal-monitoring/) — AGC and SNR register details - [HMC472A Documentation](https://hmc472.l.zmesh.systems/) — attenuator module reference - [Applications & Use Cases](/guides/applications/) — RF test and measurement context