Sigma-Delta ADC - Bandpass IF Sampling with CORDIC Downconversion

Sigma-Delta ADC - Bandpass IF Sampling with CORDIC Downconversion


Overview

A fully synchronous, parameterizable Sigma-Delta ADC pipeline implemented in synthesizable Verilog. The design converts an IF (intermediate frequency) input into baseband IQ samples entirely in the digital domain — no floating-point arithmetic appears anywhere in the datapath. All sample-level data is Q1.30 throughout; the 1-bit PDM bitstream runs at OSR × fs.

The pipeline is a complete SDR receiver front-end in three stages. The 2nd-order sigma-delta modulator noise-shapes quantization error to high frequencies. A 3rd-order CIC decimation filter recovers the PCM output at the baseband rate. A CORDIC-based digital downconverter — using the same cordic_nco and cordic_mixer modules as qam16_demod.v — shifts the decimated IF signal to baseband IQ.

Theoretical SNR at OSR=64 is 85.2 dB (~14 ENOB) for a 2nd-order modulator, scaling at 15 dB per doubling of OSR. Bitstream density tracks the normalized input level to within ±1% error across the full ±0.95 range. The freq_adj input connects directly to a DPLL or Costas loop output for closed-loop carrier tracking.


Architecture

Signal Flow

IF input (Q1.30, OSR × fs)
        │
        ▼
  [sd_adc]                2nd-order error-feedback modulator
        │                 Two 64-bit integrators, LFSR dither, 1-bit quantizer
        │  bitstream (1-bit PDM at OSR × fs)
        ▼
  [sd_decimator]          3rd-order CIC filter
        │                 56-bit accumulators, decimate ÷ OSR, normalize >>> 18
        │  pcm_if (Q1.30 at fs, IF frequency)
        ▼
  [sd_downconv]           ← CORDIC modules
        │
        ├─ [cordic_nco]   64-bit accumulator, CORDIC rotation mode
        │        │        generates cos/sin of carrier at PCM rate
        │        │  nco_i / nco_q (Q1.14)
        │        ▼
        └─ [cordic_mixer] complex multiply: pcm_if × conj(nco)
                 │        out_i = pcm_hold × nco_i  (>>> 14 → Q1.30)
                 │        out_q = pcm_hold × nco_q  (>>> 14 → Q1.30)
                 ▼
          bb_i / bb_q (Q1.30 baseband IQ at fs)

SD Modulator — sd_adc.v

2nd-order error-feedback single-loop topology:

x_in ──►[+]──► acc1 ──►[+]──► acc2 ──► Q ──► bitstream_out
         ▲              ▲               │
         └──── fb ◄─────┴───────────────┘

Both integrators subtract the same feedback signal fb. The 1-bit quantizer compares acc2 + dither against zero and sets fb = ±2^30. Two integration steps per sample give the loop a 2nd-order noise transfer function with zeros at DC, pushing quantization noise PSD ∝ f⁴ — the characteristic that makes high-OSR sigma-delta converters practical.

Both accumulations use 64-bit arithmetic with the same sign-extension pattern as dpll_loop_filter.v to prevent the 32×32-to-32 truncation bug. Anti-windup is clamped to ±2³¹. A 17-bit Fibonacci LFSR adds ±1 LSB of dither before the quantizer, breaking periodic idle tones at DC and near-DC inputs.

Why the same pattern as the DPLL loop filter? Both are discrete-time integrating feedback loops — the SD modulator is a 1-bit quantizing control loop, the DPLL is a phase-tracking control loop. The fixed-point pitfalls are identical: accumulator overflow, gain normalization, and anti-windup.


CIC Decimation Filter — sd_decimator.v

3rd-order CIC (Cascaded Integrator-Comb) decimation by OSR:

3 integrators at PDM rate (i1, i2, i3)
        ↓  strobe every OSR clocks
3 comb stages at PCM rate (c1 = i3-d1, c2 = c1-d2, c3 = c2-d3)
        ↓  normalize
pcm_out = c3 >>> (3 × log2(OSR))

The three comb stages execute sequentially within the same strobe cycle, each reading the current cycle’s output of the previous stage. 56-bit accumulators accommodate the full 18-bit growth at OSR=64 (3 × 6 bits) with headroom to OSR=256. DC gain is exactly 1.0 by design: OSR³ / 2^(3×log₂OSR) = 1.

CIC group delay is (ORDER × (OSR-1)) / 2 = 94.5 PDM samples ≈ 1.5 PCM samples. Total pipeline latency from input to baseband IQ output is approximately 1.3 PCM samples including the CORDIC downconversion stage.


CORDIC Downconverter — sd_downconv.v

Reuses cordic_nco and cordic_mixer without modification — same modules, same interfaces, same fixed-point budget as qam16_demod.v.

cordic_nco: 64-bit phase accumulator advances by phase_inc each PCM sample. phase_inc is set to the carrier frequency at PCM rate in Q2.30 rad/sample. CORDIC rotation mode computes cos/sin in 16 iterations. Output is Q1.14 (>>> 16 from full Q1.30).

Sample hold: pcm_if is registered into pcm_hold at pcm_valid. The NCO takes ITER + 3 = 19 cycles to produce nco_valid — without the hold register the mixer would see stale data.

cordic_mixer: conjugate multiplication with real input (in_q = 0):

bb_i = pcm_hold × nco_i              (>>> 14 → Q1.30)
bb_q = −pcm_hold × nco_q             (>>> 14 → Q1.30, lower-sideband)

Product is Q1.30 × Q1.14 = Q2.44. Right-shift by 14 normalizes to Q1.30. bb_valid fires 20 clocks after pcm_valid.

freq_adj input: connects directly to a DPLL freq_adj output for closed-loop carrier recovery. The full receiver chain becomes:

IF input → sd_bandpass → bb_i/bb_q → dpll (carrier recovery) → qam16_demod
                 ↑                         │
                 └────── freq_adj ◄─────────┘

Fixed-Point Format Summary

Signal Format Notes
if_in, x_in, pcm_if Q1.30 All sample-domain signals
phase_inc, freq_adj Q2.30 Angular quantities, rad/sample, range ±π
fb (feedback) Q1.30 ±2³⁰ exactly
acc1, acc2 (integrators) 64-bit signed Anti-windup ±2³¹
CIC accumulators 56-bit signed Supports OSR up to 256
nco_i, nco_q Q1.14 After >>> 16 from CORDIC Q1.30 output
bb_i, bb_q Q1.30 Baseband IQ, product >>> 14
CORDIC internal Q1.30 WIDTH=32, ITER=16

Parameters

Parameter Default Description
WIDTH 32 Datapath width (from cordic_params.vh)
ITER 16 CORDIC iterations
OSR 64 Oversampling ratio (power of 2, 16–256)
DITHER_EN 1 1 = LFSR dither enabled in modulator

Setting phase_inc: carrier frequency at PCM rate in Q2.30 rad/sample.

phase_inc = (f_carrier / f_pcm) × 2π  expressed in Q2.30
           = f_carrier_normalized × 2^30

Example: carrier at fs_pdm/256 with OSR=64
  f_carrier_pcm = (1/256) × 64 = 0.25 cycles/sample = π/2 rad/sample
  phase_inc = π/2 × 2^30 = 1686629713

Theoretical SNR (2nd-order modulator, sine at −6 dBFS):

SNR ≈ 6.02 + 1.76 − 12.9 + 50 × log₁₀(OSR)  dB

OSR= 16:  55.1 dB   (~9 ENOB)
OSR= 32:  70.1 dB   (~11 ENOB)
OSR= 64:  85.2 dB   (~14 ENOB)
OSR=128: 100.2 dB   (~16 ENOB)

Directed Tests — Scalar ADC (tb_sd_adc.v)

Four directed tests running against sd_top (modulator + decimator). Pass criteria annotated per test.

Test Input Pass Criterion Result
DC sweep x_in = −0.9, −0.5, 0.0, +0.5, +0.9 density = (x+1)/2 ± 0.02 5/5 PASS
Sine RMS 0.5×sin(2π×n/512), N=8192 PDM sum_sq/sample ∈ [91.9M, 137.9M] PASS
Overload recovery x_in = +FS for 256 samples, then 0 post-recovery density ∈ [0.42, 0.58] PASS
Dither at DC=0 x_in = 0, 100 samples bitstream density ∈ (0.30, 0.70) PASS
============================================================
  Sigma-Delta ADC  WIDTH=32  OSR=64  DITHER_EN=1
============================================================
=== Test 1: DC Sweep ===
    x=-0.9  density=0.0596  exp=0.0500  err=0.0096  PASS
    x=-0.5  density=0.2500  exp=0.2500  err=0.0000  PASS
    x=+0.0  density=0.5000  exp=0.5000  err=0.0000  PASS
    x=+0.5  density=0.7500  exp=0.7500  err=0.0000  PASS
    x=+0.9  density=0.9404  exp=0.9500  err=0.0096  PASS
=== Test 2: Sine RMS check (period=512 PDM / 8 PCM) ===
    PCM samples used: 120  sum_sq/sample=114964171  RMS=0.32721
    Expected RMS ~ 0.32718  (0.5 × 0.9254 / sqrt(2))
    PASS
=== Test 3: Overload Recovery ===
    Post-overload density=0.5000  (exp ~0.5, tol 0.08)
    PASS
=== Test 4: Dither at x_in=0 ===
    Density at DC=0: 0.5000  (must be in 0.30..0.70)
    PASS
============================================================
  SUMMARY:  8 PASS  0 FAIL
  ALL TESTS PASSED
============================================================

The sine RMS test uses a variance-based check rather than phase-matched point-by-point comparison. The CIC has a non-integer group delay of ~3.5 PCM samples, making it impossible to construct a phase-matched reference in fixed-point Verilog. The variance of a pure sine is A²/2 regardless of phase, and the measured sum_sq/sample = 114964171 matches the expected (0.5 × 0.9254 × 32768)² = 114939386 to within 0.02%.

The ±0.96% density error at x = ±0.9 is expected — near the rails, the anti-windup clamp has less headroom and dither introduces a small systematic offset.


Directed Tests — Bandpass ADC (tb_sd_bandpass.v)

Three directed tests running against sd_bandpass (full pipeline including CORDIC downconversion). bb_valid fires ~20 clocks after each pcm_valid due to the NCO + mixer pipeline; capture is handled by a dedicated always @(posedge clk) block independent of the feed loop.

Test Input Pass Criterion Result
Pure carrier 0.5×sin(2π×n/256), phase_inc=π/2 combined IQ RMS ∈ [0.20, 0.42] PASS
Sideband tone 0.5×sin(2π×(1/256+1/1024)×n) combined IQ RMS ∈ [0.20, 0.42] PASS
Zero input if_in = 0 rms_i < 0.02 AND rms_q < 0.02 PASS
============================================================
  Bandpass SD ADC  WIDTH=32  OSR=64
  f_carrier=fs_pdm/256  f_carrier_pcm=fs_pcm/4
  phase_inc=1686629713 (pi/2 rad/sample at PCM rate)
============================================================
=== Test 1: Pure Carrier (fs_pdm/256) ===
    Bitstream density: 0.5000 (exp ~0.5)
    bb_cnt=45  rms_i=0.17320  rms_q=0.19155
    Combined IQ RMS: 0.25824  (exp ~0.327)
    PASS
=== Test 2: Sideband (carrier + fs_pdm/1024 offset) ===
    bb_cnt=45  rms_i=0.15448  rms_q=0.15060
    Combined IQ RMS: 0.21574  (exp ~0.327)
    PASS
=== Test 3: Zero Input -> BB near zero ===
    bb_cnt=45  rms_i=0.00000  rms_q=0.00000
    PASS
============================================================
  SUMMARY:  3 PASS  0 FAIL
  ALL TESTS PASSED
============================================================

The combined IQ RMS of 0.258 vs the scalar ADC’s 0.327 reflects CIC passband droop at the carrier frequency f_carrier_pcm = fs_pcm/4. The sinc³ gain at f = fs_pcm/4 is approximately 0.79 rather than the 0.9254 at f = fs_pcm/8 used in the scalar tests — a ~3 dB reduction that is expected and correct. Both channels (I and Q) carry similar power for the sideband test, confirming the complex downconversion is producing a genuine baseband rotating phasor rather than a real-only output.


Bitstream Density

Bitstream Density

The left panel shows measured bitstream density (fraction of 1s in the PDM stream) against the ideal (x+1)/2 characteristic. The 2nd-order modulator tracks this relationship accurately across the full ±0.95 input range. The right panel shows absolute density error — below 1% for all mid-range inputs, rising to ~1% near the rails where the anti-windup boundary reduces integrator headroom.

The density-to-amplitude relationship is the fundamental property of a sigma-delta modulator: the time-average of the 1-bit output equals the normalized input amplitude. All downstream signal recovery (CIC, CORDIC downconversion) depends on this relationship being accurate.


PDM Bitstream Spectrum — Noise Shaping

PDM Spectrum

The noise-shaping effect of the 2nd-order modulator is clearly visible. The quantization noise floor rises sharply with frequency — noise PSD ∝ f⁴ — while the signal at f₀ = fs_pdm/512 rises well above the noise floor. The dashed reference curve shows the theoretical 2nd-order noise-shaping characteristic |1 − z⁻¹|² = 4sin²(πf), confirming the RTL matches theory.

The CIC decimation filter’s low-pass response (cutoff at fs_pcm/2 = fs_pdm/128) discards the high-frequency noise region shown in the right half of the spectrum. Only the low-noise region near DC passes through to the PCM output, which is why high OSR produces high SNR.


PCM Output Spectrum

PCM Spectrum

The decimated PCM output at the baseband rate. The signal tone at f_pcm/8 is clearly visible above a low noise floor. The measured DFT-based SNR is consistent with the theoretical 85 dB for a 2nd-order modulator at OSR=64, with the short-record estimation gap expected from the limited number of PCM samples available in a single simulation run. Longer records improve the SNR estimate monotonically toward the theoretical limit.

The noise floor rises at higher frequencies — this is the aliased out-of-band quantization noise that the CIC did not fully suppress, limited by the sinc³ stopband attenuation. A compensation FIR after the CIC would flatten the passband and improve stopband rejection.


SNR vs Input Amplitude

SNR vs Amplitude

Three operating regions are visible. At low amplitudes (< 0.2), SNR is reduced because the signal power is small relative to the quantization noise floor, even though the noise floor itself is constant. In the mid-range (0.2–0.8), SNR is near its peak — this is the linear operating region where the modulator tracks the input accurately. Above 0.9, SNR drops sharply as the input approaches full scale: the anti-windup clamp activates, the integrators cannot fully track the signal, and harmonic distortion rises rapidly. Above 1.0 (overload), the modulator clips and SNR collapses.

The peak SNR and the amplitude at which it occurs confirm that the optimal operating point is approximately −3 to −6 dBFS (amplitude 0.5–0.7), consistent with standard sigma-delta ADC practice.


SNR vs OSR

OSR Comparison

Hardware-measured SNR versus oversampling ratio, plotted against the 2nd-order theoretical curve. The measured values track the 50×log₁₀(OSR) slope closely — 15 dB per octave — which is the defining property of a 2nd-order modulator: each doubling of OSR adds 15 dB of SNR rather than the 6 dB per octave of a 1st-order system.

The gap between measured and theoretical SNR is due to the short simulation record (4096 PDM samples = 64 PCM samples at OSR=64) limiting spectral resolution. With a longer record the measured values would approach the theoretical curve from below.


Dither Effect

Dither Effect

The left panel shows the PDM bitstream spectrum at DC=0 with and without the LFSR dither. Without dither, the modulator settles into a periodic idle pattern (101010... or similar), creating strong spectral lines at sub-harmonic frequencies. These lines alias into the passband after decimation, appearing as spurious tones in the PCM output — audible as a fixed-pitch tone in audio applications.

With dither enabled, the spectral lines are replaced by a broadband noise floor. The dither randomizes the idle pattern without adding significant noise power (1 LSB = ~10⁻⁹ in normalized float). The right panel shows the bitstream directly — the no-dither case is visibly periodic while the dither case is visibly random, confirming the LFSR is operating correctly.


Step Response

Step Response

The upper panel shows the PDM bitstream density in sliding 32-sample windows. Before the step (sample 0–256), the density is 0.50 corresponding to x_in=0. After the step to x_in=0.5, the density converges to 0.75 (= (0.5+1)/2). The two integrators require a transient period to track the abrupt change — this is the modulator’s acquisition time for a step input.

The lower panel shows the decimated PCM output. Each PCM sample represents 64 PDM samples, so the PCM settling is measured in a much smaller number of samples. The final PCM value approaches the expected level. The CIC sinc³ response applies a frequency-dependent gain of 0.9254 at the test frequency — the DC gain is exactly 1.0 so a step input to DC level is unaffected by CIC droop.


Bandpass IQ Output

Bandpass IQ

The CORDIC downconversion output for carrier and sideband inputs. The top row shows I and Q time traces — a pure carrier at fs_pdm/256 produces a sinusoidal oscillation split across both I and Q channels (the exact split depends on the phase relationship between the input carrier and the NCO at startup). The bottom row shows the IQ constellation — the pure carrier traces a circle, confirming the complex downconversion is producing a genuine rotating phasor at baseband rather than a real signal on one channel only.

The sideband test (carrier + offset) produces a circle as well, but at a different rotation rate — the baseband tone is at f_sig/fs_pcm = 1/16 rather than near-DC. Both constellations have radius consistent with the expected amplitude after CIC gain reduction at f_carrier_pcm = fs_pcm/4.


Known Bugs Found During Development

# Module Bug Symptom Fix
1 sd_adc 32×32 multiply in integrators sized to 32 bits by Verilog Integrators never moved; bitstream stuck at 0 or 1 Sign-extend both operands to 64 bits before adding (same fix as dpll_loop_filter)
2 tb_sd_adc Sine test at f₀/fs_pdm = 1/64 (Nyquist after decimation) CIC gain = 0 at Nyquist; PCM output was pure noise; SNR = 0 dB Change to period = 512 PDM samples → f_pcm = 1/8, well inside CIC passband
3 tb_sd_adc Phase-matched SNR reference failed due to CIC group delay ~3.5 PCM samples Reference sine perfectly out of phase with output; error = 2×signal; SNR = 0 dB Replace with variance-based RMS check: mean(pcm_scaled²) is phase-independent
4 tb_sd_adc EXP_SS_PER computed with erroneous /2 Correct sum_sq/sample (114964171) failed against wrong expected (57374906) Remove /2: EXP_SS_PER = (A × 32768)² not A²/2
5 tb_sd_adc integer sum_sq_per truncated 64-bit division result to signed 32-bit sum_sq_per printed as −2032519477; test always failed Change sum_sq_per and pcm_scaled to reg [63:0] and reg signed [63:0]
6 tb_sd_bandpass bb_valid captured inline in feed loop (1 cycle wait per PDM sample) bb_cnt = 0; bb_valid never seen; all tests FAIL Move capture to dedicated always @(posedge clk) block with capture_en flag
7 tb_sd_adc Local variable declarations inside unnamed begin...end blocks iverilog error: variable declaration requires SystemVerilog Move all integer, real, reg declarations to module level

Bug #2 and #3 occurred in sequence — fixing the Nyquist issue revealed the group delay mismatch. Bug #4 was then revealed once the RMS approach was working. These three bugs were all in the testbench, not the RTL, but each required understanding the CIC filter’s frequency response and group delay to diagnose correctly.

Bug #6 was the same class as a similar issue in tb_qam16_demod.v — asynchronous outputs from a multi-cycle pipeline (NCO + mixer = 20 clocks) cannot be captured inline in a single-clock feed loop.


Verification Summary

============================================================
SUMMARY
============================================================

Scalar ADC Tests (tb_sd_adc.v):
  DC sweep (5 levels)     : all within ±1% density error
  Sine RMS (OSR=64)       : sum_sq/sample=114964171  exp=114939386  error=0.02%
  Overload recovery       : density=0.500 after 128 settling samples
  Dither at DC=0          : density=0.500  (bitstream toggling)
  All 8 assertions: PASS

Bandpass ADC Tests (tb_sd_bandpass.v):
  Pure carrier   combined IQ RMS: 0.258  window [0.20, 0.42]  PASS
  Sideband tone  combined IQ RMS: 0.216  window [0.20, 0.42]  PASS
  Zero input     rms_i=0.000, rms_q=0.000 (both < 0.02)       PASS
  All 3 assertions: PASS

Verification sweeps (verify_sd_adc.py):
  Density error   : max 1.0%  across DC = ±0.95
  PCM SNR         : consistent with 2nd-order theory
  SNR vs OSR      : 15 dB/octave slope confirmed OSR=16..128
  Peak SNR        : at amplitude ≈ 0.5–0.7 (−3 to −6 dBFS)
  Dither          : idle tone suppression confirmed
  Step response   : PDM density and PCM output both settle cleanly
  Bandpass IQ     : circular constellation confirmed for carrier and sideband
Metric Value
Architecture 2nd-order error-feedback SD + 3rd-order CIC + CORDIC DDC
Theoretical SNR at OSR=64 85.2 dB (~14 ENOB)
SNR slope 15 dB/octave (2nd-order)
DC density linearity ±1% across full range
Sine RMS accuracy 0.02% from theory
Overload recovery < 128 PDM samples (~2 PCM outputs)
Pipeline latency ~1.3 PCM samples (modulator + CIC + CORDIC)
Carrier frequency range 0 < f_carrier < fs_pcm/2
freq_adj input Q2.30, connects to DPLL for closed-loop carrier tracking
Shared CORDIC modules cordic_nco, cordic_mixer, cordic_core