Skip to content

Commit 6a0e4ff

Browse files
authored
Merge pull request #108 from fronzbot/add-sar-adc
Add sar adc
2 parents 7b189ad + ef5ec58 commit 6a0e4ff

File tree

7 files changed

+214
-8
lines changed

7 files changed

+214
-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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
comp_noise : float
40+
Sets or returns the stdev of the comparator noise. Default is no noise.
41+
offset : tuple of float
42+
Sets the (mean, stdev) of the offset of the converter. Default is no offset.
43+
gain_error : tuple of float
44+
Sets the (mean, stdev) of the gain error of the converter. Default is no gain error.
45+
distortion : list of float
46+
Sets the harmonic distortion values with index=0 corresponding to HD1.
47+
Example: For unity gain and only -30dB of HD3, input is [1, 0, 0.032]
48+
dout : int
49+
Digital output code for current vin value.
50+
51+
Methods
52+
-------
53+
run_step
54+
55+
"""
56+
57+
def __init__(self, nbits=8, fs=1, vref=1, seed=1, **kwargs):
58+
"""Initialization function for Generic ADC."""
59+
super().__init__(nbits, fs, vref, seed)
60+
61+
self._mismatch = None
62+
self._comp_noise = 0
63+
64+
# Get keyword arguments
65+
self._weights = kwargs.get("weights", None)
66+
67+
@property
68+
def weights(self):
69+
"""Returns capacitor unit weights."""
70+
if self._weights is None:
71+
self._weights = np.flip(2 ** np.linspace(0, self.nbits - 1, self.nbits))
72+
return np.array(self._weights)
73+
74+
@weights.setter
75+
def weights(self, values):
76+
"""Sets the capacitor unit weights."""
77+
self._weights = np.array(values)
78+
if self._weights.size < self.nbits:
79+
print(
80+
f"WARNING: Capacitor weight array size is {self._weights.size} for {self.nbits}-bit ADC."
81+
)
82+
self.mismatch = self.err["mismatch"]
83+
84+
@property
85+
def mismatch(self):
86+
"""Return noise stdev."""
87+
if self._mismatch is None:
88+
self._mismatch = np.zeros(self.weights.size)
89+
return self._mismatch
90+
91+
@mismatch.setter
92+
def mismatch(self, stdev):
93+
"""Sets mismatch stdev."""
94+
self.err["mismatch"] = stdev
95+
self._mismatch = np.random.normal(0, stdev, self.weights.size)
96+
self._mismatch /= np.sqrt(self.weights)
97+
98+
@property
99+
def comp_noise(self):
100+
"""Returns the noise of the comparator."""
101+
return self._comp_noise
102+
103+
@comp_noise.setter
104+
def comp_noise(self, value):
105+
"""Sets the noise of the comparator."""
106+
self._comp_noise = value
107+
108+
def run_step(self):
109+
"""Run a single ADC step."""
110+
vinx = self.vin
111+
112+
cweights = self.weights * (1 + self.mismatch)
113+
cdenom = sum(cweights) + 1
114+
115+
comp_noise = np.random.normal(0, self.comp_noise, cweights.size)
116+
117+
# Bit cycling
118+
vdac = vinx
119+
for n, _ in enumerate(cweights):
120+
vcomp = vdac - self.vref / 2 + comp_noise[n]
121+
compout = vcomp * 1e6
122+
compout = -1 if compout <= 0 else 1
123+
self.dbits[n] = max(0, compout)
124+
vdac -= compout * self.vref / 2 * cweights[n] / cdenom
125+
126+
# Re-scale the data
127+
scalar = 2**self.nbits / cdenom
128+
self.dval = min(2**self.nbits - 1, scalar * sum(self.weights * self.dbits))

adc_eval/signals.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,47 @@ 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(
132+
t, amp=amps[index], offset=offset, freq=nbin * fbin, ph0=phases[index]
133+
)
134+
135+
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

pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ disable=
99
duplicate-code,
1010
implicit-str-concat,
1111
too-many-arguments,
12+
too-many-positional-arguments,
1213
too-many-branches,
1314
too-many-instance-attributes,
1415
too-many-locals,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ ignore = [
7373
"PLR0912", # Too many branches ({branches} > {max_branches})
7474
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
7575
"PLR0915", # Too many statements ({statements} > {max_statements})
76+
"PLR0917", # Too many positional arguments
7677
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
7778
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
7879
"T201", # Allow print statements

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)