Skip to content

Commit 935e86d

Browse files
committed
Add basic SAR ADC model
1 parent 7b189ad commit 935e86d

File tree

5 files changed

+195
-8
lines changed

5 files changed

+195
-8
lines changed

adc_eval/adcs/basic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def offset(self):
111111
@offset.setter
112112
def offset(self, values):
113113
"""Set offset mean and stdev."""
114-
self.err["offset"] = values[0] + values[1] * np.random.randn(1)
114+
self.err["offset"] = np.random.normal(values[0], values[1])
115115

116116
@property
117117
def gain_error(self):
@@ -121,7 +121,7 @@ def gain_error(self):
121121
@gain_error.setter
122122
def gain_error(self, values):
123123
"""Set gain error mean and stdev."""
124-
self.err["gain"] = values[0] + values[1] * np.random.randn(1)
124+
self.err["gain"] = np.random.normal(values[0], values[1])
125125

126126
@property
127127
def distortion(self):

adc_eval/adcs/sar.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""SAR ADC models"""
2+
3+
import numpy as np
4+
from adc_eval.adcs.basic import ADC
5+
6+
7+
class SAR(ADC):
8+
"""
9+
SAR ADC Class.
10+
11+
Parameters
12+
----------
13+
nbits : int, optional
14+
Number of bits for the ADC. The default is 8.
15+
fs : float, optional
16+
Sample rate for the ADC in Hz. The default is 1Hz.
17+
vref : float, optional
18+
Reference level of the ADC in Volts ([0, +vref] conversion range). The default is 1.
19+
seed : int, optional
20+
Seed for random variable generation. The default is 1.
21+
**kwargs
22+
Extra arguments.
23+
weights : list, optional
24+
List of weights for SAR capacitors. Must be >= nbits. Defaults to binary weights.
25+
MSB weight should be in index 0.
26+
27+
Attributes
28+
-------
29+
vin : float
30+
Sets or returns the current input voltage level. Assumed +/-vref/2 input
31+
vlsb : float
32+
LSB voltage of the converter. vref/2^nbits
33+
noise : float, default=0
34+
Sets or returns the stdev of the noise generated by the converter.
35+
weights : list
36+
Sets or returns the capacitor weighting of the array. Default is binary weighting.
37+
mismatch : float
38+
Sets or returns the stdev of the mismatch of the converter. Default is no mismatch.
39+
offset : tuple of float
40+
Sets the (mean, stdev) of the offset of the converter. Default is no offset.
41+
gain_error : tuple of float
42+
Sets the (mean, stdev) of the gain error of the converter. Default is no gain error.
43+
distortion : list of float
44+
Sets the harmonic distortion values with index=0 corresponding to HD1.
45+
Example: For unity gain and only -30dB of HD3, input is [1, 0, 0.032]
46+
dout : int
47+
Digital output code for current vin value.
48+
49+
Methods
50+
-------
51+
run_step
52+
53+
"""
54+
55+
def __init__(self, nbits=8, fs=1, vref=1, seed=1, **kwargs):
56+
"""Initialization function for Generic ADC."""
57+
super().__init__(nbits, fs, vref, seed)
58+
59+
self._mismatch = None
60+
61+
# Get keyword arguments
62+
self._weights = kwargs.get("weights", None)
63+
64+
@property
65+
def weights(self):
66+
"""Returns capacitor unit weights."""
67+
if self._weights is None:
68+
self._weights = np.flip(2**np.linspace(0, self.nbits-1, self.nbits))
69+
return np.array(self._weights)
70+
71+
@weights.setter
72+
def weights(self, values):
73+
"""Sets the capacitor unit weights."""
74+
self._weights = np.array(values)
75+
if self._weights.size < self.nbits:
76+
print(f"WARNING: Capacitor weight array size is {self._weights.size} for {self.nbits}-bit ADC.")
77+
self.mismatch = self.err["mismatch"]
78+
79+
@property
80+
def mismatch(self):
81+
"""Return noise stdev."""
82+
if self._mismatch is None:
83+
self._mismatch = np.zeros(self.weights.size)
84+
return self._mismatch
85+
86+
@mismatch.setter
87+
def mismatch(self, stdev):
88+
"""Sets mismatch stdev."""
89+
self.err["mismatch"] = stdev
90+
self._mismatch = np.random.normal(0, stdev, self.weights.size)
91+
self._mismatch /= np.sqrt(self.weights)
92+
93+
def run_step(self):
94+
"""Run a single ADC step."""
95+
vinx = self.vin
96+
97+
cweights = self.weights * (1 + self.mismatch)
98+
cdenom = sum(cweights) + 1
99+
100+
comp_noise = np.random.normal(0, self.err["noise"], cweights.size)
101+
102+
# Bit cycling
103+
vdac = vinx
104+
for n in range(len(cweights)):
105+
vcomp = vdac - self.vref / 2
106+
compout = vcomp * 1e6
107+
compout = -1 if compout <= 0 else 1
108+
self.dbits[n] = max(0, compout)
109+
vdac -= compout * self.vref / 2 * cweights[n] / cdenom
110+
111+
# Re-scale the data
112+
scalar = 2**self.nbits / cdenom
113+
self.dval = min(2**self.nbits-1, scalar * sum(self.weights * self.dbits))

adc_eval/signals.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,45 @@ def impulse(nlen, mag=1):
8989
data = np.zeros(nlen)
9090
data[0] = mag
9191
return data
92+
93+
94+
def tones(nlen, bins, amps, offset=0, fs=1, nfft=None, phases=None):
95+
"""
96+
Generate a time-series of multiple tones.
97+
98+
Parameters
99+
----------
100+
nlen : int
101+
Length of time-series array.
102+
bins : list
103+
List of signal bins to generate tones for.
104+
amps : list
105+
List of amplitudes for given bins.
106+
offset : int, optional
107+
Offset to apply to each signal (globally applied).
108+
fs : float, optional
109+
Sample rate of the signal in Hz. The default is 1Hz.
110+
nfft : int, optional
111+
Number of FFT samples, if different than length of signal. The default is None.
112+
phases : list, optional
113+
List of phase shifts for each bin. The default is None.
114+
115+
Returns
116+
-------
117+
tuple of ndarray
118+
(time, signal)
119+
Time-series and associated tone array.
120+
"""
121+
t = time(nlen, fs=fs)
122+
123+
signal = np.zeros(nlen)
124+
if phases is None:
125+
phases = np.zeros(nlen)
126+
if nfft is None:
127+
nfft = nlen
128+
129+
fbin = fs / nfft
130+
for index, nbin in enumerate(bins):
131+
signal += sin(t, amp=amps[index], offset=offset, freq=nbin*fbin, ph0=phases[index])
132+
133+
return (t, signal)

examples/basic_adc_simulation.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@
1717
NLEN = 2**16 # Larger than NFFT to enable Bartlett method for PSD
1818
NFFT = 2**12
1919
vref = 1
20-
fin_bin = NFFT / 4 - 31
21-
fin = fin_bin * FS/NFFT
20+
fbin = NFFT / 4 - 31
21+
ftone = NFFT / 2 - 15
2222
vin_amp = 0.707 * vref / 2
2323

2424

2525
"""
2626
VIN Generation
2727
"""
28-
t = signals.time(NLEN, FS)
29-
vin = signals.sin(t, amp=vin_amp, offset=0, freq=fin)
30-
vin += signals.sin(t, amp=vin_amp*0.2, offset=0, freq=(NFFT/2-15)*FS/NFFT) # Adds tone to show intermodulation
28+
(t, vin) = signals.tones(NLEN, [fbin, ftone], [vin_amp, vin_amp*0.2], offset=0, fs=FS, nfft=NFFT)
3129

3230
"""
3331
ADC Architecture Creation

tests/test_signals.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
RTOL = 0.01
1010

11-
1211
@pytest.mark.parametrize("nlen", np.random.randint(4, 2**16, 3))
1312
@pytest.mark.parametrize("fs", np.random.uniform(1, 1e9, 3))
1413
def test_time(nlen, fs):
@@ -77,3 +76,38 @@ def test_impulse(nlen, mag):
7776
assert data.size == nlen
7877
assert data[0] == mag
7978
assert data[1:].all() == 0
79+
80+
81+
@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
82+
def test_tones_no_nfft_arg(nlen):
83+
"""Test tone generation with random length no nfft param."""
84+
(t, data) = signals.tones(nlen, [0.5], [0.5])
85+
86+
assert t.size == nlen
87+
assert t[0] == 0
88+
assert t[-1] == nlen-1
89+
assert data.size == nlen
90+
91+
92+
@pytest.mark.parametrize("fs", np.random.uniform(100, 1e9, 3))
93+
@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
94+
def test_tones_with_fs_arg(fs, nlen):
95+
"""Test tone generation with random length and fs given."""
96+
(t, data) = signals.tones(nlen, [0.5], [0.5], fs=fs)
97+
98+
assert t.size == nlen
99+
assert t[0] == 0
100+
assert np.isclose(t[-1], (nlen-1) / fs, rtol=RTOL)
101+
assert data.size == nlen
102+
103+
104+
@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
105+
def test_tones_with_empty_list( nlen):
106+
"""Test tone generation with random length and fs given."""
107+
(t, data) = signals.tones(nlen, [], [])
108+
109+
assert t.size == nlen
110+
assert t[0] == 0
111+
assert t[-1] == nlen-1
112+
assert data.size == nlen
113+
assert data.all() == np.zeros(nlen).all()

0 commit comments

Comments
 (0)