---
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';

<Badge text="tools/rf_testbench.py" variant="note" />

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

<Steps>
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)
</Steps>

```
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 |

<Aside type="danger" title="DC blocker is required">
The SkyWalker-1 can supply 13-18V DC through the F-connector for LNB power. Although `rf_testbench.py`
disables LNB power on startup, a bug, power glitch, or running a different tool without disconnecting
could send DC voltage backward through the signal path. The DC blocker prevents this from reaching
the HMC472A and NanoVNA.
</Aside>

### 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:

<Steps>
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`
</Steps>

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.

<Aside type="tip" title="HMC472A insertion loss">
The HMC472A adds 1.4-1.9 dB of insertion loss even at 0 dB attenuation setting. The calibration
sweep captures this automatically since the signal passes through the attenuator during the S21
measurement.
</Aside>

## 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
