Simple ADC example#
For a simple Verilog-only testbench#
make
For a cocotb-based testbench#
Install dependencies:
pip install pytest cocotb numpy matplotlib
Run test:
python test_flash_adc8.py
* flash ADC 8-bit
* 1-bit ADC
.subckt ad1bit in ref comp out ref_out vcc
B1 ref_out 0 V = v(ref)/2
B2 comp 0 V = (v(in) > v(ref_out)) ? v(vcc) : 0
B3 out 0 V = (v(in) > v(ref_out)) ? v(in)-v(ref_out) : v(in)
.ends
.subckt adc_ideal_8bit in ref vcc ad7 ad6 ad5 ad4 ad3 ad2 ad1 ad0
Xad7 in ref ad7 in7 ref7 vcc ad1bit
Xad6 in7 ref7 ad6 in6 ref6 vcc ad1bit
Xad5 in6 ref6 ad5 in5 ref5 vcc ad1bit
Xad4 in5 ref5 ad4 in4 ref4 vcc ad1bit
Xad3 in4 ref4 ad3 in3 ref3 vcc ad1bit
Xad2 in3 ref3 ad2 in2 ref2 vcc ad1bit
Xad1 in2 ref2 ad1 in1 ref1 vcc ad1bit
Xad0 in1 ref1 ad0 in0 ref0 vcc ad1bit
.ends
Vvin vin 0 0 external
Vcc vcc 0 1
Vref ref 0 1.0
Xadc vin ref vcc code[7] code[6] code[5] code[4] code[3] code[2] code[1] code[0] adc_ideal_8bit
.tran 1ns 1
.end
// flash_adc8.v
//
// 8-bit (256-level) flash ADC – behavioural model
// Uses `real` everywhere: good for mixed-signal verification, *not* synthesizable.
//
// vin ∈ [0.0, VREF] where VREF = 1.0 (change as you like)
// Code is straight binary (000…255)
//
// Compile with a simulator that supports IEEE Verilog-2001 real data types
`timescale 1ns/1ps
module flash_adc8(
input real vin, // analog input
output reg [7:0] code // digital output
);
`ifdef VERILOG_MODEL
// =========================================================================
// NOTES
// * Real thresholds are pre-computed at elaboration: T[i] = (i+0.5)/256
// * Conversion is done with one always_comb (single clockless flash step)
// =========================================================================
real thresholds [0:255];
real offset_msb = 0.005;
// Elaborate threshold ladder
initial begin : build_ladder
integer i;
for (i = 0; i < 256; i = i + 1) begin
thresholds[i] = (i + 0.5) / 256.0;
if (i > 127) begin
thresholds[i] = thresholds[i]+offset_msb;
end
end
end
// Combinational conversion (behavioural)
always @* begin : convert
integer k;
code = 8'hFF; // default clip high
for (k = 0; k < 256; k = k + 1) begin
if (vin < thresholds[k]) begin
code = k[7:0];
disable convert; // exit the loop
end
end
end
`endif
initial begin
$dumpfile("flash_adc8.vcd");
$dumpvars (0);
end
endmodule
`timescale 1ns/1ps
module flash_adc8_tb;
//------------------------------------------------------------------
// Parameters – change as you like
//------------------------------------------------------------------
real VREF = 1.0; // full-scale reference
real FS = 50e6; // “sample rate” (50 MHz) – for timing only
real FIN = FS / 17.0; // input tone (coherent with 4096 samples)
int NSAMPLES = 4096; // how many codes to capture
// Derived constants
real PI = 3.141592653589793;
real PHASE_INC = 2.0 * PI * FIN / FS; // Δphase per sample (rad)
time DT = time'(1e9 / FS); // 1/FS in nanoseconds (20 ns)
//------------------------------------------------------------------
// DUT connections
//------------------------------------------------------------------
real vin;
wire [7:0] code;
flash_adc8 dut (
.vin (vin),
.code (code)
);
real phase = 0.0;
//------------------------------------------------------------------
// Stimulus + dump
//------------------------------------------------------------------
initial begin
$display("time [ns] | vin | code");
$display("--------------------------------");
repeat (NSAMPLES) begin
// Full-scale, mid-rise sine in the 0 … VREF range
vin = 0.5 * VREF * (1.0 + $sin(phase));
// Wait long enough for combinational evaluation
#(DT);
// Output one line per sample
$display("%8t | %0.6f | 0x%0h", $time, vin, code);
phase = phase + PHASE_INC;
end
$finish;
end
endmodule
"""
Coherent-sampling + static-linearity testbench for the behavioural 8-bit flash
ADC (INL/DNL plots).
"""
import math
import pathlib
import os
import cocotb
from cocotb.triggers import Timer
from cocotb.runner import get_runner
import spicebind
import numpy as np
from numpy.fft import rfft, rfftfreq
import matplotlib.pyplot as plt
# -----------------------------------------------------------------------------
# User-tunable parameters
# -----------------------------------------------------------------------------
VREF = 1.0 # full-scale voltage of the ADC model
FS_NS = 20 # sample rate in [ns]
FS = int(1 / FS_NS * 1e9) # sample rate [Hz] (informational only)
N_SAMPLES = 4096 # record length for FFT (power of two)
K_BIN = 601 # coherent-tone bin index (1 ≤ K < N/2)
F_IN = K_BIN * FS / N_SAMPLES
WINDOW = np.ones(N_SAMPLES) # rectangular → unity coherent gain
DB_EPS = 1e-20 # prevents log10(0)
# Ramp for code-density test (static linearity)
RAMP_SAMPLES = 65_536 # ≥ 256× codes for fine resolution
# -----------------------------------------------------------------------------
# Dynamic-range metrics (unchanged, but isolated for reuse)
# -----------------------------------------------------------------------------
def calc_dynamic_metrics(codes: np.ndarray, vref: float = VREF):
"""Return SINAD, ENOB, SNR, THD, SFDR and the spectrum."""
analog = (codes.astype(np.float64) + 0.5) * vref / 256.0
analog -= np.mean(analog)
spec = np.abs(rfft(analog * WINDOW)) ** 2
freqs = rfftfreq(N_SAMPLES, d=1 / FS)
fund_bin = np.argmax(spec[1:]) + 1
fund_power = spec[fund_bin]
other_bins = np.arange(1, len(spec))
other_bins = other_bins[other_bins != fund_bin]
nd_power = np.sum(spec[other_bins])
harmonics = [spec[h * fund_bin] for h in range(2, 6) if h * fund_bin < len(spec)]
thd_power = np.sum(harmonics) if harmonics else 0.0
noise_power = max(nd_power - thd_power, 1e-30)
thd_power = max(thd_power, 1e-30)
sinad = 10 * np.log10(fund_power / (noise_power + thd_power))
snr = 10 * np.log10(fund_power / noise_power)
thd = 10 * np.log10(fund_power / thd_power)
sfdr = 10 * np.log10(fund_power / max(harmonics)) if harmonics else float("nan")
enob = (sinad - 1.76) / 6.02
assert 7.5 <= enob <= 8.0
return {
"SINAD": sinad,
"SNR": snr,
"THD": thd,
"SFDR": sfdr,
"ENOB": enob,
"freqs": freqs,
"spec": spec,
}
# -----------------------------------------------------------------------------
# INL / DNL from a code-density ramp
# -----------------------------------------------------------------------------
def calc_inl_dnl(codes: np.ndarray, vref: float = VREF):
"""Return INL and DNL arrays (length 256). Uses first-transition method."""
n_codes = 256
# 1. Find transition indices - first sample where code increases
transitions = np.zeros(n_codes + 1) # include 0 and VREF at ends
transitions[0] = 0.0
transitions[-1] = vref
prev_code = codes[0]
for i, code in enumerate(codes[1:], start=1):
if code != prev_code:
# record transition voltage
transitions[code] = (i / (len(codes) - 1)) * vref
prev_code = code
if code == n_codes - 1 and prev_code == code:
break # reached top code
# Fill any zeros (ideal ramp guarantees none, but be safe)
for idx in range(1, n_codes):
if transitions[idx] == 0:
transitions[idx] = transitions[idx - 1]
ideal_step = vref / n_codes
# DNL[i] relates to code i (width between transitions i and i+1)
dnl = (np.diff(transitions) / ideal_step) - 1.0 # length 256
# INL[i] uses midpoint of code i
mids_actual = (transitions[:-1] + transitions[1:]) / 2.0
mids_ideal = (np.arange(n_codes) + 0.5) * ideal_step
inl = (mids_actual - mids_ideal) / ideal_step
assert 0 <= np.max(np.abs(inl)) <= 1
assert 0 <= np.max(np.abs(dnl)) <= 1
return inl, dnl
# -----------------------------------------------------------------------------
# cocotb test - combines dynamic + static tests
# -----------------------------------------------------------------------------
@cocotb.test()
async def run_adc_characterisation(dut):
"""Drive coherent sine + ramp, analyse dynamic specs and INL/DNL."""
await Timer(10, units="ns")
# ------------------------------------------------------------------ FFT run
codes_fft = np.empty(N_SAMPLES, dtype=np.int16)
for n in range(N_SAMPLES):
t = n / FS
vin = 0.5 * VREF * (1 + math.sin(2 * math.pi * F_IN * t))
dut.vin.value = vin
await Timer(FS_NS, units="ns")
codes_fft[n] = dut.code.value.integer
dyn = calc_dynamic_metrics(codes_fft)
for k in ("SINAD", "ENOB", "SNR", "THD", "SFDR"):
dut._log.info(f"{k:5s}: {dyn[k]:6.2f}")
# ---------------------------------------------------------------- spectrum
spec_pdf = pathlib.Path("flash_adc8_spectrum.pdf")
plt.figure(figsize=(6.4, 4.8))
plt.semilogx(dyn["freqs"], 10 * np.log10(np.maximum(dyn["spec"], DB_EPS)))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Power (dB)")
plt.title("8-bit Flash ADC - Output Spectrum")
plt.grid(True, which="both", ls=":")
plt.tight_layout()
plt.savefig(spec_pdf, format="pdf")
plt.close()
dut._log.info(f"Spectrum saved to {spec_pdf.resolve()}")
dut.vin.value = 0.0
await Timer(10, units="us")
# ------------------------------------------------------------- ramp run
codes_ramp = np.empty(RAMP_SAMPLES, dtype=np.int16)
for n in range(RAMP_SAMPLES):
vin = (n / (RAMP_SAMPLES - 1)) * VREF
dut.vin.value = vin
await Timer(FS_NS, units="ns")
codes_ramp[n] = dut.code.value.integer
inl, dnl = calc_inl_dnl(codes_ramp)
dut._log.info(
f"Max |INL| = {np.max(np.abs(inl)):.3f} LSB, Max |DNL| = {np.max(np.abs(dnl)):.3f} LSB"
)
# --------------------------------------------------------------- INL/DNL plot
lin_pdf = pathlib.Path("flash_adc8_inl_dnl.pdf")
fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(6.4, 6.4), sharex=True)
codes = np.arange(256)
ax0.stem(codes, dnl, basefmt=" ")
ax0.set_ylabel("DNL [LSB]")
ax0.grid(True, ls=":")
ax1.stem(codes, inl, basefmt=" ")
ax1.set_xlabel("Code")
ax1.set_ylabel("INL [LSB]")
ax1.grid(True, ls=":")
fig.suptitle("8-bit Flash ADC - Static Linearity")
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.savefig(lin_pdf, format="pdf")
plt.close()
dut._log.info(f"INL/DNL plots saved to {lin_pdf.resolve()}")
def test_flash_adc8():
sim = os.getenv("SIM", "icarus")
verilog_model = os.getenv("VERILOG_MODEL", None)
proj_path = pathlib.Path(__file__).resolve().parent
test_args = []
extra_env = {}
defines = {}
if verilog_model:
defines = {"VERILOG_MODEL": "1"}
else:
test_args = ["-M", spicebind.get_lib_dir(), "-m", "spicebind_vpi"]
extra_env = {
"SPICE_NETLIST": str(proj_path / "flash_adc8.cir"),
"HDL_INSTANCE": "flash_adc8",
}
sources = [proj_path / "flash_adc8.v"]
runner = get_runner(sim)
runner.build(
sources=sources,
hdl_toplevel="flash_adc8",
defines=defines,
always=True,
)
runner.test(
hdl_toplevel="flash_adc8",
test_module="test_flash_adc8,",
test_args=test_args,
extra_env=extra_env,
)
if __name__ == "__main__":
test_flash_adc8()
# Makefile ─ simple run for tb_flash_adc8_sine
SRC := flash_adc8.v flash_adc8_tb.v
SRC_SPICE := flash_adc8.cir
HDL_INSTANCE := flash_adc8_tb.dut
SPICEBIND_DIR := $(shell spicebind-vpi-path)
OUT := flash_adc8_tb.vvp
# --------------------------------------------------------------------
# Default target: compile & run
all: $(OUT)
SPICE_NETLIST=$(SRC_SPICE) HDL_INSTANCE=$(HDL_INSTANCE) vvp -M $(SPICEBIND_DIR) -m spicebind_vpi $(OUT)
# Compile step
$(OUT): $(SRC)
iverilog -g2005-sv -o $@ $(SRC)
# Clean up
clean:
rm -f $(OUT) *.raw *.vcd
.PHONY: all wave clean