Skip to content

Measurement Suite — ADC characterisation

measure demo

What you're seeing

Four views built entirely from doppler.measure.ToneMeasure, the IEEE Std 1241 windowed-tone analyser. Each component's power is integrated over its window main lobe, and the noise sum excludes the leakage bins around DC, the fundamental and each harmonic — see the design guide for the equations.

(a) Annotated 12-bit ADC capture. A ~10 MHz tone is quantised to 12 bits and analysed. The fundamental reads 0 dBFS, the broadband ~74 dB quantisation SNR sets the floor, and SINAD/SFDR are limited by the 2nd harmonic — the worst spur (flagged as harmonic). The metric box is the full bag returned by one analyze() call.

(b) ENOB recovers the ideal N-bit resolution. Sweeping the ADC from 6 to 16 bits, the measured ENOB tracks the ideal ENOB = N line — the analyser recovers SINAD = 6.02 N + 1.76 to within a fraction of a bit.

(c) Per-harmonic levels. The individual harmonic distortion products that THD aggregates (THD is their power sum, shown as the dashed line).

(d) Dynamic range vs input back-off. SNR, SINAD and SFDR track the input level as the tone backs off from full scale, while the full-scale-corrected ENOB stays flat — the converter's intrinsic resolution, independent of drive level.

Reproduce

python src/doppler/examples/measure_demo.py

The measurement object

import numpy as np

from doppler.cvt import ADC
from doppler.measure import ToneMeasure
from doppler.source import AWGN, LO

FS = 100e6  # 100 MHz sample rate
N = 1 << 14  # 16384-sample segment (sets the resolution bandwidth)
NAVG = 8  # segments averaged per measurement (Welch's method)
M = NAVG * N  # total capture length fed to analyze()


def real_tone(freq, n, amp):
    """A real cosine at `freq` Hz, generated by doppler's NCO (`source.LO`)."""
    return (amp * LO(freq / FS).steps(n).real).astype(np.float32)


def adc_capture(bits, ftone, amp=0.999, harmonics=(), noise=0.0, seed=0):
    """A real ADC capture: a doppler-NCO tone (+ optional harmonics and a
    seeded noise floor) quantised to `bits` by `doppler.cvt.ADC`.  The analyzer
    is told the depth via `bits=`, so it derives the 0-dBFS reference itself.
    The capture spans `M = NAVG * N` samples, so `analyze()` averages `NAVG`
    segments (Welch's method)."""
    x = real_tone(ftone, M, amp)
    for k, dbc in harmonics:
        x = x + real_tone(k * ftone, M, amp * 10 ** (dbc / 20))
    if noise:
        x = x + (noise * AWGN(seed, 1.0).generate(M).real).astype(np.float32)
    return ADC(bits, 0.0, 0).steps(x).astype(np.float32)
codes = adc_capture(bits=12, ftone=10.017e6)  # non-coherent quant. tone
m = ToneMeasure(n=N, fs=FS, bits=12)
r = m.analyze(codes)
print(f"SNR {r.snr:.1f} dB  SINAD {r.sinad:.1f} dB  "
      f"SFDR {r.sfdr_dbc:.1f} dBc  ENOB {r.enob:.2f} bits")
# SNR 73.9 dB  SINAD 73.9 dB  SFDR 94.0 dBc  ENOB 11.98 bits

analyze() returns a named ToneMetrics result (attribute access and tuple unpacking); m.rbw, m.bin_hz and r.lobe_bins report the analysis grid.

The two-tone IMD/TOI and notched-noise NPR analysers have their own gallery page: Measurement Suite — IMD & NPR.