diff --git a/adi/__init__.py b/adi/__init__.py index a99b1c45c..2790aa52f 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -47,6 +47,8 @@ from adi.adrv9009_zu11eg_fmcomms8 import adrv9009_zu11eg_fmcomms8 +from adi.ad9081 import ad9081 + from adi.ad9094 import ad9094 from adi.ad9680 import ad9680 diff --git a/adi/ad9081.py b/adi/ad9081.py new file mode 100644 index 000000000..944112730 --- /dev/null +++ b/adi/ad9081.py @@ -0,0 +1,439 @@ +# Copyright (C) 2020 Analog Devices, Inc. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# - Neither the name of Analog Devices, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# - The use of this software may or may not infringe the patent rights +# of one or more patent holders. This license does not release you +# from the requirement that you obtain separate licenses from these +# patent holders to use this software. +# - Use of the software either in source or binary form, must be run +# on or directly connected to an Analog Devices Inc. component. +# +# THIS SOFTWARE IS PROVIDED BY ANALOG DEVICES "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. +# +# IN NO EVENT SHALL ANALOG DEVICES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, INTELLECTUAL PROPERTY +# RIGHTS, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import Dict, List + +from adi.context_manager import context_manager +from adi.rx_tx import rx_tx + + +def _map_to_dict(paths, ch): + fddc, cddc, adc = ch.attrs["label"].value.split("->") + if adc not in paths.keys(): + paths[adc] = {} + if cddc not in paths[adc].keys(): + paths[adc][cddc] = {} + if fddc not in paths[adc][cddc].keys(): + paths[adc][cddc][fddc] = {"channels": [ch._id]} + else: + paths[adc][cddc][fddc]["channels"].append(ch._id) + return paths + + +def _sortconv(chans_names, noq=False, dds=False): + tmpI = filter(lambda k: "_i" in k, chans_names) + tmpQ = filter(lambda k: "_q" in k, chans_names) + + def ignoreadc(w): + return int(w[len("voltage") : w.find("_")]) + + def ignorealt(w): + return int(w[len("altvoltage") :]) + + chans_names_out = [] + if dds: + filt = ignorealt + tmpI = chans_names + noq = True + else: + filt = ignoreadc + + tmpI = sorted(tmpI, key=filt) + tmpQ = sorted(tmpQ, key=filt) + for i in range(len(tmpI)): + chans_names_out.append(tmpI[i]) + if not noq: + chans_names_out.append(tmpQ[i]) + + return chans_names_out + + +class ad9081(rx_tx, context_manager): + """ AD9081 Mixed-Signal Front End (MxFE) """ + + _complex_data = True + _rx_channel_names: List[str] = [] + _tx_channel_names: List[str] = [] + _tx_control_channel_names: List[str] = [] + _rx_coarse_ddc_channel_names: List[str] = [] + _tx_coarse_duc_channel_names: List[str] = [] + _rx_fine_ddc_channel_names: List[str] = [] + _tx_fine_duc_channel_names: List[str] = [] + _dds_channel_names: List[str] = [] + _device_name = "" + + _rx_attr_only_channel_names: List[str] = [] + _tx_attr_only_channel_names: List[str] = [] + + _path_map: Dict[str, Dict[str, Dict[str, List[str]]]] = {} + + def __init__(self, uri=""): + + context_manager.__init__(self, uri, self._device_name) + # Default device for attribute writes + self._ctrl = self._ctx.find_device("axi-ad9081-rx-hpc") + # Devices with buffers + self._rxadc = self._ctx.find_device("axi-ad9081-rx-hpc") + self._txdac = self._ctx.find_device("axi-ad9081-tx-hpc") + + # Get DDC and DUC mappings + paths = {} + + for ch in self._rxadc.channels: + if "label" in ch.attrs: + paths = _map_to_dict(paths, ch) + self._path_map = paths + + # Get data + DDS channels + for ch in self._rxadc.channels: + if ch.scan_element and not ch.output: + self._rx_channel_names.append(ch._id) + for ch in self._txdac.channels: + if ch.scan_element: + self._tx_channel_names.append(ch._id) + else: + self._dds_channel_names.append(ch._id) + + # Sort channel names + self._rx_channel_names = _sortconv(self._rx_channel_names) + self._tx_channel_names = _sortconv(self._tx_channel_names) + self._dds_channel_names = _sortconv(self._dds_channel_names, dds=True) + + # Map unique attributes to channel properties + self._rx_fine_ddc_channel_names = [] + self._rx_coarse_ddc_channel_names = [] + self._tx_fine_duc_channel_names = [] + self._tx_coarse_duc_channel_names = [] + for converter in paths: + for cdc in paths[converter]: + channels = [] + for fdc in paths[converter][cdc]: + channels += paths[converter][cdc][fdc]["channels"] + channels = [name for name in channels if "_i" in name] + if "ADC" in converter: + self._rx_coarse_ddc_channel_names.append(channels[0]) + self._rx_fine_ddc_channel_names += channels + else: + self._tx_coarse_duc_channel_names.append(channels[0]) + self._tx_fine_duc_channel_names += channels + + rx_tx.__init__(self) + self.rx_buffer_size = 2 ** 16 + + @property + def path_map(self): + """ path_map: Map of channelizers both coarse and fine to + individual driver channel names + """ + return self._path_map + + @property + def rx_channel_nco_frequencies(self): + """rx_channel_nco_frequencies: Receive path fine DDC NCO frequencies + """ + return self._get_iio_attr_vec( + self._rx_fine_ddc_channel_names, "channel_nco_frequency", False + ) + + @rx_channel_nco_frequencies.setter + def rx_channel_nco_frequencies(self, value): + self._set_iio_attr_int_vec( + self._rx_fine_ddc_channel_names, "channel_nco_frequency", False, value + ) + + @property + def rx_channel_nco_phases(self): + """rx_channel_nco_phases: Receive path fine DDC NCO phases + """ + return self._get_iio_attr_vec( + self._rx_fine_ddc_channel_names, "channel_nco_phase", False + ) + + @rx_channel_nco_phases.setter + def rx_channel_nco_phases(self, value): + self._set_iio_attr_int_vec( + self._rx_fine_ddc_channel_names, "channel_nco_phase", False, value, + ) + + @property + def rx_main_nco_frequencies(self): + """rx_main_nco_frequencies: Receive path coarse DDC NCO frequencies + """ + return self._get_iio_attr_vec( + self._rx_coarse_ddc_channel_names, "main_nco_frequency", False + ) + + @rx_main_nco_frequencies.setter + def rx_main_nco_frequencies(self, value): + self._set_iio_attr_int_vec( + self._rx_coarse_ddc_channel_names, "main_nco_frequency", False, value, + ) + + @property + def rx_main_nco_phases(self): + """rx_main_nco_phases: Receive path coarse DDC NCO phases + """ + return self._get_iio_attr_vec( + self._rx_coarse_ddc_channel_names, "main_nco_phase", False + ) + + @rx_main_nco_phases.setter + def rx_main_nco_phases(self, value): + self._set_iio_attr_int_vec( + self._rx_coarse_ddc_channel_names, "main_nco_phase", False, value, + ) + + @property + def rx_test_mode(self): + """rx_test_mode: NCO Test Mode """ + return self._get_iio_attr_str("voltage0_i", "test_mode", False) + + @rx_test_mode.setter + def rx_test_mode(self, value): + self._set_iio_attr( + "voltage0_i", "test_mode", False, value, + ) + + @property + def rx_nyquist_zone(self): + """rx_nyquist_zone: ADC nyquist zone. Options are: odd, even """ + return self._get_iio_attr_str("voltage0_i", "nyquist_zone", False) + + @rx_nyquist_zone.setter + def rx_nyquist_zone(self, value): + self._set_iio_attr( + "voltage0_i", "nyquist_zone", False, value, + ) + + @property + def tx_channel_nco_frequencies(self): + """tx_channel_nco_frequencies: Transmit path fine DUC NCO frequencies + """ + return self._get_iio_attr_vec( + self._tx_fine_duc_channel_names, "channel_nco_frequency", True + ) + + @tx_channel_nco_frequencies.setter + def tx_channel_nco_frequencies(self, value): + self._set_iio_attr_int_vec( + self._tx_fine_duc_channel_names, "channel_nco_frequency", True, value + ) + + @property + def tx_channel_nco_phases(self): + """tx_channel_nco_phases: Transmit path fine DUC NCO phases + """ + return self._get_iio_attr_vec( + self._tx_fine_duc_channel_names, "channel_nco_phase", True + ) + + @tx_channel_nco_phases.setter + def tx_channel_nco_phases(self, value): + self._set_iio_attr_int_vec( + self._tx_fine_duc_channel_names, "channel_nco_phase", True, value, + ) + + @property + def tx_channel_nco_test_tone_en(self): + """tx_channel_nco_test_tone_en: Transmit path fine DUC NCO test tone enable + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "channel_nco_test_tone_en", True + ) + + @tx_channel_nco_test_tone_en.setter + def tx_channel_nco_test_tone_en(self, value): + self._set_iio_attr_int_vec( + self._tx_coarse_duc_channel_names, "channel_nco_test_tone_en", True, value, + ) + + @property + def tx_channel_nco_test_tone_scales(self): + """tx_channel_nco_test_tone_scales: Transmit path fine DUC NCO test tone scale + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "channel_nco_test_tone_scale", True + ) + + @tx_channel_nco_test_tone_scales.setter + def tx_channel_nco_test_tone_scales(self, value): + self._set_iio_attr_float_vec( + self._tx_coarse_duc_channel_names, + "channel_nco_test_tone_scale", + True, + value, + ) + + @property + def tx_channel_nco_gain_scales(self): + """tx_channel_nco_gain_scales Transmit path fine DUC NCO gain scale + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "channel_nco_gain_scale", True + ) + + @tx_channel_nco_gain_scales.setter + def tx_channel_nco_gain_scales(self, value): + self._set_iio_attr_float_vec( + self._tx_coarse_duc_channel_names, "channel_nco_gain_scale", True, value, + ) + + @property + def tx_main_nco_frequencies(self): + """tx_main_nco_frequencies: Transmit path coarse DUC NCO frequencies + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "main_nco_frequency", True + ) + + @tx_main_nco_frequencies.setter + def tx_main_nco_frequencies(self, value): + self._set_iio_attr_int_vec( + self._tx_coarse_duc_channel_names, "main_nco_frequency", True, value, + ) + + @property + def tx_main_nco_phases(self): + """tx_main_nco_phases: Transmit path coarse DUC NCO phases + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "main_nco_phase", True + ) + + @tx_main_nco_phases.setter + def tx_main_nco_phases(self, value): + self._set_iio_attr_int_vec( + self._tx_coarse_duc_channel_names, "main_nco_phase", True, value, + ) + + @property + def tx_main_nco_test_tone_en(self): + """tx_main_nco_test_tone_en: Transmit path coarse DUC NCO test tone enable + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "main_nco_test_tone_en", True + ) + + @tx_main_nco_test_tone_en.setter + def tx_main_nco_test_tone_en(self, value): + self._set_iio_attr_int_vec( + self._tx_coarse_duc_channel_names, "main_nco_test_tone_en", True, value, + ) + + @property + def tx_main_nco_test_tone_scales(self): + """tx_main_nco_test_tone_scales: Transmit path coarse DUC NCO test tone scale + """ + return self._get_iio_attr_vec( + self._tx_coarse_duc_channel_names, "main_nco_test_tone_scale", True + ) + + @tx_main_nco_test_tone_scales.setter + def tx_main_nco_test_tone_scales(self, value): + self._set_iio_attr_float_vec( + self._tx_coarse_duc_channel_names, "main_nco_test_tone_scale", True, value, + ) + + @property + def tx_main_ffh_frequency(self): + """tx_main_ffh_frequency: Transmitter fast frequency hop frequency. This will set + The NCO frequency of the NCO selected from the bank defined by tx_main_ffh_index + """ + return self._get_iio_attr("voltage0_i", "main_ffh_frequency", True) + + @tx_main_ffh_frequency.setter + def tx_main_ffh_frequency(self, value): + if self.tx_main_ffh_index == 0: + raise Exception( + "To set a FFH NCO bank frequency, tx_main_ffh_index must > 0" + ) + self._set_iio_attr( + "voltage0_i", "main_ffh_frequency", True, value, + ) + + @property + def tx_main_ffh_index(self): + """tx_main_ffh_index: Transmitter fast frequency hop NCO bank index + """ + return self._get_iio_attr("voltage0_i", "main_ffh_index", True) + + @tx_main_ffh_index.setter + def tx_main_ffh_index(self, value): + self._set_iio_attr( + "voltage0_i", "main_ffh_index", True, value, + ) + + @property + def tx_main_ffh_mode(self): + """tx_main_ffh_mode: Set hop transition mode of NCOs Options are: + phase_continuous, phase_incontinuous, and phase_coherent + """ + return self._get_iio_attr_str("voltage0_i", "main_ffh_mode", True) + + @tx_main_ffh_mode.setter + def tx_main_ffh_mode(self, value): + self._set_iio_attr( + "voltage0_i", "main_ffh_mode", True, value, + ) + + @property + def loopback_mode(self): + """loopback_mode: Enable loopback mode RX->TX + """ + return self._get_iio_dev_attr("loopback_mode") + + @loopback_mode.setter + def loopback_mode(self, value): + self._set_iio_dev_attr( + "loopback_mode", value, + ) + + @property + def rx_sample_rate(self): + """rx_sampling_frequency: Sample rate after decimation""" + return self._get_iio_attr("voltage0_i", "sampling_frequency", False) + + @property + def adc_frequency(self): + """adc_frequency: ADC frequency in Hz""" + return self._get_iio_attr("voltage0_i", "adc_frequency", False) + + @property + def tx_sample_rate(self): + """tx_sampling_frequency: Sample rate before interpolation""" + return self._get_iio_attr("voltage0_i", "sampling_frequency", True) + + @property + def dac_frequency(self): + """dac_frequency: DAC frequency in Hz""" + return self._get_iio_attr("voltage0_i", "dac_frequency", True) diff --git a/adi/attribute.py b/adi/attribute.py index 205d3fe7f..d59e21ac8 100644 --- a/adi/attribute.py +++ b/adi/attribute.py @@ -93,12 +93,30 @@ def _set_iio_attr_float(self, channel_name, attr_name, output, value, _ctrl=None raise Exception("Value must be a float") self._set_iio_attr(channel_name, attr_name, output, value, _ctrl) + def _set_iio_attr_float_vec( + self, channel_names, attr_name, output, values, _ctrl=None + ): + """ Set channel attribute with list of floats """ + if not isinstance(values, list): + raise Exception("Value must be a list") + for i, v in enumerate(values): + self._set_iio_attr_float(channel_names[i], attr_name, output, v, _ctrl) + def _set_iio_attr_int(self, channel_name, attr_name, output, value, _ctrl=None): """ Set channel attribute with int """ if not isinstance(value, int): raise Exception("Value must be an int") self._set_iio_attr(channel_name, attr_name, output, value, _ctrl) + def _set_iio_attr_int_vec( + self, channel_names, attr_name, output, values, _ctrl=None + ): + """ Set channel attribute with list of ints """ + if not isinstance(values, list): + raise Exception("Value must be a list") + for i, v in enumerate(values): + self._set_iio_attr_int(channel_names[i], attr_name, output, v, _ctrl) + def _get_iio_attr_str(self, channel_name, attr_name, output, _ctrl=None): """ Get channel attribute as string """ if _ctrl: @@ -115,6 +133,14 @@ def _get_iio_attr(self, channel_name, attr_name, output, _ctrl=None): self._get_iio_attr_str(channel_name, attr_name, output, _ctrl) ) + def _get_iio_attr_vec(self, channel_names, attr_name, output, _ctrl=None): + """ Get channel attributes as list of numbers """ + vals = [] + for chn in channel_names: + v = self._get_iio_attr(chn, attr_name, output, _ctrl) + vals.append(v) + return vals + def _set_iio_dev_attr_str(self, attr_name, value, _ctrl=None): """ Set device attribute with string """ try: @@ -132,6 +158,14 @@ def _get_iio_dev_attr_str(self, attr_name, _ctrl=None): else: return self._ctrl.attrs[attr_name].value + def _set_iio_dev_attr(self, attr_name, value, _ctrl=None): + """ Set device attribute """ + _dev = _ctrl or self._ctrl + try: + _dev.attrs[attr_name].value = str(value) + except Exception as ex: + raise ex + def _get_iio_dev_attr(self, attr_name, _ctrl=None): """ Set device attribute as number """ return get_numbers(self._get_iio_dev_attr_str(attr_name, _ctrl)) diff --git a/doc/source/devices/adi.ad9081.rst b/doc/source/devices/adi.ad9081.rst new file mode 100644 index 000000000..997a03d77 --- /dev/null +++ b/doc/source/devices/adi.ad9081.rst @@ -0,0 +1,7 @@ +ad9081 +================= + +.. automodule:: adi.ad9081 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/adi.adrv9009_zu11eg_multi.rst b/doc/source/devices/adi.adrv9009_zu11eg_multi.rst new file mode 100644 index 000000000..0fd6c6da9 --- /dev/null +++ b/doc/source/devices/adi.adrv9009_zu11eg_multi.rst @@ -0,0 +1,7 @@ +adrv9009\_zu11eg\_multi +================================== + +.. automodule:: adi.adrv9009_zu11eg_multi + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/adi.cn0532.rst b/doc/source/devices/adi.cn0532.rst new file mode 100644 index 000000000..40a3a8186 --- /dev/null +++ b/doc/source/devices/adi.cn0532.rst @@ -0,0 +1,7 @@ +cn0532 +================= + +.. automodule:: adi.cn0532 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/adi.cn0540.rst b/doc/source/devices/adi.cn0540.rst new file mode 100644 index 000000000..bbc6a131f --- /dev/null +++ b/doc/source/devices/adi.cn0540.rst @@ -0,0 +1,7 @@ +cn0540 +================= + +.. automodule:: adi.cn0540 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/adi.jesd.rst b/doc/source/devices/adi.jesd.rst new file mode 100644 index 000000000..9436c6311 --- /dev/null +++ b/doc/source/devices/adi.jesd.rst @@ -0,0 +1,7 @@ +jesd +=============== + +.. automodule:: adi.jesd + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 88a173ad2..2bc4cf6bc 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -10,6 +10,7 @@ Supported Devices adi.ad5627 adi.ad5686 adi.ad7124 + adi.ad9081 adi.ad9094 adi.ad9144 adi.ad9152 @@ -23,9 +24,13 @@ Supported Devices adi.adrv9009 adi.adrv9009_zu11eg adi.adrv9009_zu11eg_fmcomms8 + adi.adrv9009_zu11eg_multi adi.adxl345 + adi.cn0532 + adi.cn0540 adi.daq2 adi.daq3 adi.fmclidar1 adi.fmcomms5 + adi.jesd adi.ltc2983 diff --git a/examples/ad9081_example.py b/examples/ad9081_example.py new file mode 100644 index 000000000..1c14831fe --- /dev/null +++ b/examples/ad9081_example.py @@ -0,0 +1,78 @@ +# Copyright (C) 2020 Analog Devices, Inc. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# - Neither the name of Analog Devices, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# - The use of this software may or may not infringe the patent rights +# of one or more patent holders. This license does not release you +# from the requirement that you obtain separate licenses from these +# patent holders to use this software. +# - Use of the software either in source or binary form, must be run +# on or directly connected to an Analog Devices Inc. component. +# +# THIS SOFTWARE IS PROVIDED BY ANALOG DEVICES "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. +# +# IN NO EVENT SHALL ANALOG DEVICES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, INTELLECTUAL PROPERTY +# RIGHTS, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import time + +import adi +import matplotlib.pyplot as plt +from scipy import signal + +dev = adi.ad9081("ip:analog.local") + +# Configure properties +print("--Setting up chip") + +# Set NCOs +dev.rx_channel_nco_frequencies = [0] * 4 +dev.tx_channel_nco_frequencies = [0] * 4 + +dev.rx_main_nco_frequencies = [1000000000] * 4 +dev.tx_main_nco_frequencies = [1000000000] * 4 + +dev.rx_enabled_channels = [0] +dev.tx_enabled_channels = [0] +dev.rx_nyquist_zone = "odd" + +dev.rx_buffer_size = 2 ** 16 +dev.tx_cyclic_buffer = True + +fs = int(dev.tx_sample_rate) + +# Set single DDS tone for TX on one transmitter +dev.dds_single_tone(fs / 10, 0.5, channel=0) + +# Collect data +for r in range(20): + x = dev.rx() + + f, Pxx_den = signal.periodogram(x, fs, return_onesided=False) + plt.clf() + plt.semilogy(f, Pxx_den) + plt.ylim([1e-7, 1e5]) + plt.xlabel("frequency [Hz]") + plt.ylabel("PSD [V**2/Hz]") + plt.draw() + plt.pause(0.05) + time.sleep(0.1) + +plt.show() diff --git a/examples/pluto.py b/examples/pluto.py index 8191e443f..14f5264c7 100644 --- a/examples/pluto.py +++ b/examples/pluto.py @@ -54,8 +54,8 @@ # Create a sinewave waveform fs = int(sdr.sample_rate) -fc = 3000000 N = 1024 +fc = int(3000000 / (fs / N)) * (fs / N) ts = 1 / float(fs) t = np.arange(0, N * ts, ts) i = np.cos(2 * np.pi * t * fc) * 2 ** 14 diff --git a/test/common.py b/test/common.py index fad411af1..1d1ed6ff2 100644 --- a/test/common.py +++ b/test/common.py @@ -43,16 +43,25 @@ def dev_interface(uri, classname, val, attr, tol): # Check hardware if not hasattr(sdr, attr): raise AttributeError(attr + " not defined in " + classname) + + rval = getattr(sdr, attr) + is_list = isinstance(rval, list) + if is_list: + l = len(rval) + val = [val] * l + setattr(sdr, attr, val) rval = getattr(sdr, attr) - if not isinstance(rval, str): + + if not isinstance(rval, str) and not is_list: rval = float(rval) del sdr if not isinstance(val, str): - if abs(val - rval) > tol: + abs_val = np.argmax(abs(np.array(val) - np.array(rval))) + if abs_val > tol: print("Failed to set: " + attr) print("Set: " + str(val)) print("Got: " + str(rval)) - return abs(val - rval) + return abs_val else: return val == str(rval) diff --git a/test/conftest.py b/test/conftest.py index d3380398d..bd2db2551 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -101,6 +101,11 @@ def test_cw_loopback(request): yield cw_loopback +@pytest.fixture() +def test_tone_loopback(request): + yield nco_loopback + + @pytest.fixture() def test_gain_check(request): yield gain_check diff --git a/test/dma_tests.py b/test/dma_tests.py index c875fc236..0543da9c6 100644 --- a/test/dma_tests.py +++ b/test/dma_tests.py @@ -209,6 +209,66 @@ def dds_loopback(uri, classname, param_set, channel, frequency, scale, peak_min) assert tone_peaks[indx] > peak_min +def nco_loopback(uri, classname, param_set, channel, frequency, peak_min): + """ nco_loopback: TX/DAC Test tone loopback with connected loopback cables. + This test requires a devices with TX and RX onboard where the transmit + signal can be recovered. TX/DAC internal NCOs are used to generate a sinusoid + which is then estimated on the RX side. The receive tone must be within + 1% of its expected frequency with a specified peak + + parameters: + uri: type=string + URI of IIO context of target board/system + classname: type=string + Name of pyadi interface class which contain attribute + param_set: type=dict + Dictionary of attribute and values to be set before tone is + generated and received + channel: type=list + List of integers or list of list of integers of channels to + enable through tx_enabled_channels + frequency: type=integer + Frequency in Hz of transmitted tone + peak_min: type=float + Minimum acceptable value of maximum peak in dBFS of received tone + + """ + # See if we can tone using DMAs + sdr = eval(classname + "(uri='" + uri + "')") + # Set custom device parameters + for p in param_set.keys(): + setattr(sdr, p, param_set[p]) + + N = 2 ** 14 + sdr.rx_enabled_channels = [channel] + sdr.rx_buffer_size = N * 2 * len(sdr.rx_enabled_channels) + # Create a sinewave waveform + if hasattr(sdr, "sample_rate"): + RXFS = int(sdr.sample_rate) + elif hasattr(sdr, "rx_sample_rate"): + RXFS = int(sdr.rx_sample_rate) + else: + """ no sample_rate nor rx_sample_rate. Let's try something like + rx($channel)_sample_rate""" + attr = "rx" + str(channel) + "_sample_rate" + RXFS = int(getattr(sdr, attr)) + + # Pass through SDR + try: + for _ in range(10): # Wait + data = sdr.rx() + except Exception as e: + del sdr + raise Exception(e) + del sdr + tone_peaks, tone_freqs = spec.spec_est(data, fs=RXFS, ref=2 ** 15) + indx = np.argmax(tone_peaks) + diff = np.abs(tone_freqs[indx] - frequency) + print("Peak: " + str(tone_peaks[indx]) + "@" + str(tone_freqs[indx])) + assert (frequency * 0.01) > diff + assert tone_peaks[indx] > peak_min + + def cw_loopback(uri, classname, channel, param_set): """ cw_loopback: Test CW loopback with connected loopback cables. This test requires a devices with TX and RX onboard where the transmit @@ -239,7 +299,10 @@ def cw_loopback(uri, classname, channel, param_set): if isinstance(param_set[p], str): assert getattr(sdr, p) == param_set[p] else: - assert np.abs(getattr(sdr, p) - param_set[p]) < 4 + assert ( + np.argmax(np.abs(np.array(getattr(sdr, p)) - np.array(param_set[p]))) + < 4 + ) # Set common buffer settings sdr.tx_cyclic_buffer = True N = 2 ** 14 @@ -260,6 +323,8 @@ def cw_loopback(uri, classname, channel, param_set): A = 2 ** 15 fc = RXFS * 0.1 + fc = int(fc / (RXFS / N)) * (RXFS / N) + ts = 1 / float(RXFS) t = np.arange(0, N * ts, ts) if sdr._complex_data: @@ -291,7 +356,7 @@ def cw_loopback(uri, classname, channel, param_set): # self.assertGreater(fc * 0.01, diff, "Frequency offset") -def t_sfdr(uri, classname, channel, param_set, sfdr_min): +def t_sfdr(uri, classname, channel, param_set, sfdr_min, full_scale=0.9): """ t_sfdr: Test SFDR loopback of tone with connected loopback cables. This test requires a devices with TX and RX onboard where the transmit signal can be recovered. Sinuoidal data is passed to DMAs which is then @@ -331,12 +396,14 @@ def t_sfdr(uri, classname, channel, param_set, sfdr_min): RXFS = int(sdr.sample_rate) else: RXFS = int(sdr.rx_sample_rate) + fc = RXFS * 0.1 + fc = int(fc / (RXFS / N)) * (RXFS / N) ts = 1 / float(RXFS) t = np.arange(0, N * ts, ts) - i = np.cos(2 * np.pi * t * fc) * 2 ** 15 * 0.9 - q = np.sin(2 * np.pi * t * fc) * 2 ** 15 * 0.9 + i = np.cos(2 * np.pi * t * fc) * 2 ** 15 * full_scale + q = np.sin(2 * np.pi * t * fc) * 2 ** 15 * full_scale iq = i + 1j * q # Pass through SDR try: @@ -437,9 +504,15 @@ def cyclic_buffer(uri, classname, channel, param_set): # Set custom device parameters for p in param_set.keys(): setattr(sdr, p, param_set[p]) - fs = int(sdr.sample_rate) - fc = -3000000 + + if hasattr(sdr, "sample_rate"): + fs = int(sdr.sample_rate) + else: + fs = int(sdr.rx_sample_rate) + N = 1024 + fc = -3000000 + fc = int(fc / (fs / N)) * (fs / N) ts = 1 / float(fs) t = np.arange(0, N * ts, ts) i = np.cos(2 * np.pi * t * fc) * 2 ** 14 @@ -489,9 +562,15 @@ def cyclic_buffer_exception(uri, classname, channel, param_set): # Set custom device parameters for p in param_set.keys(): setattr(sdr, p, param_set[p]) - fs = int(sdr.sample_rate) - fc = -3000000 + + if hasattr(sdr, "sample_rate"): + fs = int(sdr.sample_rate) + else: + fs = int(sdr.rx_sample_rate) + N = 1024 + fc = -3000000 + fc = int(fc / (fs / N)) * (fs / N) ts = 1 / float(fs) t = np.arange(0, N * ts, ts) i = np.cos(2 * np.pi * t * fc) * 2 ** 14 diff --git a/test/test_ad9081.py b/test/test_ad9081.py new file mode 100644 index 000000000..0a31ef9b7 --- /dev/null +++ b/test/test_ad9081.py @@ -0,0 +1,311 @@ +from os import listdir +from os.path import dirname, join, realpath + +import pytest + +hardware = "ad9081" +classname = "adi.ad9081" + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize( + "attr, val", + [ + ("rx_nyquist_zone", ["even", "odd"]), + ("loopback_mode", [2, 1, 0]), + ( + "rx_test_mode", + [ + "midscale_short", + "pos_fullscale", + "neg_fullscale", + "checkerboard", + "pn23", + "pn9", + "one_zero_toggle", + "user", + "pn7", + "pn15", + "pn31", + "ramp", + "off", + ], + ), + ( + "tx_main_ffh_mode", + ["phase_continuous", "phase_incontinuous", "phase_coherent"], + ), + ], +) +def test_ad9081_str_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): + test_attribute_multipe_values(iio_uri, classname, attr, val, 0) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize( + "attr, start, stop, step, tol, repeats", + [ + ("rx_main_nco_frequencies", -2000000000, 2000000000, 1, 1, 10), + ("tx_main_nco_frequencies", -6000000000, 6000000000, 1, 1, 10), + ("rx_channel_nco_frequencies", -500000000, 500000000, 1, 1, 10), + ("tx_channel_nco_frequencies", -750000000, 750000000, 1, 1, 10), + ("rx_main_nco_phases", -180000, 180000, 1, 1, 10), + ("tx_main_nco_phases", -180000, 180000, 1, 1, 10), + ("rx_channel_nco_phases", -180000, 180000, 1, 1, 10), + ("tx_channel_nco_phases", -180000, 180000, 1, 1, 10), + ("tx_main_nco_test_tone_scales", 0.0, 1.0, 0.01, 0.01, 10), + ("tx_channel_nco_test_tone_scales", 0.0, 1.0, 0.01, 0.01, 10), + ("tx_main_ffh_index", 1, 31, 1, 0, 10), + ("tx_main_ffh_frequency", -6000000000, 6000000000, 1, 1, 10), + ("tx_channel_nco_gain_scales", 0.0, 0.5, 0.01, 0.01, 10), + ], +) +def test_ad9081_attr( + test_attribute_single_value, + iio_uri, + classname, + attr, + start, + stop, + step, + tol, + repeats, +): + test_attribute_single_value( + iio_uri, classname, attr, start, stop, step, tol, repeats + ) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_ad9081_rx_data(test_dma_rx, iio_uri, classname, channel): + test_dma_rx(iio_uri, classname, channel) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_ad9081_tx_data(test_dma_tx, iio_uri, classname, channel): + test_dma_tx(iio_uri, classname, channel) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_nyquist_zone="odd", + tx_channel_nco_gain_scales=[0.5, 0.5, 0.5, 0.5], + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_test_tone_en=[0, 0, 0, 0], + tx_main_nco_test_tone_en=[0, 0, 0, 0], + ) + ], +) +def test_ad9081_cyclic_buffers( + test_cyclic_buffer, iio_uri, classname, channel, param_set +): + test_cyclic_buffer(iio_uri, classname, channel, param_set) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_test_tone_en=[0, 0, 0, 0], + tx_main_nco_test_tone_en=[0, 0, 0, 0], + ) + ], +) +def test_ad9081_cyclic_buffers_exception( + test_cyclic_buffer_exception, iio_uri, classname, channel, param_set +): + test_cyclic_buffer_exception(iio_uri, classname, channel, param_set) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +def test_ad9081_loopback(test_dma_loopback, iio_uri, classname, channel): + test_dma_loopback(iio_uri, classname, channel) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_test_tone_en=[0, 0, 0, 0], + tx_main_nco_test_tone_en=[0, 0, 0, 0], + ) + ], +) +@pytest.mark.parametrize("sfdr_min", [70]) +def test_ad9081_sfdr(test_sfdr, iio_uri, classname, channel, param_set, sfdr_min): + test_sfdr(iio_uri, classname, channel, param_set, sfdr_min, full_scale=0.5) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize("frequency, scale", [(10000000, 0.5)]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + ) + ], +) +@pytest.mark.parametrize("peak_min", [-30]) +def test_ad9081_dds_loopback( + test_dds_loopback, + iio_uri, + classname, + param_set, + channel, + frequency, + scale, + peak_min, +): + test_dds_loopback( + iio_uri, classname, param_set, channel, frequency, scale, peak_min + ) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_main_nco_frequencies=[500000000, 500000000, 500000000, 500000000], + tx_main_nco_frequencies=[500000000, 500000000, 500000000, 500000000], + rx_channel_nco_frequencies=[1234567, 1234567, 1234567, 1234567], + tx_channel_nco_frequencies=[1234567, 1234567, 1234567, 1234567], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + ), + dict( + loopback_mode=0, + rx_main_nco_frequencies=[750000000, 750000000, 750000000, 750000000], + tx_main_nco_frequencies=[750000000, 750000000, 750000000, 750000000], + rx_channel_nco_frequencies=[-1234567, -1234567, -1234567, -1234567], + tx_channel_nco_frequencies=[-1234567, -1234567, -1234567, -1234567], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + ), + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + rx_main_nco_phases=[0, 0, 0, 0], + tx_main_nco_phases=[0, 0, 0, 0], + rx_channel_nco_phases=[0, 0, 0, 0], + tx_channel_nco_phases=[0, 0, 0, 0], + ), + ], +) +def test_ad9081_iq_loopback(test_iq_loopback, iio_uri, classname, channel, param_set): + test_iq_loopback(iio_uri, classname, channel, param_set) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize("frequency", [10000000]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1010000000, 1010000000, 1010000000, 1010000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[0, 0, 0, 0], + tx_main_nco_test_tone_scales=[0.5, 0.5, 0.5, 0.5], + tx_main_nco_test_tone_en=[1, 1, 1, 1], + tx_channel_nco_test_tone_en=[0, 0, 0, 0], + ), + dict( + loopback_mode=0, + rx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + tx_main_nco_frequencies=[1000000000, 1000000000, 1000000000, 1000000000], + rx_channel_nco_frequencies=[0, 0, 0, 0], + tx_channel_nco_frequencies=[10000000, 10000000, 10000000, 10000000], + tx_channel_nco_test_tone_scales=[0.5, 0.5, 0.5, 0.5], + tx_main_nco_test_tone_en=[0, 0, 0, 0], + tx_channel_nco_test_tone_en=[1, 1, 1, 1], + ), + ], +) +@pytest.mark.parametrize("peak_min", [-30]) +def test_ad9081_nco_loopback( + test_tone_loopback, iio_uri, classname, param_set, channel, frequency, peak_min, +): + test_tone_loopback(iio_uri, classname, param_set, channel, frequency, peak_min) + + +#########################################