Skip to content

Signal Processing Pipeline

The NanoVNA-H converts RF measurements to complex S-parameters through a clever signal processing pipeline. This page traces the path from the audio codec through DFT processing to the final gamma (reflection coefficient) calculation.

flowchart TB
  subgraph HARDWARE[Hardware Layer]
      MIX[Mixer Output<br/>IF at ~12 kHz]
      CODEC[TLV320AIC3204<br/>192 kHz Stereo ADC]
      DMA[DMA Transfer<br/>Double Buffer]
  end

  subgraph DSP[DSP Layer]
      ACC[Accumulator<br/>Sin/Cos Correlation]
      GAMMA[Gamma Calculation<br/>Complex Division]
  end

  subgraph CAL[Calibration Layer]
      ERROR[Error Term<br/>Application]
      RESULT[Calibrated<br/>S-Parameter]
  end

  MIX --> CODEC
  CODEC --> DMA
  DMA --> ACC
  ACC --> GAMMA
  GAMMA --> ERROR
  ERROR --> RESULT

The TLV320AIC3204 audio codec is configured for high-speed stereo sampling in tlv320aic3204.c:

// Key configuration parameters
#define AUDIO_ADC_FREQ 192000 // 192 kHz sample rate
#define AUDIO_SAMPLES_COUNT 48 // Samples per measurement
#define FREQUENCY_IF_K 12 // 12 kHz IF frequency

The codec provides:

  • Stereo input: Left = Reference signal, Right = Sample signal
  • 192 kHz sample rate: Captures ~16 samples per IF cycle at 12 kHz
  • 24-bit resolution: Provides excellent dynamic range
  • Programmable gain: Adjusts sensitivity automatically

Audio samples arrive via DMA (Direct Memory Access) without CPU intervention:

// Double buffer for continuous sampling
static audio_sample_t rx_buffer[AUDIO_BUFFER_LEN * 2];
void i2s_lld_serve_rx_interrupt(uint32_t flags) {
uint16_t count = AUDIO_BUFFER_LEN;
// Select buffer half based on DMA flags
audio_sample_t *p = (flags & STM32_DMA_ISR_TCIF) ?
rx_buffer + AUDIO_BUFFER_LEN : rx_buffer;
if (wait_count >= config._bandwidth + 2)
reset_dsp_accumerator(); // First buffer after freq change: discard
else
dsp_process(p, count); // Process the samples
--wait_count;
}

The double-buffer scheme allows:

  1. DMA fills one half while CPU processes the other
  2. No gaps in sampling
  3. Deterministic timing for DSP operations

The core of the measurement is a single-bin DFT (Discrete Fourier Transform). Rather than computing a full FFT, the firmware correlates the input with precomputed sine and cosine tables at exactly the IF frequency:

For 192 kHz sample rate and 12 kHz IF with 48 samples:

// Precomputed sin/cos values (scaled to 16-bit integer)
static const int16_t sincos_tbl[48][2] = {
{ 6393, 32138}, { 27246, 18205}, { 32138,-6393}, { 18205,-27246},
{-6393,-32138}, {-27246,-18205}, {-32138, 6393}, {-18205, 27246},
// ... repeats for 3 complete cycles
};

Each entry contains {sin(n*omega), cos(n*omega)} where:

  • omega = 2*pi*IF/sample_rate = 2*pi*12000/192000 = pi/8
  • Values scaled by 32700 for 16-bit fixed-point math

On the F072 (no hardware DSP), the correlation uses software multiplication:

void dsp_process(audio_sample_t *capture, size_t length) {
int32_t samp_s = 0, samp_c = 0; // Sample sin/cos accumulators
int32_t ref_s = 0, ref_c = 0; // Reference sin/cos accumulators
for (uint32_t i = 0; i < length; i += 2) {
int16_t ref = capture[i+0]; // Reference channel (left)
int16_t smp = capture[i+1]; // Sample channel (right)
int32_t sin = sincos_tbl[i/2][0];
int32_t cos = sincos_tbl[i/2][1];
samp_s += (smp * sin) / 16; // Accumulate sample * sin
samp_c += (smp * cos) / 16; // Accumulate sample * cos
ref_s += (ref * sin) / 16; // Accumulate reference * sin
ref_c += (ref * cos) / 16; // Accumulate reference * cos
}
// Add to global accumulators (multiple buffers averaged)
acc_samp_s += samp_s;
acc_samp_c += samp_c;
acc_ref_s += ref_s;
acc_ref_c += ref_c;
}

On the F303, hardware DSP instructions accelerate the math:

void dsp_process(audio_sample_t *capture, size_t length) {
for (uint32_t i = 0; i < length/2; i++) {
int32_t sc = ((int32_t *)sincos_tbl)[i]; // Load sin+cos as packed
int32_t sr = ((int32_t *)capture)[i]; // Load ref+samp as packed
// SIMD multiply-accumulate instructions
acc_samp_s = __smlaltb(acc_samp_s, sr, sc); // samp_s += smp * sin
acc_samp_c = __smlaltt(acc_samp_c, sr, sc); // samp_c += smp * cos
acc_ref_s = __smlalbb(acc_ref_s, sr, sc); // ref_s += ref * sin
acc_ref_c = __smlalbt(acc_ref_c, sr, sc); // ref_c += ref * cos
}
}

The __smlaltb family are ARM SIMD instructions that multiply 16-bit halves of 32-bit words and accumulate to 64-bit, processing two multiplications in one instruction.

After accumulating samples across multiple buffers (determined by bandwidth setting), the complex reflection coefficient is calculated:

void calculate_gamma(float *gamma) {
// Reference: complex number (ref_c + j*ref_s)
// Sample: complex number (samp_c + j*samp_s)
// Gamma = Sample / Reference
measure_t rs_rc = (measure_t) acc_ref_s / acc_ref_c;
measure_t sc_rc = (measure_t) acc_samp_c / acc_ref_c;
measure_t ss_rc = (measure_t) acc_samp_s / acc_ref_c;
measure_t rr = rs_rc * rs_rc + 1.0f;
gamma[0] = (sc_rc + ss_rc * rs_rc) / rr; // Real part
gamma[1] = (ss_rc - sc_rc * rs_rc) / rr; // Imaginary part
}

The formula implements complex division:

gamma = (samp_c + j*samp_s) / (ref_c + j*ref_s)

Using the identity for complex division and normalizing by ref_c to avoid overflow.

The bandwidth setting controls how many DMA buffers are averaged before calculating gamma:

// Bandwidth settings (number of buffers to average)
#define BANDWIDTH_4000 (1 - 1) // Single buffer: 4 kHz BW
#define BANDWIDTH_1000 (4 - 1) // 4 buffers: 1 kHz BW
#define BANDWIDTH_100 (40 - 1) // 40 buffers: 100 Hz BW
#define BANDWIDTH_30 (132 - 1) // 132 buffers: ~30 Hz BW

More averaging:

  • Reduces noise (averaging N samples improves SNR by sqrt(N))
  • Slows sweep (more time per point)
  • Increases dynamic range (can see weaker signals)

The sweep function coordinates the entire measurement cycle:

static bool sweep(bool break_on_operation, uint16_t mask) {
for (; p_sweep < sweep_points; p_sweep++) {
freq_t frequency = getFrequency(p_sweep);
// Set new frequency, get settling delay
delay = set_frequency(frequency);
// CH0: Reflection measurement
if (mask & SWEEP_CH0_MEASURE) {
tlv320aic3204_select(0); // Select CH0 input
DSP_START(delay + st_delay); // Start accumulation
// ... calibration lookup while waiting ...
DSP_WAIT; // Wait for buffers
(*sample_func)(&data[0]); // Calculate gamma
apply_CH0_error_term(data, c_data); // Apply calibration
}
// CH1: Transmission measurement
if (mask & SWEEP_CH1_MEASURE) {
tlv320aic3204_select(1); // Select CH1 input
DSP_START(delay);
DSP_WAIT;
(*sample_func)(&data[2]);
apply_CH1_error_term(data, c_data);
}
// Store results
measured[0][p_sweep][0] = data[0]; // S11 real
measured[0][p_sweep][1] = data[1]; // S11 imag
measured[1][p_sweep][0] = data[2]; // S21 real
measured[1][p_sweep][1] = data[3]; // S21 imag
}
}
OperationTypical TimeNotes
Frequency change (same band)100-300 usPLL relock
Frequency change (band switch)5 msFull PLL reset
Buffer accumulation250 us per buffer48 samples at 192 kHz
Gamma calculation< 50 usFloating point division
Calibration correction< 20 usComplex multiply/divide
Channel switch400 usCodec input MUX settling

The signal pipeline converts RF reflections and transmissions into calibrated S-parameters through:

  1. Mixing: RF to audio IF (hardware)
  2. Sampling: 192 kHz stereo ADC (TLV320AIC3204)
  3. DMA: Double-buffered transfer (no CPU overhead)
  4. DFT: Sin/cos correlation at IF frequency
  5. Averaging: Multiple buffers for noise reduction
  6. Gamma: Complex division (sample/reference)
  7. Calibration: Error term correction

This approach achieves >70 dB dynamic range with a simple, low-cost audio codec by leveraging coherent detection at a known IF frequency.

Learn how the RF signals are generated in Frequency Synthesis.