From 2e96fd3a1932e092307cdb64deb9febf925b47b4 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 22 Jun 2023 16:35:50 -0600 Subject: [PATCH 01/23] Fix adiplot to use updated pyqtgraph Signed-off-by: Travis F. Collins --- examples/adiplot.py | 166 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 129 insertions(+), 37 deletions(-) diff --git a/examples/adiplot.py b/examples/adiplot.py index 84fe8710b..5c62f843e 100644 --- a/examples/adiplot.py +++ b/examples/adiplot.py @@ -6,18 +6,25 @@ import adi import numpy as np -import pyqtgraph as pg -from PyQt5 import QtGui -from pyqtgraph.Qt import QtCore, QtGui + from scipy import signal from scipy.fftpack import fft +from PyQt5 import QtWidgets, QtCore +from pyqtgraph import PlotWidget, plot, GraphicsLayoutWidget +import pyqtgraph as pg +import sys # We need sys so that we can pass argv to QApplication +import os +from random import randint + +pg.setConfigOptions(antialias=True) +pg.setConfigOption("background", "k") + REAL_DEV_NAME = "cn05".lower() class ADIPlotter(object): def __init__(self, classname, uri): - self.classname = classname self.q = Queue(maxsize=20) self.stream = eval("adi." + classname + "(uri='" + uri + "')") @@ -26,23 +33,37 @@ def __init__(self, classname, uri): self.stream.rx_lo = 1000000000 self.stream.tx_lo = 1000000000 self.stream.dds_single_tone(3000000, 0.9) - self.stream.rx_buffer_size = 2 ** 12 + self.stream.rx_buffer_size = 2**12 self.stream.rx_enabled_channels = [0] - pg.setConfigOptions(antialias=True) + self.app = QtWidgets.QApplication(sys.argv) + + self.qmw = QtWidgets.QMainWindow() + + self.qmw.central_widget = QtWidgets.QWidget() + self.qmw.vertical_layout = QtWidgets.QVBoxLayout() + self.qmw.setCentralWidget(self.qmw.central_widget) + self.qmw.central_widget.setLayout(self.qmw.vertical_layout) + + #### Add Plot + self.qmw.graphWidget = pg.PlotWidget() + self.qmw.graphWidget.setBackground("black") + self.traces = {} - self.app = QtGui.QApplication(sys.argv) - self.win = pg.GraphicsWindow(title="Spectrum Analyzer") + + self.win = GraphicsLayoutWidget() + self.qmw.vertical_layout.addWidget(self.win, 1) self.win.setWindowTitle("Spectrum Analyzer") self.win.setGeometry(5, 115, 1910, 1070) + self.win.setBackground(background="black") wf_xaxis = pg.AxisItem(orientation="bottom") wf_xaxis.setLabel(units="Seconds") if REAL_DEV_NAME in classname.lower(): - wf_ylabels = [(0, "0"), (2 ** 11, "2047")] + wf_ylabels = [(0, "0"), (2**11, "2047")] else: - wf_ylabels = [(-2 * 11, "-2047"), (0, "0"), (2 ** 11, "2047")] + wf_ylabels = [(-2 * 11, "-2047"), (0, "0"), (2**11, "2047")] wf_yaxis = pg.AxisItem(orientation="left") wf_yaxis.setTicks([wf_ylabels]) @@ -50,10 +71,16 @@ def __init__(self, classname, uri): sp_xaxis.setLabel(units="Hz") self.waveform = self.win.addPlot( - title="WAVEFORM", row=1, col=1, axisItems={"bottom": wf_xaxis}, + title="WAVEFORM", + row=1, + col=1, + axisItems={"bottom": wf_xaxis}, ) self.spectrum = self.win.addPlot( - title="SPECTRUM", row=2, col=1, axisItems={"bottom": sp_xaxis}, + title="SPECTRUM", + row=2, + col=1, + axisItems={"bottom": sp_xaxis}, ) self.waveform.showGrid(x=True, y=True) self.spectrum.showGrid(x=True, y=True) @@ -70,58 +97,124 @@ def __init__(self, classname, uri): self.min = -100 self.window = signal.kaiser(self.stream.rx_buffer_size, beta=38) + #### Add a plot to contain our measurement data + # This is faster than using a table + self.measurements = self.win.addPlot(title="Measurements", row=3, col=1) + self.measurements.hideAxis("left") + self.measurements.hideAxis("bottom") + + self.qmw.show() + self.run_source = True self.thread = threading.Thread(target=self.source) self.thread.start() + self.markers_added = False + def source(self): print("Thread running") + self.counter = 0 while self.run_source: data = self.stream.rx() + self.counter += 1 try: self.q.put(data, block=False, timeout=4) except Full: continue def start(self): - if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"): - QtGui.QApplication.instance().exec_() + self.app.exec_() + + def add_markers(self, plot): + #### Add peak marker for spectrum plot + data = plot.getData() + if data[0] is None: + return + self.curve_point = pg.CurvePoint(plot) + self.spectrum.addItem(self.curve_point) + self.text_peak = pg.TextItem("TEST", anchor=(0.5, 1.0)) + self.text_peak.setParentItem(parent=self.curve_point) + + self.build_custom_table_from_textitems() + + self.markers_added = True + + def build_custom_table_from_textitems(self): + text_items = ["Peak", "Frequency", "Amplitude"] + self.custom_table = {} + self.table_x = 180 + self.table_y = 50 + scaler = 30 + for i, text in enumerate(text_items): + self.custom_table[text] = pg.TextItem(text=text) + # set parent plot + self.custom_table[text].setParentItem(parent=self.measurements) + # set position + self.custom_table[text].setPos(self.table_x, self.table_y + scaler * i) + + def update_custom_table(self): + if not self.markers_added: + return + self.custom_table["Frequency"].setText( + "Frequency: {:.2f} Hz".format(self.curve_point.pos().x()) + ) + self.custom_table["Amplitude"].setText( + "Amplitude: {:.2f} dB".format(self.curve_point.pos().y()) + ) def set_plotdata(self, name, data_x, data_y): if name in self.traces: self.traces[name].setData(data_x, data_y) - else: - if name == "spectrum": - self.traces[name] = self.spectrum.plot(pen="m", width=3) - self.spectrum.setLogMode(x=False, y=False) - self.spectrum.setYRange(self.min, 5, padding=0) - if REAL_DEV_NAME in self.classname.lower(): - start = 0 - else: - start = -1 * self.stream.sample_rate / 2 - self.spectrum.setXRange( - start, self.stream.sample_rate / 2, padding=0.005, - ) - elif name == "waveform": - self.traces[name] = self.waveform.plot(pen="c", width=3) - self.waveform.setYRange(-(2 ** 11) - 200, 2 ** 11 + 200, padding=0) - self.waveform.setXRange( - 0, - self.stream.rx_buffer_size / self.stream.sample_rate, - padding=0.005, - ) + elif name == "spectrum": + self.traces[name] = self.spectrum.plot(pen="m", width=3) + self.spectrum.setLogMode(x=False, y=False) + self.spectrum.setYRange(self.min, 5, padding=0) + start = ( + 0 + if REAL_DEV_NAME in self.classname.lower() + else -1 * self.stream.sample_rate / 2 + ) + self.spectrum.setXRange( + start, + self.stream.sample_rate / 2, + padding=0.005, + ) + elif name == "waveform": + self.traces[name] = self.waveform.plot(pen="c", width=3) + self.waveform.setYRange(-(2**11) - 200, 2**11 + 200, padding=0) + self.waveform.setXRange( + 0, + self.stream.rx_buffer_size / self.stream.sample_rate, + padding=0.005, + ) def update(self): while not self.q.empty(): wf_data = self.q.get() self.set_plotdata( - name="waveform", data_x=self.x, data_y=np.real(wf_data), + name="waveform", + data_x=self.x, + data_y=np.real(wf_data), ) sp_data = np.fft.fft(wf_data) sp_data = np.abs(np.fft.fftshift(sp_data)) / self.stream.rx_buffer_size - sp_data = 20 * np.log10(sp_data / (2 ** 11)) + sp_data = 20 * np.log10(sp_data / (2**11)) self.set_plotdata(name="spectrum", data_x=self.f, data_y=sp_data) + if not self.markers_added: + self.add_markers(self.traces["spectrum"]) + + # Find peak of spectrum + index = np.argmax(sp_data) + + # Add label to plot at the peak of the spectrum + if self.markers_added: + self.curve_point.setPos(float(index) / (len(self.f) - 1)) + self.text_peak.setText( + "[%0.1f, %0.1f]" % (self.f[index], sp_data[index]) + ) + self.update_custom_table() + def animation(self): timer = QtCore.QTimer() timer.timeout.connect(self.update) @@ -131,7 +224,6 @@ def animation(self): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="ADI fast plotting app") parser.add_argument( "class", help="pyadi class name to use as plot source", action="store" From efc4b19140f52bb7cd72d8ecbce254544c15f2fd Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 22 Jun 2023 17:48:53 -0600 Subject: [PATCH 02/23] Extend adiplot to support genalyzer Signed-off-by: Travis F. Collins --- examples/adiplot.py | 142 +++++++++++++++++++++++++++++++++----------- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/examples/adiplot.py b/examples/adiplot.py index 5c62f843e..107d55a93 100644 --- a/examples/adiplot.py +++ b/examples/adiplot.py @@ -1,21 +1,27 @@ import argparse -import sys +import os +import sys # We need sys so that we can pass argv to QApplication import threading import time from queue import Full, Queue +from random import randint import adi +import matplotlib.pyplot as plt import numpy as np - +import pyqtgraph as pg +from PyQt5 import QtCore, QtWidgets +from pyqtgraph import GraphicsLayoutWidget, PlotWidget, plot from scipy import signal from scipy.fftpack import fft -from PyQt5 import QtWidgets, QtCore -from pyqtgraph import PlotWidget, plot, GraphicsLayoutWidget -import pyqtgraph as pg -import sys # We need sys so that we can pass argv to QApplication -import os -from random import randint +try: + import genalyzer + + use_genalyzer = True + print("Using genalyzer :)") +except ImportError: + use_genalyzer = False pg.setConfigOptions(antialias=True) pg.setConfigOption("background", "k") @@ -24,6 +30,18 @@ class ADIPlotter(object): + def setup_genalyzer(self, fftsize, fs): + + bits = 12 + navg = 1 + window = 2 + + c = genalyzer.config_fftz(fftsize, bits, navg, fftsize, window) + genalyzer.config_set_sample_rate(fs, c) + genalyzer.gn_config_fa_auto(ssb_width=120, c=c) + + return c + def __init__(self, classname, uri): self.classname = classname self.q = Queue(maxsize=20) @@ -33,9 +51,16 @@ def __init__(self, classname, uri): self.stream.rx_lo = 1000000000 self.stream.tx_lo = 1000000000 self.stream.dds_single_tone(3000000, 0.9) - self.stream.rx_buffer_size = 2**12 + self.stream.rx_buffer_size = 2 ** 12 self.stream.rx_enabled_channels = [0] + if use_genalyzer: + self.c = self.setup_genalyzer( + self.stream.rx_buffer_size, self.stream.sample_rate + ) + self.update_interval = 100 + self.current_count = 0 + self.app = QtWidgets.QApplication(sys.argv) self.qmw = QtWidgets.QMainWindow() @@ -61,9 +86,9 @@ def __init__(self, classname, uri): wf_xaxis.setLabel(units="Seconds") if REAL_DEV_NAME in classname.lower(): - wf_ylabels = [(0, "0"), (2**11, "2047")] + wf_ylabels = [(0, "0"), (2 ** 11, "2047")] else: - wf_ylabels = [(-2 * 11, "-2047"), (0, "0"), (2**11, "2047")] + wf_ylabels = [(-2 * 11, "-2047"), (0, "0"), (2 ** 11, "2047")] wf_yaxis = pg.AxisItem(orientation="left") wf_yaxis.setTicks([wf_ylabels]) @@ -71,16 +96,10 @@ def __init__(self, classname, uri): sp_xaxis.setLabel(units="Hz") self.waveform = self.win.addPlot( - title="WAVEFORM", - row=1, - col=1, - axisItems={"bottom": wf_xaxis}, + title="WAVEFORM", row=1, col=1, axisItems={"bottom": wf_xaxis}, ) self.spectrum = self.win.addPlot( - title="SPECTRUM", - row=2, - col=1, - axisItems={"bottom": sp_xaxis}, + title="SPECTRUM", row=2, col=1, axisItems={"bottom": sp_xaxis}, ) self.waveform.showGrid(x=True, y=True) self.spectrum.showGrid(x=True, y=True) @@ -125,7 +144,7 @@ def source(self): def start(self): self.app.exec_() - def add_markers(self, plot): + def add_markers(self, plot, genalyzer_results=None): #### Add peak marker for spectrum plot data = plot.getData() if data[0] is None: @@ -135,11 +154,11 @@ def add_markers(self, plot): self.text_peak = pg.TextItem("TEST", anchor=(0.5, 1.0)) self.text_peak.setParentItem(parent=self.curve_point) - self.build_custom_table_from_textitems() + self.build_custom_table_from_textitems(genalyzer_results) self.markers_added = True - def build_custom_table_from_textitems(self): + def build_custom_table_from_textitems(self, genalyzer_results): text_items = ["Peak", "Frequency", "Amplitude"] self.custom_table = {} self.table_x = 180 @@ -152,9 +171,39 @@ def build_custom_table_from_textitems(self): # set position self.custom_table[text].setPos(self.table_x, self.table_y + scaler * i) - def update_custom_table(self): + if use_genalyzer: + offset = (len(text_items) + 2) * scaler + for i, key in enumerate(genalyzer_results): + self.custom_table[key] = pg.TextItem(text=key) + self.custom_table[key].setParentItem(parent=self.measurements) + x_pos = i % 6 + y_pos = np.floor(i // 6) + self.custom_table[key].setPos( + self.table_x + x_pos * 300, self.table_y + offset + scaler * y_pos + ) + + def update_custom_table(self, genalyzer_updates=None): + if not self.markers_added: + return + self.custom_table["Frequency"].setText( + "Frequency: {:.2f} Hz".format(self.curve_point.pos().x()) + ) + self.custom_table["Amplitude"].setText( + "Amplitude: {:.2f} dB".format(self.curve_point.pos().y()) + ) + + if use_genalyzer: + for key in genalyzer_updates: + self.custom_table[key].setText( + "{}: {:.2f}".format(key, genalyzer_updates[key]) + ) + + def update_genalyzer_table(self, table): if not self.markers_added: return + + for key in table: + self.custom_table[key].setText("{}: {:.2f}".format(key, table[key])) self.custom_table["Frequency"].setText( "Frequency: {:.2f} Hz".format(self.curve_point.pos().x()) ) @@ -175,34 +224,54 @@ def set_plotdata(self, name, data_x, data_y): else -1 * self.stream.sample_rate / 2 ) self.spectrum.setXRange( - start, - self.stream.sample_rate / 2, - padding=0.005, + start, self.stream.sample_rate / 2, padding=0.005, ) elif name == "waveform": self.traces[name] = self.waveform.plot(pen="c", width=3) - self.waveform.setYRange(-(2**11) - 200, 2**11 + 200, padding=0) + self.waveform.setYRange(-(2 ** 11) - 200, 2 ** 11 + 200, padding=0) self.waveform.setXRange( - 0, - self.stream.rx_buffer_size / self.stream.sample_rate, - padding=0.005, + 0, self.stream.rx_buffer_size / self.stream.sample_rate, padding=0.005, ) def update(self): while not self.q.empty(): wf_data = self.q.get() self.set_plotdata( - name="waveform", - data_x=self.x, - data_y=np.real(wf_data), + name="waveform", data_x=self.x, data_y=np.real(wf_data), ) + if use_genalyzer: + self.current_count = self.current_count + 1 + if self.current_count >= self.update_interval: + self.current_count = 0 + # Convert array to list of ints + i = [int(np.real(a)) for a in wf_data] + q = [int(np.imag(b)) for b in wf_data] + fft_out_i, fft_out_q = genalyzer.fftz(i, q, self.c) + fft_out = [ + val for pair in zip(fft_out_i, fft_out_q) for val in pair + ] + + # sp_data = np.array(fft_out_i) + 1j * np.array(fft_out_q) + # sp_data = np.abs(np.fft.fftshift(sp_data)) / self.stream.rx_buffer_size + # sp_data = 20 * np.log10(sp_data / (2**11)) + + # get all Fourier analysis results + all_results = genalyzer.get_fa_results(fft_out, self.c) + + else: + all_results = None + sp_data = np.fft.fft(wf_data) sp_data = np.abs(np.fft.fftshift(sp_data)) / self.stream.rx_buffer_size - sp_data = 20 * np.log10(sp_data / (2**11)) + sp_data = 20 * np.log10(sp_data / (2 ** 11)) + self.set_plotdata(name="spectrum", data_x=self.f, data_y=sp_data) + if use_genalyzer and self.current_count != 0: + return + if not self.markers_added: - self.add_markers(self.traces["spectrum"]) + self.add_markers(self.traces["spectrum"], all_results) # Find peak of spectrum index = np.argmax(sp_data) @@ -213,7 +282,7 @@ def update(self): self.text_peak.setText( "[%0.1f, %0.1f]" % (self.f[index], sp_data[index]) ) - self.update_custom_table() + self.update_custom_table(all_results) def animation(self): timer = QtCore.QTimer() @@ -236,4 +305,5 @@ def animation(self): app = ADIPlotter(args["class"], args["uri"]) app.animation() + print("Exiting...") app.thread.join() From 03840593e7b8b26097b6958dc91a9838e8c84fbf Mon Sep 17 00:00:00 2001 From: Michael Hennerich Date: Wed, 5 Jul 2023 16:30:02 +0200 Subject: [PATCH 03/23] AD9084: Basic support for AD9084 This adds pyadi-iio support for the MxFE Quad, 16-Bit, 12 GSPS RF DAC and Dual, 12-Bit, 6 GSPS RF ADC Signed-off-by: Michael Hennerich --- adi/__init__.py | 2 + adi/ad9084.py | 490 +++++++++++++++++++++++++++ adi/ad9084_mc.py | 348 +++++++++++++++++++ doc/source/devices/adi.ad9084.rst | 7 + doc/source/devices/adi.ad9084_mc.rst | 52 +++ doc/source/devices/index.rst | 2 + examples/Triton_example.py | 212 ++++++++++++ examples/ad9084_example.py | 55 +++ supported_parts.md | 1 + tasks.py | 2 + test/emu/devices/ad9084.xml | 14 + test/emu/hardware_map.yml | 10 + test/test_ad9084.py | 306 +++++++++++++++++ 13 files changed, 1501 insertions(+) create mode 100644 adi/ad9084.py create mode 100644 adi/ad9084_mc.py create mode 100644 doc/source/devices/adi.ad9084.rst create mode 100644 doc/source/devices/adi.ad9084_mc.rst create mode 100644 examples/Triton_example.py create mode 100644 examples/ad9084_example.py create mode 100644 test/emu/devices/ad9084.xml create mode 100644 test/test_ad9084.py diff --git a/adi/__init__.py b/adi/__init__.py index 39c23e94b..177e8a108 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -25,6 +25,8 @@ from adi.ad9081 import ad9081 from adi.ad9081_mc import QuadMxFE, ad9081_mc from adi.ad9083 import ad9083 +from adi.ad9084 import ad9084 +from adi.ad9084_mc import Triton, ad9084_mc from adi.ad9094 import ad9094 from adi.ad9136 import ad9136 from adi.ad9144 import ad9144 diff --git a/adi/ad9084.py b/adi/ad9084.py new file mode 100644 index 000000000..c9e896534 --- /dev/null +++ b/adi/ad9084.py @@ -0,0 +1,490 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from typing import Dict, List + +from adi.context_manager import context_manager +from adi.rx_tx import rx_tx +from adi.sync_start import sync_start + + +def _map_to_dict(paths, ch): + if ch.attrs["label"].value == "buffer_only": + return paths + side, fddc, cddc, adc = ch.attrs["label"].value.replace(":", "->").split("->") + if side not in paths.keys(): + paths[side] = {} + if adc not in paths[side].keys(): + paths[side][adc] = {} + if cddc not in paths[side][adc].keys(): + paths[side][adc][cddc] = {} + if fddc not in paths[side][adc][cddc].keys(): + paths[side][adc][cddc][fddc] = {"channels": [ch._id]} + else: + paths[side][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 ad9084(rx_tx, context_manager, sync_start): + """AD9084 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-ad9084-rx-hpc") + # Devices with buffers + self._rxadc = self._ctx.find_device("axi-ad9084-rx-hpc") + self._txdac = self._ctx.find_device("axi-ad9084-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 side in paths: + for converter in paths[side]: + for cdc in paths[side][converter]: + channels = [] + for fdc in paths[side][converter][cdc]: + channels += paths[side][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) + sync_start.__init__(self) + self.rx_buffer_size = 2 ** 16 + + def _get_iio_attr_str_single(self, channel_name, attr, output): + # This is overridden by subclasses + return self._get_iio_attr_str(channel_name, attr, output) + + def _set_iio_attr_str_single(self, channel_name, attr, output, value): + # This is overridden by subclasses + return self._set_iio_attr(channel_name, attr, output, value) + + def _get_iio_attr_single(self, channel_name, attr, output): + # This is overridden by subclasses + return self._get_iio_attr(channel_name, attr, output) + + def _set_iio_attr_single(self, channel_name, attr, output, value): + # This is overridden by subclasses + return self._set_iio_attr(channel_name, attr, output, value) + + def _get_iio_dev_attr_single(self, attr): + # This is overridden by subclasses + return self._get_iio_dev_attr(attr) + + def _set_iio_dev_attr_single(self, attr, value): + # This is overridden by subclasses + return self._set_iio_dev_attr(attr, value) + + def _get_iio_dev_attr_str_single(self, attr): + # This is overridden by subclasses + return self._get_iio_dev_attr_str(attr) + + def _set_iio_dev_attr_str_single(self, attr, value): + # This is overridden by subclasses + return self._set_iio_dev_attr_str(attr, value) + + @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_single("voltage0_i", "test_mode", False) + + @rx_test_mode.setter + def rx_test_mode(self, value): + self._set_iio_attr_single( + "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_vec( + self._rx_coarse_ddc_channel_names, "nyquist_zone", False + ) + + @rx_nyquist_zone.setter + def rx_nyquist_zone(self, value): + self._set_iio_attr_str_vec( + self._rx_coarse_ddc_channel_names, "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 loopback_mode(self): + """loopback_mode: Enable loopback mode RX->TX + + When enabled JESD RX FIFO is connected to JESD TX FIFO, + making the entire datasource for the TX path the RX path. No + data is passed into the TX path from off-chip when 1. For + this mode to function correctly the JESD configuration + between RX and TX must be identical and only use a single + link. + """ + return self._get_iio_dev_attr_single("loopback_mode") + + @loopback_mode.setter + def loopback_mode(self, value): + self._set_iio_dev_attr_single( + "loopback_mode", value, + ) + + @property + def tx_ddr_offload(self): + """tx_ddr_offload: Enable DDR offload + + When true the DMA will pass buffers into the BRAM FIFO for data repeating. + This is necessary when operating at high DAC sample rates. This can reduce + the maximum buffer size but data passed to DACs in cyclic mode will not + underflow due to memory bottlenecks. + """ + return self._get_iio_debug_attr("pl_ddr_fifo_enable", self._txdac) + + @tx_ddr_offload.setter + def tx_ddr_offload(self, value): + self._set_iio_debug_attr_str("pl_ddr_fifo_enable", str(value * 1), self._txdac) + + @property + def rx_sample_rate(self): + """rx_sampling_frequency: Sample rate after decimation""" + return self._get_iio_attr_single("voltage0_i", "sampling_frequency", False) + + @property + def adc_frequency(self): + """adc_frequency: ADC frequency in Hz""" + return self._get_iio_attr_single("voltage0_i", "adc_frequency", False) + + @property + def tx_sample_rate(self): + """tx_sampling_frequency: Sample rate before interpolation""" + return self._get_iio_attr_single("voltage0_i", "sampling_frequency", True) + + @property + def dac_frequency(self): + """dac_frequency: DAC frequency in Hz""" + return self._get_iio_attr_single("voltage0_i", "dac_frequency", True) + + @property + def jesd204_fsm_ctrl(self): + """jesd204_fsm_ctrl: jesd204-fsm control""" + return self._get_iio_dev_attr("jesd204_fsm_ctrl", self._rxadc) + + @jesd204_fsm_ctrl.setter + def jesd204_fsm_ctrl(self, value): + self._set_iio_dev_attr("jesd204_fsm_ctrl", value, self._rxadc) + + @property + def jesd204_fsm_resume(self): + """jesd204_fsm_resume: jesd204-fsm resume""" + return self._get_iio_dev_attr("jesd204_fsm_resume", self._rxadc) + + @jesd204_fsm_resume.setter + def jesd204_fsm_resume(self, value): + self._set_iio_dev_attr_str("jesd204_fsm_resume", value, self._rxadc) + + @property + def jesd204_fsm_state(self): + """jesd204_fsm_state: jesd204-fsm state""" + return self._get_iio_dev_attr_str("jesd204_fsm_state", self._rxadc) + + @property + def jesd204_fsm_paused(self): + """jesd204_fsm_paused: jesd204-fsm paused""" + return self._get_iio_dev_attr("jesd204_fsm_paused", self._rxadc) + + @property + def jesd204_fsm_error(self): + """jesd204_fsm_error: jesd204-fsm error""" + return self._get_iio_dev_attr("jesd204_fsm_error", self._rxadc) + + @property + def jesd204_device_status(self): + """jesd204_device_status: Device jesd204 link status information""" + return self._get_iio_debug_attr_str("status", self._rxadc) + + @property + def jesd204_device_status_check(self): + """jesd204_device_status_check: Device jesd204 link status check + + Returns 'True' in case error conditions are detected, 'False' otherwise + """ + stat = self._get_iio_debug_attr_str("status", self._rxadc) + + for s in stat.splitlines(0): + if "JRX" in s: + if "204C" in s: + if "Link is good" not in s: + return True + elif "204B" in s: + if "0x0 lanes in DATA" in s: + return True + elif "JTX" in s: + if any( + substr in s + for substr in [" asserted", "unlocked", "lost", "invalid"] + ): + return True + return False + + @property + def chip_version(self): + """chip_version: Chip version information""" + return self._get_iio_debug_attr_str("chip_version", self._rxadc) + + @property + def api_version(self): + """api_version: API version""" + return self._get_iio_debug_attr_str("api_version", self._rxadc) diff --git a/adi/ad9084_mc.py b/adi/ad9084_mc.py new file mode 100644 index 000000000..a2948cc1e --- /dev/null +++ b/adi/ad9084_mc.py @@ -0,0 +1,348 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from typing import Dict, List + +from adi.ad9084 import ad9084 +from adi.attribute import attribute +from adi.context_manager import context_manager +from adi.gen_mux import genmux +from adi.one_bit_adc_dac import one_bit_adc_dac +from adi.rx_tx import rx_tx +from adi.sync_start import sync_start + + +def _map_to_dict(paths, ch, dev_name): + if "label" in ch.attrs and "buffer_only" in ch.attrs["label"].value: + return paths, False + side, fddc, cddc, adc = ch.attrs["label"].value.replace(":", "->").split("->") + if dev_name not in paths.keys(): + paths[dev_name] = {} + if side not in paths[dev_name].keys(): + paths[dev_name][side] = {} + if adc not in paths[dev_name][side].keys(): + paths[dev_name][side][adc] = {} + if cddc not in paths[dev_name][side][adc].keys(): + paths[dev_name][side][adc][cddc] = {} + if fddc not in paths[dev_name][side][adc][cddc].keys(): + paths[dev_name][side][adc][cddc][fddc] = {"channels": [ch._id]} + else: + paths[dev_name][side][adc][cddc][fddc]["channels"].append(ch._id) + return paths, True + + +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 + + +def _find_dev_with_buffers(ctx, output=False, contains=""): + for dev in ctx.devices: + for chan in dev.channels: + if ( + chan.output == output + and chan.scan_element + and dev.name + and contains in dev.name + ): + return dev + + +class ad9084_mc(ad9084): + """ad9084 Mixed-Signal Front End (MxFE) Multi-Chip Interface + + This class is a generic interface for boards that utilize multiple ad9084 + devices. + + parameters: + uri: type=string + Optional parameter for the URI of IIO context with ad9084(s). + phy_dev_name: type=string + Optional parameter name of main control driver for multi-ad9084 board. + If no argument is given the driver with the most channel attributes is + assumed to be the main PHY driver + + """ + + _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="", phy_dev_name=""): + + self._rx_channel_names: List[str] = [] + context_manager.__init__(self, uri, self._device_name) + + if not phy_dev_name: + # Get ad9084 dev name with most channel attributes + channel_attr_count = { + dev.name: sum(len(chan.attrs) for chan in dev.channels) + for dev in self._ctx.devices + if dev.name and "ad9084" in dev.name + } + phy_dev_name = max(channel_attr_count, key=channel_attr_count.get) + + self._ctrl = self._ctx.find_device(phy_dev_name) + if not self._ctrl: + raise Exception("phy_dev_name not found with name: {}".format(phy_dev_name)) + + # Find device with buffers + self._txdac = _find_dev_with_buffers(self._ctx, True, "axi-ad9084") + self._rxadc = _find_dev_with_buffers(self._ctx, False, "axi-ad9084") + + # Get DDC and DUC mappings + # Labels span all devices so they must all be processed + paths = {} + self._default_ctrl_names = [] + for dev in self._ctx.devices: + if dev.name and "ad9084" not in dev.name: + continue + for ch in dev.channels: + not_buffer = False + if "label" in ch.attrs: + paths, not_buffer = _map_to_dict(paths, ch, dev.name) + if not_buffer and dev.name not in self._default_ctrl_names: + self._default_ctrl_names.append(dev.name) + + self._default_ctrl_names = sorted(self._default_ctrl_names) + self._ctrls = [self._ctx.find_device(dn) for dn in self._default_ctrl_names] + 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._map_unique(paths) + + # Bring up DMA and DDS interfaces + rx_tx.__init__(self) + sync_start.__init__(self) + self.rx_buffer_size = 2 ** 16 + + def _map_unique(self, paths): + 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 chip in paths: + for side in paths[chip]: + for converter in paths[chip][side]: + for cdc in paths[chip][side][converter]: + channels = [] + for fdc in paths[chip][side][converter][cdc]: + channels += paths[chip][side][converter][cdc][fdc][ + "channels" + ] + channels = [name for name in channels if "_i" in name] + + if "ADC" in converter: + if chip not in self._rx_coarse_ddc_channel_names.keys(): + self._rx_coarse_ddc_channel_names[chip] = [] + if chip not in self._rx_fine_ddc_channel_names.keys(): + self._rx_fine_ddc_channel_names[chip] = [] + + self._rx_coarse_ddc_channel_names[chip].append(channels[0]) + self._rx_fine_ddc_channel_names[chip] += channels + else: + if chip not in self._tx_coarse_duc_channel_names.keys(): + self._tx_coarse_duc_channel_names[chip] = [] + if chip not in self._tx_fine_duc_channel_names.keys(): + self._tx_fine_duc_channel_names[chip] = [] + + self._tx_coarse_duc_channel_names[chip].append(channels[0]) + self._tx_fine_duc_channel_names[chip] += channels + + def _map_inputs_to_dict(self, channel_names_dict, attr, output, values): + if not isinstance(values, dict): + # If passed an array it must be split across the devices + # Check to make sure length is accurate + d = self._get_iio_attr_vec(channel_names_dict, attr, output) + t_len = sum(len(d[e]) for e in d) + if len(values) != t_len: + raise Exception("Input must be of length {}".format(t_len)) + h = {} + c = 0 + for dev in self._default_ctrl_names: + data_index = len(d[dev]) + h[dev] = values[c : c + data_index] + c += data_index + values = h + return values + + def _map_inputs_to_dict_single(self, channel_names_dict, values): + if not isinstance(values, dict): + # If passed an array it must be split across the devices + # Check to make sure length is accurate + t_len = len(channel_names_dict) + if len(values) != t_len: + raise Exception("Input must be of length {}".format(t_len)) + h = {dev: values[i] for i, dev in enumerate(self._default_ctrl_names)} + values = h + return values + + # Vector function intercepts + def _get_iio_attr_vec(self, channel_names_dict, attr, output): + return { + dev: ad9084._get_iio_attr_vec( + self, channel_names_dict[dev], attr, output, self._ctx.find_device(dev), + ) + for dev in channel_names_dict + } + + def _set_iio_attr_int_vec(self, channel_names_dict, attr, output, values): + values = self._map_inputs_to_dict(channel_names_dict, attr, output, values) + for dev in channel_names_dict: + ad9084._set_iio_attr_int_vec( + self, + channel_names_dict[dev], + attr, + output, + values[dev], + self._ctx.find_device(dev), + ) + + def _set_iio_attr_float_vec(self, channel_names_dict, attr, output, values): + values = self._map_inputs_to_dict(channel_names_dict, attr, output, values) + for dev in channel_names_dict: + ad9084._set_iio_attr_float_vec( + self, + channel_names_dict[dev], + attr, + output, + values[dev], + self._ctx.find_device(dev), + ) + + def _set_iio_attr_str_vec(self, channel_names_dict, attr, output, values): + values = self._map_inputs_to_dict(channel_names_dict, attr, output, values) + for dev in channel_names_dict: + ad9084._set_iio_attr_str_vec( + self, + channel_names_dict[dev], + attr, + output, + values[dev], + self._ctx.find_device(dev), + ) + + # Singleton function intercepts + def _get_iio_attr_str_single(self, channel_name, attr, output): + channel_names_dict = self._rx_coarse_ddc_channel_names + return { + dev: attribute._get_iio_attr_str( + self, channel_name, attr, output, self._ctx.find_device(dev) + ) + for dev in channel_names_dict + } + + def _get_iio_attr_single(self, channel_name, attr, output): + channel_names_dict = self._rx_coarse_ddc_channel_names + return { + dev: attribute._get_iio_attr( + self, channel_name, attr, output, self._ctx.find_device(dev) + ) + for dev in channel_names_dict + } + + def _set_iio_attr_single(self, channel_name, attr, output, values): + channel_names_dict = self._rx_coarse_ddc_channel_names + values = self._map_inputs_to_dict_single(channel_names_dict, values) + for dev in channel_names_dict: + self._set_iio_attr( + channel_name, attr, output, values[dev], self._ctx.find_device(dev) + ) + + def _get_iio_dev_attr_single(self, attr): + channel_names_dict = self._rx_coarse_ddc_channel_names + return { + dev: attribute._get_iio_dev_attr(self, attr, self._ctx.find_device(dev)) + for dev in channel_names_dict + } + + def _set_iio_dev_attr_single(self, attr, values): + channel_names_dict = self._rx_coarse_ddc_channel_names + values = self._map_inputs_to_dict_single(channel_names_dict, values) + for dev in channel_names_dict: + self._set_iio_dev_attr(attr, values[dev], self._ctx.find_device(dev)) + + +class Triton(ad9084_mc): + """Quad ad9084 Mixed-Signal Front End (MxFE) Development System + + parameters: + uri: type=string + Optional parameter for the URI of IIO context with QuadMxFE. + """ + + def __init__(self, uri="", calibration_board_attached=False): + ad9084_mc.__init__(self, uri=uri, phy_dev_name="axi-ad9084-rx-hpc") + one_bit_adc_dac.__init__(self, uri) + + self._clock_chip_c = self._ctx.find_device("ltc6953_c") + self._clock_chip_f = self._ctx.find_device("ltc6953_f") + + self._rx_dsa = self._ctx.find_device("hmc425a") + + self.lpf_ctrl = genmux(uri, device_name="lpf-ctrl") + self.hpf_ctrl = genmux(uri, device_name="hpf-ctrl") + + if calibration_board_attached: + self._ad5592r = self._ctx.find_device("ad5592r") + self._cb_gpio = self._ctx.find_device("one-bit-adc-dac") + + @property + def rx_dsa_gain(self): + """rx_dsa_gain: Receiver digital step attenuator gain""" + return self._get_iio_attr("voltage0", "hardwaregain", True, self._rx_dsa) + + @rx_dsa_gain.setter + def rx_dsa_gain(self, value): + self._set_iio_attr("voltage0", "hardwaregain", True, value, self._rx_dsa) diff --git a/doc/source/devices/adi.ad9084.rst b/doc/source/devices/adi.ad9084.rst new file mode 100644 index 000000000..7025ab869 --- /dev/null +++ b/doc/source/devices/adi.ad9084.rst @@ -0,0 +1,7 @@ +ad9081 +================= + +.. automodule:: adi.ad9084 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/adi.ad9084_mc.rst b/doc/source/devices/adi.ad9084_mc.rst new file mode 100644 index 000000000..92a25371c --- /dev/null +++ b/doc/source/devices/adi.ad9084_mc.rst @@ -0,0 +1,52 @@ +ad9084\_mc +===================== + +The multi-chip python interface for ad9084 is scalable to any number of ad9084s within a single libIIO context. It will automatically determine the correct main driver, manage the CDDC/FDDC/CDUC/FDUC arrangement uniquely for each chip, and DMA/DDS IP. However, the interface a bit unique with **pyadi-iio** since it is almost identical to the single ad9084 class but it exposes properties in a slightly different way. + +When using **adi.ad9084**, properties are generally simple types like strings, ints, floats, or lists of these types. For example, when reading back the **rx_channel_nco_frequencies** you would observe something like: + +.. code-block:: bash + + >>> import adi + >>> dev = adi.ad9084() + >>> dev.rx_channel_nco_frequencies + [0, 0, 0, 0] + + +For the case of a multi-chip configuration a dict is returned with an entry for each MxFE chip: + +.. code-block:: bash + + >>> import adi + >>> dev = adi.ad9084_mc() + >>> dev.rx_channel_nco_frequencies + {'axi-ad9084-rx1': [0, 0, 0, 0], + 'axi-ad9084-rx2': [0, 0, 0, 0], + 'axi-ad9084-rx3': [0, 0, 0, 0], + 'axi-ad9084-rx-hpc': [0, 0, 0, 0]} + +The same dict can be passed back to the property when writing, which will contain all or a subset of the chips to be address if desired. Alternatively, a list can be passed with only the values themselves if a dict does not want to be used. This is useful when performing array based DSP were data is approach in aggregate. However, in this case entries must be provided for all chip, not just a subset. Otherwise an error is returned. + +When passing a list only, the chips are address based on the attribute **_default_ctrl_names**. Below is an example of this API: + +.. code-block:: bash + + >>> import adi + >>> dev = adi.ad9084_mc() + >>> dev.rx_channel_nco_frequencies + {'axi-ad9084-rx1': [0, 0, 0, 0], + 'axi-ad9084-rx2': [0, 0, 0, 0], + 'axi-ad9084-rx3': [0, 0, 0, 0], + 'axi-ad9084-rx-hpc': [0, 0, 0, 0]} + >>> dev.rx_channel_nco_frequencies = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] + >>> dev.rx_channel_nco_frequencies + {'axi-ad9084-rx1': [0, 1, 2, 3], + 'axi-ad9084-rx2': [4, 5, 6, 7], + 'axi-ad9084-rx3': [8, 9, 10, 11], + 'axi-ad9084-rx-hpc': [12, 13, 14, 15]} + + +.. automodule:: adi.ad9084_mc + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 520821b85..291609962 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -31,6 +31,8 @@ Supported Devices adi.ad9081 adi.ad9081_mc adi.ad9083 + adi.ad9084 + adi.ad9084_mc adi.ad9094 adi.ad9136 adi.ad9144 diff --git a/examples/Triton_example.py b/examples/Triton_example.py new file mode 100644 index 000000000..be2788e7a --- /dev/null +++ b/examples/Triton_example.py @@ -0,0 +1,212 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import time +from datetime import datetime + +import adi +import matplotlib.pyplot as plt +import numpy as np +from scipy import signal + + +def measure_phase_and_delay(chan0, chan1, window=None): + assert len(chan0) == len(chan1) + if window == None: + window = len(chan0) + phases = [] + delays = [] + indx = 0 + sections = len(chan0) // window + for sec in range(sections): + chan0_tmp = chan0[indx : indx + window] + chan1_tmp = chan1[indx : indx + window] + indx = indx + window + 1 + cor = np.correlate(chan0_tmp, chan1_tmp, "full") + # plt.plot(np.real(cor)) + # plt.plot(np.imag(cor)) + # plt.plot(np.abs(cor)) + # plt.show() + i = np.argmax(np.abs(cor)) + m = cor[i] + sample_delay = len(chan0_tmp) - i - 1 + phases.append(np.angle(m) * 180 / np.pi) + delays.append(sample_delay) + return (np.mean(phases), np.mean(delays)) + + +def measure_phase(chan0, chan1): + assert len(chan0) == len(chan1) + errorV = np.angle(chan0 * np.conj(chan1)) * 180 / np.pi + error = np.mean(errorV) + return error + + +def sub_phases(x, y): + return [e1 - e2 for (e1, e2) in zip(x, y)] + + +def measure_and_adjust_phase_offset(chan0, chan1, phase_correction): + assert len(chan0) == len(chan1) + (p, s) = measure_phase_and_delay(chan0, chan1) + # print("Across Chips Sample delay: ",s) + # print("Phase delay: ",p,"(Degrees)") + # print(phase_correction) + return (sub_phases(phase_correction, [int(p * 1000)] * 4), s) + + +dev = adi.Triton("ip:10.44.3.185", calibration_board_attached=False) + +print(dev.rx_channel_nco_frequencies["axi-ad9084-rx-hpc"]) +print(dev.rx_main_nco_frequencies["axi-ad9084-rx-hpc"]) +# Number of MxFE Devices +D = len(dev.rx_test_mode) + +print(dev.rx_test_mode) + +# Total number of channels +N_RX = len(dev.rx_channel_nco_frequencies["axi-ad9084-rx-hpc"]) * D +N_TX = len(dev.tx_channel_nco_frequencies["axi-ad9084-rx-hpc"]) * D + +# Total number of CDDCs/CDUCs +NM_RX = len(dev.rx_main_nco_frequencies["axi-ad9084-rx-hpc"]) * D +NM_TX = len(dev.tx_main_nco_frequencies["axi-ad9084-rx-hpc"]) * D + +# Enable the first RX of each MxFE +RX_CHAN_EN = [] +for i in range(N_RX): + if i % (N_RX / D) == 0: + RX_CHAN_EN = RX_CHAN_EN + [i] + +# In case the channelizers are not used (bypassed) compensate phase offsets using the main NCOs +channelizer_bypass = ( + dev._rxadc.find_channel("voltage0_i").attrs["channel_nco_frequency_available"].value +) +if channelizer_bypass == "[0 1 0]": + COMPENSATE_MAIN_PHASES = True +else: + COMPENSATE_MAIN_PHASES = False + +# Configure properties +print("--Setting up chip") + +# Loop Combined Tx Channels Back Into Combined Rx Path +# dev.gpio_ctrl_ind = 1 +# dev.gpio_5045_v1 = 1 +# dev.gpio_5045_v2 = 1 +# dev.gpio_ctrl_rx_combined = 0 + +# Zero attenuation +dev.rx_dsa_gain = 0 + +# Set NCOs +dev.rx_channel_nco_frequencies = [0] * N_RX +dev.tx_channel_nco_frequencies = [0] * N_TX + +dev.rx_main_nco_frequencies = [1000000000] * NM_RX +dev.tx_main_nco_frequencies = [3000000000] * NM_TX + +dev.rx_enabled_channels = RX_CHAN_EN +dev.tx_enabled_channels = [1] * N_TX +dev.rx_nyquist_zone = ["even"] * NM_TX + +dev.rx_buffer_size = 2 ** 12 +dev.tx_cyclic_buffer = True + +fs = int(dev.tx_sample_rate["axi-ad9084-rx-hpc"]) + +# Set single DDS tone for TX on one transmitter +dev.dds_single_tone(fs / 50, 0.9, channel=0) + +phases_a = [] +phases_b = [] +phases_c = [] +phases_d = [] + +so_a = [] +so_b = [] +so_c = [] +so_d = [] + +run_plot = True + +for i in range(10): + if COMPENSATE_MAIN_PHASES: + dev.rx_main_nco_phases = [0] * NM_RX + rx_nco_phases = dev.rx_main_nco_phases + else: + dev.rx_channel_nco_phases = [0] * N_RX + rx_nco_phases = dev.rx_channel_nco_phases + + for r in range(2): + # Collect data + x = dev.rx() + rx_nco_phases["axi-ad9084-rx1"], s_b = measure_and_adjust_phase_offset( + x[0], x[1], rx_nco_phases["axi-ad9084-rx1"] + ) + rx_nco_phases["axi-ad9084-rx2"], s_c = measure_and_adjust_phase_offset( + x[0], x[2], rx_nco_phases["axi-ad9084-rx2"] + ) + rx_nco_phases["axi-ad9084-rx3"], s_d = measure_and_adjust_phase_offset( + x[0], x[3], rx_nco_phases["axi-ad9084-rx3"] + ) + phase_b = str(rx_nco_phases["axi-ad9084-rx1"][0] / 1000) + "\t" + str(int(s_b)) + phase_c = str(rx_nco_phases["axi-ad9084-rx2"][0] / 1000) + "\t" + str(int(s_c)) + phase_d = str(rx_nco_phases["axi-ad9084-rx3"][0] / 1000) + "\t" + str(int(s_d)) + phases_a.insert(i, rx_nco_phases["axi-ad9084-rx-hpc"][0] / 1000) + phases_b.insert(i, rx_nco_phases["axi-ad9084-rx1"][0] / 1000) + phases_c.insert(i, rx_nco_phases["axi-ad9084-rx2"][0] / 1000) + phases_d.insert(i, rx_nco_phases["axi-ad9084-rx3"][0] / 1000) + so_a.insert(i, 0) + so_b.insert(i, s_b) + so_c.insert(i, s_c) + so_d.insert(i, s_d) + result = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + "\t" + + phase_b + + "\t\t" + + phase_c + + "\t\t" + + phase_d + + "\n" + ) + print(result) + + with open("test.txt", "a") as myfile: + myfile.write(result) + + if run_plot == True & r == 1: + plt.xlim(0, 100) + plt.plot(np.real(x[0]), label="(1) reference", alpha=0.7) + plt.plot(np.real(x[1]), label="(2) phase " + phase_b, alpha=0.7) + plt.plot(np.real(x[2]), label="(3) phase " + phase_c, alpha=0.7) + plt.plot(np.real(x[3]), label="(4) phase " + phase_d, alpha=0.7) + plt.legend() + plt.title("Quad MxFE Phase Sync @ " + str(fs / 1000000) + " MSPS") + plt.show() + print("FYI: Close figure to do next capture") + + dev.rx_destroy_buffer() + if COMPENSATE_MAIN_PHASES: + dev.rx_main_nco_phases = rx_nco_phases + else: + dev.rx_channel_nco_phases = rx_nco_phases + +if True: + plt.xlim(0, 24) + plt.plot(phases_a, label="(1) MxFE0 phase", alpha=0.7) + plt.plot(phases_b, label="(2) MxFE1 phase", alpha=0.7) + plt.plot(phases_c, label="(3) MxFE2 phase", alpha=0.7) + plt.plot(phases_d, label="(4) MxFE3 phase", alpha=0.7) + plt.plot(so_a, label="(1) MxFE0 Samp. Offset", alpha=0.7) + plt.plot(so_b, label="(2) MxFE1 Samp. Offset", alpha=0.7) + plt.plot(so_c, label="(3) MxFE2 Samp. Offset", alpha=0.7) + plt.plot(so_d, label="(4) MxFE3 Samp. Offset", alpha=0.7) + plt.legend() + plt.title("Quad MxFE Phase Sync @ " + str(fs / 1000000) + " MSPS") + plt.show() + print("FYI: Close figure to do next capture") + +input("Press Enter to exit...") diff --git a/examples/ad9084_example.py b/examples/ad9084_example.py new file mode 100644 index 000000000..60260bd14 --- /dev/null +++ b/examples/ad9084_example.py @@ -0,0 +1,55 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import time + +import adi +import matplotlib.pyplot as plt +from scipy import signal + +dev = adi.ad9084("ip:10.44.3.185") + +print("CHIP Version:", dev.chip_version) +print("API Version:", dev.api_version) + +print("TX SYNC START AVAILABLE:", dev.tx_sync_start_available) +print("RX SYNC START AVAILABLE:", dev.rx_sync_start_available) + +# 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 = [-2800000000] * 4 +dev.tx_main_nco_frequencies = [10000000000] * 4 + +dev.rx_enabled_channels = [0] +dev.tx_enabled_channels = [0] +dev.rx_nyquist_zone = ["odd"] * 4 + +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/supported_parts.md b/supported_parts.md index 203614346..8fb28080a 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -78,6 +78,7 @@ - AD7799 - AD9081 - AD9083 +- AD9084 - AD9094 - AD9136 - AD9144 diff --git a/tasks.py b/tasks.py index 5af5aac8a..64255a2cf 100644 --- a/tasks.py +++ b/tasks.py @@ -170,6 +170,7 @@ def checkparts(c): "adrv9009_zu11eg_fmcomms8", "adar1000_array", "ad9081_mc", + "ad9084_mc", "ad717x", "ad916x", "ad719x", @@ -214,6 +215,7 @@ def checkemulation(c): "adrv9009_zu11eg_fmcomms8", "adar1000_array", "ad9081_mc", + "ad9084_mc", "ad717x", "ad916x", "ad719x", diff --git a/test/emu/devices/ad9084.xml b/test/emu/devices/ad9084.xml new file mode 100644 index 000000000..93f44fff4 --- /dev/null +++ b/test/emu/devices/ad9084.xml @@ -0,0 +1,14 @@ +]> \ No newline at end of file diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index ae20cd4b6..a222d0a3d 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -115,6 +115,16 @@ ad9081_tdd: - data_devices: - iio:device3 - iio:device4 +ad9084: + - axi-ad9084-tx-hpc + - axi-ad9084-rx-hpc + - pyadi_iio_class_support: + - ad9084 + - emulate: + - filename: ad9084.xml + - data_devices: + - iio:device13 + - iio:device14 ad9172: - axi-ad9172-hpc,2 - hmc7044 diff --git a/test/test_ad9084.py b/test/test_ad9084.py new file mode 100644 index 000000000..71e5d4d29 --- /dev/null +++ b/test/test_ad9084.py @@ -0,0 +1,306 @@ +from os import listdir +from os.path import dirname, join, realpath + +import pytest + +hardware = ["ad9084", "ad9084_tdd"] +classname = "adi.ad9084" + + +def scale_field(param_set, iio_uri): + # Scale fields to match number of channels + import adi + + dev = adi.ad9084(uri=iio_uri) + for field in param_set: + if param_set[field] is not list: + continue + existing_val = getattr(dev, field) + param_set[field] = param_set[field][0] * len(existing_val) + return param_set + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize( + "attr, val", + [ + ("rx_nyquist_zone", ["even", "odd"]), + ( + "rx_test_mode", + [ + "midscale_short", + "pos_fullscale", + "neg_fullscale", + "checkerboard", + "pn23", + "pn9", + "one_zero_toggle", + "user", + "pn7", + "pn15", + "pn31", + "ramp", + "off", + ], + ), + ], +) +def test_ad9084_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, 3, 10), + ("tx_main_nco_frequencies", -6000000000, 6000000000, 1, 3, 10), + ("rx_channel_nco_frequencies", -500000000, 500000000, 1, 3, 10), + ("tx_channel_nco_frequencies", -750000000, 750000000, 1, 3, 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), + ], +) +def test_ad9084_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, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_ad9084_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_ad9084_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( + rx_nyquist_zone=["odd", "odd", "odd", "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_ad9084_cyclic_buffers( + test_cyclic_buffer, iio_uri, classname, channel, param_set +): + param_set = scale_field(param_set, iio_uri) + 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( + 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_ad9084_cyclic_buffers_exception( + test_cyclic_buffer_exception, iio_uri, classname, channel, param_set +): + param_set = scale_field(param_set, iio_uri) + test_cyclic_buffer_exception(iio_uri, classname, channel, param_set) + + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + 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_ad9084_sfdr(test_sfdr, iio_uri, classname, channel, param_set, sfdr_min): + param_set = scale_field(param_set, iio_uri) + test_sfdr(iio_uri, classname, channel, param_set, sfdr_min, full_scale=0.5) + + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize("frequency, scale", [(10000000, 0.5)]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + 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_ad9084_dds_loopback( + test_dds_loopback, + iio_uri, + classname, + param_set, + channel, + frequency, + scale, + peak_min, +): + param_set = scale_field(param_set, iio_uri) + test_dds_loopback( + iio_uri, classname, param_set, channel, frequency, scale, peak_min + ) + + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + 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( + 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( + 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_ad9084_iq_loopback(test_iq_loopback, iio_uri, classname, channel, param_set): + param_set = scale_field(param_set, iio_uri) + test_iq_loopback(iio_uri, classname, channel, param_set) + + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +@pytest.mark.parametrize("frequency", [10000000]) +@pytest.mark.parametrize( + "param_set", + [ + dict( + 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( + 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_ad9084_nco_loopback( + test_tone_loopback, iio_uri, classname, param_set, channel, frequency, peak_min, +): + param_set = scale_field(param_set, iio_uri) + test_tone_loopback(iio_uri, classname, param_set, channel, frequency, peak_min) + + +######################################### From 6287a9bdb3ae42efa707b1e7f2140aa198c6c764 Mon Sep 17 00:00:00 2001 From: MarielTinaco Date: Wed, 12 Jul 2023 08:24:38 +0800 Subject: [PATCH 04/23] Added ad7690 to supported devices of ad7689 (PulSAR) Signed-off-by: MarielTinaco --- adi/ad7689.py | 4 ++-- supported_parts.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/adi/ad7689.py b/adi/ad7689.py index 233088bb7..96d6c5744 100644 --- a/adi/ad7689.py +++ b/adi/ad7689.py @@ -12,14 +12,13 @@ class ad7689(rx, context_manager): - """ AD7689 ADC """ + """AD7689 ADC""" _complex_data = False channel = [] # type: ignore _device_name = "" def __init__(self, uri="", device_name=""): - context_manager.__init__(self, uri, self._device_name) compatible_parts = [ @@ -27,6 +26,7 @@ def __init__(self, uri="", device_name=""): "ad7682", "ad7949", "ad7699", + "ad7690", ] self._ctrl = None diff --git a/supported_parts.md b/supported_parts.md index 8fb28080a..90dc8f663 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -159,3 +159,4 @@ - MAX31855 - MAX31865 - MAX9611 +- AD7690 From d9b05aac075378d1f302400008cd59766d2679a3 Mon Sep 17 00:00:00 2001 From: MThoren <36937756+mthoren-adi@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:19:55 -0700 Subject: [PATCH 05/23] Add support for CN0566 "Phaser" (#435) Signed-off-by: Mark Thoren --- adi/__init__.py | 1 + adi/cn0566.py | 622 ++++ doc/source/devices/adi.cn0566.rst | 7 + doc/source/devices/index.rst | 1 + examples/phaser/ADAR_pyadi_functions.py | 101 + examples/phaser/LTE10_MHz.ftr | 138 + examples/phaser/LTE20_MHz.ftr | 138 + examples/phaser/LTE5_MHz.ftr | 138 + examples/phaser/RADAR_FFT_Waterfall.py | 492 ++++ examples/phaser/SDR_functions.py | 148 + examples/phaser/config.py | 42 + examples/phaser/phaser_examples.py | 338 +++ examples/phaser/phaser_find_hb100.py | 185 ++ examples/phaser/phaser_functions.py | 583 ++++ examples/phaser/phaser_gui.py | 2520 +++++++++++++++++ examples/phaser/phaser_minimal_example.py | 193 ++ examples/phaser/phaser_prod_tst.py | 417 +++ examples/phaser/requirements_phaser.txt | 3 + .../CN0566_1234_Sun Jan 22 09-02-39 2023.txt | 9 + supported_parts.md | 1 + tasks.py | 3 +- test/emu/hardware_map.yml | 11 +- 22 files changed, 6089 insertions(+), 2 deletions(-) create mode 100644 adi/cn0566.py create mode 100644 doc/source/devices/adi.cn0566.rst create mode 100644 examples/phaser/ADAR_pyadi_functions.py create mode 100644 examples/phaser/LTE10_MHz.ftr create mode 100644 examples/phaser/LTE20_MHz.ftr create mode 100644 examples/phaser/LTE5_MHz.ftr create mode 100644 examples/phaser/RADAR_FFT_Waterfall.py create mode 100644 examples/phaser/SDR_functions.py create mode 100644 examples/phaser/config.py create mode 100644 examples/phaser/phaser_examples.py create mode 100644 examples/phaser/phaser_find_hb100.py create mode 100644 examples/phaser/phaser_functions.py create mode 100644 examples/phaser/phaser_gui.py create mode 100644 examples/phaser/phaser_minimal_example.py create mode 100644 examples/phaser/phaser_prod_tst.py create mode 100644 examples/phaser/requirements_phaser.txt create mode 100644 examples/phaser/results/CN0566_1234_Sun Jan 22 09-02-39 2023.txt diff --git a/adi/__init__.py b/adi/__init__.py index 177e8a108..c3a5be043 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -72,6 +72,7 @@ from adi.cn0511 import cn0511 from adi.cn0532 import cn0532 from adi.cn0554 import cn0554 +from adi.cn0566 import CN0566 from adi.cn0575 import cn0575 from adi.daq2 import DAQ2 from adi.daq3 import DAQ3 diff --git a/adi/cn0566.py b/adi/cn0566.py new file mode 100644 index 000000000..4d381ccd1 --- /dev/null +++ b/adi/cn0566.py @@ -0,0 +1,622 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import pickle +from time import sleep + +import adi +import numpy as np +from adi.adar1000 import adar1000_array +from adi.adf4159 import adf4159 + + +class CN0566(adf4159, adar1000_array): + """CN0566 class inherits from adar1000_array and adf4159 and adds + operations for beamforming like default configuration, + calibration, set_beam_phase_diff, etc. + _gpios (as one-bit-adc-dac) are instantiated internally. + ad7291 temperature / voltage monitor instantiated internally. + CN0566.sdr property is an instance of a Pluto SDR with updated firmware, + and updated to 2t2r. + + parameters: + uri: type=string + URI of Raspberry Pi attached to the phaser board + verbose: type=boolean + Print extra debug information + """ + + # MWT: Open question: Refactor to nest rather than inherit? + + num_elements = 8 + """Number of antenna elements""" + phase_step_size = 2.8125 # it is 360/2**number of bits. (number of bits = 6) + """Phase adjustment resolution""" + c = 299792458 + """speed of light in m/s""" + element_spacing = 0.015 + """Element to element spacing of the antenna in meters""" + device_mode = "rx" + """For future RX/TX operation. Set to RX.""" + + # Scaling factors for voltage AD7291 monitor, straight from schematic. + _v0_vdd1v8_scale = 1.0 + (10.0 / 10.0) # Resistances in k ohms. + _v1_vdd3v0_scale = 1.0 + (10.0 / 10.0) + _v2_vdd3v3_scale = 1.0 + (10.0 / 10.0) + _v3_vdd4v5_scale = 1.0 + (30.1 / 10.0) + _v4_vdd_amp_scale = 1.0 + (69.8 / 10.0) + _v5_vinput_scale = 1.0 + (30.1 / 10.0) + _v6_imon_scale = 1.0 # LTC4217 IMON = 50uA/A * 20k = 1 V / A + _v7_vtune_scale = 1.0 + (69.8 / 10.0) + + def __init__( + self, + uri=None, + sdr=None, + _chip_ids=["BEAM0", "BEAM1"], + _device_map=[[1], [2]], + _element_map=[[1, 2, 3, 4, 5, 6, 7, 8]], # [[1, 2, 3, 4], [5, 6, 7, 8]], + _device_element_map={ + 1: [7, 8, 5, 6], # i.e. channel2 of device1 (BEAM0), maps to element 8 + 2: [3, 4, 1, 2], + }, + verbose=False, + ): + """ Set up devices, properties, helper methods, etc. """ + if verbose is True: + print("attempting to open ADF4159, uri: ", str(uri)) + adf4159.__init__(self, uri) + if verbose is True: + print("attempting to open ADAR1000 array, uri: ", str(uri)) + sleep(0.5) + adar1000_array.__init__( + self, uri, _chip_ids, _device_map, _element_map, _device_element_map + ) + + if verbose is True: + print("attempting to open gpios , uri: ", str(uri)) + sleep(0.5) + self._gpios = adi.one_bit_adc_dac(uri) + + if verbose is True: + print("attempting to open AD7291 v/t monitor, uri: ", str(uri)) + sleep(0.5) + self._monitor = adi.ad7291(uri) + + """ Initialize all the class variables for the project. """ + + self.Averages = 16 # Number of Avg to be taken. + + self.pcal = [0.0 for i in range(0, (self.num_elements))] + """ Phase calibration array. Add this value to the desired phase. Initialize to zero (no correction). """ + self.ccal = [0.0, 0.0] + """ Gain compensation for the two RX channels in dB. Includes all errors, including the SDRs """ + self.gcal = [1.0 for i in range(0, self.num_elements)] + """ Per-element gain compensation, AFTER above channel compensation. Use to scale value sent to ADAR1000. """ + self.ph_deltas = [ + 0 for i in range(0, (self.num_elements) - 1) + ] # Phase delta between elements + self.sdr = sdr # rx_device/sdr that rx and plots + self.gain_cal = False # gain/phase calibration status flag it goes True when performing calibration + self.phase_cal = False + + ### Initialize ADF4159 / Local Oscillator ### + + self.lo = 10.5e9 # Nominal operating frequency + + BW = 500e6 / 4 + num_steps = 1000 + self.freq_dev_range = int( + BW + ) # frequency deviation range in Hz. This is the total freq deviation of the complete freq ramp + self.freq_dev_step = int( + BW / num_steps + ) # frequency deviation step in Hz. This is fDEV, in Hz. Can be positive or negative + self.freq_dev_time = int( + 1e3 + ) # total time (in us) of the complete frequency ramp + self.ramp_mode = "disabled" # ramp_mode can be: "disabled", "continuous_sawtooth", "continuous_triangular", "single_sawtooth_burst", "single_ramp_burst" + self.delay_word = 4095 # 12 bit delay word. 4095*PFD = 40.95 us. For sawtooth ramps, this is also the length of the Ramp_complete signal + self.delay_clk = "PFD" # can be 'PFD' or 'PFD*CLK1' + self.delay_start_en = 0 # delay start + self.ramp_delay_en = 0 # delay between ramps. + self.trig_delay_en = 0 # triangle delay + self.sing_ful_tri = 0 # full triangle enable/disable -- this is used with the single_ramp_burst mode + self.tx_trig_en = 0 # start a ramp with TXdata + # pll.clk1_value = 100 + # pll.phase_value = 3 + self.powerdown = 0 + self.enable = 0 # 0 = PLL enable. Write this last to update all the registers + + ### Initialize gpios / set outputs ### + self._gpios.gpio_vctrl_1 = 1 # Onboard PLL/LO source + self._gpios.gpio_vctrl_2 = 1 # Send LO to TX circuitry + + self._gpios.gpio_div_mr = 0 # TX switch toggler divider reset + self._gpios.gpio_div_s0 = 0 # TX toggle divider lsb (1s) + self._gpios.gpio_div_s1 = 0 # TX toggle divider 2s + self._gpios.gpio_div_s2 = 0 # TX toggle divider 4s + self._gpios.gpio_rx_load = 0 # ADAR1000 RX load (cycle through RAM table) + self._gpios.gpio_tr = 0 # ADAR1000 transmit / receive mode. RX = 0 (assuming) + self._gpios.gpio_tx_sw = ( + 0 # Direct control of TX switch when div=[000]. 0 = TX_OUT_2, 1 = TX_OUT_1 + ) + # Read input + self.muxout = ( + self._gpios.gpio_muxout + ) # PLL MUXOUT, assign to PLL lock in the future + + def set_tx_sw_div(self, div_ratio): + """ Set TX switch toggle divide ratio. Possible values are: + 0 (direct TX_OUT control via gpio_tx_sw) + divide by 2, 4, 8, 16, 32, 64, 128""" + div_pin_map = {0: 0, 2: 1, 4: 2, 8: 3, 16: 4, 32: 5, 64: 6, 128: 7} + if div_pin_map.__contains__(div_ratio): + self._gpios.gpio_div_s0 = 0b001 & div_pin_map[div_ratio] + self._gpios.gpio_div_s1 = (0b010 & div_pin_map[div_ratio]) >> 1 + self._gpios.gpio_div_s2 = (0b100 & div_pin_map[div_ratio]) >> 2 + else: + print( + "Invalid divide ratio, options are 0 for direct control" + " via gpio_tx_sw, 2, 4, 8, 16, 32, 64, 128" + ) + + def read_monitor(self, verbose=False): + """ Read all voltage / temperature monitor channels. + + Parameters + ---------- + verbose: type=bool + Print each channel's information if true. + returns: + An array of all readings in SI units (deg. C, Volts) + """ + board_temperature = self._monitor.temp0() + v0_vdd1v8 = self._monitor.voltage0() * self._v0_vdd1v8_scale / 1000.0 + v1_vdd3v0 = self._monitor.voltage1() * self._v1_vdd3v0_scale / 1000.0 + v2_vdd3v3 = self._monitor.voltage2() * self._v2_vdd3v3_scale / 1000.0 + v3_vdd4v5 = self._monitor.voltage3() * self._v3_vdd4v5_scale / 1000.0 + v4_vdd_amp = self._monitor.voltage4() * self._v4_vdd_amp_scale / 1000.0 + v5_vinput = self._monitor.voltage5() * self._v5_vinput_scale / 1000.0 + v6_imon = self._monitor.voltage6() * self._v6_imon_scale / 1000.0 + v7_vtune = self._monitor.voltage7() * self._v7_vtune_scale / 1000.0 + if verbose is True: + print("Board temperature: ", board_temperature) + print("1.8V supply: ", v0_vdd1v8) + print("3.0V supply: ", v1_vdd3v0) + print("3.3V supply: ", v2_vdd3v3) + print("4.5V supply: ", v3_vdd4v5) + print("Vtune amp supply: ", v4_vdd_amp) + print("USB C input supply: ", v5_vinput) + print("Board current: ", v6_imon) + print("VTune: ", v7_vtune) + return [ + board_temperature, + v0_vdd1v8, + v1_vdd3v0, + v2_vdd3v3, + v3_vdd4v5, + v4_vdd_amp, + v5_vinput, + v6_imon, + v7_vtune, + ] + + @property + def lo(self): + """Get the VCO output frequency, accounting for the /4 ahead of the ADF4159 RFIN.""" + return self.frequency * 4.0 + + @lo.setter + def lo(self, value): + """Set the VCO output frequency, accounting for the /4 ahead of the ADF4159 RFIN.""" + self.frequency = int(value / 4) + + def configure(self, device_mode="rx"): + """ + Configure the device/beamformer properties like RAM bypass, Tr source etc. + + Parameters + ---------- + device_mode: type=string + ("rx", "tx", "disabled", default = "rx") + """ + self.device_mode = device_mode + for device in self.devices.values(): # Configure ADAR1000s + device.sequencer_enable = False + # False sets a bit high and SPI control + device.beam_mem_enable = ( + False # RAM control vs SPI control of the adar state, reg 0x38, bit 6. + ) + device.bias_mem_enable = ( + False # RAM control vs SPI control of the bias state, reg 0x38, bit 5. + ) + device.pol_state = False # Polarity switch state, reg 0x31, bit 0. True outputs -5V, False outputs 0V + device.pol_switch_enable = ( + False # Enables switch driver for ADTR1107 switch, reg 0x31, bit 3 + ) + device.tr_source = "spi" # TR source for chip, reg 0x31 bit 2. 'ext' sets bit high, 'spi' sets a bit low + device.tr_spi = "rx" # TR SPI control, reg 0x31 bit 1. 'tx' sets bit high, 'rx' sets a bit low + device.tr_switch_enable = ( + True # Switch driver for external switch, reg0x31, bit 4 + ) + device.external_tr_polarity = ( + True # Sets polarity of TR switch compared to TR state of ADAR1000. + ) + + device.rx_vga_enable = True # Enables Rx VGA, reg 0x2E, bit 0. + device.rx_vm_enable = True # Enables Rx VGA, reg 0x2E, bit 1. + device.rx_lna_enable = True # Enables Rx LNA, reg 0x2E, bit 2. bit3,4,5,6 enables RX for all the channels + device._ctrl.reg_write( + 0x2E, 0x7F + ) # bit3,4,5,6 enables RX for all the channels. + device.rx_lna_bias_current = ( + 8 # Sets the LNA bias to the middle of its range + ) + device.rx_vga_vm_bias_current = ( + 22 # Sets the VGA and vector modulator bias. + ) + + device.tx_vga_enable = True # Enables Tx VGA, reg 0x2F, bit0 + device.tx_vm_enable = True # Enables Tx Vector Modulator, reg 0x2F, bit1 + device.tx_pa_enable = True # Enables Tx channel drivers, reg 0x2F, bit2 + device.tx_pa_bias_current = 6 # Sets Tx driver bias current + device.tx_vga_vm_bias_current = 22 # Sets Tx VGA and VM bias. + + if self.device_mode == "rx": + # Configure the device for Rx mode + device.mode = "rx" # Mode of operation, bit 5 of reg 0x31. "rx", "tx", or "disabled". + + SELF_BIASED_LNAs = True + if SELF_BIASED_LNAs: + # Allow the external LNAs to self-bias + # this writes 0xA0 0x30 0x00. Disabling it allows LNAs to stay in self bias mode all the time + device.lna_bias_out_enable = False + # self._ctrl.reg_write(0x30, 0x00) #Disables PA and DAC bias + else: + # Set the external LNA bias + device.lna_bias_on = -0.7 # this writes 0x25 to register 0x2D. + # self._ctrl.reg_write(0x30, 0x20) #Enables PA and DAC bias. + + # Enable the Rx path for each channel + for channel in device.channels: + channel.rx_enable = True # this writes reg0x2E with data 0x00, then reg0x2E with data 0x20. + channel.rx_gain = 127 + # So it overwrites 0x2E, and enables only one channel + + # Configure the device for Tx mode + elif self.device_mode == "tx": + device.mode = "tx" + + # Enable the Tx path for each channel and set the external PA bias + for channel in device.channels: + channel.tx_enable = True + channel.tx_gain = 127 + channel.pa_bias_on = -2 + + else: + raise ValueError( + "Configure Device in proper mode" + ) # If device mode is neither Rx nor Tx + + if self.device_mode == "rx": + device.latch_rx_settings() # writes 0x01 to reg 0x28. + elif self.device_mode == "tx": + device.latch_tx_settings() # writes 0x02 to reg 0x28. + + def save_channel_cal(self, filename="channel_cal_val.pkl"): + """ Saves channel calibration file.""" + with open(filename, "wb") as file1: + pickle.dump(self.ccal, file1) # save calibrated gain value to a file + file1.close() + + def save_gain_cal(self, filename="gain_cal_val.pkl"): + """ Saves gain calibration file.""" + with open(filename, "wb") as file1: + pickle.dump(self.gcal, file1) # save calibrated gain value to a file + file1.close() + + def save_phase_cal(self, filename="phase_cal_val.pkl"): + """ Saves phase calibration file.""" + with open(filename, "wb") as file: + pickle.dump(self.pcal, file) # save calibrated phase value to a file + file.close() + + def load_channel_cal(self, filename="channel_cal_val.pkl"): + """ + Load channel gain compensation values, if not calibrated set all to 0. + + Parameters + ---------- + filename: string + Path/name of channel calibration file + """ + try: + with open(filename, "rb") as file: + self.ccal = pickle.load(file) # Load gain cal values + except FileNotFoundError: + print("file not found, loading default (no channel gain compensation)") + self.ccal = [0.0] * 2 + + def load_gain_cal(self, filename="gain_cal_val.pkl"): + """Load gain calibrated value, if not calibrated set all channel gain to maximum. + + Parameters + ---------- + filename: type=string + Provide path of gain calibration file + """ + try: + with open(filename, "rb") as file1: + self.gcal = pickle.load(file1) # Load gain cal values + except FileNotFoundError: + print("file not found, loading default (all gain at maximum)") + self.gcal = [1.0] * 8 # .append(0x7F) + + def load_phase_cal(self, filename="phase_cal_val.pkl"): + """Load phase calibrated value, if not calibrated set all channel phase correction to 0. + + Parameters + ---------- + filename: type=string + Provide path of phase calibration file + """ + try: + with open(filename, "rb") as file: + self.pcal = pickle.load(file) # Load gain cal values + except FileNotFoundError: + print("file not found, loading default (no phase shift)") + self.pcal = [0.0] * 8 + + def set_rx_hardwaregain(self, gain, apply_cal=True): + """ Set Pluto channel gains + + Parameters + ---------- + gain: type=float + Gain to set both channels to + apply_cal: type=bool + Optionally apply channel gain correction + """ + if apply_cal is True: + self.sdr.rx_hardwaregain_chan0 = int(gain + self.ccal[0]) + self.sdr.rx_hardwaregain_chan1 = int(gain + self.ccal[1]) + + else: + self.sdr.rx_hardwaregain_chan0 = int(gain) + self.sdr.rx_hardwaregain_chan1 = int(gain) + + def set_all_gain(self, value=127, apply_cal=True): + """ Set all channel gains to a single value + + Parameters + ---------- + value: type=int + gain for all channels. Default value is 127 (maximum). + apply_cal: type=bool + Optionally apply gain calibration to all channels. + """ + for i in range(0, 8): + if apply_cal is True: + self.elements.get(i + 1).rx_gain = int(value * self.gcal[i]) + else: # Don't apply gain calibration + self.elements.get(i + 1).rx_gain = value + # Important if you're relying on elements being truly zero'd out + self.elements.get(i + 1).rx_attenuator = not bool(value) + self.latch_tx_settings() # writes 0x01 to reg 0x28 + + def set_chan_gain(self, chan_no: int, gain_val, apply_cal=True): + """ Setl gain of the individua channel/s. + + Parameters + ---------- + chan_no: type=int + It is the index of channel whose gain you want to set + gain_val: type=int or hex + gain_val is the value of gain that you want to set + apply_cal: type=bool + Optionally apply gain calibration for the selected channel + """ + if apply_cal is True: + cval = int(gain_val * self.gcal[chan_no]) + # print( + # "Cal = true, setting channel x to gain y, gcal value: ", + # chan_no, + # ", ", + # cval, + # ", ", + # self.gcal[chan_no], + # ) + self.elements.get(chan_no + 1).rx_gain = cval + # print("reading back: ", self.elements.get(chan_no + 1).rx_gain) + else: # Don't apply gain calibration + # print( + # "Cal = false, setting channel x to gain y: ", + # chan_no, + # ", ", + # int(gain_val), + # ) + self.elements.get(chan_no + 1).rx_gain = int(gain_val) + # Important if you're relying on elements being truly zero'd out + self.elements.get(chan_no + 1).rx_attenuator = not bool(gain_val) + self.latch_rx_settings() + + def set_chan_phase(self, chan_no: int, phase_val, apply_cal=True): + """ Setl phase of the individua channel/s. + + Parameters + ---------- + chan_no: type=int + It is the index of channel whose gain you want to set + phase_val: float + phase_val is the value of phase that you want to set + apply_cal: type=bool + Optionally apply phase calibration + + Notes + ----- + Each device has 4 channels but for top level channel numbers are 1 to 8 so took device number as Quotient of + channel num div by 4 and channel of that dev is overall chan num minus 4 x that dev number. For e.g: + if you want to set gain of channel at index 5 it is 6th channel or 2nd channel of 2nd device so 5//4 = 1 + i.e. index of 2nd device and (5 - 4*(5//4) = 1 i.e. index of channel + """ + + # list(self.devices.values())[chan_no // 4].channels[(chan_no - (4 * (chan_no // 4)))].rx_phase = phase_val + # list(self.devices.values())[chan_no // 4].latch_rx_settings() + if apply_cal is True: + self.elements.get(chan_no + 1).rx_phase = ( + phase_val + self.pcal[chan_no] + ) % 360.0 + else: # Don't apply gain calibration + self.elements.get(chan_no + 1).rx_phase = (phase_val) % 360.0 + + self.latch_rx_settings() + + def set_beam_phase_diff(self, Ph_Diff): + """ Set phase difference between the adjacent channels of devices + + Parameters + ---------- + Ph-Diff: type=float + Ph_diff is the phase difference b/w the adjacent channels of devices + + Notes + ----- + A public method to sweep the phase value from -180 to 180 deg, calculate phase values of all the channel + and set them. If we want beam angle at fixed angle you can pass angle value at which you want center lobe + + Create an empty list. Based on the device number and channel of that device append phase value to that empty + list this creates a list of 4 items. Now write channel of each device, phase values acc to created list + values. This is the structural integrity mentioned above. + """ + + # j = 0 # j is index of device and device indicate the adar1000 on which operation is currently done + # for device in list(self.devices.values()): # device in dict of all adar1000 connected + # channel_phase_value = [] # channel phase value to be written on ind channel + # for ind in range(0, 4): # ind is index of current channel of current device + # channel_phase_value.append((((np.rint(Ph_Diff * ((j * 4) + ind) / self.phase_step_size)) * + # self.phase_step_size) + self.pcal[((j * 4) + ind)]) % 360) + # j += 1 + # i = 0 # i is index of channel of each device + # for channel in device.channels: + # # Set phase depending on the device mode + # if self.device_mode == "rx": + # channel.rx_phase = channel_phase_value[ + # i] # writes to I and Q registers values according to Table 13-16 from datasheet. + # i = i + 1 + # if self.device_mode == "rx": + # device.latch_rx_settings() + # else: + # device.latch_tx_settings() + # # print(channel_phase_value) + + for ch in range(0, 8): + self.elements.get(ch + 1).rx_phase = ( + ((np.rint(Ph_Diff * ch / self.phase_step_size)) * self.phase_step_size) + + self.pcal[ch] + ) % 360.0 + + self.latch_rx_settings() + + def SDR_init(self, SampleRate, TX_freq, RX_freq, Rx_gain, Tx_gain, buffer_size): + """ Initialize Pluto rev C for operation with the phaser. This is a convenience + method that sets several default values, and provides a handle for a few + other CN0566 methods that need access (i.e. set_rx_hardwaregain()) + + parameters + ---------- + SampleRate: type=int + ADC/DAC sample rate. + TX_freq: type=float + Transmit frequency. lo-sdr.TX_freq is what shows up at the TX connector. + RX_freq: type=float + Receive frequency. lo-sdr.RX_freq is what shows up at RX outputs. + Rx_gain: type=float + Receive gain. Set indirectly via set_rx_hardwaregain() + Tx_gain: type=float + Transmit gain, controls TX output amplitude. + buffer_size: type=int + Receive buffer size + """ + self.sdr._ctrl.debug_attrs[ + "adi,frequency-division-duplex-mode-enable" + ].value = "1" # set to fdd mode + self.sdr._ctrl.debug_attrs[ + "adi,ensm-enable-txnrx-control-enable" + ].value = "0" # Disable pin control so spi can move the states + self.sdr._ctrl.debug_attrs["initialize"].value = "1" + self.sdr.rx_enabled_channels = [ + 0, + 1, + ] # enable Rx1 (voltage0) and Rx2 (voltage1) + self.sdr.gain_control_mode_chan0 = "manual" # We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) + self.sdr.gain_control_mode_chan1 = "manual" # We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) + self.sdr._rxadc.set_kernel_buffers_count( + 1 + ) # Default is 4 Rx buffers are stored, but we want to change and immediately measure the result, so buffers=1 + rx = self.sdr._ctrl.find_channel("voltage0") + rx.attrs[ + "quadrature_tracking_en" + ].value = "1" # set to '1' to enable quadrature tracking + self.sdr.sample_rate = int(SampleRate) + self.sdr.rx_lo = int(RX_freq) + self.sdr.rx_buffer_size = int( + buffer_size + ) # small buffers make the scan faster -- and we're primarily just looking at peak power + self.sdr.tx_lo = int(TX_freq) + self.sdr.tx_cyclic_buffer = True + self.sdr.tx_hardwaregain_chan0 = int(-88) # turn off Tx1 + self.sdr.tx_hardwaregain_chan1 = int(Tx_gain) + self.sdr.rx_hardwaregain_chan0 = int(Rx_gain) + self.sdr.rx_hardwaregain_chan1 = int(Rx_gain) + self.sdr.filter = ( + "LTE20_MHz.ftr" # Handy filter for fairly widdeband measurements + ) + # sdr.filter = "/usr/local/lib/osc/filters/LTE5_MHz.ftr" + # sdr.rx_rf_bandwidth = int(SampleRate*2) + # sdr.tx_rf_bandwidth = int(SampleRate*2) + signal_freq = int(SampleRate / 8) + if ( + True + ): # use either DDS or sdr.tx(iq) to generate the Tx waveform. But don't do both! + self.sdr.dds_enabled = [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ] # DDS generator enable state + self.sdr.dds_frequencies = [ + signal_freq, + 0, + signal_freq, + 0, + signal_freq, + 0, + signal_freq, + 0, + ] # Frequencies of DDSs in Hz + self.sdr.dds_scales = [ + 0.5, + 0, + 0.5, + 0, + 0.9, + 0, + 0.9, + 0, + ] # Scale of DDS signal generators Ranges [0,1] + else: + fs = int(SampleRate) + N = 1000 + fc = int(signal_freq / (fs / N)) * (fs / N) + ts = 1 / float(fs) + t = np.arange(0, N * ts, ts) + i = np.cos(2 * np.pi * t * fc) * 2 ** 15 + q = np.sin(2 * np.pi * t * fc) * 2 ** 15 + iq = 0.9 * (i + 1j * q) + self.sdr.tx([iq, iq]) diff --git a/doc/source/devices/adi.cn0566.rst b/doc/source/devices/adi.cn0566.rst new file mode 100644 index 000000000..63dd6b521 --- /dev/null +++ b/doc/source/devices/adi.cn0566.rst @@ -0,0 +1,7 @@ +adi.cn0566 module +================= + +.. automodule:: adi.cn0566 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 291609962..b22625bc1 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -81,6 +81,7 @@ Supported Devices adi.cn0532 adi.cn0540 adi.cn0554 + adi.cn0566 adi.cn0575 adi.daq2 adi.daq3 diff --git a/examples/phaser/ADAR_pyadi_functions.py b/examples/phaser/ADAR_pyadi_functions.py new file mode 100644 index 000000000..47c031780 --- /dev/null +++ b/examples/phaser/ADAR_pyadi_functions.py @@ -0,0 +1,101 @@ +# ADAR_functions.py + +import pickle + +import numpy as np + +verbose = True + + +def ADAR_init(beam): + beam.sequencer_enable = False + beam.beam_mem_enable = False # RAM control vs SPI control of the beam state, reg 0x38, bit 6. False sets bit high and SPI control + beam.bias_mem_enable = False # RAM control vs SPI control of the bias state, reg 0x38, bit 5. False sets bit high and SPI control + beam.pol_state = False # Polarity switch state, reg 0x31, bit 0. True outputs -5V, False outputs 0V + beam.pol_switch_enable = ( + False # Enables switch driver for ADTR1107 switch, reg 0x31, bit 3 + ) + beam.tr_source = "spi" # TR source for chip, reg 0x31 bit 2. 'external' sets bit high, 'spi' sets bit low + beam.tr_spi = ( + "rx" # TR SPI control, reg 0x31 bit 1. 'tx' sets bit high, 'rx' sets bit low + ) + beam.tr_switch_enable = True # Switch driver for external switch, reg0x31, bit 4 + beam.external_tr_polarity = True # Sets polarity of TR switch compared to TR state of ADAR1000. True outputs 0V in Rx mode + + beam.rx_vga_enable = True # Enables Rx VGA, reg 0x2E, bit 0. + beam.rx_vm_enable = True # Enables Rx VGA, reg 0x2E, bit 1. + beam.rx_lna_enable = True # Enables Rx LNA, reg 0x2E, bit 2. + beam.rx_lna_bias_current = 8 # Sets the LNA bias to the middle of its range + beam.rx_vga_vm_bias_current = 22 # Sets the VGA and vector modulator bias. + + +def ADAR_set_mode(beam, mode): + if mode == "rx": + # Configure the device for Rx mode + beam.mode = ( + "rx" # Mode of operation, bit 5 of reg 0x31. "rx", "tx", or "disabled" + ) + # print("When TR pin is low, ADAR1000 is in Rx mode.") + # beam._ctrl.reg_write(0x031, 180) #Enables T/R switch control. When TR is low, ADAR1000 is Rx mode + SELF_BIASED_LNAs = True + if SELF_BIASED_LNAs: + beam.lna_bias_out_enable = False # Allow the external LNAs to self-bias + else: + beam.lna_bias_on = -0.7 # Set the external LNA bias + # Enable the Rx path for each channel + for channel in beam.channels: + channel.rx_enable = True + + +def ADAR_set_Taper(array, gainList): + for index, element in enumerate(array.elements.values()): + element.rx_gain = int(gainList[index] * 127 / 100 * gcal[index]) + element.rx_attenuator = not bool(gainList[index]) + array.latch_rx_settings() + + +def ADAR_set_Phase(array, PhDelta, phase_step_size, phaseList): + for index, element in enumerate(array.elements.values()): + element.rx_phase = ( + (np.rint(PhDelta * index / phase_step_size) * phase_step_size) + + phaseList[index] + + pcal[index] + ) % 360 + array.latch_rx_settings() + + +def load_gain_cal(filename="gain_cal_val.pkl"): + """ Load gain calibrated value, if not calibrated set all channel gain to maximum. + parameters: + filename: type=string + Provide path of gain calibration file + """ + try: + with open(filename, "rb") as file1: + return pickle.load(file1) # Load gain cal values + except FileNotFoundError: + print("file not found, loading default (all gain at maximum)") + return [1.0] * 8 # .append(0x7F) + + +def load_phase_cal(filename="phase_cal_val.pkl"): + """ Load phase calibrated value, if not calibrated set all channel phase correction to 0. + parameters: + filename: type=string + Provide path of phase calibration file + """ + + try: + with open(filename, "rb") as file: + return pickle.load(file) # Load gain cal values + except FileNotFoundError: + print("file not found, loading default (no phase shift)") + return [0.0] * 8 # .append(0) # if it fails load default value i.e. 0 + + +gcal = load_gain_cal() +pcal = load_phase_cal() + +if verbose == True: + print("Gain cal: ", gcal) + print("Phase cal: ", pcal) diff --git a/examples/phaser/LTE10_MHz.ftr b/examples/phaser/LTE10_MHz.ftr new file mode 100644 index 000000000..8b035065e --- /dev/null +++ b/examples/phaser/LTE10_MHz.ftr @@ -0,0 +1,138 @@ +# Generated with the MATLAB AD9361 Filter Design Wizard +# Generated 05-Feb-2015 18:09:59 +# Inputs: +# Data Sample Frequency = 15360000.000000 Hz +TX 3 GAIN 0 INT 2 +RX 3 GAIN -6 DEC 2 +RTX 983040000 245760000 122880000 61440000 30720000 15360000 +RRX 983040000 245760000 122880000 61440000 30720000 15360000 +BWTX 9037204 +BWRX 9113183 +-5,-9 +0,-23 +4,-20 +23,-22 +36,12 +40,20 +18,29 +-13,-5 +-36,-30 +-26,-43 +11,-6 +48,40 +46,64 +-2,24 +-60,-47 +-72,-90 +-16,-49 +70,49 +104,122 +43,85 +-74,-44 +-142,-158 +-83,-134 +71,29 +184,196 +136,196 +-57,-1 +-229,-234 +-205,-274 +26,-46 +272,270 +291,368 +25,114 +-312,-298 +-396,-481 +-102,-212 +342,315 +520,612 +211,344 +-357,-312 +-664,-762 +-362,-522 +349,282 +831,935 +566,757 +-308,-212 +-1024,-1133 +-843,-1074 +215,80 +1253,1365 +1228,1510 +-43,149 +-1539,-1646 +-1796,-2146 +-269,-551 +1933,2016 +2734,3173 +876,1310 +-2624,-2634 +-4727,-5298 +-2426,-3194 +4784,4431 +13988,14435 +20393,21470 +20393,21470 +13988,14435 +4784,4431 +-2426,-3194 +-4727,-5298 +-2624,-2634 +876,1310 +2734,3173 +1933,2016 +-269,-551 +-1796,-2146 +-1539,-1646 +-43,149 +1228,1510 +1253,1365 +215,80 +-843,-1074 +-1024,-1133 +-308,-212 +566,757 +831,935 +349,282 +-362,-522 +-664,-762 +-357,-312 +211,344 +520,612 +342,315 +-102,-212 +-396,-481 +-312,-298 +25,114 +291,368 +272,270 +26,-46 +-205,-274 +-229,-234 +-57,-1 +136,196 +184,196 +71,29 +-83,-134 +-142,-158 +-74,-44 +43,85 +104,122 +70,49 +-16,-49 +-72,-90 +-60,-47 +-2,24 +46,64 +48,40 +11,-6 +-26,-43 +-36,-30 +-13,-5 +18,29 +40,20 +36,12 +23,-22 +4,-20 +0,-23 +-5,-9 diff --git a/examples/phaser/LTE20_MHz.ftr b/examples/phaser/LTE20_MHz.ftr new file mode 100644 index 000000000..504848db3 --- /dev/null +++ b/examples/phaser/LTE20_MHz.ftr @@ -0,0 +1,138 @@ +# Generated with the MATLAB AD9361 Filter Design Wizard +# Generated 05-Feb-2015 17:28:10 +# Inputs: +# Data Sample Frequency = 30720000.000000 Hz +TX 3 GAIN 0 INT 2 +RX 3 GAIN -6 DEC 2 +RTX 983040000 245760000 245760000 122880000 61440000 30720000 +RRX 983040000 491520000 245760000 122880000 61440000 30720000 +BWTX 19365438 +BWRX 19365514 +-5,-9 +0,-23 +4,-20 +23,-22 +36,12 +39,20 +18,29 +-13,-5 +-36,-30 +-26,-43 +11,-6 +48,40 +46,64 +-2,24 +-60,-47 +-72,-90 +-16,-49 +69,49 +104,122 +43,85 +-74,-44 +-142,-158 +-82,-134 +71,29 +184,196 +136,196 +-57,-1 +-228,-234 +-205,-274 +26,-46 +272,270 +291,368 +25,114 +-311,-298 +-395,-481 +-101,-212 +342,315 +519,612 +210,344 +-357,-312 +-663,-762 +-361,-522 +349,282 +829,935 +564,757 +-307,-212 +-1022,-1133 +-841,-1074 +216,80 +1251,1365 +1225,1510 +-44,149 +-1536,-1646 +-1793,-2146 +-268,-551 +1930,2016 +2728,3173 +873,1310 +-2622,-2634 +-4718,-5298 +-2416,-3194 +4788,4431 +13982,14435 +20380,21470 +20380,21470 +13982,14435 +4788,4431 +-2416,-3194 +-4718,-5298 +-2622,-2634 +873,1310 +2728,3173 +1930,2016 +-268,-551 +-1793,-2146 +-1536,-1646 +-44,149 +1225,1510 +1251,1365 +216,80 +-841,-1074 +-1022,-1133 +-307,-212 +564,757 +829,935 +349,282 +-361,-522 +-663,-762 +-357,-312 +210,344 +519,612 +342,315 +-101,-212 +-395,-481 +-311,-298 +25,114 +291,368 +272,270 +26,-46 +-205,-274 +-228,-234 +-57,-1 +136,196 +184,196 +71,29 +-82,-134 +-142,-158 +-74,-44 +43,85 +104,122 +69,49 +-16,-49 +-72,-90 +-60,-47 +-2,24 +46,64 +48,40 +11,-6 +-26,-43 +-36,-30 +-13,-5 +18,29 +39,20 +36,12 +23,-22 +4,-20 +0,-23 +-5,-9 diff --git a/examples/phaser/LTE5_MHz.ftr b/examples/phaser/LTE5_MHz.ftr new file mode 100644 index 000000000..6b8128caf --- /dev/null +++ b/examples/phaser/LTE5_MHz.ftr @@ -0,0 +1,138 @@ +# Generated with the MATLAB AD9361 Filter Design Wizard +# Generated 05-Feb-2015 17:28:36 +# Inputs: +# Data Sample Frequency = 7680000.000000 Hz +TX 3 GAIN 0 INT 2 +RX 3 GAIN -6 DEC 2 +RTX 983040000 122880000 61440000 30720000 15360000 7680000 +RRX 983040000 122880000 61440000 30720000 15360000 7680000 +BWTX 4372840 +BWRX 4694670 +-5,-10 +0,-21 +4,-21 +23,-19 +36,11 +40,20 +18,28 +-13,-5 +-36,-30 +-26,-41 +11,-5 +48,39 +46,61 +-2,22 +-60,-46 +-72,-87 +-16,-46 +70,48 +104,117 +43,81 +-74,-44 +-142,-152 +-83,-127 +71,31 +184,189 +136,187 +-57,-4 +-229,-227 +-205,-261 +26,-40 +272,262 +291,352 +25,105 +-312,-290 +-396,-460 +-102,-197 +342,308 +520,586 +211,323 +-357,-307 +-664,-732 +-362,-492 +349,281 +831,900 +566,717 +-308,-218 +-1024,-1093 +-842,-1020 +215,97 +1253,1322 +1228,1438 +-43,113 +-1539,-1604 +-1796,-2053 +-269,-485 +1933,1987 +2734,3058 +876,1197 +-2624,-2642 +-4727,-5173 +-2426,-3014 +4784,4532 +13988,14372 +20393,21276 +20393,21276 +13988,14372 +4784,4532 +-2426,-3014 +-4727,-5173 +-2624,-2642 +876,1197 +2734,3058 +1933,1987 +-269,-485 +-1796,-2053 +-1539,-1604 +-43,113 +1228,1438 +1253,1322 +215,97 +-842,-1020 +-1024,-1093 +-308,-218 +566,717 +831,900 +349,281 +-362,-492 +-664,-732 +-357,-307 +211,323 +520,586 +342,308 +-102,-197 +-396,-460 +-312,-290 +25,105 +291,352 +272,262 +26,-40 +-205,-261 +-229,-227 +-57,-4 +136,187 +184,189 +71,31 +-83,-127 +-142,-152 +-74,-44 +43,81 +104,117 +70,48 +-16,-46 +-72,-87 +-60,-46 +-2,22 +46,61 +48,39 +11,-5 +-26,-41 +-36,-30 +-13,-5 +18,28 +40,20 +36,11 +23,-19 +4,-21 +0,-21 +-5,-10 diff --git a/examples/phaser/RADAR_FFT_Waterfall.py b/examples/phaser/RADAR_FFT_Waterfall.py new file mode 100644 index 000000000..60b65e5df --- /dev/null +++ b/examples/phaser/RADAR_FFT_Waterfall.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + +"""Many thanks to Marshall Bruner at Colorado State University for originally developing this script""" + +# Imports +import sys +import time +import warnings + +import adi +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +import pyqtgraph as pg +from matplotlib import cm +from numpy import arange, cos, log10, pi, sin +from numpy.fft import fft, fft2, fftshift, ifft2, ifftshift +from PyQt5.QtCore import Qt +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtWidgets import * +from pyqtgraph.Qt import QtCore, QtGui +from scipy import interpolate, signal + +mpl.rcParams["mathtext.fontset"] = "cm" +warnings.filterwarnings("ignore", category=DeprecationWarning) + + +# Instantiate all the Devices +try: + import phaser_config + + rpi_ip = phaser_config.rpi_ip + sdr_ip = "ip:192.168.2.1" # "192.168.2.1, or pluto.local" # IP address of the Transreceiver Block +except: + print("No config file found...") + rpi_ip = "ip:phaser.local" # IP address of the Raspberry Pi + sdr_ip = "ip:192.168.2.1" # "192.168.2.1, or pluto.local" # IP address of the Transreceiver Block + +try: + x = my_sdr.uri + print("Pluto already connected") +except NameError: + print("Pluto not connected...") + my_sdr = adi.ad9361(uri=sdr_ip) + +time.sleep(0.5) + +try: + x = my_phaser.uri + print("cn0566 already connected") +except NameError: + print("cn0566 not open...") + my_phaser = adi.CN0566(uri=rpi_ip, sdr=my_sdr) + +# Initialize both ADAR1000s, set gains to max, and all phases to 0 +my_phaser.configure(device_mode="rx") +my_phaser.load_gain_cal() +my_phaser.load_phase_cal() +for i in range(0, 8): + my_phaser.set_chan_phase(i, 0) + +gain_list = [8, 34, 84, 127, 127, 84, 34, 8] # Blackman taper +for i in range(0, len(gain_list)): + my_phaser.set_chan_gain(i, gain_list[i], apply_cal=True) + +sample_rate = 0.6e6 +center_freq = 2.1e9 +signal_freq = 100e3 +num_slices = 200 +fft_size = 1024 * 16 +img_array = np.zeros((num_slices, fft_size)) + +# Create radio. +# This script is for Pluto Rev C, dual channel setup +my_sdr.sample_rate = int(sample_rate) + +# Configure Rx +my_sdr.rx_lo = int(center_freq) # set this to output_freq - (the freq of the HB100) +my_sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) +my_sdr.rx_buffer_size = int(fft_size) +my_sdr.gain_control_mode_chan0 = "manual" # manual or slow_attack +my_sdr.gain_control_mode_chan1 = "manual" # manual or slow_attack +my_sdr.rx_hardwaregain_chan0 = int(30) # must be between -3 and 70 +my_sdr.rx_hardwaregain_chan1 = int(30) # must be between -3 and 70 +# Configure Tx +my_sdr.tx_lo = int(center_freq) +my_sdr.tx_enabled_channels = [0, 1] +my_sdr.tx_cyclic_buffer = True # must set cyclic buffer to true for the tdd burst mode. Otherwise Tx will turn on and off randomly +my_sdr.tx_hardwaregain_chan0 = -88 # must be between 0 and -88 +my_sdr.tx_hardwaregain_chan1 = -0 # must be between 0 and -88 + +# Enable TDD logic in pluto (this is for synchronizing Rx Buffer to ADF4159 TX input) +# gpio = adi.one_bit_adc_dac(sdr_ip) +# gpio.gpio_phaser_enable = True + +# Configure the ADF4159 Rampling PLL +output_freq = 12.1e9 +BW = 500e6 +num_steps = 1000 +ramp_time = 1e3 # us +ramp_time_s = ramp_time / 1e6 +my_phaser.frequency = int(output_freq / 4) # Output frequency divided by 4 +my_phaser.freq_dev_range = int( + BW / 4 +) # frequency deviation range in Hz. This is the total freq deviation of the complete freq ramp +my_phaser.freq_dev_step = int( + BW / num_steps +) # frequency deviation step in Hz. This is fDEV, in Hz. Can be positive or negative +my_phaser.freq_dev_time = int( + ramp_time +) # total time (in us) of the complete frequency ramp +my_phaser.delay_word = 4095 # 12 bit delay word. 4095*PFD = 40.95 us. For sawtooth ramps, this is also the length of the Ramp_complete signal +my_phaser.delay_clk = "PFD" # can be 'PFD' or 'PFD*CLK1' +my_phaser.delay_start_en = 0 # delay start +my_phaser.ramp_delay_en = 0 # delay between ramps. +my_phaser.trig_delay_en = 0 # triangle delay +my_phaser.ramp_mode = "continuous_triangular" # ramp_mode can be: "disabled", "continuous_sawtooth", "continuous_triangular", "single_sawtooth_burst", "single_ramp_burst" +my_phaser.sing_ful_tri = ( + 0 # full triangle enable/disable -- this is used with the single_ramp_burst mode +) +my_phaser.tx_trig_en = 0 # start a ramp with TXdata +my_phaser.enable = 0 # 0 = PLL enable. Write this last to update all the registers + +# Print config +print( + """ +CONFIG: +Sample rate: {sample_rate}MHz +Num samples: 2^{Nlog2} +Bandwidth: {BW}MHz +Ramp time: {ramp_time}ms +Output frequency: {output_freq}MHz +IF: {signal_freq}kHz +""".format( + sample_rate=sample_rate / 1e6, + Nlog2=int(np.log2(fft_size)), + BW=BW / 1e6, + ramp_time=ramp_time / 1e3, + output_freq=output_freq / 1e6, + signal_freq=signal_freq / 1e3, + ) +) + +# Create a sinewave waveform +fs = int(my_sdr.sample_rate) +print("sample_rate:", fs) +N = int(my_sdr.rx_buffer_size) +fc = int(signal_freq / (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 +q = np.sin(2 * np.pi * t * fc) * 2 ** 14 +iq = 1 * (i + 1j * q) + +fc = int(300e3 / (fs / N)) * (fs / N) +i = np.cos(2 * np.pi * t * fc) * 2 ** 14 +q = np.sin(2 * np.pi * t * fc) * 2 ** 14 +iq_300k = 1 * (i + 1j * q) + +# Send data +my_sdr._ctx.set_timeout(0) +my_sdr.tx([iq * 0.5, iq]) # only send data to the 2nd channel (that's all we need) + +c = 3e8 +default_rf_bw = 500e6 +N_frame = fft_size +freq = np.linspace(-fs / 2, fs / 2, int(N_frame)) +slope = BW / ramp_time_s +dist = (freq - signal_freq) * c / (4 * slope) + +xdata = freq +plot_dist = False + + +class Window(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Interactive FFT") + self.setGeometry(100, 100, 800, 1200) + self.num_rows = 12 + self.UiComponents() + # showing all the widgets + self.show() + + # method for components + def UiComponents(self): + widget = QWidget() + + global layout + layout = QGridLayout() + + # Control Panel + control_label = QLabel("ADALM-PHASER Simple FMCW Radar") + font = control_label.font() + font.setPointSize(20) + control_label.setFont(font) + control_label.setAlignment(Qt.AlignHCenter) # | Qt.AlignVCenter) + layout.addWidget(control_label, 0, 0, 1, 2) + + # Check boxes + self.x_axis_check = QCheckBox("Toggle Range/Frequency x-axis") + font = self.x_axis_check.font() + font.setPointSize(15) + self.x_axis_check.setFont(font) + + self.x_axis_check.stateChanged.connect(self.change_x_axis) + layout.addWidget(self.x_axis_check, 2, 0) + + # Range resolution + # Changes with the RF BW slider + self.range_res_label = QLabel( + "BRF: %0.2f MHz - Rres: %0.2f m" + % (default_rf_bw / 1e6, c / (2 * default_rf_bw)) + ) + font = self.range_res_label.font() + font.setPointSize(15) + self.range_res_label.setFont(font) + self.range_res_label.setAlignment(Qt.AlignRight) + self.range_res_label.setMinimumWidth(300) + layout.addWidget(self.range_res_label, 4, 1) + + # RF bandwidth slider + self.bw_slider = QSlider(Qt.Horizontal) + self.bw_slider.setMinimum(100) + self.bw_slider.setMaximum(500) + self.bw_slider.setValue(int(default_rf_bw / 1e6)) + self.bw_slider.setTickInterval(50) + self.bw_slider.setTickPosition(QSlider.TicksBelow) + self.bw_slider.valueChanged.connect(self.get_range_res) + layout.addWidget(self.bw_slider, 4, 0) + + self.set_bw = QPushButton("Set RF Bandwidth") + self.set_bw.pressed.connect(self.set_range_res) + layout.addWidget(self.set_bw, 5, 0, 1, 2) + + # waterfall level slider + self.low_slider = QSlider(Qt.Horizontal) + self.low_slider.setMinimum(-100) + self.low_slider.setMaximum(100) + self.low_slider.setValue(20) + self.low_slider.setTickInterval(20) + self.low_slider.setTickPosition(QSlider.TicksBelow) + self.low_slider.valueChanged.connect(self.get_water_levels) + layout.addWidget(self.low_slider, 8, 0) + + self.high_slider = QSlider(Qt.Horizontal) + self.high_slider.setMinimum(-100) + self.high_slider.setMaximum(100) + self.high_slider.setValue(60) + self.high_slider.setTickInterval(20) + self.high_slider.setTickPosition(QSlider.TicksBelow) + self.high_slider.valueChanged.connect(self.get_water_levels) + layout.addWidget(self.high_slider, 10, 0) + + self.water_label = QLabel("Waterfall Intensity Levels") + self.water_label.setFont(font) + self.water_label.setAlignment(Qt.AlignCenter) + self.water_label.setMinimumWidth(300) + layout.addWidget(self.water_label, 7, 0) + self.low_label = QLabel("LOW LEVEL: %0.0f" % (self.low_slider.value())) + self.low_label.setFont(font) + self.low_label.setAlignment(Qt.AlignLeft) + self.low_label.setMinimumWidth(300) + layout.addWidget(self.low_label, 8, 1) + self.high_label = QLabel("HIGH LEVEL: %0.0f" % (self.high_slider.value())) + self.high_label.setFont(font) + self.high_label.setAlignment(Qt.AlignLeft) + self.high_label.setMinimumWidth(300) + layout.addWidget(self.high_label, 10, 1) + + self.steer_slider = QSlider(Qt.Horizontal) + self.steer_slider.setMinimum(-80) + self.steer_slider.setMaximum(80) + self.steer_slider.setValue(0) + self.steer_slider.setTickInterval(20) + self.steer_slider.setTickPosition(QSlider.TicksBelow) + self.steer_slider.valueChanged.connect(self.get_steer_angle) + layout.addWidget(self.steer_slider, 14, 0) + self.steer_title = QLabel("Receive Steering Angle") + self.steer_title.setFont(font) + self.steer_title.setAlignment(Qt.AlignCenter) + self.steer_title.setMinimumWidth(300) + layout.addWidget(self.steer_title, 13, 0) + self.steer_label = QLabel("%0.0f DEG" % (self.steer_slider.value())) + self.steer_label.setFont(font) + self.steer_label.setAlignment(Qt.AlignLeft) + self.steer_label.setMinimumWidth(300) + layout.addWidget(self.steer_label, 14, 1) + + # FFT plot + self.fft_plot = pg.plot() + self.fft_plot.setMinimumWidth(1200) + self.fft_curve = self.fft_plot.plot(freq, pen="y", width=6) + title_style = {"size": "20pt"} + label_style = {"color": "#FFF", "font-size": "14pt"} + self.fft_plot.setLabel("bottom", text="Frequency", units="Hz", **label_style) + self.fft_plot.setLabel("left", text="Magnitude", units="dB", **label_style) + self.fft_plot.setTitle("Received Signal - Frequency Spectrum", **title_style) + layout.addWidget(self.fft_plot, 0, 2, self.num_rows, 1) + self.fft_plot.setYRange(-60, 0) + self.fft_plot.setXRange(100e3, 200e3) + + # Waterfall plot + self.waterfall = pg.PlotWidget() + self.imageitem = pg.ImageItem() + self.waterfall.addItem(self.imageitem) + # self.imageitem.scale(0.35, sample_rate / (N)) # this is deprecated -- we have to use setTransform instead + tr = QtGui.QTransform() + tr.scale(0.35, sample_rate / (N)) + self.imageitem.setTransform(tr) + zoom_freq = 40e3 + self.waterfall.setRange(yRange=(100e3, 100e3 + zoom_freq)) + self.waterfall.setTitle("Waterfall Spectrum", **title_style) + self.waterfall.setLabel("left", "Frequency", units="Hz") + self.waterfall.setLabel("bottom", "Time", units="sec") + layout.addWidget(self.waterfall, 0 + self.num_rows + 1, 2, self.num_rows, 1) + self.img_array = np.zeros((num_slices, fft_size)) + + widget.setLayout(layout) + # setting this widget as central widget of the main window + self.setCentralWidget(widget) + + def get_range_res(self): + """ Updates the slider bar label with RF bandwidth and range resolution + Returns: + None + """ + bw = self.bw_slider.value() * 1e6 + range_res = c / (2 * bw) + self.range_res_label.setText( + "BRF: %0.2f MHz - Rres: %0.2f m" + % (bw / 1e6, c / (2 * bw)) + ) + + def get_water_levels(self): + """ Updates the waterfall intensity levels + Returns: + None + """ + if self.low_slider.value() > self.high_slider.value(): + self.low_slider.setValue(self.high_slider.value()) + self.low_label.setText("LOW LEVEL: %0.0f" % (self.low_slider.value())) + self.high_label.setText("HIGH LEVEL: %0.0f" % (self.high_slider.value())) + + def get_steer_angle(self): + """ Updates the steering angle readout + Returns: + None + """ + self.steer_label.setText("%0.0f DEG" % (self.steer_slider.value())) + phase_delta = ( + 2 + * 3.14159 + * 10.25e9 + * 0.014 + * np.sin(np.radians(self.steer_slider.value())) + / (3e8) + ) + my_phaser.set_beam_phase_diff(np.degrees(phase_delta)) + + def set_range_res(self): + """ Sets the RF bandwidth + Returns: + None + """ + global dist, slope + bw = self.bw_slider.value() * 1e6 + slope = bw / ramp_time_s + dist = (freq - signal_freq) * c / (4 * slope) + print("New slope: %0.2fMHz/s" % (slope / 1e6)) + if self.x_axis_check.isChecked() == True: + print("Range axis") + plot_dist = True + range_x = (100e3) * c / (4 * slope) + self.fft_plot.setXRange(0, range_x) + else: + print("Frequency axis") + plot_dist = False + self.fft_plot.setXRange(100e3, 200e3) + my_phaser.freq_dev_range = int(bw / 4) # frequency deviation range in Hz + my_phaser.enable = 0 + + def change_x_axis(self, state): + """ Toggles between showing frequency and range for the x-axis + Args: + state (QtCore.Qt.Checked) : State of check box + Returns: + None + """ + global plot_dist, slope + plot_state = win.fft_plot.getViewBox().state + if state == QtCore.Qt.Checked: + print("Range axis") + plot_dist = True + range_x = (100e3) * c / (4 * slope) + self.fft_plot.setXRange(0, range_x) + else: + print("Frequency axis") + plot_dist = False + self.fft_plot.setXRange(100e3, 200e3) + + +# create pyqt5 app +App = QApplication(sys.argv) + +# create the instance of our Window +win = Window() +index = 0 + + +def update(): + """ Updates the FFT in the window + Returns: + None + """ + global index, xdata, plot_dist, freq, dist + label_style = {"color": "#FFF", "font-size": "14pt"} + + data = my_sdr.rx() + data = data[0] + data[1] + win_funct = np.blackman(len(data)) + y = data * win_funct + sp = np.absolute(np.fft.fft(y)) + sp = np.fft.fftshift(sp) + s_mag = np.abs(sp) / np.sum(win_funct) + s_mag = np.maximum(s_mag, 10 ** (-15)) + s_dbfs = 20 * np.log10(s_mag / (2 ** 11)) + """there's a scaling issue on the y-axis of the waterfallcthe data is off by 300kHz. To fix, I'm just shifting the freq""" + data_shift = data * iq_300k + y = data_shift * win_funct + sp = np.absolute(np.fft.fft(y)) + sp = np.fft.fftshift(sp) + s_mag = np.abs(sp) / np.sum(win_funct) + s_mag = np.maximum(s_mag, 10 ** (-15)) + s_dbfs_shift = 20 * np.log10(s_mag / (2 ** 11)) + + if plot_dist: + win.fft_curve.setData(dist, s_dbfs) + win.fft_plot.setLabel("bottom", text="Distance", units="m", **label_style) + else: + win.fft_curve.setData(freq, s_dbfs) + win.fft_plot.setLabel("bottom", text="Frequency", units="Hz", **label_style) + + win.img_array = np.roll(win.img_array, 1, axis=0) + win.img_array[1] = s_dbfs_shift + win.imageitem.setLevels([win.low_slider.value(), win.high_slider.value()]) + win.imageitem.setImage(win.img_array, autoLevels=False) + + if index == 1: + win.fft_plot.enableAutoRange("xy", False) + index = index + 1 + + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + +# start the app +sys.exit(App.exec()) diff --git a/examples/phaser/SDR_functions.py b/examples/phaser/SDR_functions.py new file mode 100644 index 000000000..3ed7dc720 --- /dev/null +++ b/examples/phaser/SDR_functions.py @@ -0,0 +1,148 @@ +# SDR_functions.py +# These are Pluto control functions + +import pickle +import sys +import time + +import adi +import numpy as np + + +# MWT: Add uri argument... +def SDR_LO_init( + uri, LO_freq +): # program the ADF4159 to be the LO of the external LTC555x mixers + pll = adi.adf4159(uri) + output_freq = int(LO_freq) + pll.frequency = int(output_freq / 4) # Output frequency divided by 4 + BW = 500e6 / 4 + num_steps = 1000 + pll.freq_dev_range = int( + BW + ) # frequency deviation range in Hz. This is the total freq deviation of the complete freq ramp + pll.freq_dev_step = int( + BW / num_steps + ) # frequency deviation step in Hz. This is fDEV, in Hz. Can be positive or negative + pll.freq_dev_time = int(1e3) # total time (in us) of the complete frequency ramp + pll.ramp_mode = "disabled" # ramp_mode can be: "disabled", "continuous_sawtooth", "continuous_triangular", "single_sawtooth_burst", "single_ramp_burst" + pll.delay_word = 4095 # 12 bit delay word. 4095*PFD = 40.95 us. For sawtooth ramps, this is also the length of the Ramp_complete signal + pll.delay_clk = "PFD" # can be 'PFD' or 'PFD*CLK1' + pll.delay_start_en = 0 # delay start + pll.ramp_delay_en = 0 # delay between ramps. + pll.trig_delay_en = 0 # triangle delay + pll.sing_ful_tri = 0 # full triangle enable/disable -- this is used with the single_ramp_burst mode + pll.tx_trig_en = 0 # start a ramp with TXdata + # pll.clk1_value = 100 + # pll.phase_value = 3 + pll.enable = 0 # 0 = PLL enable. Write this last to update all the registers + + +def SDR_init(sdr_address, SampleRate, TX_freq, RX_freq, Rx_gain, Tx_gain, buffer_size): + """Setup contexts""" + sdr = adi.ad9361(uri=sdr_address) + sdr._ctrl.debug_attrs[ + "adi,frequency-division-duplex-mode-enable" + ].value = "1" # set to fdd mode + sdr._ctrl.debug_attrs[ + "adi,ensm-enable-txnrx-control-enable" + ].value = "0" # Disable pin control so spi can move the states + sdr._ctrl.debug_attrs["initialize"].value = "1" + sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) + sdr.gain_control_mode_chan0 = "manual" # We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) + sdr.gain_control_mode_chan1 = "manual" # We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) + sdr._rxadc.set_kernel_buffers_count( + 1 + ) # Default is 4 Rx buffers are stored, but we want to change and immediately measure the result, so buffers=1 + rx = sdr._ctrl.find_channel("voltage0") + rx.attrs[ + "quadrature_tracking_en" + ].value = "1" # set to '1' to enable quadrature tracking + sdr.sample_rate = int(SampleRate) + sdr.rx_lo = int(RX_freq) + sdr.rx_buffer_size = int( + buffer_size + ) # small buffers make the scan faster -- and we're primarily just looking at peak power + sdr.tx_lo = int(TX_freq) + sdr.tx_cyclic_buffer = True + sdr.tx_hardwaregain_chan0 = int(-80) # turn off Tx1 + sdr.tx_hardwaregain_chan1 = int(Tx_gain) + sdr.rx_hardwaregain_chan0 = int(Rx_gain + ccal[0]) + sdr.rx_hardwaregain_chan1 = int(Rx_gain + ccal[1]) + # sdr.filter = "/usr/local/lib/osc/filters/LTE5_MHz.ftr" + # sdr.rx_rf_bandwidth = int(SampleRate*2) + # sdr.tx_rf_bandwidth = int(SampleRate*2) + signal_freq = int(SampleRate / 8) + if ( + True + ): # use either DDS or sdr.tx(iq) to generate the Tx waveform. But don't do both! + sdr.dds_enabled = [1, 1, 1, 1, 1, 1, 1, 1] # DDS generator enable state + sdr.dds_frequencies = [ + signal_freq, + 0, + signal_freq, + 0, + signal_freq, + 0, + signal_freq, + 0, + ] # Frequencies of DDSs in Hz + sdr.dds_scales = [ + 0.5, + 0, + 0.5, + 0, + 0.9, + 0, + 0.9, + 0, + ] # Scale of DDS signal generators Ranges [0,1] + else: + fs = int(SampleRate) + N = 1000 + fc = int(signal_freq / (fs / N)) * (fs / N) + ts = 1 / float(fs) + t = np.arange(0, N * ts, ts) + i = np.cos(2 * np.pi * t * fc) * 2 ** 15 + q = np.sin(2 * np.pi * t * fc) * 2 ** 15 + iq = 0.9 * (i + 1j * q) + sdr.tx([iq, iq]) + return sdr + + +def SDR_setRx(sdr, Rx1_gain, Rx2_gain): + sdr.rx_hardwaregain_chan0 = int(Rx1_gain + ccal[0]) + sdr.rx_hardwaregain_chan1 = int(Rx2_gain + ccal[1]) + + +def SDR_setTx(sdr, Tx_gain): + sdr.tx_hardwaregain_chan0 = int(-80) # turn off Tx1 + sdr.tx_hardwaregain_chan1 = int(Tx_gain) + + +def SDR_getData(sdr): + data = sdr.rx() # read a buffer of data from Pluto using pyadi-iio library (adi.py) + return data + + +def SDR_TxBuffer_Destroy(sdr): + if sdr.tx_cyclic_buffer == True: + sdr.tx_destroy_buffer() + + +def load_channel_cal(filename="channel_cal_val.pkl"): + """ Load Pluto Rx1 and Rx2 calibrated value, if not calibrated set all channel gain correction to 0. + parameters: + filename: type=string + Provide path of phase calibration file + """ + + try: + with open(filename, "rb") as file: + return pickle.load(file) # Load channel cal values + except: + print("file not found, loading default (no channel gain shift)") + return [0.0] * 2 # .append(0) # if it fails load default value i.e. 0 + + +ccal = load_channel_cal() diff --git a/examples/phaser/config.py b/examples/phaser/config.py new file mode 100644 index 000000000..6dd5727f0 --- /dev/null +++ b/examples/phaser/config.py @@ -0,0 +1,42 @@ +# standard config files for the beamformer setups +# This is for use with Pluto (and either 1 or 2 ADAR1000 boards) + +sdr_address = "ip:192.168.2.1" # This is the default Pluto address (You can check/change this in the config.txt file on the Pluto "usb drive") +SignalFreq = ( + 10.525e9 # if hb100_freq_val.pkl is present then this value will be overwritten +) +Rx_freq = int(2.2e9) +Tx_freq = Rx_freq +SampleRate = (3 * 1024) * 1e3 +buffer_size = 1024 # small buffers make the scan faster -- and we're primarily just looking at peak power +Rx_gain = 1 +Tx_gain = -10 +Averages = 1 + +# these Rx_cal phase adjustments get added to the phase_cal_val.pkl (if present) +# use them to tweak the auto cal adjusments +# or delete phase_cal_val.pkl and use whatever phase adjustments you want here +Rx1_cal = 0 +Rx2_cal = 0 +Rx3_cal = 0 +Rx4_cal = 0 +Rx5_cal = 0 +Rx6_cal = 0 +Rx7_cal = 0 +Rx8_cal = 0 + +refresh_time = 100 # refresh time in ms. Auto beam sweep will update at this rate. Too fast makes it hard to adjust the GUI values when sweeping is active +d = 0.014 # element to element spacing of the antenna +use_tx = False # Enable TX path if True (if false, HB100 source) + +start_lab = "Enable All" +# Lab choices are: +# "Lab 1: Steering Angle", +# "Lab 2: Array Factor", +# "Lab 3: Tapering", +# "Lab 4: Grating Lobes", +# "Lab 5: Beam Squint", +# "Lab 6: Quantization", +# "Lab 7: Hybrid Control", +# "Lab 8: Monopulse Tracking", +# "Enable All" diff --git a/examples/phaser/phaser_examples.py b/examples/phaser/phaser_examples.py new file mode 100644 index 000000000..c79238638 --- /dev/null +++ b/examples/phaser/phaser_examples.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + + +# Basic utility script for working with the CN0566 "Phaser" board. Accepts the following +# command line arguments: + +# plot - plot beam pattern, rectangular element weighting. If cal files are present, +# they will be loaded. + +# cal - perform both gain and phase calibration, save to files. + +import sys +import time +from time import sleep + +import matplotlib.pyplot as plt +import numpy as np +from adi import ad9361 +from adi.cn0566 import CN0566 +from phaser_functions import ( + calculate_plot, + channel_calibration, + gain_calibration, + load_hb100_cal, + phase_calibration, +) +from scipy import signal + +try: + import config_custom as config # this has all the key parameters that the user would want to change (i.e. calibration phase and antenna element spacing) + + print("Found custom config file") +except: + print("Didn't find custom config, looking for default.") + try: + import config as config + except: + print("Make sure config.py is in this directory") + sys.exit(0) + +colors = ["black", "gray", "red", "orange", "yellow", "green", "blue", "purple"] + + +def do_cal_channel(): + my_phaser.set_beam_phase_diff(0.0) + channel_calibration(my_phaser, verbose=True) + + +def do_cal_gain(): + my_phaser.set_beam_phase_diff(0.0) + # plot_data = my_phaser.gain_calibration(verbose=True) # Start Gain Calibration + plot_data = gain_calibration(my_phaser, verbose=True) # Start Gain Calibration + plt.figure(4) + plt.title("Gain calibration FFTs") + plt.xlabel("FFT Bin number") + plt.ylabel("Amplitude (ADC counts)") + for i in range(0, 8): + plt.plot(plot_data[i], color=colors[i]) + plt.show() + + +def do_cal_phase(): + # PhaseValues, plot_data = my_phaser.phase_calibration( + # verbose=True + # ) # Start Phase Calibration + PhaseValues, plot_data = phase_calibration( + my_phaser, verbose=True + ) # Start Phase Calibration + plt.figure(5) + plt.title("Phase sweeps of adjacent elements") + plt.xlabel("Phase difference (degrees)") + plt.ylabel("Amplitude (ADC counts)") + for i in range(0, 7): + plt.plot(PhaseValues, plot_data[i], color=colors[i]) + plt.show() + + +# First try to connect to a locally connected CN0566. On success, connect, +# on failure, connect to remote CN0566 + +try: + print("Attempting to connect to CN0566 via ip:localhost...") + my_phaser = CN0566(uri="ip:localhost") + print("Found CN0566. Connecting to PlutoSDR via default IP address...") + my_sdr = ad9361(uri="ip:192.168.2.1") + print("PlutoSDR connected.") + +except: + print("CN0566 on ip.localhost not found, connecting via ip:phaser.local...") + my_phaser = CN0566(uri="ip:phaser.local") + print("Found CN0566. Connecting to PlutoSDR via shared context...") + my_sdr = ad9361(uri="ip:phaser.local:50901") + print("Found SDR on shared phaser.local.") + +my_phaser.sdr = my_sdr # Set my_phaser.sdr + +time.sleep(0.5) + + +# By default device_mode is "rx" +my_phaser.configure(device_mode="rx") + + +my_phaser.SDR_init(30000000, config.Tx_freq, config.Rx_freq, 6, -6, 1024) + +my_phaser.load_channel_cal() +# First crack at compensating for channel gain mismatch +my_phaser.sdr.rx_hardwaregain_chan0 = ( + my_phaser.sdr.rx_hardwaregain_chan0 + my_phaser.ccal[0] +) +my_phaser.sdr.rx_hardwaregain_chan1 = ( + my_phaser.sdr.rx_hardwaregain_chan1 + my_phaser.ccal[1] +) + +# Set up receive frequency. When using HB100, you need to know its frequency +# fairly accurately. Use the cn0566_find_hb100.py script to measure its frequency +# and write out to the cal file. IF using the onboard TX generator, delete +# the cal file and set frequency via config.py or config_custom.py. + +try: + my_phaser.SignalFreq = load_hb100_cal() + print("Found signal freq file, ", my_phaser.SignalFreq) +except: + my_phaser.SignalFreq = config.SignalFreq + print("No signal freq found, keeping at ", my_phaser.SignalFreq) + print("And using TX path. Make sure antenna is connected.") + config.use_tx = True # Assume no HB100, use TX path. + +# Configure SDR parameters. + +my_sdr.filter = "LTE20_MHz.ftr" # Load LTE 20 MHz filter + + +# use_tx = config.use_tx +use_tx = True + +if use_tx is True: + # To use tx path, set chan1 gain "high" keep chan0 attenuated. + my_sdr.tx_hardwaregain_chan0 = int( + -88 + ) # this is a negative number between 0 and -88 + my_sdr.tx_hardwaregain_chan1 = int(-3) + my_sdr.tx_lo = config.Tx_freq # int(2.2e9) + + my_sdr.dds_single_tone( + int(2e6), 0.9, 1 + ) # sdr.dds_single_tone(tone_freq_hz, tone_scale_0to1, tx_channel) +else: + # To disable rx, set attenuation to a high value and set frequency far from rx. + my_sdr.tx_hardwaregain_chan0 = int( + -88 + ) # this is a negative number between 0 and -88 + my_sdr.tx_hardwaregain_chan1 = int(-88) + my_sdr.tx_lo = int(1.0e9) + + +# Configure CN0566 parameters. +# ADF4159 and ADAR1000 array attributes are exposed directly, although normally +# accessed through other methods. + + +# my_phaser.frequency = (10492000000 + 2000000000) // 4 #6247500000//2 + +# Onboard source w/ external Vivaldi +my_phaser.frequency = ( + int(my_phaser.SignalFreq) + config.Rx_freq +) // 4 # PLL feedback via /4 VCO output +my_phaser.freq_dev_step = 5690 +my_phaser.freq_dev_range = 0 +my_phaser.freq_dev_time = 0 +my_phaser.powerdown = 0 +my_phaser.ramp_mode = "disabled" + +# If you want to use previously calibrated values load_gain and load_phase values by passing path of previously +# stored values. If this is not done system will be working as uncalibrated system. +# These will fail gracefully and default to no calibration if files not present. + +my_phaser.load_gain_cal("gain_cal_val.pkl") +my_phaser.load_phase_cal("phase_cal_val.pkl") + +# This can be useful in Array size vs beam width experiment or beamtappering experiment. +# Set the gain of outer channels to 0 and beam width will increase and so on. + +# To set gain of all channels with different values. +# Here's where you would apply a window / taper function, +# but we're starting with rectangular / SINC1. + +gain_list = [127, 127, 127, 127, 127, 127, 127, 127] +for i in range(0, len(gain_list)): + my_phaser.set_chan_gain(i, gain_list[i], apply_cal=True) + +# Averages decide number of time samples are taken to plot and/or calibrate system. By default it is 1. +my_phaser.Averages = 4 + +# Aim the beam at boresight by default +my_phaser.set_beam_phase_diff(0.0) + +# Really basic options - "plot" to plot continuously, "cal" to calibrate both gain and phase. +func = sys.argv[1] if len(sys.argv) >= 2 else "plot" + + +if func == "cal": + input( + "Calibrating gain and phase - place antenna at mechanical boresight in front of the array, then press enter..." + ) + print("Calibrating gain mismatch between SDR channels, then saving cal file...") + do_cal_channel() + my_phaser.save_channel_cal() + print("Calibrating Gain, verbosely, then saving cal file...") + do_cal_gain() # Start Gain Calibration + my_phaser.save_gain_cal() # Default filename + print("Calibrating Phase, verbosely, then saving cal file...") + do_cal_phase() # Start Phase Calibration + my_phaser.save_phase_cal() # Default filename + print("Done calibration") + +if func == "plot": + do_plot = True +else: + do_plot = False + +while do_plot == True: + try: + start = time.time() + my_phaser.set_beam_phase_diff(0.0) + time.sleep(0.25) + data = my_sdr.rx() + data = my_sdr.rx() + ch0 = data[0] + ch1 = data[1] + f, Pxx_den0 = signal.periodogram( + ch0[1:-1], 30000000, "blackman", scaling="spectrum" + ) + f, Pxx_den1 = signal.periodogram( + ch1[1:-1], 30000000, "blackman", scaling="spectrum" + ) + + plt.figure(1) + plt.clf() + plt.plot(np.real(ch0), color="red") + plt.plot(np.imag(ch0), color="blue") + plt.plot(np.real(ch1), color="green") + plt.plot(np.imag(ch1), color="black") + np.real + plt.xlabel("data point") + plt.ylabel("output code") + plt.draw() + + plt.figure(2) + plt.clf() + plt.semilogy(f, Pxx_den0) + plt.semilogy(f, Pxx_den1) + plt.ylim([1e-5, 1e6]) + plt.xlabel("frequency [Hz]") + plt.ylabel("PSD [V**2/Hz]") + plt.draw() + + # Plot the output based on experiment that you are performing + print("Plotting...") + + plt.figure(3) + plt.ion() + # plt.show() + ( + gain, + angle, + delta, + diff_error, + beam_phase, + xf, + max_gain, + PhaseValues, + ) = calculate_plot(my_phaser) + print("Sweeping took this many seconds: " + str(time.time() - start)) + # gain, = my_phaser.plot(plot_type="monopulse") + plt.clf() + plt.scatter(angle, gain, s=10) + plt.scatter(angle, delta, s=10) + plt.show() + + plt.pause(0.05) + time.sleep(0.05) + print("Total took this many seconds: " + str(time.time() - start)) + except KeyboardInterrupt: + do_plot = False + print("Exiting Loop") + +if func == "div_test": + while True: + my_phaser.set_tx_sw_div(0) + sleep(1.0) + my_phaser.set_tx_sw_div(2) + sleep(1.0) + my_phaser.set_tx_sw_div(4) + sleep(1.0) + my_phaser.set_tx_sw_div(8) + sleep(1.0) + my_phaser.set_tx_sw_div(16) + sleep(1.0) + my_phaser.set_tx_sw_div(32) + sleep(1.0) + my_phaser.set_tx_sw_div(64) + sleep(1.0) + my_phaser.set_tx_sw_div(128) + sleep(1.0) diff --git a/examples/phaser/phaser_find_hb100.py b/examples/phaser/phaser_find_hb100.py new file mode 100644 index 000000000..f669b2576 --- /dev/null +++ b/examples/phaser/phaser_find_hb100.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + + +# Utility script to find the frequency of an HB100 microwave source. +# Also serves as basic example for setting / stepping the frequency of +# the phaser's PLL, capturing data, calculating FFTs, and stitching together +# FFTs that span several bands. + + +import os +import pickle +import socket +import sys +import time +from time import sleep + +import matplotlib.pyplot as plt +import numpy as np +from adi import ad9361 +from adi.cn0566 import CN0566 +from phaser_functions import save_hb100_cal, spec_est +from scipy import signal + +# First try to connect to a locally connected CN0566. On success, connect, +# on failure, connect to remote CN0566 + +try: + print("Attempting to connect to CN0566 via ip:localhost...") + my_phaser = CN0566(uri="ip:localhost") + print("Found CN0566. Connecting to PlutoSDR via default IP address...") + my_sdr = ad9361(uri="ip:192.168.2.1") + print("PlutoSDR connected.") + +except: + print("CN0566 on ip.localhost not found, connecting via ip:phaser.local...") + my_phaser = CN0566(uri="ip:phaser.local") + print("Found CN0566. Connecting to PlutoSDR via shared context...") + my_sdr = ad9361(uri="ip:phaser.local:50901") + print("Found SDR on shared phaser.local.") + +my_phaser.sdr = my_sdr # Set my_phaser.sdr + +time.sleep(0.5) + +# By default device_mode is "rx" +my_phaser.configure(device_mode="rx") + +# Configure SDR parameters. + +my_sdr._ctrl.debug_attrs["adi,frequency-division-duplex-mode-enable"].value = "1" +my_sdr._ctrl.debug_attrs[ + "adi,ensm-enable-txnrx-control-enable" +].value = "0" # Disable pin control so spi can move the states +my_sdr._ctrl.debug_attrs["initialize"].value = "1" + +my_sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) +my_sdr._rxadc.set_kernel_buffers_count(1) # No stale buffers to flush +rx = my_sdr._ctrl.find_channel("voltage0") +rx.attrs["quadrature_tracking_en"].value = "1" # enable quadrature tracking +my_sdr.sample_rate = int(30000000) # Sampling rate +my_sdr.rx_buffer_size = int(4 * 256) +my_sdr.rx_rf_bandwidth = int(10e6) +# We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) +my_sdr.gain_control_mode_chan0 = "manual" # DISable AGC +my_sdr.gain_control_mode_chan1 = "manual" +my_sdr.rx_hardwaregain_chan0 = 0 # dB +my_sdr.rx_hardwaregain_chan1 = 0 # dB + +my_sdr.rx_lo = int(2.0e9) # Downconvert by 2GHz # Receive Freq + +my_sdr.filter = "LTE20_MHz.ftr" # Handy filter for fairly widdeband measurements + +# Make sure the Tx channels are attenuated (or off) and their freq is far away from Rx +# this is a negative number between 0 and -88 +my_sdr.tx_hardwaregain_chan0 = int(-80) +my_sdr.tx_hardwaregain_chan1 = int(-80) + + +# Configure CN0566 parameters. +# ADF4159 and ADAR1000 array attributes are exposed directly, although normally +# accessed through other methods. + + +# Set initial PLL frequency to HB100 nominal + +my_phaser.SignalFreq = 10.525e9 +my_phaser.lo = int(my_phaser.SignalFreq) + my_sdr.rx_lo + + +gain_list = [64] * 8 +for i in range(0, len(gain_list)): + my_phaser.set_chan_gain(i, gain_list[i], apply_cal=False) + +# Aim the beam at boresight (zero degrees). Place HB100 right in front of array. +my_phaser.set_beam_phase_diff(0.0) + +# Averages decide number of time samples are taken to plot and/or calibrate system. By default it is 1. +my_phaser.Averages = 8 + +# Initialize arrays for amplitudes, frequencies +full_ampl = np.empty(0) +full_freqs = np.empty(0) + +# Set up range of frequencies to sweep. Sample rate is set to 30Msps, +# for a total of 30MHz of bandwidth (quadrature sampling) +# Filter is 20MHz LTE, so you get a bit less than 20MHz of usable +# bandwidth. Set step size to something less than 20MHz to ensure +# complete coverage. +f_start = 10.0e9 +f_stop = 10.7e9 +f_step = 10e6 + +for freq in range(int(f_start), int(f_stop), int(f_step)): + # print("frequency: ", freq) + my_phaser.SignalFreq = freq + my_phaser.frequency = ( + int(my_phaser.SignalFreq) + my_sdr.rx_lo + ) // 4 # PLL feedback via /4 VCO output + + data = my_sdr.rx() + data_sum = data[0] + data[1] + # max0 = np.max(abs(data[0])) + # max1 = np.max(abs(data[1])) + # print("max signals: ", max0, max1) + ampl, freqs = spec_est(data_sum, 30000000, ref=2 ^ 12, plot=False) + ampl = np.fft.fftshift(ampl) + ampl = np.flip(ampl) # Just an experiment... + freqs = np.fft.fftshift(freqs) + freqs += freq + full_freqs = np.concatenate((full_freqs, freqs)) + full_ampl = np.concatenate((full_ampl, ampl)) + sleep(0.1) +full_freqs /= 1e9 # Hz -> GHz + +peak_index = np.argmax(full_ampl) +peak_freq = full_freqs[peak_index] +print("Peak frequency found at ", full_freqs[peak_index], " GHz.") + +plt.figure(2) +plt.title("Full Spectrum, peak at " + str(full_freqs[peak_index]) + " GHz.") +plt.plot(full_freqs, full_ampl, linestyle="", marker="o", ms=2) +plt.xlabel("Frequency [GHz]") +plt.ylabel("Signal Strength") +plt.show() +print("You may need to close plot to continue...") + +prompt = input("Save cal file? (y or n)") +if prompt.upper() == "Y": + save_hb100_cal(peak_freq * 1e9) + +del my_sdr +del my_phaser diff --git a/examples/phaser/phaser_functions.py b/examples/phaser/phaser_functions.py new file mode 100644 index 000000000..02935d335 --- /dev/null +++ b/examples/phaser/phaser_functions.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + + +# Utility functions for CN0566 Phaser + +import pickle +from time import sleep + +import numpy as np +from numpy import ( + absolute, + argmax, + argsort, + cos, + exp, + floor, + linspace, + log10, + multiply, + pi, +) +from numpy.fft import fft, fftfreq, fftshift +from scipy import signal + + +def to_sup(angle): + """ Return suplimentary angle if greater than 180 degrees. """ + if angle > 180.0: + angle -= 360.0 + return angle + + +def find_peak_bin(cn0566, verbose=False): + """ Simple function to find the peak frequency bin of the incoming signal. + sets nomial phases and gains first.""" + win = np.blackman(cn0566.sdr.rx_buffer_size) + # First, locate fundamental. + cn0566.set_all_gain(127) + cn0566.set_beam_phase_diff(0.0) + data = cn0566.sdr.rx() # read a buffer of data + y_sum = (data[0] + data[1]) * win + s_sum = np.fft.fftshift(np.absolute(np.fft.fft(y_sum))) + return np.argmax(s_sum) + + +def calculate_plot(cn0566, gcal_element=0, cal_element=0): + """ Calculate all the values required to do different antenna pattern plots. + parameters: + cn0566: Handle to CN0566 instance + returns: + gain: Antenna gain data + angle: Antenna angles (calculated from phases sent to array) + delta: Delta between sub-arrays (for monopulse tracking) + diff_error: + beam_phase: + xf: + max_gain: + PhaseValues: Actual phase values sent to array + """ + + sweep_angle = 180 # This swweps from -70 deg to +70 + # These are all the phase deltas (i.e. phase difference between Rx1 and Rx2, then Rx2 and Rx3, etc.) we'll sweep + PhaseValues = np.arange(-(sweep_angle), (sweep_angle), cn0566.phase_step_size) + max_signal = ( + -100000 + ) # Reset max_signal. We'll keep track of the maximum signal we get as we do this 140 loop. + max_angle = -90 # Reset max_angle. This is the angle where we saw the max signal. + gain, delta, beam_phase, angle, diff_error = ( + [], + [], + [], + [], + [], + ) # Create empty lists + NumSamples = cn0566.sdr.rx_buffer_size + win = np.blackman(NumSamples) + win /= np.average(win) + + for PhDelta in PhaseValues: # These sweeps phase value from -180 to 180 + # set Phase of channels based on Calibration Flag status and calibration element + + cn0566.set_beam_phase_diff(PhDelta) + # arcsin argument must be between 1 and -1, or numpy will throw a warning + if PhDelta >= 0: + SteerAngle = np.degrees( + np.arcsin( + max( + min( + 1, + (cn0566.c * np.radians(np.abs(PhDelta))) + / (2 * np.pi * cn0566.SignalFreq * cn0566.element_spacing), + ), + -1, + ) + ) + ) # positive PhaseDelta covers 0deg to 90 deg + else: + SteerAngle = -( + np.degrees( + np.arcsin( + max( + min( + 1, + (cn0566.c * np.radians(np.abs(PhDelta))) + / ( + 2 + * np.pi + * cn0566.SignalFreq + * cn0566.element_spacing + ), + ), + -1, + ) + ) + ) + ) # negative phase delta covers 0 deg to -90 deg + + total_sum, total_delta, total_angle = 0, 0, 0 + + for count in range(0, cn0566.Averages): # repeat loop and average the results + data = ( + cn0566.sdr.rx() + ) # read a buffer of data from Pluto using pyadi-iio library (adi.py) + y_sum = (data[0] + data[1]) * win + y_delta = (data[0] - data[1]) * win + s_sum = np.fft.fftshift(np.absolute(np.fft.fft(y_sum))) + s_delta = np.fft.fftshift(np.absolute(np.fft.fft(y_delta))) + total_angle = total_angle + ( + np.angle(s_sum[np.argmax(s_sum)]) - np.angle(s_delta[np.argmax(s_sum)]) + ) + s_mag_sum = np.maximum( + np.abs(s_sum[np.argmax(s_sum)]) * 2 / np.sum(win), 10 ** (-15) + ) # Prevent taking log of zero + s_mag_delta = np.maximum( + np.abs(s_delta[np.argmax(s_sum)]) * 2 / np.sum(win), 10 ** (-15) + ) + total_sum = total_sum + ( + 20 * np.log10(s_mag_sum / (2 ** 12)) + ) # sum up all the loops, then we'll avg + total_delta = total_delta + (20 * np.log10(s_mag_delta / (2 ** 12))) + + PeakValue_sum = total_sum / cn0566.Averages + PeakValue_delta = total_delta / cn0566.Averages + PeakValue_angle = total_angle / cn0566.Averages + + if np.sign(PeakValue_angle) == -1: + target_error = min( + -0.01, + ( + np.sign(PeakValue_angle) * (PeakValue_sum - PeakValue_delta) + + np.sign(PeakValue_angle) * (PeakValue_sum + PeakValue_delta) / 2 + ) + / (PeakValue_sum + PeakValue_delta), + ) + else: + target_error = max( + 0.01, + ( + np.sign(PeakValue_angle) * (PeakValue_sum - PeakValue_delta) + + np.sign(PeakValue_angle) * (PeakValue_sum + PeakValue_delta) / 2 + ) + / (PeakValue_sum + PeakValue_delta), + ) + + if ( + PeakValue_sum > max_signal + ): # take the largest value, so that we know where to point the compass + max_signal = PeakValue_sum + # max_angle = PeakValue_angle + # max_PhDelta = PhDelta + data_fft = data[0] + data[1] + gain.append(PeakValue_sum) + delta.append(PeakValue_delta) + beam_phase.append(PeakValue_angle) + angle.append(SteerAngle) + diff_error.append(target_error) + + y = data_fft * win + sp = np.absolute(np.fft.fft(y)) + sp = np.fft.fftshift(sp) + s_mag = ( + np.abs(sp) * 2 / np.sum(win) + ) # Scale FFT by window and /2 since we are using half the FFT spectrum + s_mag = np.maximum(s_mag, 10 ** (-15)) + max_gain = 20 * np.log10( + s_mag / (2 ** 12) + ) # Pluto is a 12 bit ADC, so use that to convert to dBFS + ts = 1 / float(cn0566.sdr.sample_rate) + xf = np.fft.fftfreq(NumSamples, ts) + xf = np.fft.fftshift(xf) # this is the x axis (freq in Hz) for our fft plot + # Return values/ parameter based on Calibration Flag status + + return gain, angle, delta, diff_error, beam_phase, xf, max_gain, PhaseValues + + +def get_signal_levels(cn0566, verbose=False): + """" Measure signal levels. Without a decent signal, all bets are off. """ + peak_bin = find_peak_bin(cn0566) + # channel_levels, plot_data = measure_channel_gains(cn0566, peak_bin, verbose=False) + # return channel_levels + + channel_levels = [] + + if verbose is True: + print("Peak bin at ", peak_bin, " out of ", cn0566.sdr.rx_buffer_size) + # gcal_element indicates current element/channel which is being calibrated + for element in range(0, (cn0566.num_elements)): + if verbose is True: + print("Calibrating Element " + str(element)) + + gcal_val, spectrum = measure_element_gain(cn0566, element, peak_bin, verbose) + if verbose is True: + print("Measured signal level (ADC counts): " + str(gcal_val)) + channel_levels.append(gcal_val) # make a list of intermediate cal values + return channel_levels + + +def channel_calibration(cn0566, verbose=False): + """" Do this BEFORE gain_calibration. + Performs calibration between the two ADAR1000 channels. Accounts for all + sources of mismatch between the two channels: ADAR1000s, mixers, and + the SDR (Pluto) inputs. """ + peak_bin = find_peak_bin(cn0566) + channel_levels, plot_data = measure_channel_gains(cn0566, peak_bin, verbose=False) + ch_mismatch = 20.0 * np.log10(channel_levels[0] / channel_levels[1]) + if verbose is True: + print("channel mismatch: ", ch_mismatch, " dB") + if ch_mismatch > 0: # Channel 0 hihger, boost ch1: + cn0566.ccal = [0.0, ch_mismatch] + else: # Channel 1 higher, boost ch0: + cn0566.ccal = [-ch_mismatch, 0.0] + pass + + +def gain_calibration(cn0566, verbose=False): + """ Perform the Gain Calibration routine.""" + + """Set the gain calibration flag and create an empty gcal list. Looping through all the possibility i.e. setting + gain of one of the channel to max and all other to 0 create a zero-list where number of 0's depend on total + channels. Replace only 1 element with max gain at a time. Now set gain values according to above Note.""" + + cn0566.gain_cal = True # Gain Calibration Flag + gcalibrated_values = [] # Intermediate cal values list + plot_data = [] + peak_bin = find_peak_bin(cn0566) + if verbose is True: + print("Peak bin at ", peak_bin, " out of ", cn0566.sdr.rx_buffer_size) + # gcal_element indicates current element/channel which is being calibrated + for gcal_element in range(0, (cn0566.num_elements)): + if verbose is True: + print("Calibrating Element " + str(gcal_element)) + + gcal_val, spectrum = measure_element_gain( + cn0566, gcal_element, peak_bin, verbose=True + ) + if verbose is True: + print("Measured signal level (ADC counts): " + str(gcal_val)) + gcalibrated_values.append(gcal_val) # make a list of intermediate cal values + plot_data.append(spectrum) + + """ Minimum gain of intermediated cal val is set to Maximum value as we cannot go beyond max value and gain + of all other channels are set accordingly""" + print("gcalibrated values: ", gcalibrated_values) + for k in range(0, 8): + # x = ((gcalibrated_values[k] * 127) / (min(gcalibrated_values))) + cn0566.gcal[k] = min(gcalibrated_values) / (gcalibrated_values[k]) + + cn0566.gain_cal = ( + False # Reset the Gain calibration Flag once system gain is calibrated + ) + return plot_data + # print(cn0566.gcal) + + +def measure_channel_gains( + cn0566, peak_bin, verbose=False +): # Default to central element + """ Calculate all the values required to do different plots. It method calls set_beam_phase_diff and + sets the Phases of all channel. All the math is done here. + parameters: + gcal_element: type=int + If gain calibration is taking place, it indicates element number whose gain calibration is + is currently taking place + cal_element: type=int + If Phase calibration is taking place, it indicates element number whose phase calibration is + is currently taking place + peak_bin: type=int + Peak bin to examine around for amplitude + """ + width = 10 # Bins around fundamental to sum + win = signal.windows.flattop(cn0566.sdr.rx_buffer_size) + win /= np.average(np.abs(win)) # Normalize to unity gain + plot_data = [] + channel_level = [] + cn0566.set_rx_hardwaregain(6, False) + for channel in range(0, 2): + # Start with sdr CH0 elements + cn0566.set_all_gain(0, apply_cal=False) # Start with all gains set to zero + cn0566.set_chan_gain( + (1 - channel) * 4 + 0, + 127, + apply_cal=False, # 1-channel because wonky channel mapping!! + ) # Set element to max + cn0566.set_chan_gain( + (1 - channel) * 4 + 1, 127, apply_cal=False + ) # Set element to max + cn0566.set_chan_gain( + (1 - channel) * 4 + 2, 127, apply_cal=False + ) # Set element to max + cn0566.set_chan_gain( + (1 - channel) * 4 + 3, 127, apply_cal=False + ) # Set element to max + + sleep(1.0) # todo - remove when driver fixed to compensate for ADAR1000 quirk + if verbose: + print("measuring channel ", channel) + total_sum = 0 + # win = np.blackman(cn0566.sdr.rx_buffer_size) + + spectrum = np.zeros(cn0566.sdr.rx_buffer_size) + + for count in range( + 0, cn0566.Averages + ): # repeatsnip loop and average the results + data = cn0566.sdr.rx() # todo - remove once confirmed no flushing necessary + data = cn0566.sdr.rx() # read a buffer of data + y_sum = (data[0] + data[1]) * win + + s_sum = np.fft.fftshift(np.absolute(np.fft.fft(y_sum))) + spectrum += s_sum + + # Look for peak value within window around fundamental (reject interferers) + s_mag_sum = np.max(s_sum[peak_bin - width : peak_bin + width]) + total_sum += s_mag_sum + + spectrum /= cn0566.Averages * cn0566.sdr.rx_buffer_size + PeakValue_sum = total_sum / (cn0566.Averages * cn0566.sdr.rx_buffer_size) + plot_data.append(spectrum) + channel_level.append(PeakValue_sum) + + return channel_level, plot_data + + +def measure_element_gain( + cn0566, cal, peak_bin, verbose=False +): # Default to central element + """ Calculate all the values required to do different plots. It method calls set_beam_phase_diff and + sets the Phases of all channel. All the math is done here. + parameters: + gcal_element: type=int + If gain calibration is taking place, it indicates element number whose gain calibration is + is currently taking place + cal_element: type=int + If Phase calibration is taking place, it indicates element number whose phase calibration is + is currently taking place + peak_bin: type=int + Peak bin to examine around for amplitude + """ + width = 10 # Bins around fundamental to sum + cn0566.set_rx_hardwaregain(6) # Channel calibration defaults to True + cn0566.set_all_gain(0, apply_cal=False) # Start with all gains set to zero + cn0566.set_chan_gain(cal, 127, apply_cal=False) # Set element to max + sleep(1.0) # todo - remove when driver fixed to compensate for ADAR1000 quirk + if verbose: + print("measuring element: ", cal) + total_sum = 0 + # win = np.blackman(cn0566.sdr.rx_buffer_size) + win = signal.windows.flattop(cn0566.sdr.rx_buffer_size) + win /= np.average(np.abs(win)) # Normalize to unity gain + spectrum = np.zeros(cn0566.sdr.rx_buffer_size) + + for count in range(0, cn0566.Averages): # repeatsnip loop and average the results + data = cn0566.sdr.rx() # todo - remove once confirmed no flushing necessary + data = cn0566.sdr.rx() # read a buffer of data + y_sum = (data[0] + data[1]) * win + + s_sum = np.fft.fftshift(np.absolute(np.fft.fft(y_sum))) + spectrum += s_sum + + # Look for peak value within window around fundamental (reject interferers) + s_mag_sum = np.max(s_sum[peak_bin - width : peak_bin + width]) + total_sum += s_mag_sum + + spectrum /= cn0566.Averages * cn0566.sdr.rx_buffer_size + PeakValue_sum = total_sum / (cn0566.Averages * cn0566.sdr.rx_buffer_size) + + return PeakValue_sum, spectrum + + +def phase_cal_sweep(cn0566, peak_bin, ref=0, cal=1): + """ Calculate all the values required to do different plots. It method + calls set_beam_phase_diff and sets the Phases of all channel. + parameters: + gcal_element: type=int + If gain calibration is taking place, it indicates element number whose gain calibration is + is currently taking place + cal_element: type=int + If Phase calibration is taking place, it indicates element number whose phase calibration is + is currently taking place + peak_bin: type=int + Which bin the fundamental is in. + This prevents detecting other spurs when deep in a null. + """ + + cn0566.set_all_gain(0) # Reset all elements to zero + cn0566.set_chan_gain(ref, 127, apply_cal=True) # Set two adjacent elements to zero + cn0566.set_chan_gain(cal, 127, apply_cal=True) + sleep(1.0) + + cn0566.set_chan_phase(ref, 0.0, apply_cal=False) # Reference element + # win = np.blackman(cn0566.sdr.rx_buffer_size) + win = signal.windows.flattop(cn0566.sdr.rx_buffer_size) # Super important! + win /= np.average(np.abs(win)) # Normalize to unity gain + width = 10 # Bins around fundamental to sum + sweep_angle = 180 + # These are all the phase deltas (i.e. phase difference between Rx1 and Rx2, then Rx2 and Rx3, etc.) we'll sweep + PhaseValues = np.arange(-(sweep_angle), (sweep_angle), cn0566.phase_step_size) + + gain = [] # Create empty lists + for phase in PhaseValues: # These sweeps phase value from -180 to 180 + # set Phase of channels based on Calibration Flag status and calibration element + cn0566.set_chan_phase(cal, phase, apply_cal=False) + total_sum = 0 + for count in range(0, cn0566.Averages): # repeat loop and average the results + data = cn0566.sdr.rx() # read a buffer of data + data = cn0566.sdr.rx() + y_sum = (data[0] + data[1]) * win + s_sum = np.fft.fftshift(np.absolute(np.fft.fft(y_sum))) + + # Pick (uncomment) one: + # 1) RSS sum a few bins around max + # s_mag_sum = np.sqrt( + # np.sum(np.square(s_sum[peak_bin - width : peak_bin + width])) + # ) + + # 2) Take maximum value + # s_mag_sum = np.maximum(s_mag_sum, 10 ** (-15)) + + # 3) Apparently the correct way, use flat-top window, look for peak + s_mag_sum = np.max(s_sum[peak_bin - width : peak_bin + width]) + s_mag_sum = np.max(s_sum) + total_sum += s_mag_sum + PeakValue_sum = total_sum / (cn0566.Averages * cn0566.sdr.rx_buffer_size) + gain.append(PeakValue_sum) + + return ( + PhaseValues, + gain, + ) # beam_phase, max_gain + + +def phase_calibration(cn0566, verbose=False): + """ Perform the Phase Calibration routine.""" + + """ Set the phase calibration flag and create an empty pcal list. Looping through all the possibility + i.e. setting gain of two adjacent channels to gain calibrated values and all other to 0 create a zero-list + where number of 0's depend on total channels. Replace gain value of 2 adjacent channel. + Now set gain values according to above Note.""" + peak_bin = find_peak_bin(cn0566) + if verbose is True: + print("Peak bin at ", peak_bin, " out of ", cn0566.sdr.rx_buffer_size) + + # cn0566.phase_cal = True # Gain Calibration Flag + # cn0566.load_gain_cal('gain_cal_val.pkl') # Load gain cal val as phase cal is dependent on gain cal + cn0566.pcal = [0, 0, 0, 0, 0, 0, 0, 0] + cn0566.ph_deltas = [0, 0, 0, 0, 0, 0, 0] + plot_data = [] + # cal_element indicates current element/channel which is being calibrated + # As there are 8 channels and we take two adjacent chans for calibration we have 7 cal_elements + for cal_element in range(0, 7): + if verbose is True: + print("Calibrating Element " + str(cal_element)) + + PhaseValues, gain, = phase_cal_sweep( + cn0566, peak_bin, cal_element, cal_element + 1 + ) + + ph_delta = to_sup((180 - PhaseValues[gain.index(min(gain))]) % 360.0) + if verbose is True: + print("Null found at ", PhaseValues[gain.index(min(gain))]) + print("Phase Delta to correct: ", ph_delta) + cn0566.ph_deltas[cal_element] = ph_delta + + cn0566.pcal[cal_element + 1] = to_sup( + (cn0566.pcal[cal_element] - ph_delta) % 360.0 + ) + plot_data.append(gain) + return PhaseValues, plot_data + + +def save_hb100_cal(freq, filename="hb100_freq_val.pkl"): + """ Saves measured frequency calibration file.""" + with open(filename, "wb") as file1: + pickle.dump(freq, file1) # save calibrated gain value to a file + file1.close() + + +def load_hb100_cal(filename="hb100_freq_val.pkl"): + """ Load frequency measurement value, set to 10.5GHz if no + parameters: + filename: type=string + Provide path of gain calibration file + """ + try: + with open(filename, "rb") as file1: + freq = pickle.load(file1) # Load gain cal values + except Exception: + print("file not found, loading default 10.5GHz") + return freq + + +def spec_est(x, fs, ref=2 ** 15, plot=False): + + N = len(x) + + # Apply window + window = signal.kaiser(N, beta=38) + window /= np.average(window) + x = multiply(x, window) + + # Use FFT to get the amplitude of the spectrum + ampl = 1 / N * fftshift(absolute(fft(x))) + ampl = 20 * log10(ampl / ref + 10 ** -20) + + # FFT frequency bins + freqs = fftshift(fftfreq(N, 1 / fs)) + + # ampl and freqs for real data + if not np.iscomplexobj(x): + ampl = ampl[0 : len(ampl) // 2] + freqs = freqs[0 : len(freqs) // 2] + + if plot: + # Plot signal, showing how endpoints wrap from one chunk to the next + import matplotlib.pyplot as plt + + plt.subplot(2, 1, 1) + plt.plot(x, ".-") + plt.plot(1, 1, "r.") # first sample of next chunk + plt.margins(0.1, 0.1) + plt.xlabel("Time [s]") + # Plot shifted data on a shifted axis + plt.subplot(2, 1, 2) + plt.plot((freqs), (ampl)) + plt.margins(0.1, 0.1) + plt.xlabel("Frequency [Hz]") + plt.tight_layout() + plt.show() + + return ampl, freqs diff --git a/examples/phaser/phaser_gui.py b/examples/phaser/phaser_gui.py new file mode 100644 index 000000000..2bb02385c --- /dev/null +++ b/examples/phaser/phaser_gui.py @@ -0,0 +1,2520 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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 datetime +import os +import socket +import sys +import time +import tkinter as tk +import tkinter.messagebox as mb +import warnings +from tkinter import * +from tkinter import ttk + +import adi +import matplotlib.pyplot as plt +import numpy as np +from ADAR_pyadi_functions import * # import the ADAR1000 functions (These all start with ADAR_xxxx) +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.widgets import Cursor +from phaser_functions import load_hb100_cal +from scipy import signal +from SDR_functions import * # import the SDR functions (These all start with SDR_xxxx) + +try: + import config_custom as config # this has all the key parameters that the user would want to change (i.e. calibration phase and antenna element spacing) + + print("Found custom config file") +except: + print("Didn't find custom config, looking for default.") + try: + import config as config + except: + print("Make sure config.py is in this directory") + sys.exit(0) + +try: + config.SignalFreq = load_hb100_cal() + print("Found signal freq file, ", config.SignalFreq) +except: + print("No signal freq found, keeping at ", config.SignalFreq) + + +# Figure out if we're running on the Raspberry Pi, indicated by a host name of "phaser". + +if socket.gethostname().find(".") >= 0: + my_hostname = socket.gethostname() +else: + my_hostname = socket.gethostbyaddr(socket.gethostname())[0] + +if "phaser" in my_hostname: # See if we're running locally on Raspberry Pi + rpi_ip = "ip:localhost" + sdr_ip = "ip:192.168.2.1" # Historical - assume default Pluto IP + print("Hostname is phaser, connecting to ", rpi_ip, " and ", sdr_ip) + +else: # NOT running on the phaser, connect to phaser.local over network + rpi_ip = "ip:phaser.local" # IP address of the remote Raspberry Pi + # rpi_ip = "ip:169.254.225.48" # Hard code an IP here for debug + # sdr_ip = "ip:pluto.local" # Pluto IP, with modified IP address or not + sdr_ip = "ip:phaser.local:50901" # Context Forwarding in libiio 0.24! + print("Hostname is NOT phaser, connecting to ", rpi_ip, " and ", sdr_ip) + + +gpios = adi.one_bit_adc_dac(rpi_ip) +gpios.gpio_vctrl_1 = 1 # 1=Use onboard PLL/LO source (0=disable PLL and VCO, and set switch to use external LO input) +gpios.gpio_vctrl_2 = ( + 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) +) + +# setup GPIOs to control if Tx is output on OUT1 or OUT2 +gpios.gpio_div_mr = 1 +gpios.gpio_div_s0 = 0 +gpios.gpio_div_s1 = 0 +gpios.gpio_div_s2 = 0 +gpios.gpio_tx_sw = 0 # gpio_tx_sw is "gpio_w" on schematic. 0=OUT1, 1=OUT2 +time.sleep(0.5) + + +class App: + def __init__(self, master): + """SET DEFAULT VALUES""" + self.time0 = datetime.datetime.now() + self.sdr_address = sdr_ip + self.SignalFreq = config.SignalFreq + self.Tx_freq = config.Tx_freq # Pluto's Tx LO freq. + self.Rx_freq = config.Rx_freq # Pluto's Rx LO freq + self.LO_freq = self.SignalFreq + self.Rx_freq # freq of the LTC5548 mixer LO + self.SampleRate = config.SampleRate + self.Rx_gain = config.Rx_gain + self.Tx_gain = config.Tx_gain + self.Averages = config.Averages + self.RxGain1 = 100 + self.RxGain2 = 100 + self.RxGain3 = 100 + self.RxGain4 = 100 + self.RxGain5 = 100 + self.RxGain6 = 100 + self.RxGain7 = 100 + self.RxGain8 = 100 + self.Rx1_cal = config.Rx1_cal + self.Rx2_cal = config.Rx2_cal + self.Rx3_cal = config.Rx3_cal + self.Rx4_cal = config.Rx4_cal + self.Rx5_cal = config.Rx5_cal + self.Rx6_cal = config.Rx6_cal + self.Rx7_cal = config.Rx7_cal + self.Rx8_cal = config.Rx8_cal + self.refresh_time = config.refresh_time + self.c = 299792458 # speed of light in m/s + self.d = config.d + self.saved_gain = [] + self.saved_angle = [] + self.saved_gainB = [] + self.saved_angleB = [] + self.ArrayGain = [] + self.ArrayAngle = [] + self.ArrayError = [] + self.ArrayBeamPhase = [] + self.TrackArray = [] + self.Gain_time = [-100] * 100 + self.win_width = 0 + self.win_height = 0 + for i in range(0, 1000): + self.TrackArray.append(0) # array of zeros + self.max_hold = -1000 + self.min_hold = 1000 + """Initialize Pluto""" + self.sdr = SDR_init( + self.sdr_address, + self.SampleRate, + self.Tx_freq, + self.Rx_freq, + self.Rx_gain, + self.Tx_gain, + config.buffer_size, + ) + SDR_LO_init(rpi_ip, self.LO_freq) + + """Initialize the ADAR1000""" + time.sleep(0.5) + self.array = adi.adar1000_array( + uri=rpi_ip, + chip_ids=[ + "BEAM0", + "BEAM1", + ], # these are the ADAR1000s' labels in the device tree + device_map=[[1], [2]], + element_map=[[1, 2, 3, 4, 5, 6, 7, 8]], + device_element_map={ + 1: [7, 8, 5, 6], # i.e. channel2 of device1 (BEAM0), maps to element 8 + 2: [3, 4, 1, 2], + }, + ) + for device in self.array.devices.values(): + ADAR_init( + device + ) # resets the ADAR1000, then reprograms it to the standard config + ADAR_set_mode( + device, "rx" + ) # configures for rx or tx. And sets the LNAs for Receive mode or the PA's for Transmit mode + gainList = [ + self.RxGain1, + self.RxGain2, + self.RxGain3, + self.RxGain4, + self.RxGain5, + self.RxGain6, + self.RxGain7, + self.RxGain8, + ] + ADAR_set_Taper(self.array, gainList) + + master.protocol( + "WM_DELETE_WINDOW", self.closeProgram + ) # clicking the "x" button to close the window will shut things down properly (using the closeProgram method) + self.master = master + + """BUILD THE GUI: Master Frame""" + self.refresh = tk.IntVar() + check_refresh = tk.Checkbutton( + self.master, + text="Auto Refresh Data", + highlightthickness=0, + variable=self.refresh, + command=self.updater, + onvalue=1, + offvalue=0, + anchor=W, + relief=FLAT, + ) + check_refresh.grid(row=14, column=0, columnspan=2, sticky=E + W) + self.refresh.set(0) + b1 = Button(self.master, text="Acquire Data", command=self.updater) + b1.grid(row=14, column=2, columnspan=2, sticky=E + W) + button_exit = Button( + self.master, + text="Close Program", + command=self.closeProgram, + bd=4, + bg="LightYellow3", + relief=RAISED, + ) + button_save = Button( + self.master, text="Copy Plot A", fg="green", command=self.savePlot + ) + button_save.grid(row=14, column=5, columnspan=1, sticky=E + W) + button_save2 = Button( + self.master, text="Copy Plot B", fg="purple", command=self.savePlotB + ) + button_save2.grid(row=14, column=6, columnspan=1, sticky=E + W) + button_clear = Button(self.master, text="Clear Memory", command=self.clearPlot) + button_clear.grid(row=14, column=7, columnspan=1, sticky=E + W) + button_exit.grid(row=14, column=8, columnspan=1, padx=50, pady=10, sticky=E + W) + + cntrl_width = 450 + cntrl_height = 600 + cntrl_tabs = ttk.Notebook(self.master, height=cntrl_height, width=cntrl_width) + cntrl_tabs.grid(padx=10, pady=10, row=0, column=0, columnspan=4, sticky=N) + frame1 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame2 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame3 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame4 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame5 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame6 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame7 = Frame(cntrl_tabs, width=cntrl_width, height=cntrl_height) + frame1.grid(row=0, column=0) + frame2.grid(row=0, column=1) + frame3.grid(row=0, column=1) + frame4.grid(row=0, column=1) + frame5.grid(row=0, column=1) + frame6.grid(row=0, column=1) + frame7.grid(row=0, column=1) + cntrl_tabs.add(frame1, text="Config") + cntrl_tabs.add(frame2, text="Gain") + cntrl_tabs.add(frame3, text="Phase") + cntrl_tabs.add(frame4, text="BW") + cntrl_tabs.add(frame5, text="Bits") + cntrl_tabs.add(frame6, text="Digital") + cntrl_tabs.add(frame7, text="Plot Options") + """Frame1: Config""" + + def Tx_mode_select(value): + if value == "Transmit on OUT1": + gpios.gpio_tx_sw = ( + 0 # gpio_tx_sw is "gpio_w" on schematic. 0=OUT1, 1=OUT2 + ) + gpios.gpio_vctrl_2 = 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) + slide_TxGain.config(state=ACTIVE, troughcolor="LightYellow3") + slide_TxGain.set(config.Tx_gain) + elif value == "Transmit on OUT2": + gpios.gpio_tx_sw = ( + 1 # gpio_tx_sw is "gpio_w" on schematic. 0=OUT1, 1=OUT2 + ) + gpios.gpio_vctrl_2 = 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) + slide_TxGain.config(state=ACTIVE, troughcolor="LightYellow3") + slide_TxGain.set(config.Tx_gain) + else: + gpios.gpio_vctrl_2 = 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) + slide_TxGain.set(-80) + slide_TxGain.config(state=DISABLED, troughcolor="light gray") + + self.freq = tk.DoubleVar() + self.RxGain = tk.DoubleVar() + self.TxGain = tk.DoubleVar() + self.Tx_select = StringVar() + self.Tx_select.set("Transmit Disabled") + slide_SignalFreq = Scale( + frame1, + from_=9.5, + to=11, + variable=self.freq, + resolution=0.001, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_RxGain = Scale( + frame1, + from_=0, + to=60, + resolution=1, + variable=self.RxGain, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_TxGain = Scale( + frame1, + from_=-80, + to=-3, + resolution=1, + variable=self.TxGain, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + Tx_sel_menu = OptionMenu( + frame1, + self.Tx_select, + "Transmit on OUT1", + "Transmit on OUT2", + "Transmit Disabled", + command=Tx_mode_select, + ) + Tx_sel_menu.grid( + row=9, column=0, padx=100, pady=2, rowspan=3, columnspan=3, sticky=E + W + ) + # slide_Average=Scale(frame1, from_=1, to=20, resolution=1, variable=self.Avg, troughcolor="LightYellow3", bd=2, orient=HORIZONTAL, relief=SUNKEN, length=200) + slide_SignalFreq.grid(row=0, column=0, padx=10, pady=10, rowspan=3) + slide_RxGain.grid(row=3, column=0, padx=10, pady=10, rowspan=3) + slide_TxGain.grid(row=6, column=0, padx=10, pady=10, rowspan=3) + # slide_Average.grid(row=9, column=0, padx=10, pady=10, rowspan=3) + slide_SignalFreq.set(self.SignalFreq / 1e9) + slide_RxGain.set(int(self.Rx_gain)) + slide_TxGain.set(int(self.Tx_gain)) + Tx_mode_select(self.Tx_select) + tk.Label(frame1, text="Signal Freq (GHz)", relief=SUNKEN, anchor=W).grid( + row=1, column=2, sticky=E + W + ) + tk.Label(frame1, text="Rx Gain (dB)", relief=SUNKEN, anchor=W).grid( + row=4, column=2, sticky=E + W + ) + tk.Label(frame1, text="Tx Gain (dB)", relief=SUNKEN, anchor=W).grid( + row=7, column=2, sticky=E + W + ) + # tk.Label(frame1, text="Times to Average", relief=SUNKEN, anchor=W).grid(row=10, column=2, sticky=E+W) + self.update_time = tk.IntVar() + slide_refresh_time = Scale( + frame1, + from_=0, + to=1000, + variable=self.update_time, + resolution=50, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_refresh_time.grid(row=16, column=0, padx=10, pady=10, rowspan=3) + tk.Label(frame1, text="Wait Time (ms)", relief=SUNKEN, anchor=W).grid( + row=17, column=2, pady=20, sticky=E + W + ) + self.update_time.set(self.refresh_time) + + # self.Divider = tk.Label(frame1, relief=SUNKEN, anchor=W).grid(row=18, column=0, columnspan=3, pady=10, padx=5, sticky=E+W) + self.RxPhaseDelta = tk.DoubleVar() + slide_RxPhaseDelta = Scale( + frame1, + from_=-75, + to=75, + resolution=1, + digits=2, + variable=self.RxPhaseDelta, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_RxPhaseDelta.grid(row=23, column=0, padx=10, pady=10, rowspan=3) + slide_RxPhaseDelta.set(0) + + self.PhaseLabel_text = tk.StringVar() + self.PhaseVal_text = tk.StringVar() + self.label_phase = tk.Label( + frame1, textvariable=self.PhaseLabel_text, anchor=W + ).grid(row=26, column=0, columnspan=3, pady=10, padx=2, sticky=E + W) + self.PhaseVal_label = tk.Label( + frame1, textvariable=self.PhaseVal_text, anchor=W + ).grid(row=27, column=0, columnspan=3, pady=2, padx=2, sticky=E + W) + self.PhaseLabel_text.set("") + + def zero_PhaseDelta(): + slide_RxPhaseDelta.set(0) + + static_phase_label = tk.Button( + frame1, + text="Steering Angle", + relief=RAISED, + anchor=W, + command=zero_PhaseDelta, + highlightthickness=0, + ) + static_phase_label.grid(row=24, column=2, sticky=E + W) + self.PhaseVal_text.set("") + + def mode_select(value): + if value == "Signal vs Time": + print("Plotting Signal vs Time") + self.plot_tabs.add(self.frame15) # Signal Tracking Plot + self.plot_tabs.select(4) + self.Gain_time = [-100] * 100 + if value == "Static Phase" or value == "Signal vs Time": + slide_RxPhaseDelta.grid() + static_phase_label.grid() + self.PhaseLabel_text.set("Element 1-8 Phase Values: ") + self.PhaseVal_text.set("El 1-8 Phase Values: ") + else: + slide_RxPhaseDelta.grid_remove() + static_phase_label.grid_remove() + self.PhaseLabel_text.set("") + self.PhaseVal_text.set("") + if value == "Tracking": + self.plot_tabs.select(3) + self.find_peak() + else: + self.updater() + + self.mode_var = StringVar() + self.mode_var.set("Beam Sweep") + slide_RxPhaseDelta.grid_remove() + static_phase_label.grid_remove() + mode_Menu = OptionMenu( + frame1, + self.mode_var, + "Beam Sweep", + "Static Phase", + "Signal vs Time", + "Tracking", + command=mode_select, + ) + mode_Menu.grid(row=19, column=0, padx=10, pady=10, rowspan=1, sticky=E + W) + tk.Label(frame1, text="Mode Selection", relief=SUNKEN, anchor=W).grid( + row=19, column=2, pady=20, sticky=E + W + ) + + """Frame2: Gain""" + self.Rx1Gain_set = tk.IntVar() + self.Rx2Gain_set = tk.IntVar() + self.Rx3Gain_set = tk.IntVar() + self.Rx4Gain_set = tk.IntVar() + self.Rx5Gain_set = tk.IntVar() + self.Rx6Gain_set = tk.IntVar() + self.Rx7Gain_set = tk.IntVar() + self.Rx8Gain_set = tk.IntVar() + self.Sym_set = tk.IntVar() + self.Sym_set = 0 + + def sym_Rx1(val): + if self.Sym_set.get() == 1: + slide_Rx8Gain.configure(state="normal") + slide_Rx8Gain.set(val) + slide_Rx8Gain.configure(state="disabled") + + def sym_Rx2(val): + if self.Sym_set.get() == 1: + slide_Rx7Gain.configure(state="normal") + slide_Rx7Gain.set(val) + slide_Rx7Gain.configure(state="disabled") + + def sym_Rx3(val): + if self.Sym_set.get() == 1: + slide_Rx6Gain.configure(state="normal") + slide_Rx6Gain.set(val) + slide_Rx6Gain.configure(state="disabled") + + def sym_Rx4(val): + if self.Sym_set.get() == 1: + slide_Rx5Gain.configure(state="normal") + slide_Rx5Gain.set(val) + slide_Rx5Gain.configure(state="disabled") + + slide_Rx1Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx1Gain_set, + command=sym_Rx1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx2Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx2Gain_set, + command=sym_Rx2, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx3Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx3Gain_set, + command=sym_Rx3, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx4Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx4Gain_set, + command=sym_Rx4, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx5Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx5Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx6Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx6Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx7Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx7Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx8Gain = Scale( + frame2, + from_=0, + to=100, + resolution=1, + variable=self.Rx8Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + + slide_Rx1Gain.grid(row=0, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx2Gain.grid(row=3, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx3Gain.grid(row=6, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx4Gain.grid(row=9, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx5Gain.grid(row=12, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx6Gain.grid(row=15, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx7Gain.grid(row=18, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx8Gain.grid(row=21, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_Rx1Gain.set(100) + slide_Rx2Gain.set(100) + slide_Rx3Gain.set(100) + slide_Rx4Gain.set(100) + slide_Rx5Gain.set(100) + slide_Rx6Gain.set(100) + slide_Rx7Gain.set(100) + slide_Rx8Gain.set(100) + + def Rx1_toggle(): + if slide_Rx1Gain.get() == 0: + slide_Rx1Gain.set(100) + else: + slide_Rx1Gain.set(0) + + def Rx2_toggle(): + if slide_Rx2Gain.get() == 0: + slide_Rx2Gain.set(100) + else: + slide_Rx2Gain.set(0) + + def Rx3_toggle(): + if slide_Rx3Gain.get() == 0: + slide_Rx3Gain.set(100) + else: + slide_Rx3Gain.set(0) + + def Rx4_toggle(): + if slide_Rx4Gain.get() == 0: + slide_Rx4Gain.set(100) + else: + slide_Rx4Gain.set(0) + + def Rx5_toggle(): + if slide_Rx5Gain.get() == 0: + slide_Rx5Gain.set(100) + else: + slide_Rx5Gain.set(0) + + def Rx6_toggle(): + if slide_Rx6Gain.get() == 0: + slide_Rx6Gain.set(100) + else: + slide_Rx6Gain.set(0) + + def Rx7_toggle(): + if slide_Rx7Gain.get() == 0: + slide_Rx7Gain.set(100) + else: + slide_Rx7Gain.set(0) + + def Rx8_toggle(): + if slide_Rx8Gain.get() == 0: + slide_Rx8Gain.set(100) + else: + slide_Rx8Gain.set(0) + + tk.Button( + frame2, + text="Rx1 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx1_toggle, + highlightthickness=0, + ).grid(row=1, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx2 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx2_toggle, + highlightthickness=0, + ).grid(row=4, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx3 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx3_toggle, + highlightthickness=0, + ).grid(row=7, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx4 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx4_toggle, + highlightthickness=0, + ).grid(row=10, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx5 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx5_toggle, + highlightthickness=0, + ).grid(row=13, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx6 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx6_toggle, + highlightthickness=0, + ).grid(row=16, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx7 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx7_toggle, + highlightthickness=0, + ).grid(row=19, column=3, sticky=E + W) + tk.Button( + frame2, + text="Rx8 Gain (%)", + relief=RAISED, + anchor=W, + command=Rx8_toggle, + highlightthickness=0, + ).grid(row=22, column=3, sticky=E + W) + + def sym_sel(): + if self.Sym_set.get() == 1: + slide_Rx5Gain.configure(state="normal") # 'normal' + slide_Rx6Gain.configure(state="normal") # 'normal' + slide_Rx7Gain.configure(state="normal") # 'normal' + slide_Rx8Gain.configure(state="normal") # 'normal' + slide_Rx5Gain.set(self.Rx4Gain_set.get()) + slide_Rx6Gain.set(self.Rx3Gain_set.get()) + slide_Rx7Gain.set(self.Rx2Gain_set.get()) + slide_Rx8Gain.set(self.Rx1Gain_set.get()) + slide_Rx5Gain.configure(state="disabled") # 'normal' + slide_Rx6Gain.configure(state="disabled") # 'normal' + slide_Rx7Gain.configure(state="disabled") # 'normal' + slide_Rx8Gain.configure(state="disabled") # 'normal' + if self.Sym_set.get() == 0: + slide_Rx5Gain.configure(state="normal") # 'disabled' + slide_Rx6Gain.configure(state="normal") # 'disabled' + slide_Rx7Gain.configure(state="normal") # 'disabled' + slide_Rx8Gain.configure(state="normal") # 'disabled' + + self.Sym_set = tk.IntVar() + check_Sym = tk.Checkbutton( + frame2, + text="Symmetric Taper", + highlightthickness=0, + variable=self.Sym_set, + onvalue=1, + offvalue=0, + command=sym_sel, + relief=SUNKEN, + anchor=W, + ) + check_Sym.grid(row=24, column=0, columnspan=2, padx=5, pady=5, sticky=E + W) + + def taper_profile(taper_var): + if taper_var == 1: # Rect Window + gain1 = 100 + gain2 = 100 + gain3 = 100 + gain4 = 100 + elif taper_var == 2: # Chebyshev + gain1 = 4 + gain2 = 23 + gain3 = 62 + gain4 = 100 + elif taper_var == 3: # Hamming + gain1 = 9 + gain2 = 27 + gain3 = 67 + gain4 = 100 + elif taper_var == 4: # Hann + gain1 = 12 + gain2 = 43 + gain3 = 77 + gain4 = 100 + elif taper_var == 5: # Blackman + gain1 = 6 + gain2 = 27 + gain3 = 66 + gain4 = 100 + slide_Rx1Gain.configure(state="normal") # 'disabled' + slide_Rx2Gain.configure(state="normal") # 'disabled' + slide_Rx3Gain.configure(state="normal") # 'disabled' + slide_Rx4Gain.configure(state="normal") # 'disabled' + slide_Rx5Gain.configure(state="normal") # 'disabled' + slide_Rx6Gain.configure(state="normal") # 'disabled' + slide_Rx7Gain.configure(state="normal") # 'disabled' + slide_Rx8Gain.configure(state="normal") # 'disabled' + slide_Rx1Gain.set(gain1) + slide_Rx2Gain.set(gain2) + slide_Rx3Gain.set(gain3) + slide_Rx4Gain.set(gain4) + slide_Rx5Gain.set(gain4) + slide_Rx6Gain.set(gain3) + slide_Rx7Gain.set(gain2) + slide_Rx8Gain.set(gain1) + + button_rect = Button( + frame2, text="Rect Window", command=lambda: taper_profile(1) + ) + button_rect.grid(row=25, column=0, columnspan=1, padx=2, pady=1, sticky=E + W) + button_cheb = Button(frame2, text="Chebyshev", command=lambda: taper_profile(2)) + button_cheb.grid(row=25, column=1, columnspan=1, padx=2, pady=1, sticky=E + W) + # button_hamming = Button(frame2, text="Hamming", command=lambda: taper_profile(3)) + # button_hamming.grid(row=25, column=2, columnspan=1, padx=2, pady=1, sticky=E+W) + button_hann = Button(frame2, text="Hann", command=lambda: taper_profile(4)) + button_hann.grid(row=26, column=0, columnspan=1, padx=2, pady=1, sticky=E + W) + button_black = Button(frame2, text="Blackman", command=lambda: taper_profile(5)) + button_black.grid(row=26, column=1, columnspan=1, padx=2, pady=1, sticky=E + W) + """Frame3: Phase""" + self.Rx1Phase_set = tk.DoubleVar() + self.Rx2Phase_set = tk.DoubleVar() + self.Rx3Phase_set = tk.DoubleVar() + self.Rx4Phase_set = tk.DoubleVar() + self.Rx5Phase_set = tk.DoubleVar() + self.Rx6Phase_set = tk.DoubleVar() + self.Rx7Phase_set = tk.DoubleVar() + self.Rx8Phase_set = tk.DoubleVar() + slide_Rx1Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx1Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx2Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx2Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx3Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx3Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx4Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx4Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx1Phase.grid(row=0, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_Rx2Phase.grid(row=3, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_Rx3Phase.grid(row=6, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_Rx4Phase.grid(row=9, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_Rx1Phase.set(0) + slide_Rx2Phase.set(0) + slide_Rx3Phase.set(0) + slide_Rx4Phase.set(0) + + def zero_Rx1(): + slide_Rx1Phase.set(0) + + def zero_Rx2(): + slide_Rx2Phase.set(0) + + def zero_Rx3(): + slide_Rx3Phase.set(0) + + def zero_Rx4(): + slide_Rx4Phase.set(0) + + def zero_Rx5(): + slide_Rx5Phase.set(0) + + def zero_Rx6(): + slide_Rx6Phase.set(0) + + def zero_Rx7(): + slide_Rx7Phase.set(0) + + def zero_Rx8(): + slide_Rx8Phase.set(0) + + tk.Button( + frame3, + text="Rx1 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx1, + highlightthickness=0, + ).grid(row=1, column=3, sticky=E + W) + tk.Button( + frame3, + text="Rx2 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx2, + highlightthickness=0, + ).grid(row=4, column=3, sticky=E + W) + tk.Button( + frame3, + text="Rx3 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx3, + highlightthickness=0, + ).grid(row=7, column=3, sticky=E + W) + tk.Button( + frame3, + text="Rx4 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx4, + highlightthickness=0, + ).grid(row=10, column=3, sticky=E + W) + Phase_set = tk.IntVar() + + def phase_sel(): + global phase1 + global phase2 + global phase3 + global phase4 + global phase5 + global phase6 + global phase7 + global phase8 + if Phase_set.get() == 1: + phase1 = slide_Rx1Phase.get() + phase2 = slide_Rx2Phase.get() + phase3 = slide_Rx3Phase.get() + phase4 = slide_Rx4Phase.get() + phase5 = slide_Rx5Phase.get() + phase6 = slide_Rx6Phase.get() + phase7 = slide_Rx7Phase.get() + phase8 = slide_Rx8Phase.get() + slide_Rx1Phase.set(0) + slide_Rx2Phase.set(0) + slide_Rx3Phase.set(0) + slide_Rx4Phase.set(0) + slide_Rx5Phase.set(0) + slide_Rx6Phase.set(0) + slide_Rx7Phase.set(0) + slide_Rx8Phase.set(0) + slide_Rx1Phase.configure(state="disabled") # 'normal' + slide_Rx2Phase.configure(state="disabled") # 'normal' + slide_Rx3Phase.configure(state="disabled") # 'normal' + slide_Rx4Phase.configure(state="disabled") # 'normal' + slide_Rx5Phase.configure(state="disabled") # 'normal' + slide_Rx6Phase.configure(state="disabled") # 'normal' + slide_Rx7Phase.configure(state="disabled") # 'normal' + slide_Rx8Phase.configure(state="disabled") # 'normal' + else: + slide_Rx1Phase.configure(state="normal") # 'disabled' + slide_Rx2Phase.configure(state="normal") # 'disabled' + slide_Rx3Phase.configure(state="normal") # 'disabled' + slide_Rx4Phase.configure(state="normal") # 'disabled' + slide_Rx5Phase.configure(state="normal") # 'disabled' + slide_Rx6Phase.configure(state="normal") # 'disabled' + slide_Rx7Phase.configure(state="normal") # 'disabled' + slide_Rx8Phase.configure(state="normal") # 'disabled' + slide_Rx1Phase.set(phase1) + slide_Rx2Phase.set(phase2) + slide_Rx3Phase.set(phase3) + slide_Rx4Phase.set(phase4) + slide_Rx5Phase.set(phase5) + slide_Rx6Phase.set(phase6) + slide_Rx7Phase.set(phase7) + slide_Rx8Phase.set(phase8) + + check_Phase = tk.Checkbutton( + frame3, + text="Set All Phase to 0", + variable=Phase_set, + highlightthickness=0, + onvalue=1, + offvalue=0, + command=phase_sel, + anchor=W, + relief=SUNKEN, + ) + check_Phase.grid(row=24, column=0, padx=10, pady=10, sticky=E + W) + slide_Rx5Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx5Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx6Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx6Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx7Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx7Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx8Phase = Scale( + frame3, + from_=-180, + to=180, + resolution=2.8125, + digits=7, + variable=self.Rx8Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_Rx5Phase.grid( + row=12, column=0, padx=2, pady=2, columnspan=3, rowspan=3, sticky=E + W + ) + slide_Rx6Phase.grid( + row=15, column=0, padx=2, pady=2, columnspan=3, rowspan=3, sticky=E + W + ) + slide_Rx7Phase.grid( + row=18, column=0, padx=2, pady=2, columnspan=3, rowspan=3, sticky=E + W + ) + slide_Rx8Phase.grid( + row=21, column=0, padx=2, pady=2, columnspan=3, rowspan=3, sticky=E + W + ) + slide_Rx5Phase.set(0) + slide_Rx6Phase.set(0) + slide_Rx7Phase.set(0) + slide_Rx8Phase.set(0) + tk.Button( + frame3, + text="Rx5 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx5, + highlightthickness=0, + ).grid(row=13, column=3, padx=1, sticky=E + W) + tk.Button( + frame3, + text="Rx6 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx6, + highlightthickness=0, + ).grid(row=16, column=3, padx=1, sticky=E + W) + tk.Button( + frame3, + text="Rx7 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx7, + highlightthickness=0, + ).grid(row=19, column=3, padx=1, sticky=E + W) + tk.Button( + frame3, + text="Rx8 Phase", + relief=RAISED, + anchor=W, + command=zero_Rx8, + highlightthickness=0, + ).grid(row=22, column=3, padx=1, sticky=E + W) + """Frame4: BW""" + self.BW = tk.DoubleVar() + slide_SignalBW = Scale( + frame4, + from_=0, + to=2000, + variable=self.BW, + resolution=100, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_SignalBW.grid(row=0, column=0, padx=10, pady=10, rowspan=3) + slide_SignalBW.set(self.SignalFreq / 1e9) + tk.Label(frame4, text="Signal BW (MHz)", relief=SUNKEN, anchor=W).grid( + row=1, column=2, sticky=E + W + ) + self.RxSignal_text = tk.StringVar() + self.RxSignal = tk.Label( + frame4, textvariable=self.RxSignal_text, relief=SUNKEN, anchor=W + ).grid(row=8, column=0, pady=10, padx=5, sticky=E + W) + self.RxSignal_text.set("Signal Bandwidth = ") + self.MixerLO_text = tk.StringVar() + self.MixerLO = tk.Label( + frame4, textvariable=self.MixerLO_text, relief=SUNKEN, anchor=W + ).grid(row=9, column=0, pady=10, padx=5, sticky=E + W) + self.MixerLO_text.set("Mixer LO Freq = ") + self.PlutoRxLO_text = tk.StringVar() + self.PlutoRxLO = tk.Label( + frame4, textvariable=self.PlutoRxLO_text, relief=SUNKEN, anchor=W + ).grid(row=10, column=0, pady=10, padx=5, sticky=E + W) + self.PlutoRxLO_text.set("Pluto Rx LO = ") + self.BeamCalc_text = tk.StringVar() + self.BeamCalc = tk.Label( + frame4, textvariable=self.BeamCalc_text, relief=SUNKEN, anchor=W + ).grid(row=11, column=0, pady=10, padx=5, sticky=E + W) + self.BeamCalc_text.set("Beam Calculated at ") + self.AngleMeas_text = tk.StringVar() + self.AngleMeas = tk.Label( + frame4, textvariable=self.AngleMeas_text, relief=SUNKEN, anchor=W + ).grid(row=12, column=0, pady=10, padx=5, sticky=E + W) + self.AngleMeas_text.set("Beam Measured at ") + """Frame5: Bits""" + self.res = tk.DoubleVar() + self.bits = tk.IntVar() + slide_res = Scale( + frame5, + from_=0.1, + to=5, + variable=self.res, + resolution=0.1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_bits = Scale( + frame5, + from_=1, + to=7, + resolution=1, + variable=self.bits, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_res.grid(row=0, column=0, padx=10, pady=10, rowspan=3) + slide_bits.grid(row=3, column=0, padx=10, pady=10, rowspan=3) + slide_res.set(2.8125) + slide_bits.set(7) + tk.Label(frame5, text="Steer Resolution", relief=SUNKEN, anchor=W).grid( + row=1, column=2, sticky=E + W + ) + tk.Label(frame5, text="Phase Shift Bits", relief=SUNKEN, anchor=W).grid( + row=4, column=2, sticky=E + W + ) + self.res_text = tk.StringVar() + self.res_degrees = tk.Label( + frame5, textvariable=self.res_text, relief=SUNKEN, anchor=W + ).grid(row=8, column=0, columnspan=3, pady=10, padx=5, sticky=E + W) + self.res_text.set("Phase Shift Steps = ") + self.res_bits = tk.IntVar() + check_res_bits = tk.Checkbutton( + frame5, + text="Ignore Steer Res", + highlightthickness=0, + variable=self.res_bits, + onvalue=1, + offvalue=0, + anchor=W, + relief=FLAT, + ) + check_res_bits.grid(row=10, column=0, columnspan=1, sticky=E + W) + self.res_bits.set(1) + """Frame6: Digital Controls""" + self.Beam0_Phase_set = tk.DoubleVar() + self.Beam1_Phase_set = tk.DoubleVar() + slide_B0_Phase = Scale( + frame6, + from_=-180, + to=180, + resolution=1, + digits=3, + variable=self.Beam0_Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_B1_Phase = Scale( + frame6, + from_=-180, + to=180, + resolution=1, + digits=3, + variable=self.Beam1_Phase_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_B0_Phase.grid(row=0, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_B1_Phase.grid(row=3, column=0, padx=2, pady=2, columnspan=3, rowspan=3) + slide_B0_Phase.set(0) + slide_B1_Phase.set(0) + + def zero_B0_phase(): + slide_B0_Phase.set(0) + + def zero_B1_phase(): + slide_B1_Phase.set(0) + + tk.Button( + frame6, + text="Beam0 Phase Shift", + relief=RAISED, + anchor=W, + command=zero_B0_phase, + highlightthickness=0, + ).grid(row=1, column=3, sticky=E + W) + tk.Button( + frame6, + text="Beam1 Phase Shift", + relief=RAISED, + anchor=W, + command=zero_B1_phase, + highlightthickness=0, + ).grid(row=4, column=3, sticky=E + W) + + self.B0_Gain_set = tk.DoubleVar() + self.B1_Gain_set = tk.DoubleVar() + slide_B0_Gain = Scale( + frame6, + from_=0, + to=2, + resolution=0.1, + variable=self.B0_Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_B1_Gain = Scale( + frame6, + from_=0, + to=2, + resolution=0.1, + variable=self.B1_Gain_set, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_B0_Gain.grid(row=12, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_B1_Gain.grid(row=15, column=0, padx=2, pady=2, rowspan=3, columnspan=3) + slide_B0_Gain.set(1) + slide_B1_Gain.set(1) + + def B0_toggle(): + if slide_B0_Gain.get() == 0: + slide_B0_Gain.set(1) + else: + slide_B0_Gain.set(0) + + def B1_toggle(): + if slide_B1_Gain.get() == 0: + slide_B1_Gain.set(1) + else: + slide_B1_Gain.set(0) + + tk.Button( + frame6, + text="Beam0 Gain", + relief=RAISED, + anchor=W, + command=B0_toggle, + highlightthickness=0, + ).grid(row=13, column=3, sticky=E + W) + tk.Button( + frame6, + text="Beam1 Gain", + relief=RAISED, + anchor=W, + command=B1_toggle, + highlightthickness=0, + ).grid(row=16, column=3, sticky=E + W) + + self.show_delta = tk.IntVar() + check_delta = tk.Checkbutton( + frame6, + text="Show Delta", + highlightthickness=0, + variable=self.show_delta, + onvalue=1, + offvalue=0, + anchor=W, + relief=SUNKEN, + ) + check_delta.grid(row=21, column=0, columnspan=3, padx=5, pady=15, sticky=E + W) + self.show_delta.set(0) + self.show_error = tk.IntVar() + check_error = tk.Checkbutton( + frame6, + text="Show Error", + highlightthickness=0, + variable=self.show_error, + onvalue=1, + offvalue=0, + anchor=W, + relief=SUNKEN, + ) + check_error.grid(row=22, column=0, columnspan=3, padx=5, pady=5, sticky=E + W) + self.show_error.set(0) + + """Frame7: Plot Options""" + + def clearMax(): + if self.PlotMax_set.get() == 0: + self.max_hold = -1000 + self.min_hold = 1000 + + self.PlotMax_set = tk.IntVar() + check_PlotMax = tk.Checkbutton( + frame7, + text="Show Peak Gain", + highlightthickness=0, + variable=self.PlotMax_set, + command=clearMax, + onvalue=1, + offvalue=0, + anchor=W, + relief=SUNKEN, + ) + check_PlotMax.grid( + row=0, column=0, columnspan=3, padx=20, pady=10, sticky=E + W + ) + self.AngleMax_set = tk.IntVar() + check_AngleMax = tk.Checkbutton( + frame7, + text="Show Peak Angle", + highlightthickness=0, + variable=self.AngleMax_set, + onvalue=1, + offvalue=0, + anchor=W, + relief=SUNKEN, + ) + check_AngleMax.grid( + row=1, column=0, columnspan=3, padx=20, pady=10, sticky=E + W + ) + # self.HPBW_set = tk.IntVar() + # check_HPBW = tk.Checkbutton(frame7, text="Shade 3dB Area (HPBW)", highlightthickness=0, variable=self.HPBW_set, onvalue=1, offvalue=0, anchor=W, relief=SUNKEN) + # check_HPBW.grid(row=2, column=0, columnspan=3, padx=20, pady=20, sticky=E+W) + # self.show_sum = tk.IntVar() + # check_sum = tk.Checkbutton(frame7, text="Show Sum", highlightthickness=0, variable=self.show_sum, onvalue=1, offvalue=0, anchor=W, relief=SUNKEN) + # check_sum.grid(row=0, column=3, columnspan=1, padx=20, pady=20, sticky=E+W) + # self.show_sum.set(1) + self.x_min = tk.DoubleVar() + self.x_max = tk.DoubleVar() + self.y_min = tk.DoubleVar() + self.y_max = tk.DoubleVar() + + def check_axis(var_axis): + x_minval = slide_x_min.get() + x_maxval = slide_x_max.get() + y_minval = slide_y_min.get() + y_maxval = slide_y_max.get() + if (x_minval) >= x_maxval: + slide_x_min.set(x_maxval - 1) + if y_minval >= y_maxval: + slide_y_min.set(y_maxval - 1) + + slide_x_min = Scale( + frame7, + from_=-100, + to=99, + variable=self.x_min, + command=check_axis, + resolution=1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_x_min.grid(row=3, column=0, padx=10, pady=10, rowspan=3, columnspan=3) + slide_x_min.set(-89) + tk.Label(frame7, text="X axis min", relief=SUNKEN, anchor=W).grid( + row=4, column=3, sticky=E + W + ) + slide_x_max = Scale( + frame7, + from_=-99, + to=100, + variable=self.x_max, + command=check_axis, + resolution=1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_x_max.grid(row=6, column=0, padx=10, pady=10, rowspan=3, columnspan=3) + slide_x_max.set(89) + tk.Label(frame7, text="X axis max", relief=SUNKEN, anchor=W).grid( + row=7, column=3, sticky=E + W + ) + slide_y_min = Scale( + frame7, + from_=-100, + to=9, + variable=self.y_min, + command=check_axis, + resolution=1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_y_min.grid(row=9, column=0, padx=10, pady=10, rowspan=3, columnspan=3) + slide_y_min.set(-50) + tk.Label(frame7, text="Y axis min", relief=SUNKEN, anchor=W).grid( + row=10, column=3, sticky=E + W + ) + slide_y_max = Scale( + frame7, + from_=-59, + to=10, + variable=self.y_max, + command=check_axis, + resolution=1, + troughcolor="LightYellow3", + bd=2, + orient=HORIZONTAL, + relief=SUNKEN, + length=200, + ) + slide_y_max.grid(row=12, column=0, padx=10, pady=10, rowspan=3, columnspan=3) + slide_y_max.set(0) + tk.Label(frame7, text="Y axis max", relief=SUNKEN, anchor=W).grid( + row=13, column=3, sticky=E + W + ) + tk.Label( + frame7, + text="These options take effect with the next plot refresh", + relief=SUNKEN, + anchor=W, + ).grid(row=20, column=0, columnspan=4, padx=20, pady=20, sticky=E + W) + + """CONFIGURE THE TABS FOR PLOTTING""" + self.plot_tabs = ttk.Notebook(self.master) + self.plot_tabs.grid(padx=10, pady=10, row=0, column=4, columnspan=5) + self.plot_tabs.columnconfigure((0, 1, 2, 3, 4, 5, 6, 7), weight=1) + self.plot_tabs.rowconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame11 = Frame(self.plot_tabs, width=700, height=500) + self.frame11.grid(row=0, column=2) + self.frame11.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame11.rowconfigure((0), weight=1) + self.plot_tabs.add(self.frame11, text="Rectangular Plot") + self.frame12 = Frame(self.plot_tabs, width=700, height=500) + self.frame12.grid(row=0, column=2) + self.frame12.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame12.rowconfigure((0), weight=1) + self.plot_tabs.add(self.frame12, text="Polar Plot") + self.frame13 = Frame(self.plot_tabs, width=700, height=500) + self.frame13.grid(row=0, column=2) + self.frame13.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame13.rowconfigure((0), weight=1) + self.plot_tabs.add(self.frame13, text="FFT") + self.frame14 = Frame(self.plot_tabs, width=700, height=500) + self.frame14.grid(row=0, column=2) + self.frame14.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame14.rowconfigure((0), weight=1) + self.plot_tabs.add(self.frame14, text="Signal Tracking") + self.plot_tabs.select(0) + self.frame15 = Frame(self.plot_tabs, width=700, height=500) + self.frame15.grid(row=0, column=2) + self.frame15.columnconfigure((0, 1, 2, 3, 4, 5), weight=1) + self.frame15.rowconfigure((0), weight=1) + self.plot_tabs.add(self.frame15, text="Signal vs Time") + self.plot_tabs.select(0) + + def conf(event): + self.plot_tabs.config( + height=max(root.winfo_height() - 100, 500), + width=max(root.winfo_width() - 450, 300), + ) + + def on_tab_change(event): + if ( + self.plot_tabs.index(self.plot_tabs.select()) < 2 + ): # so either rect plot of polar plot + try: + self.plotData(1, 1, 0) + except: + x = 0 + if self.plot_tabs.index(self.plot_tabs.select()) == 2: # fft plot tab + self.plotData(0, 1, 0) + + self.plot_tabs.bind("<>", on_tab_change) + + root.bind("", conf) + self.generate_Figures() + + """ Add Lab Selection box """ + + def Lab_mode_select(value): + """ Reset all GUI values to default """ + print("Resetting all GUI values...") + cntrl_tabs.select(frame1) # Config tab + self.plot_tabs.select(self.frame11) # select rectangular plot + cntrl_tabs.add(frame2) # Gain tab + cntrl_tabs.add(frame3) # Phase tab + cntrl_tabs.add(frame4) # BW tab + cntrl_tabs.add(frame5) # Bits tab + cntrl_tabs.add(frame6) # Digital tab + self.plot_tabs.add(self.frame14) # Signal Tracking Plot + self.plot_tabs.hide(self.frame15) # Signal vs Time Plot + mode_Menu.configure(state="normal") + self.update_time.set(config.refresh_time) + self.refresh.set(0) + self.Tx_select.set("Transmit Disabled") + Tx_mode_select("Transmit Disabled") + slide_SignalFreq.set(config.SignalFreq / 1e9) + slide_RxGain.set(int(config.Rx_gain)) + slide_RxPhaseDelta.set(0) + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + self.Sym_set.set(0) + taper_profile(1) # set all gains to max (100) + slide_Rx1Phase.set(0) + slide_Rx2Phase.set(0) + slide_Rx3Phase.set(0) + slide_Rx4Phase.set(0) + slide_Rx5Phase.set(0) + slide_Rx6Phase.set(0) + slide_Rx7Phase.set(0) + slide_Rx8Phase.set(0) + slide_SignalBW.set(self.SignalFreq / 1e9) + slide_res.set(2.8125) + slide_bits.set(7) + self.res_bits.set(1) + slide_B0_Phase.set(0) + slide_B1_Phase.set(0) + slide_B0_Gain.set(1) + slide_B1_Gain.set(1) + self.show_delta.set(0) + self.show_error.set(0) + self.PlotMax_set.set(0) + self.AngleMax_set.set(0) + if value == "Lab 1: Steering Angle": + print(value) + self.plot_tabs.select(self.frame13) # select FFT plot + cntrl_tabs.hide(frame2) # Gain tab + cntrl_tabs.hide(frame3) # Phase tab + cntrl_tabs.hide(frame4) # BW tab + cntrl_tabs.hide(frame5) # Bits tab + cntrl_tabs.hide(frame6) # Digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Static Phase") + mode_select("Static Phase") + mode_Menu.configure(state="disabled") + self.PlotMax_set.set(1) + self.AngleMax_set.set(1) + self.refresh.set(1) + self.updater() + elif value == "Lab 2: Array Factor": + print(value) + self.plot_tabs.select(self.frame11) # select rect plot + # cntrl_tabs.hide(frame2) # Gain tab + cntrl_tabs.hide(frame3) # Phase tab + cntrl_tabs.hide(frame4) # BW tab + cntrl_tabs.hide(frame5) # Bits tab + cntrl_tabs.hide(frame6) # Digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + elif value == "Lab 3: Tapering" or value == "Lab 4: Grating Lobes": + print(value) + self.plot_tabs.select(self.frame11) # select rect plot + cntrl_tabs.select(frame2) # select gain tab + cntrl_tabs.hide(frame3) # Phase tab + cntrl_tabs.hide(frame4) # BW tab + cntrl_tabs.hide(frame5) # Bits tab + cntrl_tabs.hide(frame6) # Digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + elif value == "Lab 5: Beam Squint": + print(value) + self.plot_tabs.select(self.frame11) # select rect plot + cntrl_tabs.select(frame4) # select BW tab + cntrl_tabs.hide(frame3) # Phase tab + cntrl_tabs.hide(frame5) # Bits tab + cntrl_tabs.hide(frame6) # Digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + elif value == "Lab 6: Quantization": + print(value) + slide_res.set(1) + slide_bits.set(7) + self.res_bits.set(0) + taper_profile(5) # set blackman taper on the elements + self.plot_tabs.add(self.frame15) # Signal Tracking Plot + self.plot_tabs.select(self.frame15) + cntrl_tabs.select(frame1) # select Config tab + cntrl_tabs.hide(frame3) # Phase tab + cntrl_tabs.hide(frame6) # Digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Signal vs Time") + mode_select("Signal vs Time") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + elif value == "Lab 7: Hybrid Control": + print(value) + self.plot_tabs.select(self.frame11) # select rect plot + cntrl_tabs.select(frame6) # select digital tab + self.plot_tabs.hide(self.frame14) # Signal Tracking Plot + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + elif value == "Lab 8: Monopulse Tracking": + print(value) + taper_profile(5) # set blackman taper on the elements + self.plot_tabs.select(self.frame11) # select rect plot + self.mode_var.set("Beam Sweep") + mode_select("Beam Sweep") + mode_Menu.configure(state="normal") + self.refresh.set(1) + self.updater() + else: + print("Enable All GUI Controls") + + self.Lab_select = StringVar() + Lab_sel_menu = OptionMenu( + self.master, + self.Lab_select, + "Lab 1: Steering Angle", + "Lab 2: Array Factor", + "Lab 3: Tapering", + "Lab 4: Grating Lobes", + "Lab 5: Beam Squint", + "Lab 6: Quantization", + "Lab 7: Hybrid Control", + "Lab 8: Monopulse Tracking", + "Enable All", + command=Lab_mode_select, + ) + Lab_sel_menu.grid( + row=13, column=4, padx=20, pady=2, rowspan=13, columnspan=1, sticky=E + W + ) + self.Lab_select.set(config.start_lab) + Lab_mode_select(config.start_lab) + + """START THE UPDATER""" + self.updater() + + def updater(self): + # time1=datetime.datetime.now() + self.programBeam() + # time2=datetime.datetime.now() + # time3=datetime.datetime.now() + self.plotData(1, 1, 0) # plot_gain, plot_fft, plot_tracking + # time4=datetime.datetime.now() + + # for some reason, my Rpi4/2GB doesn't want to refersh the plot display over VNC. So just do something quick to force it to update. + do_something_to_refresh_gui = self.refresh.get() + self.refresh.set(1) + self.refresh.set(0) + self.refresh.set(do_something_to_refresh_gui) + + self.refresh_time = self.update_time.get() + if self.refresh.get() == 1: + self.master.after(int(self.refresh_time / 1000), self.updater) + + # time5=datetime.datetime.now() + # print("phase_update=", time2-time1," plot_update=", time4-time3) + # print("total time=", time5-self.time0) + # self.time0=datetime.datetime.now() + + def find_peak(self): + self.programBeam() + print("Begin Tracking Mode. This will last about 20 seconds.") + print( + "Initial Steering Angle = ", + int(self.ConvertPhaseToSteerAngle(self.max_PhDelta)), + " deg", + ) + i = 0 + # start=time.time() + if ( + self.mode_var.get() == "Tracking" + ): # I realize this gets caught in a loop that you can't use the GUI during. But I'm not having success with threading and TKinter.... Any ideas are welcome! + # track_thread= Thread(target=self.track, args=(self.max_PhDelta,)) + # track_thread.start() + # plot_thread = Thread(self.plotData, args=(0,0,1,)) + # plot_thread.start() + # track_thread.join() + for i in range(0, 1500): + self.track(self.max_PhDelta) + if i % 20 == 0: + self.plotData(0, 0, 1) + self.mode_var.set("Beam Sweep") + print( + "End of Tracking Operation. To do tracking again, select the Tracking mode from the pulldown menu." + ) + + def track(self, PhDelta): + phaseList = [ + self.RxPhase1, + self.RxPhase2, + self.RxPhase3, + self.RxPhase4, + self.RxPhase5, + self.RxPhase6, + self.RxPhase7, + self.RxPhase8, + ] + ADAR_set_Phase(self.array, self.max_PhDelta, 2.8125, phaseList) + ( + PeakValue_sum, + PeakValue_delta, + PeakValue_beam_phase, + sum_chan, + target_error, + ) = self.getData(1) + error_thresh = 0 + if target_error > (-1 * error_thresh): + PhDelta = ( + PhDelta - 2.8125 + ) # depending on the orientation of Phaser to the source, this might need to be -2.8125 + elif target_error < (-1 * error_thresh): + PhDelta = ( + PhDelta + 2.8125 + ) # depending on the orientation of Phaser to the source, this might need to be +2.8125 + SteerAngle = self.ConvertPhaseToSteerAngle(PhDelta) + self.TrackArray.append(SteerAngle) + self.max_PhDelta = PhDelta + + def closeProgram(self): + self.refresh.set(0) + time.sleep(0.5) + """power down the Phaser board""" + # this drops Pdiss from about 8.5W to 3.5W + gpios.gpio_vctrl_1 = 0 # 1=Use onboard PLL/LO source (0=disable PLL and VCO, and set switch to use external LO input) + gpios.gpio_vctrl_2 = 0 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) + for device in self.array.devices.values(): + device.reset() # resets the ADAR1000s + SDR_TxBuffer_Destroy(self.sdr) + # self.master.destroy() + sys.exit(0) + + def programTaper(self): + g1 = self.Rx1Gain_set.get() + g2 = self.Rx2Gain_set.get() + g3 = self.Rx3Gain_set.get() + g4 = self.Rx4Gain_set.get() + g5 = self.Rx5Gain_set.get() + g6 = self.Rx6Gain_set.get() + g7 = self.Rx7Gain_set.get() + g8 = self.Rx8Gain_set.get() + # MWT: use np.array_equal function to detect any differences. + if ( + self.RxGain1 != g1 + or self.RxGain2 != g2 + or self.RxGain3 != g3 + or self.RxGain4 != g4 + or self.RxGain5 != g5 + or self.RxGain6 != g6 + or self.RxGain7 != g7 + or self.RxGain8 != g8 + ): + self.RxGain1 = g1 + self.RxGain2 = g2 + self.RxGain3 = g3 + self.RxGain4 = g4 + self.RxGain5 = g5 + self.RxGain6 = g6 + self.RxGain7 = g7 + self.RxGain8 = g8 + gainList = [ + self.RxGain1, + self.RxGain2, + self.RxGain3, + self.RxGain4, + self.RxGain5, + self.RxGain6, + self.RxGain7, + self.RxGain8, + ] + ADAR_set_Taper(self.array, gainList) + + def programLO(self): + if self.SignalFreq != int(self.freq.get() * 1e9): + self.SignalFreq = self.freq.get() * 1e9 + self.LO_freq = self.SignalFreq + self.Rx_freq + SDR_LO_init(rpi_ip, self.LO_freq) + if self.Rx_gain != int(self.RxGain.get()): + self.Rx_gain = int(self.RxGain.get()) + SDR_setRx(self.sdr, self.Rx_gain, self.Rx_gain) + if self.Tx_gain != int(self.TxGain.get()): + self.Tx_gain = int(self.TxGain.get()) + SDR_setTx(self.sdr, self.Tx_gain) + + def ConvertPhaseToSteerAngle(self, PhDelta): + # steering angle theta = arcsin(c*deltaphase/(2*pi*f*d) + value1 = (self.c * np.radians(np.abs(PhDelta))) / ( + 2 * 3.14159 * (self.SignalFreq - self.bandwidth * 1000000) * self.d + ) + clamped_value1 = max( + min(1, value1), -1 + ) # arcsin argument must be between 1 and -1, or numpy will throw a warning + theta = np.degrees(np.arcsin(clamped_value1)) + if PhDelta >= 0: + SteerAngle = theta # positive PhaseDelta covers 0deg to 90 deg + else: + SteerAngle = -theta # negative phase delta covers 0 deg to -90 deg + return SteerAngle + + def getData(self, Averages): + total_sum = 0 + total_delta = 0 + total_beam_phase = 0 + for count in range(0, Averages): + data = SDR_getData(self.sdr) + chan1 = data[0] # Rx1 data + chan2 = data[1] # Rx2 data + + # scale the amplitude from the "digital tab" + chan1 = chan1 * self.B0_Gain_set.get() + chan2 = chan2 * self.B1_Gain_set.get() + + # shift phase from the "digital tab" + NumSamples = len(chan1) + dig_Beam0_phase = np.deg2rad(self.Beam0_Phase_set.get()) + dig_Beam1_phase = np.deg2rad(self.Beam1_Phase_set.get()) + if dig_Beam0_phase != 0: + chan1_fft_shift = np.fft.fft(chan1) * np.exp(1.0j * dig_Beam0_phase) + chan1 = np.fft.ifft(chan1_fft_shift, n=NumSamples) + chan1 = chan1[0:NumSamples] + if dig_Beam1_phase != 0: + chan2_fft_shift = np.fft.fft(chan2) * np.exp(1.0j * dig_Beam1_phase) + chan2 = np.fft.ifft(chan2_fft_shift, n=NumSamples) + chan2 = chan2[0:NumSamples] + + sum_chan = chan1 + chan2 + delta_chan = chan1 - chan2 + # for the electrical scan, the right way to do this is to take an FFT the get the peak of it + # but, for our purposes, keeping it in the time domain and looking at peak and angles there is faster + # this works the same as freq domain, as long as there is one strong signal (i.e. not multiple peaks) + max_index = np.argmax(sum_chan) + s_mag_sum = np.max( + [np.abs(sum_chan[max_index]), 10 ** (-15)] + ) # make sure this gives something >0, otherwise the log10 function will give an error + s_mag_delta = np.max([np.abs(delta_chan[max_index]), 10 ** (-15)]) + s_dbfs_sum = 20 * np.log10(s_mag_sum / (2 ** 11)) + s_dbfs_delta = 20 * np.log10(s_mag_delta / (2 ** 11)) + total_beam_phase = total_beam_phase + ( + np.angle(sum_chan[max_index]) - np.angle(delta_chan[max_index]) + ) + total_sum = total_sum + ( + s_dbfs_sum + ) # sum up all the loops, then we'll average + total_delta = total_delta + ( + s_dbfs_delta + ) # sum up all the loops, then we'll average + + PeakValue_sum = total_sum / Averages + PeakValue_delta = total_delta / Averages + PeakValue_beam_phase = total_beam_phase / Averages + if np.sign(PeakValue_beam_phase) == -1: + target_error = min( + -0.01, + ( + np.sign(PeakValue_beam_phase) * (PeakValue_sum - PeakValue_delta) + + np.sign(PeakValue_beam_phase) + * (PeakValue_sum + PeakValue_delta) + / 2 + ) + / (PeakValue_sum + PeakValue_delta), + ) + else: + target_error = max( + 0.01, + ( + np.sign(PeakValue_beam_phase) * (PeakValue_sum - PeakValue_delta) + + np.sign(PeakValue_beam_phase) + * (PeakValue_sum + PeakValue_delta) + / 2 + ) + / (PeakValue_sum + PeakValue_delta), + ) + return ( + PeakValue_sum, + PeakValue_delta, + PeakValue_beam_phase, + sum_chan, + target_error, + ) + + def programBeam(self): + self.programLO() + self.programTaper() + steer_res = max(self.res.get(), 0.1) + phase_step_size = 360 / (2 ** self.bits.get()) + self.bandwidth = self.BW.get() + self.RxSignal_text.set( + str("Signal Bandwidth = " + str(self.bandwidth) + " MHz") + ) + self.MixerLO_text.set( + str("LTC5548 LO Freq = " + str(int(self.LO_freq / 1000000)) + " MHz") + ) + self.PlutoRxLO_text.set( + str("Pluto Rx LO = " + str(int(self.Rx_freq / 1000000)) + " MHz") + ) + self.BeamCalc_text.set( + str( + "Beam Calculated at " + + str(int(self.SignalFreq / 1000000 - self.bandwidth)) + + " MHz" + ) + ) + self.AngleMeas_text.set( + str("Beam Measured at " + str(int(self.SignalFreq / 1000000)) + " MHz") + ) + + SteerValues = np.arange( + -90, 90 + steer_res, steer_res + ) # convert degrees to radians + # Phase delta = 2*Pi*d*sin(theta)/lambda = 2*Pi*d*sin(theta)*f/c + PhaseValues = np.degrees( + 2 + * 3.14159 + * self.d + * np.sin(np.radians(SteerValues)) + * self.SignalFreq + / self.c + ) + self.res_text.set(str("Phase Shift LSB = " + str(phase_step_size)) + " deg") + if self.res_bits.get() == 1: + phase_limit = int(225 / phase_step_size) * phase_step_size + phase_step_size + PhaseValues = np.arange(-phase_limit, phase_limit, phase_step_size) + if ( + self.mode_var.get() == "Static Phase" + or self.mode_var.get() == "Signal vs Time" + ): + PhaseValues = np.degrees( + 2 + * 3.14159 + * self.d + * np.sin(np.radians([self.RxPhaseDelta.get()])) + * self.SignalFreq + / self.c + ) + step_size = phase_step_size # 2.8125 + if int(self.RxPhaseDelta.get()) < 0: + e1 = 360 + else: + e1 = 0 + e2 = ((np.rint(PhaseValues[0] * 1 / step_size) * step_size)) % 360 + e3 = ((np.rint(PhaseValues[0] * 2 / step_size) * step_size)) % 360 + e4 = ((np.rint(PhaseValues[0] * 3 / step_size) * step_size)) % 360 + e5 = ((np.rint(PhaseValues[0] * 4 / step_size) * step_size)) % 360 + e6 = ((np.rint(PhaseValues[0] * 5 / step_size) * step_size)) % 360 + e7 = ((np.rint(PhaseValues[0] * 6 / step_size) * step_size)) % 360 + e8 = ((np.rint(PhaseValues[0] * 7 / step_size) * step_size)) % 360 + self.PhaseVal_text.set( + str(int(e1)) + + ", " + + str(int(e2)) + + ", " + + str(int(e3)) + + ", " + + str(int(e4)) + + ", " + + str(int(e5)) + + ", " + + str(int(e6)) + + ", " + + str(int(e7)) + + ", " + + str(int(e8)) + ) + """if self.mode_var.get() == "Signal vs Time": + PhaseValues = np.degrees( + 2 * 3.14159 * self.d + * np.sin(np.radians([self.RxPhaseDelta.get()])) + * self.SignalFreq / self.c )""" + gain = [] + delta = [] + beam_phase = [] + angle = [] + diff_error = [] + self.max_gain = [] + max_signal = -100000 + max_angle = -90 + self.RxPhase1 = self.Rx1Phase_set.get() + self.Rx1_cal + self.RxPhase2 = self.Rx2Phase_set.get() + self.Rx2_cal + self.RxPhase3 = self.Rx3Phase_set.get() + self.Rx3_cal + self.RxPhase4 = self.Rx4Phase_set.get() + self.Rx4_cal + self.RxPhase5 = self.Rx5Phase_set.get() + self.Rx5_cal + self.RxPhase6 = self.Rx6Phase_set.get() + self.Rx6_cal + self.RxPhase7 = self.Rx7Phase_set.get() + self.Rx7_cal + self.RxPhase8 = self.Rx8Phase_set.get() + self.Rx8_cal + + phaseList = [ + self.RxPhase1, + self.RxPhase2, + self.RxPhase3, + self.RxPhase4, + self.RxPhase5, + self.RxPhase6, + self.RxPhase7, + self.RxPhase8, + ] + for PhDelta in PhaseValues: + # if self.refresh.get()==1: + # time.sleep((self.update_time.get()/1000)/100) + ADAR_set_Phase(self.array, PhDelta, phase_step_size, phaseList) + # self.array.devices[1].generate_clocks() + # gpios.gpio_rx_load = 1 # RX_LOAD for ADAR1000 RAM access + # gpios.gpio_rx_load = 0 # RX_LOAD for ADAR1000 RAM access + SteerAngle = self.ConvertPhaseToSteerAngle(PhDelta) + ( + PeakValue_sum, + PeakValue_delta, + PeakValue_beam_phase, + sum_chan, + target_error, + ) = self.getData(self.Averages) + + if ( + PeakValue_sum > max_signal + ): # for the largest value, save the data so we can plot it in the FFT window + max_signal = PeakValue_sum + max_angle = PeakValue_beam_phase + self.max_PhDelta = PhDelta + data_fft = sum_chan + if self.mode_var.get() != "Signal vs Time": + gain.append(PeakValue_sum) + delta.append(PeakValue_delta) + beam_phase.append(PeakValue_beam_phase) + angle.append(SteerAngle) + diff_error.append(target_error) + + if self.mode_var.get() == "Signal vs Time": + time.sleep(0.1) + self.Gain_time.append(PeakValue_sum) + self.Gain_time = self.Gain_time[-100:] + + # take the FFT of the raw data ("data_fft") which corresponded to the peak gain + NumSamples = len(data_fft) # number of samples + win = np.blackman(NumSamples) + y = data_fft * win + sp = np.absolute(np.fft.fft(y)) + sp = np.fft.fftshift(sp) + s_mag = np.abs(sp) / np.sum( + win + ) # Scale FFT by window and /2 since we are using half the FFT spectrum + s_mag = np.maximum(s_mag, 10 ** (-15)) + self.max_gain = 20 * np.log10( + s_mag / (2 ** 11) + ) # Pluto is a 12 bit ADC, but we're only looking at positive #'s, so use 2**11 + ts = 1 / float(self.SampleRate) + self.xf = np.fft.fftfreq(NumSamples, ts) + self.xf = np.fft.fftshift( + self.xf + ) # this is the x axis (freq in Hz) for our fft plot + + if self.mode_var.get() != "Signal vs Time": + self.ArrayGain = gain + self.ArrayDelta = delta + self.ArrayBeamPhase = beam_phase + self.ArrayAngle = angle + self.ArrayError = diff_error + self.peak_gain = max(self.ArrayGain) + index_peak_gain = np.where(self.ArrayGain == self.peak_gain) + index_peak_gain = index_peak_gain[0] + self.max_angle = self.ArrayAngle[int(index_peak_gain[0])] + + def generate_Figures(self): + plt.clf() + self.figure1 = plt.Figure(figsize=(4, 3), dpi=100) + self.ax2 = self.figure1.add_subplot(3, 1, 3) # this plot is in the 3rd cell + + self.ax1 = self.figure1.add_subplot( + 3, 1, (1, 2) + ) # this plot is in the first and second cell, so has 2x the height + self.ax1.set_title("Array Signal Strength vs Steering Angle") + # self.ax1.set_xlabel('Steering Angle (deg)') + self.ax1.set_ylabel("Amplitude (dBFS)") + x_axis_min = self.x_min.get() + x_axis_max = self.x_max.get() + y_axis_min = self.y_min.get() + y_axis_max = self.y_max.get() + self.ax1.set_xlim([x_axis_min, x_axis_max]) + self.ax1.set_ylim([y_axis_min, y_axis_max]) + self.sum_line = self.ax1.plot( + self.ArrayAngle, + self.ArrayGain, + "-o", + ms=3, + alpha=0.7, + mfc="blue", + color="blue", + ) + self.delta_line = self.ax1.plot( + self.ArrayAngle, + self.ArrayGain, + "-o", + ms=3, + alpha=0.7, + mfc="red", + color="red", + ) + self.saved_line = self.ax1.plot( + self.saved_angle, + self.saved_gain, + "-o", + ms=1, + alpha=0.5, + mfc="green", + color="green", + ) + self.saved_lineB = self.ax1.plot( + self.saved_angleB, + self.saved_gainB, + "-o", + ms=1, + alpha=0.5, + mfc="purple", + color="purple", + ) + self.ax1.legend(["Sum", "Delta"], loc="lower right") + self.max_gain_line = self.ax1.plot( + self.ArrayAngle, + np.full(len(self.ArrayAngle), 0), + color="blue", + linestyle="--", + alpha=0.3, + ) + self.max_angle_line = self.ax1.plot( + self.ArrayAngle, + np.full(len(self.ArrayAngle), 0), + color="red", + linestyle=":", + alpha=0.3, + ) + + self.ax1.grid(True) + + self.phase_line = self.ax2.plot( + self.ArrayAngle, + np.sign(self.ArrayBeamPhase), + "-o", + ms=3, + alpha=0.7, + mfc="blue", + color="blue", + ) + self.error_line = self.ax2.plot( + self.ArrayAngle, + self.ArrayError, + "-o", + ms=3, + alpha=0.7, + mfc="red", + color="red", + ) + self.ax2.legend(["Phase Difference", "Error Function"], loc="upper right") + self.ax2.set_xlabel("Steering Angle (deg)") + self.ax2.set_ylabel("Error Function") + self.ax2.set_xlim([x_axis_min, x_axis_max]) + self.ax2.set_ylim([-1.5, 1.5]) + self.ax2.grid(True) + + self.graph1 = FigureCanvasTkAgg(self.figure1, self.frame11) + self.graph1.draw() + self.graph1.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + + plt.show(block=False) + plt.pause(0.01) + plt.close() + + self.ax1.add_line(self.sum_line[0]) + self.ax1.add_line(self.saved_line[0]) + self.ax1.add_line(self.saved_lineB[0]) + self.ax1.add_line(self.delta_line[0]) + self.ax1.add_line(self.max_gain_line[0]) + self.ax1.add_line(self.max_angle_line[0]) + self.ax2.add_line(self.phase_line[0]) + self.ax2.add_line(self.error_line[0]) + + self.toolbar1 = NavigationToolbar2Tk(self.graph1, self.frame11) + self.toolbar1.update() + + self.polar1 = plt.Figure(figsize=(1, 1), dpi=100) + self.pol_ax1 = self.polar1.add_subplot(111, projection="polar") + self.pol_graph1 = FigureCanvasTkAgg(self.polar1, self.frame12) + self.polar1.subplots_adjust(0, 0, 1, 1) + self.pol_graph1.draw() + self.pol_graph1.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + self.toolbar2 = NavigationToolbar2Tk(self.pol_graph1, self.frame12) + self.toolbar2.update() + + self.figure3 = plt.Figure(figsize=(4, 3), dpi=100) + self.ax3 = self.figure3.add_subplot(111) + self.graph3 = FigureCanvasTkAgg(self.figure3, self.frame13) + self.graph3.draw() + self.graph3.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + self.toolbar3 = NavigationToolbar2Tk(self.graph3, self.frame13) + self.toolbar3.update() + + self.figure4 = plt.Figure(figsize=(4, 3), dpi=100) + self.ax4 = self.figure4.add_subplot(111) + self.graph4 = FigureCanvasTkAgg(self.figure4, self.frame14) + self.graph4.draw() + self.graph4.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + self.toolbar4 = NavigationToolbar2Tk(self.graph4, self.frame14) + self.toolbar4.update() + + self.figure5 = plt.Figure(figsize=(4, 3), dpi=100) + self.ax5 = self.figure5.add_subplot(111) + self.graph5 = FigureCanvasTkAgg(self.figure5, self.frame15) + self.graph5.draw() + self.graph5.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + self.toolbar5 = NavigationToolbar2Tk(self.graph5, self.frame15) + self.toolbar5.update() + + self.background1 = self.graph1.copy_from_bbox(self.ax1.bbox) + self.background2 = self.graph1.copy_from_bbox(self.ax2.bbox) + + def savePlot(self): + self.saved_gain = self.ArrayGain + self.saved_angle = self.ArrayAngle + np.savetxt( + "saved_plot_A.txt", + (self.saved_angle, self.saved_gain), + delimiter=",", + header="steering angle array (first), then FFT amplitude array", + ) + print("data saved to saved_plot_A.txt") + + def savePlotB(self): + self.saved_gainB = self.ArrayGain + self.saved_angleB = self.ArrayAngle + np.savetxt( + "saved_plot_B.txt", + (self.saved_angleB, self.saved_gainB), + delimiter=",", + header="steering angle array (first), then FFT amplitude array", + ) + print("data saved to saved_plot_B.txt") + + def clearPlot(self): + self.saved_gain = [] + self.saved_angle = [] + self.saved_gainB = [] + self.saved_angleB = [] + + def plotData(self, plot_gain, plot_fft, plot_tracking): + # plot sum of both channels and subtraction of both channels + x_axis_min = self.x_min.get() + x_axis_max = self.x_max.get() + y_axis_min = self.y_min.get() + y_axis_max = self.y_max.get() + self.ax1.set_xlim([x_axis_min, x_axis_max]) + self.ax1.set_ylim([y_axis_min, y_axis_max]) + + if self.mode_var.get() == "Signal vs Time": + # time.sleep(0.1) + self.ArrayGain = self.Gain_time + self.ArrayAngle = [] + num_gains = len(self.ArrayGain) + for i in range(num_gains): + self.ArrayAngle.append(180 / num_gains * i - 90) + else: + self.saved_line[0].set_data(self.saved_angle, self.saved_gain) + self.saved_lineB[0].set_data(self.saved_angleB, self.saved_gainB) + if self.show_delta.get() == 1: + self.delta_line[0].set_data(self.ArrayAngle, self.ArrayDelta) + if self.PlotMax_set.get() == 1: + self.max_gain_line[0].set_data( + [x_axis_min, x_axis_max], [self.peak_gain, self.peak_gain] + ) + if self.AngleMax_set.get() == 1: + self.max_angle_line[0].set_data( + [self.max_angle, self.max_angle], [y_axis_min, y_axis_max] + ) + + self.sum_line[0].set_data(self.ArrayAngle, self.ArrayGain) + + self.graph1.restore_region(self.background1) + self.graph1.restore_region(self.background2) + + self.ax1.draw_artist(self.sum_line[0]) + if self.mode_var.get() != "Signal vs Time": + self.ax1.draw_artist(self.saved_line[0]) + self.ax1.draw_artist(self.saved_lineB[0]) + if self.show_delta.get() == 1: + self.ax1.draw_artist(self.delta_line[0]) + if self.PlotMax_set.get() == 1: + self.ax1.draw_artist(self.max_gain_line[0]) + if self.AngleMax_set.get() == 1: + self.ax1.draw_artist(self.max_angle_line[0]) + if self.show_error.get() == 1: + self.ax2.set_xlim([x_axis_min, x_axis_max]) + self.ax2.set_ylim([-1.5, 1.5]) + self.phase_line[0].set_data( + self.ArrayAngle, np.sign(self.ArrayBeamPhase) + ) + self.error_line[0].set_data(self.ArrayAngle, self.ArrayError) + # self.graph1.restore_region(self.background2) + self.ax2.draw_artist(self.phase_line[0]) + self.ax2.draw_artist(self.error_line[0]) + + self.graph1.blit(self.ax1.bbox) + self.graph1.blit(self.ax2.bbox) + self.graph1.flush_events() + + # Polar Plot + if plot_gain == 1 and self.plot_tabs.index(self.plot_tabs.select()) == 1: + y_axis_min = self.y_min.get() + y_axis_max = self.y_max.get() + self.pol_ax1.cla() + self.pol_ax1.plot( + np.radians(self.ArrayAngle), + self.ArrayGain, + "-o", + ms=3, + alpha=0.7, + mfc="blue", + color="blue", + ) + if self.mode_var.get() != "Signal vs Time": + self.pol_ax1.plot( + np.radians(self.saved_angle), + self.saved_gain, + "-o", + ms=1, + alpha=0.5, + mfc="green", + color="green", + ) + self.pol_ax1.plot( + np.radians(self.saved_angleB), + self.saved_gainB, + "-o", + ms=1, + alpha=0.5, + mfc="purple", + color="purple", + ) + self.pol_ax1.set_theta_offset( + np.pi / 2 + ) # rotate polar axis so that 0 deg is on top + self.pol_ax1.set_theta_direction(-1) + self.pol_ax1.set_thetamin(-89) + self.pol_ax1.set_thetamax(89) + self.pol_ax1.set_rmin(y_axis_min) + self.pol_ax1.set_rmax(y_axis_max) + self.pol_graph1.draw() + + # FFT Spectrum + if plot_fft == 1 and self.plot_tabs.index(self.plot_tabs.select()) == 2: + x_axis_min = self.x_min.get() + x_axis_max = self.x_max.get() + y_axis_min = self.y_min.get() + y_axis_max = self.y_max.get() + self.ax3.cla() + self.ax3.plot(-self.xf / 1e6, self.max_gain) + self.ax3.set_title("FFT at Peak Steering Angle") + self.ax3.set_xlabel("Freq (MHz)") + # self.ax3.set_xlabel(f"Freq - {int(self.SignalFreq/1e6)} MHz") + self.ax3.set_ylabel("Amplitude (dBFS)") + # self.ax3.set_xlim([x_axis_min, x_axis_max]) + self.ax3.set_ylim([-100, y_axis_max]) + self.ax3.grid() + max_fft_gain = max(self.max_gain) + index_max_fft_gain = np.where(self.max_gain == max_fft_gain) + try: + max_freq = ( + self.xf[int(index_max_fft_gain[0])] / 1e6 + ) # if both digital gains are 0, then this barfs. + except: + max_freq = 0 + if self.PlotMax_set.get() == 1: + self.max_hold = max(self.max_hold, max_fft_gain) + self.min_hold = min(self.min_hold, max_fft_gain) + self.ax3.axhline( + y=max_fft_gain, color="blue", linestyle="--", alpha=0.3 + ) + self.ax3.axhline( + y=self.max_hold, color="green", linestyle="--", alpha=0.3 + ) + self.ax3.axhline( + y=self.min_hold, color="red", linestyle="--", alpha=0.3 + ) + if self.AngleMax_set.get() == 1: + self.ax3.axvline(x=max_freq, color="orange", linestyle=":", alpha=0.5) + self.graph3.draw() + + # Monopulse Tracking Waterfall Plot + if plot_tracking == 1: + self.TrackArray = self.TrackArray[-1000:] # just use the last elements + self.ax4.cla() + self.ax4.plot( + self.TrackArray[-1000:], + range(0, 10000, 10), + "-o", + ms=3, + alpha=0.7, + mfc="blue", + ) + self.ax4.set_xlabel("Steering Angle (deg)") + self.ax4.set_ylabel("time (ms)") + self.ax4.set_xlim([-60, 60]) + # self.ax4.set_ylim([-1, 1]) + self.ax4.grid() + self.graph4.draw() + + if self.mode_var.get() == "Signal vs Time": + self.ax5.cla() + self.ax5.plot( + self.ArrayAngle, + self.ArrayGain, + "-o", + ms=3, + alpha=0.7, + mfc="blue", + color="blue", + ) + self.ax5.set_xlabel("time") + self.ax5.set_ylabel("Amplitude (dBFS)") + self.ax5.set_xlim([-89, 89]) + self.ax5.set_ylim([-50, 0]) + self.ax5.set_xticklabels([]) # hide the x axis numbers + self.ax5.grid() + self.graph5.draw() + + +# Some notes on TKinter scaling issues: +# The root.tk.call('tk', 'scaling', 2.0) takes one argument, which is the number of pixels in one "point". +# A point is 1/72 of an inch, so a scaling factor of 1.0 is appropriate for a 72DPI display. +# https://coderslegacy.com/python/problem-solving/improve-tkinter-resolution/ + + +# Another method that might help scaling issues... so far the scaling method seems to work better. +# import ctypes +# ctypes.windll.shcore.SetProcessDpiAwareness(1) + +root = Tk() +root.title("Phased Array Beamforming") +root.geometry("1600x800+100+20") +root.call("tk", "scaling", 1.50) # MWT: This seemed to fix scaling issues in Windows. +# Verify it doesn't mess anything up on the Pi, detect os if so. +root.resizable( + False, False +) # the GUI is setup to be resizeable--and it all works great. +# EXCEPT when we do the blitting of the graphs ( to speed up redraw time). I can't figure that out. So, for now, I just made it un-resizeable... +root.minsize(700, 600) +app = App(root) +root.mainloop() diff --git a/examples/phaser/phaser_minimal_example.py b/examples/phaser/phaser_minimal_example.py new file mode 100644 index 000000000..3b62fc32e --- /dev/null +++ b/examples/phaser/phaser_minimal_example.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + + +# A minimal example script to demonstrate some basic concepts of controlling +# the Pluto SDR. +# The script reads in the measured HB100 source's frequency (previously stored +# with the find_hb100 utility), sets the phaser's pll such that the HB100's frequency +# shows up at 1MHz, captures a buffer of data, take FFT, plot time and frequency domain + +# Since the Pluto is attached to the phaser board, we do have +# to do some setup and housekeeping of the ADAR1000s, but other than that it's +# trimmed down about as much as possible. + +# Import libraries. +from time import sleep + +import matplotlib.pyplot as plt +import numpy as np +from adi import ad9361 +from adi.cn0566 import CN0566 +from phaser_functions import load_hb100_cal, spec_est + +# First try to connect to a locally connected CN0566. On success, connect, +# on failure, connect to remote CN0566 + +try: + print("Attempting to connect to CN0566 via ip:localhost...") + my_phaser = CN0566(uri="ip:localhost") + print("Found CN0566. Connecting to PlutoSDR via default IP address...") + my_sdr = ad9361(uri="ip:192.168.2.1") + print("PlutoSDR connected.") + +except: + print("CN0566 on ip.localhost not found, connecting via ip:phaser.local...") + my_phaser = CN0566(uri="ip:phaser.local") + print("Found CN0566. Connecting to PlutoSDR via shared context...") + my_sdr = ad9361(uri="ip:phaser.local:50901") + print("Found SDR on shared phaser.local.") + +my_phaser.sdr = my_sdr # Set my_phaser.sdr + +sleep(0.5) + +# By default device_mode is "rx" +my_phaser.configure(device_mode="rx") + +try: + my_phaser.SignalFreq = load_hb100_cal() + print("Found signal freq file, ", my_phaser.SignalFreq) +except: + my_phaser.SignalFreq = 10.525e9 + print("No signal freq file found, setting to 10.525 GHz") + + +# Configure CN0566 parameters. +# ADF4159 and ADAR1000 array attributes are exposed directly, although normally +# accessed through other methods. + + +# Set all antenna elements to half scale - a typical HB100 will have plenty +# of signal power. + +gain_list = [64] * 8 # (64 is about half scale) +for i in range(0, len(gain_list)): + my_phaser.set_chan_gain(i, gain_list[i], apply_cal=False) + +# Aim the beam at boresight (zero degrees). Place HB100 right in front of array. +my_phaser.set_beam_phase_diff(0.0) + + +# Configure SDR parameters. Start with the more involved settings, don't +# pay too much attention to these. They are covered in much more detail in +# Software Defined Radio for Engineers. + +my_sdr._ctrl.debug_attrs["adi,frequency-division-duplex-mode-enable"].value = "1" +my_sdr._ctrl.debug_attrs[ + "adi,ensm-enable-txnrx-control-enable" +].value = "0" # Disable pin control so spi can move the states +my_sdr._ctrl.debug_attrs["initialize"].value = "1" + +my_sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) +my_sdr._rxadc.set_kernel_buffers_count(1) # No stale buffers to flush +rx = my_sdr._ctrl.find_channel("voltage0") +rx.attrs["quadrature_tracking_en"].value = "1" # enable quadrature tracking +# Make sure the Tx channels are attenuated (or off) and their freq is far away from Rx +# this is a negative number between 0 and -88 +my_sdr.tx_hardwaregain_chan0 = int(-80) +my_sdr.tx_hardwaregain_chan1 = int(-80) + + +# These parameters are more closely related to analog radio design +# and are what you would adjust to change the IFs, signal bandwidths, sample rate, etc. +# +# Sample rate is set to 30Msps, +# for a total of 30MHz of bandwidth (quadrature sampling) +# Filter is 20MHz LTE, so you get a bit less than 20MHz of usable +# bandwidth. + +my_sdr.sample_rate = int(30e6) # Sampling rate +my_sdr.rx_buffer_size = int(1024) # Number of samples per buffer +my_sdr.rx_rf_bandwidth = int(10e6) # Analog bandwidth + +# Manually control gain - in most applications, you want to enable AGC to keep +# to adapt to changing conditions. Since we're taking quasi-quantitative measurements, +# we want to set the gain to a fixed value. +my_sdr.gain_control_mode_chan0 = "manual" # DISable AGC +my_sdr.gain_control_mode_chan1 = "manual" +my_sdr.rx_hardwaregain_chan0 = 0 # dB +my_sdr.rx_hardwaregain_chan1 = 0 # dB + +my_sdr.rx_lo = int(2.2e9) # Downconvert by 2GHz # Receive Freq +my_sdr.filter = "LTE20_MHz.ftr" # Handy filter for fairly widdeband measurements + +# Now set the phaser's PLL. This is the ADF4159, and we'll set it to the HB100 frequency +# plus the desired 2GHz IF, minus a small offset so we don't land at exactly DC. +# If the HB100 is at exactly 10.525 GHz, setting the PLL to 12.724 GHz will result +# in an IF at 2.201 GHz. + +offset = 1000000 # add a small offset +my_phaser.frequency = ( + int(my_phaser.SignalFreq + my_sdr.rx_lo - offset) +) // 4 # PLL feedback is from the VCO's /4 output + +# Capture data! +data = my_sdr.rx() +# Add both channels for calculating spectrum +data_sum = data[0] + data[1] + +# spec_est is a simple estimation function that applies a window, takes the FFT, +# scales and converts to dB. +ampl, freqs = spec_est(data_sum, 30e6, ref=2 ^ 12, plot=False) +ampl = np.fft.fftshift(ampl) +ampl = np.flip(ampl) # Just an experiment... +freqs = np.fft.fftshift(freqs) + +freqs /= 1e6 # Scale Hz -> MHz + +peak_index = np.argmax(ampl) # Locate the peak frequency's index +peak_freq = freqs[peak_index] # And the frequency itself. +print("Peak frequency found at ", freqs[peak_index], " MHz.") + +# Now plot the data +plt.figure(1) +plt.subplot(2, 1, 1) +plt.title("Time Domain I/Q Data") +plt.plot(data[0].real, marker="o", ms=2, color="red") # Only plot real part +plt.plot(data[1].real, marker="o", ms=2, color="blue") +plt.xlabel("Data Point") +plt.ylabel("ADC output") +plt.subplot(2, 1, 2) +plt.title("Spectrum, peak at " + str(freqs[peak_index]) + " MHz.") +plt.plot(freqs, ampl, marker="o", ms=2) +plt.xlabel("Frequency [MHz]") +plt.ylabel("Signal Strength") +plt.tight_layout() +plt.show() + +# Clean up / close connections +del my_sdr +del my_phaser diff --git a/examples/phaser/phaser_prod_tst.py b/examples/phaser/phaser_prod_tst.py new file mode 100644 index 000000000..a199a26ae --- /dev/null +++ b/examples/phaser/phaser_prod_tst.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +# Must use Python 3 +# Copyright (C) 2022 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. + + +# First cut at a production test. Just a template at this point. +# Read all board voltages, current, ambient temperature, and compare against limits + +# Run gain calibration, verify signal levels are within some fairly wide limits, +# and that the spread between minimum and maximum gains is within TBD limits. + +# Run phase calibration, verify that phase corretionn values are within TBD degrees. + + +# Also consider having a minimal board-level test just to verify basic functionality, +# with wider test limits. This would be run before assembling the entire phaser +# (with Pluto, Pi, Standoffs, etc.) + + +import os +import socket +import sys +import time + +import matplotlib.pyplot as plt +import numpy as np +from adi import ad9361 +from adi.cn0566 import CN0566 +from phaser_functions import ( + calculate_plot, + channel_calibration, + gain_calibration, + get_signal_levels, + phase_calibration, +) +from scipy import signal + +start = time.time() + +failures = [] +# temp 1.8V 3.0 3.3 4.5 15? USB curr. Vtune +monitor_hi_limits = [60.0, 1.85, 3.15, 3.45, 4.75, 16.0, 5.25, 1.6, 14.0] +monitor_lo_limts = [20.0, 1.75, 2.850, 3.15, 4.25, 13.0, 4.75, 1.2, 1.0] +monitor_ch_names = [ + "Board temperature: ", + "1.8V supply: ", + "3.0V supply: ", + "3.3V supply: ", + "4.5V supply: ", + "Vtune amp supply: ", + "USB C input supply: ", + "Board current: ", + "VTune: ", +] + + +channel_cal_limits = 10.0 # Fail if channels mismatched by more than 10 dB +gain_cal_limits = ( + 0.50 # Fail if any channel is less than 60% of the highest gain channel +) +# Phase delta limits between channels. Extra tolerance between 3rd and 4th element, +# which split across the two Pluto channels. +phase_cal_limits = [90.0, 90.0, 90.0, 120.0, 90.0, 90.0, 90.0] + +# Set up RF / IF / LO frequencies +rx_lo = 2.2e9 +SignalFreq = 10.2e9 + +use_tx = True # Use on board TX w/ cabled antenna, NOT external HB100 + + +# First try to connect to a locally connected CN0566. On success, connect, +# on failure, connect to remote CN0566 + +try: + print("Attempting to connect to CN0566 via ip:localhost...") + my_phaser = CN0566(uri="ip:localhost") + print("Found CN0566. Connecting to PlutoSDR via default IP address...") + my_sdr = ad9361(uri="ip:192.168.2.1") + print("PlutoSDR connected.") + +except: + print("CN0566 on ip.localhost not found, connecting via ip:phaser.local...") + my_phaser = CN0566(uri="ip:phaser.local") + print("Found CN0566. Connecting to PlutoSDR via shared context...") + my_sdr = ad9361(uri="ip:phaser.local:50901") + print("Found SDR on shared phaser.local.") + +my_phaser.sdr = my_sdr # Set my_phaser.sdr + +time.sleep(0.5) + +# By default device_mode is "rx" +my_phaser.configure(device_mode="rx") + +# Averages decide number of time samples are taken to plot and/or calibrate system. By default it is 1. +my_phaser.Averages = 8 + +# Set up receive frequency. When using HB100, you need to know its frequency +# fairly accurately. Use the cn0566_find_hb100.py script to measure its frequency +# and write out to the cal file. IF using the onboard TX generator, delete +# the cal file and set frequency via config.py or config_custom.py. + +# Set the PLL once here, so that we can measure VTune below. +my_phaser.SignalFreq = SignalFreq +my_phaser.lo = int(SignalFreq) + rx_lo # This actually sets the ADF4159. +time.sleep(0.5) # Give the ADF4159 time to settle + +# try: +# my_phaser.SignalFreq = load_hb100_cal() +# print("Found signal freq file, ", my_phaser.SignalFreq) +# use_tx = False +# except: +# my_phaser.SignalFreq = 10.525e9 +# print("No signal freq found, keeping at ", my_phaser.SignalFreq) +# use_tx = True + +# MWT: Do NOT load in cal values during production test. That's what we're doing, after all :) +# But running a second time with saved cal values may be useful in development. +# my_phaser.load_gain_cal('gain_cal_val.pkl') +# my_phaser.load_phase_cal('phase_cal_val.pkl') + + +print("Using TX output closest to tripod mount, 10.525 GHz for production test.") + + +# Configure CN0566 parameters. +# ADF4159 and ADAR1000 array attributes are exposed directly, although normally +# accessed through other methods. + +print("Reading voltage monitor...") +monitor_vals = my_phaser.read_monitor() + +for i in range(0, len(monitor_vals)): + if not (monitor_lo_limts[i] <= monitor_vals[i] <= monitor_hi_limits[i]): + print("Fails ", monitor_ch_names[i], ": ", monitor_vals[i]) + failures.append( + "Monitor fails " + monitor_ch_names[i] + ": " + str(monitor_vals[i]) + ) + else: + print("Passes ", monitor_ch_names[i], monitor_vals[i]) + +if len(failures) > 0: + print("Fails one or more supply voltage tests. Please set board aside for debug.") + sys.exit() +else: + print("Passes all monitor readings, proceeding...") + +if (monitor_vals[5] - monitor_vals[8]) < 1: + print("Warning: Less than 1V headroom on Vtune") + + +print("\nSetting up Pluto and Getting signal levels...") +success = False +attempts = 0 +max_attempts = 5 + +while attempts < max_attempts and not success: + try: + my_phaser.lo = int(SignalFreq) + rx_lo # This actually sets the ADF4159. + time.sleep(0.5) # Give the ADF4159 time to settle + + # Configure SDR parameters. + + # configure sdr/pluto according to above-mentioned freq plan + # my_sdr._ctrl.debug_attrs["adi,frequency-division-duplex-mode-enable"].value = "1" + # my_sdr._ctrl.debug_attrs["adi,ensm-enable-txnrx-control-enable"].value = "0" # Disable pin control so spi can move the states + # my_sdr._ctrl.debug_attrs["initialize"].value = "1" + my_sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) + my_sdr._rxadc.set_kernel_buffers_count( + 1 + ) # Super important - don't want to have to flush stale buffers + rx = my_sdr._ctrl.find_channel("voltage0") + rx.attrs[ + "quadrature_tracking_en" + ].value = "1" # set to '1' to enable quadrature tracking + my_sdr.sample_rate = int(30000000) # Sampling rate + my_sdr.rx_buffer_size = int(4 * 256) + my_sdr.rx_rf_bandwidth = int(10e6) + # We must be in manual gain control mode (otherwise we won't see the peaks and nulls!) + my_sdr.gain_control_mode_chan0 = "manual" + my_sdr.gain_control_mode_chan1 = "manual" + my_sdr.rx_hardwaregain_chan0 = 12 + my_sdr.rx_hardwaregain_chan1 = 12 + + my_sdr.rx_lo = int(rx_lo) # 4495000000 # Receive Freq + + print("Loading filter") + my_sdr.filter = ( + os.getcwd() + "/LTE20_MHz.ftr" + ) # MWT: Using this for now, may not be necessary. + rx_buffer_size = int(4 * 256) + my_sdr.rx_buffer_size = rx_buffer_size + + my_sdr.tx_cyclic_buffer = True + my_sdr.tx_buffer_size = int(2 ** 16) + + if use_tx is True: + # To disable rx, set attenuation to a high value and set frequency far from rx. + my_sdr.tx_hardwaregain_chan0 = int( + -88 + ) # this is a negative number between 0 and -88 + my_sdr.tx_hardwaregain_chan1 = int(-3) + my_sdr.tx_lo = int(2.2e9) + else: + # To disable rx, set attenuation to a high value and set frequency far from rx. + my_sdr.tx_hardwaregain_chan0 = int( + -88 + ) # this is a negative number between 0 and -88 + my_sdr.tx_hardwaregain_chan1 = int(-88) + my_sdr.tx_lo = int(1.0e9) + + # my_sdr.dds_enabled = [1, 1, 1, 1] #DDS generator enable state + # my_sdr.dds_frequencies = [0.1e6, 0.1e6, 0.1e6, 0.1e6] #Frequencies of DDSs in Hz + # my_sdr.dds_scales = [1, 1, 0, 0] #Scale of DDS signal generators Ranges [0,1] + my_sdr.dds_single_tone( + int(0.5e6), 0.9, 1 + ) # sdr.dds_single_tone(tone_freq_hz, tone_scale_0to1, tx_channel) + + sig_levels = get_signal_levels(my_phaser) + print(sig_levels) + if min(sig_levels) < 80.0: + print("Low signal levels on attempt ", attempts, " of ", max_attempts) + attempts += 1 + else: + success = True + if attempts == max_attempts: + raise Exception("Max attempts reached") + except: + print( + "failed after " + str(max_attempts) + " attempts, please set board aside." + ) + sys.exit() + + +print( + "Calibrating SDR channel mismatch, gain and phase - place antenna at " + "mechanical boresight in front of the array.\n\n" +) + + +print("\nCalibrating SDR channel mismatch, verbosely...") +channel_calibration(my_phaser, verbose=True) + +print("\nCalibrating Gain, verbosely, then saving cal file...") +gain_calibration(my_phaser, verbose=True) # Start Gain Calibration + +print("\nCalibrating Phase, verbosely, then saving cal file...") +phase_calibration(my_phaser, verbose=True) # Start Phase Calibration + +print("Done calibration") + + +# my_phaser.save_gain_cal() # Default filename +# my_phaser.save_phase_cal() # Default filename + +for i in range(0, len(my_phaser.ccal)): + if my_phaser.ccal[i] > channel_cal_limits: + print("Channel cal failure on channel ", i, ", ", my_phaser.gcal[i]) + failures.append("Channel cal failure on channel " + str(i)) + +for i in range(0, len(my_phaser.gcal)): + if my_phaser.gcal[i] < gain_cal_limits: + print("Gain cal failure on element ", i, ", ", my_phaser.gcal[i]) + failures.append("Gain cal failure on element " + str(i)) + + +# Important - my_phaser.pcal represents the CUMULATIVE phase shift across the +# array. Element 0 will always be zero, so we just need to check the delta between +# 0-1, 1-2, 2-3, etc. This IS sort of un-doing what the pcal routine does, but oh well... + +for i in range(0, len(my_phaser.pcal) - 1): + delta = my_phaser.pcal[i + 1] - my_phaser.pcal[i] + if abs(delta) > phase_cal_limits[i]: + print("Phase cal failure on elements ", i - 1, ", ", i, str(delta)) + failures.append( + "Phase cal failure on elements " + + str(i - 1) + + ", " + + str(i) + + ", delta: " + + str(delta) + ) + +print("Test took " + str(time.time() - start) + " seconds.") + +if len(failures) == 0: + print("\nWooHoo! BOARD PASSES!!\n") +else: + print("\nD'oh! BOARD FAILS!\n") + for failure in failures: + print(failure) + print("\n\n") + + +ser_no = input("Please enter serial number of board, then press enter.\n") +filename = str("results/CN0566_" + ser_no + "_" + time.asctime() + ".txt") +filename = filename.replace(":", "-") +filename = os.getcwd() + "/" + filename + +with open(filename, "w") as f: + f.write("Phaser Test Results:\n") + f.write("\nMonitor Readings:\n") + f.write(str(monitor_vals)) + f.write("\nSignal Levels:\n") + f.write(str(sig_levels)) + f.write("\nChannel Calibration:\n") + f.write(str(my_phaser.ccal)) + f.write("\nGain Calibration:\n") + f.write(str(my_phaser.gcal)) + f.write("\nPhase Calibration:\n") + f.write(str(my_phaser.pcal)) + if len(failures) == 0: + f.write("\nThis is a PASSING board!\n") + else: + f.write("\nThis is a FAILING board!\n") + +do_plot = ( + False # Do a plot just for debug purposes. Suppress for actual production test. +) + +while do_plot == True: + + start = time.time() + my_phaser.set_beam_phase_diff(0.0) + time.sleep(0.25) + data = my_sdr.rx() + data = my_sdr.rx() + ch0 = data[0] + ch1 = data[1] + f, Pxx_den0 = signal.periodogram( + ch0[1:-1], 30000000, "blackman", scaling="spectrum" + ) + f, Pxx_den1 = signal.periodogram( + ch1[1:-1], 30000000, "blackman", scaling="spectrum" + ) + + plt.figure(1) + plt.clf() + plt.plot(np.real(ch0), color="red") + plt.plot(np.imag(ch0), color="blue") + plt.plot(np.real(ch1), color="green") + plt.plot(np.imag(ch1), color="black") + np.real + plt.xlabel("data point") + plt.ylabel("output code") + plt.draw() + + plt.figure(2) + plt.clf() + plt.semilogy(f, Pxx_den0) + plt.semilogy(f, Pxx_den1) + plt.ylim([1e-5, 1e6]) + plt.xlabel("frequency [Hz]") + plt.ylabel("PSD [V**2/Hz]") + plt.draw() + + # Plot the output based on experiment that you are performing + print("Plotting...") + + plt.figure(3) + plt.ion() + # plt.show() + ( + gain, + angle, + delta, + diff_error, + beam_phase, + xf, + max_gain, + PhaseValues, + ) = calculate_plot(my_phaser) + print("Sweeping took this many seconds: " + str(time.time() - start)) + # gain, = my_phaser.plot(plot_type="monopulse") + plt.clf() + plt.scatter(angle, gain, s=10) + plt.scatter(angle, delta, s=10) + plt.show() + + plt.pause(0.05) + time.sleep(0.05) + print("Total took this many seconds: " + str(time.time() - start)) + + do_plot = False + print("Exiting Loop") diff --git a/examples/phaser/requirements_phaser.txt b/examples/phaser/requirements_phaser.txt new file mode 100644 index 000000000..76390029c --- /dev/null +++ b/examples/phaser/requirements_phaser.txt @@ -0,0 +1,3 @@ +matplotlib +scipy +pyqtgraph diff --git a/examples/phaser/results/CN0566_1234_Sun Jan 22 09-02-39 2023.txt b/examples/phaser/results/CN0566_1234_Sun Jan 22 09-02-39 2023.txt new file mode 100644 index 000000000..d8f1f7b13 --- /dev/null +++ b/examples/phaser/results/CN0566_1234_Sun Jan 22 09-02-39 2023.txt @@ -0,0 +1,9 @@ +Phaser Test Results: +Monitor Readings: +[49.75, 1.8115234360159997, 3.00292968504, 3.2678222629479996, 4.52055053340614, 13.715624988764159, 4.985577388493939, 1.4569091784939998, 12.683056630235038] +Channel Calibration: +[1.4381635018470393, 0.0] +Gain Calibration: +[0.9349683654346784, 0.8985753770697283, 0.9273724517689751, 0.7505707089172452, 1.0, 0.9467012396797406, 0.8109937844703348, 0.9919321400013712] +Phase Calibration: +[0, 16.875, 87.1875, 8.4375, 154.6875, -174.375, 154.6875, -112.5] diff --git a/supported_parts.md b/supported_parts.md index 90dc8f663..90f668343 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -129,6 +129,7 @@ - CN0540 - CN0554 - CN0549 +- CN0566 - CN0575 - DAQ2 - DAQ3 diff --git a/tasks.py b/tasks.py index 64255a2cf..213358030 100644 --- a/tasks.py +++ b/tasks.py @@ -193,7 +193,7 @@ def checkparts(c): count = 0 for p in parts: p = p.replace("_", "-") - if not p in rm.lower(): + if not p.lower() in rm.lower(): count += 1 print("Missing", p, "from README") if count == 0: @@ -268,6 +268,7 @@ def checkemulation(c): "adxl345", "adxrs290", "cn0532", + "cn0566", "ltc2314_14", "ltc2499", "ltc2983", diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index a222d0a3d..b4e153ebf 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -165,7 +165,16 @@ cn0554: - filename: cn0554.xml - data_devices: - iio:device0 - - iio:device1 + - iio:device1 + +cn0566: + - ad7291 + - pyadi_iio_class_support: + - CN0566 + - emulate: + - filename: cn0566.xml + - data_devices: + - iio:device1 cn0575: - adt75 From ff613beec25480ede2406ba0c95e2d95c9445e9c Mon Sep 17 00:00:00 2001 From: Nathaniel Acut <127278797+nsacut@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:11:38 +0800 Subject: [PATCH 06/23] Adding Support for CN0579 (#450) * Add support to cn0579 Signed-off-by: Nathaniel Acut * Add docstring to current source controller Also, reused sin_params from ad4639 folder. Signed-off-by: Nathaniel Acut * Was rearranged by pre-commit Signed-off-by: Nathaniel Acut * Removed var 'dac_scale' is assigned to but never used Signed-off-by: Nathaniel Acut * Fix remaining linting issues Signed-off-by: Nathaniel Acut * Fix Codacy Static Code Analysis Issues Signed-off-by: Nathaniel Acut * Remove unused import Signed-off-by: Nathaniel Acut --------- Signed-off-by: Nathaniel Acut --- adi/__init__.py | 1 + adi/cn0579.py | 107 +++++++++++++++++++++++++++++ doc/source/devices/adi.cn0579.rst | 7 ++ doc/source/devices/index.rst | 1 + examples/cn0579/cn0579_example.py | 108 ++++++++++++++++++++++++++++++ examples/cn0579/save_for_scope.py | 61 +++++++++++++++++ supported_parts.md | 1 + test/emu/devices/cn0579.xml | 1 + test/emu/hardware_map.yml | 13 +++- test/test_cn0579.py | 61 +++++++++++++++++ 10 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 adi/cn0579.py create mode 100644 doc/source/devices/adi.cn0579.rst create mode 100644 examples/cn0579/cn0579_example.py create mode 100644 examples/cn0579/save_for_scope.py create mode 100644 test/emu/devices/cn0579.xml create mode 100644 test/test_cn0579.py diff --git a/adi/__init__.py b/adi/__init__.py index c3a5be043..0cb356152 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -74,6 +74,7 @@ from adi.cn0554 import cn0554 from adi.cn0566 import CN0566 from adi.cn0575 import cn0575 +from adi.cn0579 import cn0579 from adi.daq2 import DAQ2 from adi.daq3 import DAQ3 from adi.fmc_vna import fmcvna diff --git a/adi/cn0579.py b/adi/cn0579.py new file mode 100644 index 000000000..c20971535 --- /dev/null +++ b/adi/cn0579.py @@ -0,0 +1,107 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from adi.ad7768 import ad7768_4 + + +class cn0579(ad7768_4): + + """ CN0579 - Multichannel IEPE DAQ for CbM """ + + def __init__( + self, uri="ip:analog.local", + ): + + ad7768_4.__init__(self, uri) + + self._gpio = self._ctx.find_device("cn0579_control") + self._ad5696 = self._ctx.find_device("ad5696") + + @property + def shift_voltage0(self): + """shift_voltage: Shift voltage in mV from AD5696 to bias sensor data""" + dac_chan = self._ad5696 + raw = self._get_iio_attr("voltage0", "raw", True, dac_chan) + return raw # * dac_scale * 1.22 + + @shift_voltage0.setter + def shift_voltage0(self, value): + dac_chan = self._ad5696 + self._set_iio_attr_int("voltage0", "raw", True, int(value), dac_chan) + + @property + def shift_voltage1(self): + """shift_voltage: Shift voltage in mV from AD5696 to bias sensor data""" + dac_chan = self._ad5696 + raw = self._get_iio_attr("voltage1", "raw", True, dac_chan) + return raw # * dac_scale * 1.22 + + @shift_voltage1.setter + def shift_voltage1(self, value): + dac_chan = self._ad5696 + self._set_iio_attr_int("voltage1", "raw", True, int(value), dac_chan) + + @property + def shift_voltage2(self): + """shift_voltage: Shift voltage in mV from AD5696 to bias sensor data""" + dac_chan = self._ad5696 + raw = self._get_iio_attr("voltage2", "raw", True, dac_chan) + return raw # * dac_scale * 1.22 + + @shift_voltage2.setter + def shift_voltage2(self, value): + dac_chan = self._ad5696 + self._set_iio_attr_int("voltage2", "raw", True, int(value), dac_chan) + + @property + def shift_voltage3(self): + """shift_voltage: Shift voltage in mV from AD5696 to bias sensor data""" + dac_chan = self._ad5696 + raw = self._get_iio_attr("voltage3", "raw", True, dac_chan) + return raw # * dac_scale * 1.22 + + @shift_voltage3.setter + def shift_voltage3(self, value): + dac_chan = self._ad5696 + self._set_iio_attr_int("voltage3", "raw", True, int(value), dac_chan) + + @property + def CC_CH0(self): + """Get Channel 0 Current Source Control""" + return self._get_iio_attr("voltage0", "raw", True, self._gpio) + + @CC_CH0.setter + def CC_CH0(self, value): + """Set Channel 0 Current Source Control""" + self._set_iio_attr_int("voltage0", "raw", True, value, self._gpio) + + @property + def CC_CH1(self): + """Get Channel 1 Current Source Control""" + return self._get_iio_attr("voltage1", "raw", True, self._gpio) + + @CC_CH1.setter + def CC_CH1(self, value): + """Set Channel 1 Current Source Control""" + self._set_iio_attr_int("voltage1", "raw", True, value, self._gpio) + + @property + def CC_CH2(self): + """Get Channel 2 Current Source Control""" + return self._get_iio_attr("voltage2", "raw", True, self._gpio) + + @CC_CH2.setter + def CC_CH2(self, value): + """Set Channel 2 Current Source Control""" + self._set_iio_attr_int("voltage2", "raw", True, value, self._gpio) + + @property + def CC_CH3(self): + """Get Channel 3 Current Source Control""" + return self._get_iio_attr("voltage3", "raw", True, self._gpio) + + @CC_CH3.setter + def CC_CH3(self, value): + """Set Channel 3 Current Source Control""" + self._set_iio_attr_int("voltage3", "raw", True, value, self._gpio) diff --git a/doc/source/devices/adi.cn0579.rst b/doc/source/devices/adi.cn0579.rst new file mode 100644 index 000000000..bb5a88196 --- /dev/null +++ b/doc/source/devices/adi.cn0579.rst @@ -0,0 +1,7 @@ +cn0579 +================= + +.. automodule:: adi.cn0579 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index b22625bc1..51ca275ea 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -83,6 +83,7 @@ Supported Devices adi.cn0554 adi.cn0566 adi.cn0575 + adi.cn0579 adi.daq2 adi.daq3 adi.fmc_vna diff --git a/examples/cn0579/cn0579_example.py b/examples/cn0579/cn0579_example.py new file mode 100644 index 000000000..eafb8673a --- /dev/null +++ b/examples/cn0579/cn0579_example.py @@ -0,0 +1,108 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import inspect +import os +import sys + +import matplotlib.pyplot as plt +import numpy as np +from adi import cn0579 + +# Lets try to reuse the ./examples/ad4630/sin_params.py file instead of having +# our own copy. Add path prior to importing sin_params +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +ad4630_dir = os.path.join(parentdir, "ad4630") +sys.path.insert(0, ad4630_dir) + +import sin_params # isort:skip + +# from save_for_pscope import save_for_pscope + +# Optionally pass URI as command line argument, +# else use default ip:analog.local +my_uri = sys.argv[1] if len(sys.argv) >= 2 else "ip:analog.local" +print("uri: " + str(my_uri)) + +my_adc = cn0579(uri=my_uri) +my_adc.rx_buffer_size = 1024 + +# Set Sample Rate. Options are 1ksps to 256ksps, 1k* power of 2. +# Note that sample rate and power mode are not orthogonal - refer +# to datasheet. +my_adc.sampling_frequency = 256000 + +# Choose a power mode: +# my_adc.power_mode_avail = 'LOW_POWER_MODE MEDIAN_MODE FAST_MODE' +my_adc.power_mode = "FAST_MODE" + +# Choose a filter type: +# my_adc.filter_type_avail = 'WIDEBAND SINC5' +my_adc.filter_type = "WIDEBAND" + +# Choose output format: +# my_adc.rx_output_type = "raw" +my_adc.rx_output_type = "SI" + +# Set Shift Voltage: +vshift = 43355 +my_adc.shift_voltage0 = vshift +my_adc.shift_voltage1 = vshift +my_adc.shift_voltage2 = vshift +my_adc.shift_voltage3 = vshift + +# Current Source for each channel: +my_adc.CC_CH0 = 1 +my_adc.CC_CH1 = 1 +my_adc.CC_CH2 = 0 +my_adc.CC_CH3 = 0 + +# Verify settings: +print("Power Mode: ", my_adc.power_mode) +print("Sampling Frequency: ", my_adc.sampling_frequency) +print("Filter Type: ", my_adc.filter_type) +print("Enabled Channels: ", my_adc.rx_enabled_channels) +print("Ch0 Shift Voltage: ", my_adc.shift_voltage0) +print("Ch1 Shift Voltage: ", my_adc.shift_voltage1) +print("Ch2 Shift Voltage: ", my_adc.shift_voltage2) +print("Ch3 Shift Voltage: ", my_adc.shift_voltage3) + + +plt.clf() +data = my_adc.rx() +for ch in my_adc.rx_enabled_channels: + plt.plot(range(0, len(data[0])), data[ch], label="voltage" + str(ch)) +plt.xlabel("Data Point") +if my_adc.rx_output_type == "SI": + plt.ylabel("Millivolts") +else: + plt.ylabel("ADC counts") +plt.legend( + bbox_to_anchor=(0.0, 1.02, 1.0, 0.102), + loc="lower left", + ncol=4, + mode="expand", + borderaxespad=0.0, +) + +for ch in my_adc.rx_enabled_channels: + parameters = sin_params.sin_params(data[ch]) + snr = parameters[1] + thd = parameters[2] + sinad = parameters[3] + enob = parameters[4] + sfdr = parameters[5] + floor = parameters[6] + print("\nChannel " + str(ch)) + print("SNR = " + str(snr)) + print("THD = " + str(thd)) + print("SINAD = " + str(sinad)) + print("ENOB = " + str(enob)) + print("SFDR = " + str(sfdr)) + print("FLOOR = " + str(floor)) + +plt.show() + +# save_for_pscope("Vshift=" + str(my_adc.shift_voltage2) + "_" + str(my_adc.sampling_frequency) + "_cn0579_data.adc" , 24, True, len(data), "DC0000", "LTC1111", data, data, ) diff --git a/examples/cn0579/save_for_scope.py b/examples/cn0579/save_for_scope.py new file mode 100644 index 000000000..081966a8c --- /dev/null +++ b/examples/cn0579/save_for_scope.py @@ -0,0 +1,61 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import math as m + + +def save_for_pscope( + out_path, num_bits, is_bipolar, num_samples, dc_num, ltc_num, *data +): + num_channels = len(data) + if num_channels < 0 or num_channels > 16: + raise ValueError("pass in a list for each channel (between 1 and 16)") + + full_scale = 1 << num_bits + if is_bipolar: + min_val = -full_scale // 2 + max_val = full_scale // 2 + else: + min_val = 0 + max_val = full_scale + + with open(out_path, "w") as out_file: + out_file.write("Version,115\n") + out_file.write( + "Retainers,0,{0:d},{1:d},1024,0,{2:0.15f},1,1\n".format( + num_channels, num_samples, 0.0 + ) + ) + out_file.write("Placement,44,0,1,-1,-1,-1,-1,10,10,1031,734\n") + out_file.write("DemoID," + dc_num + "," + ltc_num + ",0\n") + for i in range(num_channels): + out_file.write( + "RawData,{0:d},{1:d},{2:d},{3:d},{4:d},{5:0.15f},{3:e},{4:e}\n".format( + i + 1, int(num_samples), int(num_bits), min_val, max_val, 1.0 + ) + ) + for samp in range(num_samples): + out_file.write(str(data[0][samp])) + for ch in range(1, num_channels): + out_file.write(", ," + str(data[ch][samp])) + out_file.write("\n") + out_file.write("End\n") + + +if __name__ == "__main__": + num_bits = 16 + num_samples = 65536 + channel_1 = [int(8192 * m.cos(0.12 * d)) for d in range(num_samples)] + channel_2 = [int(8192 * m.cos(0.034 * d)) for d in range(num_samples)] + + save_for_pscope( + "test.adc", + num_bits, + True, + num_samples, + "DC9876A-A", + "LTC9999", + channel_1, + channel_2, + ) diff --git a/supported_parts.md b/supported_parts.md index 90f668343..c4d430d98 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -131,6 +131,7 @@ - CN0549 - CN0566 - CN0575 +- CN0579 - DAQ2 - DAQ3 - FMCADC3 diff --git a/test/emu/devices/cn0579.xml b/test/emu/devices/cn0579.xml new file mode 100644 index 000000000..3219438a9 --- /dev/null +++ b/test/emu/devices/cn0579.xml @@ -0,0 +1 @@ +]> \ No newline at end of file diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index b4e153ebf..43841da98 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -185,7 +185,18 @@ cn0575: - filename: cn0575.xml - data_devices: - hwmon2 - - iio:device0 + - iio:device0 + +cn0579: + - cf_axi_adc + - cn0579_control + - ad5696 + - pyadi_iio_class_support: + - cn0579 + - emulate: + - filename: cn0579.xml + - data_devices: + - iio:device1 # SOMS adrv9361: diff --git a/test/test_cn0579.py b/test/test_cn0579.py new file mode 100644 index 000000000..15465a29f --- /dev/null +++ b/test/test_cn0579.py @@ -0,0 +1,61 @@ +import pytest + +hardware = "cn0579" +classname = "adi.cn0579" + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_cn0579_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( + "attr, val", + [ + ( + "sampling_frequency", + [ + 1000, + 2000, + 4000, + 8000, + 16000, + 32000, + 64000, + 128000, + 256000, + 32000, + ], # End on a rate compatible with all power modes + ), + ("filter_type", ["WIDEBAND", "SINC5"],), + ("power_mode", ["MEDIAN_MODE", "FAST_MODE"],), + ("sync_start_enable", ["arm"],), + ], +) +def test_cn0579_attr_multiple( + 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", + [ + ("CC_CH0", 0, 1, 1, 0), + ("CC_CH1", 0, 1, 1, 0), + ("CC_CH2", 0, 1, 1, 0), + ("CC_CH3", 0, 1, 1, 0), + ], +) +def test_cn0579_attr_single( + test_attribute_single_value, iio_uri, classname, attr, start, stop, step, tol +): + test_attribute_single_value(iio_uri, classname, attr, start, stop, step, tol) From b9b08550e6a3fde61bce2ba1ac738741f4af7085 Mon Sep 17 00:00:00 2001 From: MPhalke Date: Thu, 18 May 2023 17:38:23 +0530 Subject: [PATCH 07/23] ad4858 pyadi-iio support Implemented ad4858 pyadi-iio device driver class and example script Signed-off-by: MPhalke --- adi/__init__.py | 1 + adi/ad4858.py | 206 ++++++++++++++++++++++++++++++ doc/source/devices/adi.ad4858.rst | 7 + doc/source/devices/index.rst | 1 + examples/ad4858_example.py | 78 +++++++++++ supported_parts.md | 1 + test/emu/devices/ad4858.xml | 1 + test/emu/hardware_map.yml | 13 +- test/test_ad4858.py | 11 ++ 9 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 adi/ad4858.py create mode 100644 doc/source/devices/adi.ad4858.rst create mode 100644 examples/ad4858_example.py create mode 100644 test/emu/devices/ad4858.xml create mode 100644 test/test_ad4858.py diff --git a/adi/__init__.py b/adi/__init__.py index 0cb356152..831d0194e 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -11,6 +11,7 @@ from adi.ad4110 import ad4110 from adi.ad4130 import ad4130 from adi.ad4630 import ad4630 +from adi.ad4858 import ad4858 from adi.ad5592r import ad5592r from adi.ad5686 import ad5686 from adi.ad5940 import ad5940 diff --git a/adi/ad4858.py b/adi/ad4858.py new file mode 100644 index 000000000..ef20d815d --- /dev/null +++ b/adi/ad4858.py @@ -0,0 +1,206 @@ +# Copyright (C) 2020-2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from decimal import Decimal + +import numpy as np +from adi.attribute import attribute +from adi.context_manager import context_manager +from adi.rx_tx import rx + + +class ad4858(rx, context_manager): + + """ AD4858 ADC """ + + _complex_data = False + channel = [] # type: ignore + _device_name = "" + + def __init__(self, uri="", device_name=""): + """Constructor for ad4858 class.""" + context_manager.__init__(self, uri, self._device_name) + + compatible_parts = ["ad4858"] + + self._ctrl = None + + if not device_name: + device_name = compatible_parts[0] + else: + if device_name not in compatible_parts: + raise Exception(f"Not a compatible device: {device_name}") + + # Select the device matching device_name as working device + for device in self._ctx.devices: + if device.name == device_name: + self._ctrl = device + self._rxadc = device + break + + if not self._ctrl: + raise Exception("Error in selecting matching device") + + if not self._rxadc: + raise Exception("Error in selecting matching device") + + for ch in self._ctrl.channels: + name = ch._id + self._rx_channel_names.append(name) + self.channel.append(self._channel(self._ctrl, name)) + + rx.__init__(self) + + @property + def sampling_frequency(self): + """Get sampling frequency.""" + return self._get_iio_dev_attr("sampling_frequency") + + @sampling_frequency.setter + def sampling_frequency(self, value): + """Set sampling frequency.""" + self._set_iio_dev_attr("sampling_frequency", value) + + @property + def oversampling_ratio_avail(self): + """Get list of all available oversampling rates.""" + return self._get_iio_dev_attr_str("oversampling_ratio_available") + + @property + def oversampling_ratio(self): + """Get oversampling ratio.""" + return self._get_iio_dev_attr_str("oversampling_ratio") + + @oversampling_ratio.setter + def oversampling_ratio(self, value): + """Set oversampling ratio.""" + if value in self.oversampling_ratio_avail: + self._set_iio_dev_attr_str("oversampling_ratio", value) + else: + raise ValueError( + "Error: oversampling ratio not supported \nUse one of: " + + str(self.oversampling_ratio_avail) + ) + + @property + def packet_format_avail(self): + """Get list of all available packet formats.""" + return self._get_iio_dev_attr_str("packet_format_available") + + @property + def packet_format(self): + """Get packet format.""" + return self._get_iio_dev_attr_str("packet_format") + + @packet_format.setter + def packet_format(self, value): + """Set packet format.""" + if value in self.packet_format_avail: + self._set_iio_dev_attr_str("packet_format", value) + else: + raise ValueError( + "Error: packet format not supported \nUse one of: " + + str(self.packet_format_avail) + ) + + class _channel(attribute): + + """ ad4858 channel """ + + def __init__(self, ctrl, channel_name): + self.name = channel_name + self._ctrl = ctrl + + @property + def raw(self): + """Get channel raw value.""" + return self._get_iio_attr(self.name, "raw", False) + + @property + def scale(self): + """Get channel scale.""" + return self._get_iio_attr(self.name, "scale", False) + + @scale.setter + def scale(self, value): + """Set channel scale.""" + self._set_iio_attr(self.name, "scale", False, Decimal(value).real) + + @property + def offset(self): + """Get channel offset.""" + return self._get_iio_attr(self.name, "offset", False) + + @offset.setter + def offset(self, value): + """Set channel offset.""" + self._set_iio_attr(self.name, "offset", False, value) + + @property + def calibbias(self): + """Get calibration bias/offset value.""" + return self._get_iio_attr(self.name, "calibbias", False) + + @calibbias.setter + def calibbias(self, value): + """Set channel calibration bias/offset.""" + self._set_iio_attr(self.name, "calibbias", False, value) + + @property + def calibphase(self): + """Get calibration phase value.""" + return self._get_iio_attr(self.name, "calibphase", False) + + @calibphase.setter + def calibphase(self, value): + """Set channel calibration phase.""" + self._set_iio_attr(self.name, "calibphase", False, value) + + @property + def hardwaregain(self): + """Get calibration gain value.""" + return self._get_iio_attr(self.name, "hardwaregain", False) + + @hardwaregain.setter + def hardwaregain(self, value): + """Set channel calibration gain.""" + self._set_iio_attr(self.name, "hardwaregain", False, value) + + @property + def softspan_avail(self): + """Get list of all available softspans.""" + return self._get_iio_attr_str(self.name, "softspan_available", False) + + @property + def softspan(self): + """Get softspan value.""" + return self._get_iio_attr_str(self.name, "softspan", False) + + @softspan.setter + def softspan(self, value): + """Set softspan value.""" + if value in self.softspan_avail: + self._set_iio_attr(self.name, "softspan", False, value) + else: + raise ValueError( + "Error: softspan not supported \nUse one of: " + + str(self.softspan_avail) + ) + + def to_volts(self, index, val): + """Converts raw value to SI.""" + _scale = self.channel[index].scale + + ret = None + + if isinstance(val, np.int32): + ret = val * _scale + + if isinstance(val, np.ndarray): + ret = [x * _scale for x in val] + + if ret is None: + raise Exception("Error in converting to actual voltage") + + return ret diff --git a/doc/source/devices/adi.ad4858.rst b/doc/source/devices/adi.ad4858.rst new file mode 100644 index 000000000..fea1406c3 --- /dev/null +++ b/doc/source/devices/adi.ad4858.rst @@ -0,0 +1,7 @@ +ad4858 +================= + +.. automodule:: adi.ad4858 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 51ca275ea..b2339eef0 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -48,6 +48,7 @@ Supported Devices adi.ad9467 adi.ad9625 adi.ad9680 + adi.ad4858 adi.ad9739a adi.ada4961 adi.adaq8092 diff --git a/examples/ad4858_example.py b/examples/ad4858_example.py new file mode 100644 index 000000000..82c7dc92f --- /dev/null +++ b/examples/ad4858_example.py @@ -0,0 +1,78 @@ +# Copyright (C) 2023 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 sys +from time import sleep + +import matplotlib.pyplot as plt +from adi import ad4858 + +# Optionally pass URI as command line argument, +# else use default ip:analog.local +my_uri = sys.argv[1] if len(sys.argv) >= 2 else "ip:analog.local" +print("uri: " + str(my_uri)) + +my_adc = ad4858(uri=my_uri) +my_adc.rx_buffer_size = 1024 + +# Set Sample Rate. +my_adc.sampling_frequency = 500000 + +# Choose output format: +# my_adc.rx_output_type = "raw" +my_adc.rx_output_type = "SI" + +# Verify settings: +print("Sampling Frequency: ", my_adc.sampling_frequency) +print("Enabled Channels: ", my_adc.rx_enabled_channels) + +plt.clf() +sleep(0.5) +data = my_adc.rx() +for ch in my_adc.rx_enabled_channels: + plt.plot(range(0, len(data[0])), data[ch], label="voltage" + str(ch)) +plt.xlabel("Data Point") +if my_adc.rx_output_type == "SI": + plt.ylabel("Millivolts") +else: + plt.ylabel("ADC counts") +plt.legend( + bbox_to_anchor=(0.0, 1.02, 1.0, 0.102), + loc="lower left", + ncol=4, + mode="expand", + borderaxespad=0.0, +) +plt.pause(0.01) + +del my_adc diff --git a/supported_parts.md b/supported_parts.md index c4d430d98..edbf4044d 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -95,6 +95,7 @@ - AD9467 - AD9625 - AD9680 +- AD4858 - AD9739A - ADA4961 - ADAQ8092 diff --git a/test/emu/devices/ad4858.xml b/test/emu/devices/ad4858.xml new file mode 100644 index 000000000..55e72a3a0 --- /dev/null +++ b/test/emu/devices/ad4858.xml @@ -0,0 +1 @@ +]> \ No newline at end of file diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index 43841da98..d9b32a422 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -385,4 +385,15 @@ max9611: - emulate: - filename: max9611.xml - data_devices: - - iio:device0 \ No newline at end of file + - iio:device0 + +ad4858: + - xadc + - ad4858 + - pyadi_iio_class_support: + - ad4858 + - emulate: + - filename: ad4858.xml + - data_devices: + - iio:device0 + - iio:device2 diff --git a/test/test_ad4858.py b/test/test_ad4858.py new file mode 100644 index 000000000..334c37dcb --- /dev/null +++ b/test/test_ad4858.py @@ -0,0 +1,11 @@ +import pytest + +hardware = ["ad4858"] +classname = "adi.ad4858" + +######################################### +@pytest.mark.iio_hardware(hardware, True) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", [0]) +def test_ad4858_rx_data(test_dma_rx, iio_uri, classname, channel): + test_dma_rx(iio_uri, classname, channel, buffer_size=2 ** 11) From efd841c3ce37d334e966cc5be619b7288b1bcdbb Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Thu, 3 Aug 2023 10:29:36 +0800 Subject: [PATCH 08/23] test: fix classname of ad9467, fmcjesdadc1, fmcadc3 Signed-off-by: Trecia Agoylo --- test/test_ad9467.py | 2 +- test/test_fmcadc3.py | 2 +- test/test_fmcjesdadc1.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_ad9467.py b/test/test_ad9467.py index d362c5b9b..5727c5ab4 100644 --- a/test/test_ad9467.py +++ b/test/test_ad9467.py @@ -1,7 +1,7 @@ import pytest hardware = ["ad9467"] -classname = ["adi.ad9467"] +classname = "adi.ad9467" ######################################### diff --git a/test/test_fmcadc3.py b/test/test_fmcadc3.py index 8158c6dc9..db793bc90 100644 --- a/test/test_fmcadc3.py +++ b/test/test_fmcadc3.py @@ -1,7 +1,7 @@ import pytest hardware = ["fmcadc3"] -classname = ["adi.fmcadc3"] +classname = "adi.fmcadc3" ######################################### diff --git a/test/test_fmcjesdadc1.py b/test/test_fmcjesdadc1.py index b3613b831..8e7e505c2 100644 --- a/test/test_fmcjesdadc1.py +++ b/test/test_fmcjesdadc1.py @@ -1,7 +1,7 @@ import pytest hardware = ["fmcjesdadc1"] -classname = ["adi.fmcjesdadc1"] +classname = "adi.fmcjesdadc1" ######################################### From 28917785e9a1e824420353e4062c76c0084bf9d2 Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Thu, 3 Aug 2023 13:17:04 +0800 Subject: [PATCH 09/23] fix: updated adrv9002 rx and tx lo frequency attribute name Signed-off-by: Trecia Agoylo --- adi/adrv9002.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/adi/adrv9002.py b/adi/adrv9002.py index 6d5b00a96..163196f81 100644 --- a/adi/adrv9002.py +++ b/adi/adrv9002.py @@ -187,7 +187,7 @@ def tx_ensm_mode_chan1(self, value): @property def gain_control_mode_chan0(self): """gain_control_mode_chan0: Mode of receive path AGC. Options are: - manual_spi, manual_pin, automatic""" + spi, pin, automatic""" return self._get_iio_attr_str("voltage0", "gain_control_mode", False) @gain_control_mode_chan0.setter @@ -197,7 +197,7 @@ def gain_control_mode_chan0(self, value): @property def gain_control_mode_chan1(self): """gain_control_mode_chan1: Mode of receive path AGC. Options are: - manual_spi, manual_pin, automatic""" + spi, pin, automatic""" return self._get_iio_attr_str("voltage1", "gain_control_mode", False) @gain_control_mode_chan1.setter @@ -687,35 +687,35 @@ def tx1_sample_rate(self): @property def rx0_lo(self): """rx0_lo: Carrier frequency of RX1 path""" - return self._get_iio_attr("altvoltage0", "RX1_LO_frequency", True) + return self._get_iio_attr("altvoltage0", "frequency", True) @rx0_lo.setter def rx0_lo(self, value): - self._set_iio_attr("altvoltage0", "RX1_LO_frequency", True, value) + self._set_iio_attr("altvoltage0", "frequency", True, value) @property def rx1_lo(self): """rx1_lo: Carrier frequency of RX2 path""" - return self._get_iio_attr("altvoltage1", "RX2_LO_frequency", True) + return self._get_iio_attr("altvoltage1", "frequency", True) @rx1_lo.setter def rx1_lo(self, value): - self._set_iio_attr("altvoltage1", "RX2_LO_frequency", True, value) + self._set_iio_attr("altvoltage1", "frequency", True, value) @property def tx0_lo(self): """tx1_lo: Carrier frequency of TX1 path""" - return self._get_iio_attr("altvoltage2", "TX1_LO_frequency", True) + return self._get_iio_attr("altvoltage2", "frequency", True) @tx0_lo.setter def tx0_lo(self, value): - self._set_iio_attr("altvoltage2", "TX1_LO_frequency", True, value) + self._set_iio_attr("altvoltage2", "frequency", True, value) @property def tx1_lo(self): """tx1_lo: Carrier frequency of TX2 path""" - return self._get_iio_attr("altvoltage3", "TX2_LO_frequency", True) + return self._get_iio_attr("altvoltage3", "frequency", True) @tx1_lo.setter def tx1_lo(self, value): - self._set_iio_attr("altvoltage3", "TX2_LO_frequency", True, value) + self._set_iio_attr("altvoltage3", "frequency", True, value) From f7be601254d0f8f7075313a01cd5e63a3464b208 Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Thu, 3 Aug 2023 13:37:45 +0800 Subject: [PATCH 10/23] fix: updated ad9371 rx, tx, and sn lo frequency attribute name Signed-off-by: Trecia Agoylo --- adi/ad9371.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/adi/ad9371.py b/adi/ad9371.py index 553a724f9..cb20fb3d5 100644 --- a/adi/ad9371.py +++ b/adi/ad9371.py @@ -235,29 +235,29 @@ def tx_sample_rate(self): @property def rx_lo(self): """rx_lo: Carrier frequency of RX path""" - return self._get_iio_attr("altvoltage0", "RX_LO_frequency", True) + return self._get_iio_attr("altvoltage0", "frequency", True) @rx_lo.setter def rx_lo(self, value): - self._set_iio_attr("altvoltage0", "RX_LO_frequency", True, value) + self._set_iio_attr("altvoltage0", "frequency", True, value) @property def tx_lo(self): """tx_lo: Carrier frequency of TX path""" - return self._get_iio_attr("altvoltage1", "TX_LO_frequency", True) + return self._get_iio_attr("altvoltage1", "frequency", True) @tx_lo.setter def tx_lo(self, value): - self._set_iio_attr("altvoltage1", "TX_LO_frequency", True, value) + self._set_iio_attr("altvoltage1", "frequency", True, value) @property def sn_lo(self): """sn_lo: Carrier frequency of Sniffer/ORx path""" - return self._get_iio_attr("altvoltage2", "RX_SN_LO_frequency", True) + return self._get_iio_attr("altvoltage2", "frequency", True) @sn_lo.setter def sn_lo(self, value): - self._set_iio_attr("altvoltage2", "RX_SN_LO_frequency", True, value) + self._set_iio_attr("altvoltage2", "frequency", True, value) @property def obs_gain_control_mode(self): From 09f8d852525dcd498ab30ead33d94b412f597f93 Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Fri, 4 Aug 2023 08:03:01 +0800 Subject: [PATCH 11/23] emu: update adrv9002 and ad9371 xml files Signed-off-by: Trecia Agoylo --- test/emu/devices/ad9371.xml | 2 +- test/emu/devices/adrv9002.xml | 35 ++++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/test/emu/devices/ad9371.xml b/test/emu/devices/ad9371.xml index de4d68600..ab2cfc580 100644 --- a/test/emu/devices/ad9371.xml +++ b/test/emu/devices/ad9371.xml @@ -1 +1 @@ -]> \ No newline at end of file +]> \ No newline at end of file diff --git a/test/emu/devices/adrv9002.xml b/test/emu/devices/adrv9002.xml index 3400d00bb..dbc6608bf 100644 --- a/test/emu/devices/adrv9002.xml +++ b/test/emu/devices/adrv9002.xml @@ -1,18 +1,31 @@ -]>]> \ No newline at end of file +AUX: Unlocked" /> \ No newline at end of file From 82ea8d67545bef45b04d4d6aa0193d3e85bd637e Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Fri, 4 Aug 2023 10:55:03 +0800 Subject: [PATCH 12/23] test: update test_adrv9002_generic to reflect updated attribute name Signed-off-by: Trecia Agoylo --- test/test_adrv9002_generic_p.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_adrv9002_generic_p.py b/test/test_adrv9002_generic_p.py index 55c594011..954338047 100644 --- a/test/test_adrv9002_generic_p.py +++ b/test/test_adrv9002_generic_p.py @@ -11,8 +11,8 @@ "attrtype, dev_name, chan_name, inout, attr, start, stop, step, tol, repeats", [ ("channel", "adrv9002-phy", "voltage0", False, "hardwaregain", -89.75, 0.0, 0.25, 0, 100), - ("channel", "adrv9002-phy", "altvoltage0", True, "RX1_LO_frequency", 70000000, 6000000000, 1, 4, 100), - ("channel", "adrv9002-phy", "altvoltage1", True, "RX2_LO_frequency", 70000000, 6000000000, 1, 4, 100) + ("channel", "adrv9002-phy", "altvoltage0", True, "frequency", 70000000, 6000000000, 1, 4, 100), + ("channel", "adrv9002-phy", "altvoltage1", True, "frequency", 70000000, 6000000000, 1, 4, 100) ], ) def test_iio_attr( From 13ddc17b0ed75228e31969b1679427fa09036a2f Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 4 Aug 2023 13:59:41 -0600 Subject: [PATCH 13/23] Bump to version v0.0.17 Signed-off-by: Travis F. Collins --- adi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adi/__init__.py b/adi/__init__.py index 831d0194e..1f1143935 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -105,5 +105,5 @@ except ImportError: pass -__version__ = "0.0.16" +__version__ = "0.0.17" name = "Analog Devices Hardware Interfaces" From 28352f4c0ca7f44cb8b33a137c17ea58a7099899 Mon Sep 17 00:00:00 2001 From: Julia Pineda Date: Thu, 3 Aug 2023 13:16:26 +0800 Subject: [PATCH 14/23] [FIX] Update condition to trim parameter list based on AD9081 configuration Signed-off-by: Julia Pineda --- test/test_ad9081.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test_ad9081.py b/test/test_ad9081.py index e03d6f151..f02da4824 100644 --- a/test/test_ad9081.py +++ b/test/test_ad9081.py @@ -13,10 +13,9 @@ def scale_field(param_set, iio_uri): dev = adi.ad9081(uri=iio_uri) for field in param_set: - if param_set[field] is not list: - continue - existing_val = getattr(dev, field) - param_set[field] = param_set[field][0] * len(existing_val) + if isinstance(param_set[field], list): + existing_val = getattr(dev, field) + param_set[field] = [param_set[field][0]] * len(existing_val) return param_set From 3007fc08289edbf2a70aafd00809f636f2b1bfc8 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 10 Aug 2023 08:08:38 -0600 Subject: [PATCH 15/23] Update pylibiio dep to fix #462 Signed-off-by: Travis F. Collins --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45da55fdd..2ebf5c698 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ maintainers = [ ] dependencies = [ 'numpy >= 1.20', - 'pylibiio >= 0.23.1', + 'pylibiio >= 0.25', ] [tool.setuptools.dynamic] From c3ef8ae4ddc6b4cb1299e0e34a10c59586bd181b Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Fri, 18 Aug 2023 12:19:55 +0800 Subject: [PATCH 16/23] fix: random_values_in_range in test_adrv9002 Signed-off-by: Trecia Agoylo --- test/test_adrv9002_p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_adrv9002_p.py b/test/test_adrv9002_p.py index 15f2551f2..bb94a491d 100644 --- a/test/test_adrv9002_p.py +++ b/test/test_adrv9002_p.py @@ -42,7 +42,7 @@ def random_values_in_range(start, stop, step, to_generate=1): ind = randint(0, numints) val = start + step * ind if isinstance(val, float): - val = floor(val / step) * step + val = round(floor(val / step) * step, 2) values.append(val) return values From 22ba03b8a74f11d0be21825636c6db63954d4e96 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Tue, 22 Aug 2023 10:47:37 -0600 Subject: [PATCH 17/23] Add doc for virtual env installs Signed-off-by: Travis F. Collins --- doc/source/guides/quick.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/source/guides/quick.rst b/doc/source/guides/quick.rst index d74dc1932..7895e32d5 100644 --- a/doc/source/guides/quick.rst +++ b/doc/source/guides/quick.rst @@ -38,6 +38,40 @@ Note that this is only needed for the ADRV9009-ZU11EG multi-SOM configuration. export PYTHONPATH=$PYTHONPATH:/usr/lib/python{PYTHON VERSION}/site-packages +Using Virtual Environments +-------------------------- + +It is recommended to use virtual environments when installing pyadi-iio. This will prevent any conflicts with other python packages that may be installed on your system. Newer versions of such Linux distributions, like Debian, do not allow the installation of global packages either. Therefore, if a package is not within their package managers you must your virtual environments. To create a virtual environment run: + +.. code-block:: bash + + python3 -m venv /path/to/new/virtual/environment + +To activate the virtual environment run: + +.. code-block:: bash + + source /path/to/new/virtual/environment/bin/activate + +To deactivate the virtual environment run: + +.. code-block:: bash + + deactivate + +Once the virtual environment is activated, you can install pyadi-iio as normal with pip. + +Here is a full example of a virtual environment setup and install of pyadi-iio: + +.. code-block:: bash + + dave@hal:~$ python3 -m venv /home/dave/venv/pyadi-iio + dave@hal:~$ source /home/dave/venv/pyadi-iio/bin/activate + (pyadi-iio) dave@hal:~$ pip install pyadi-iio + Collecting pyadi-iio + Downloading ... + + Conda Install ------------- From 96aeb92b1f16a15feb9c2aa79bd08b30787e6da1 Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Wed, 23 Aug 2023 14:16:19 +0800 Subject: [PATCH 18/23] Update JenkinsfileHW Signed-off-by: Trecia Agoylo --- JenkinsfileHW | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/JenkinsfileHW b/JenkinsfileHW index a8b51f7c7..5b006df0d 100644 --- a/JenkinsfileHW +++ b/JenkinsfileHW @@ -5,29 +5,21 @@ lock(label: 'adgt_test_harness_boards') { def linuxBranch = "NA" def bootPartitionBranch = "master" def jenkins_job_trigger = "ci" - def firmwareVersion = 'v0.34' + def firmwareVersion = 'v0.37' def bootfile_source = 'artifactory' // options: sftp, artifactory, http, local def harness = getGauntlet(hdlBranch, linuxBranch, bootPartitionBranch, firmwareVersion, bootfile_source) - //Update repos - harness.set_env('nebula_repo', 'https://github.com/sdgtt/nebula.git') - harness.set_env('nebula_branch','dev') - harness.set_env('telemetry_repo', 'https://github.com/sdgtt/telemetry.git') - harness.set_env('telemetry_branch', 'master') - - def pyadi_branch = scm.branches.first().getExpandedName(env.getEnvironment()) - harness.set_env('pyadi_iio_branch', pyadi_branch.toString()) - //Update nebula config from netbox harness.set_update_nebula_config(true) harness.set_env('nebula_config_source','netbox') - harness.set_env('netbox_ip','192.168.10.11') + harness.set_env('netbox_ip','primary.englab') harness.set_env('netbox_port','8000') harness.set_env('netbox_base_url','netbox') withCredentials([string(credentialsId: 'netbox_token', variable: 'TOKEN')]) { harness.set_env('netbox_token', TOKEN) } harness.set_env('netbox_devices_tag','active') + //update first the agent with the required deps harness.set_required_agent(["sdg-nuc-01","sdg-nuc-02"]) harness.update_agents() @@ -36,11 +28,11 @@ lock(label: 'adgt_test_harness_boards') { harness.set_nebula_debug(true) harness.set_enable_docker(true) harness.set_docker_host_mode(true) - harness.set_send_telemetry(true) + harness.set_send_telemetry(false) harness.set_log_artifacts(false) harness.set_log_jira(false) harness.set_enable_resource_queuing(true) - harness.set_elastic_server('192.168.10.11') + harness.set_elastic_server('primary.englab') harness.set_docker_args(['Vivado']) harness.set_nebula_local_fs_source_root("artifactory.analog.com") From 3717525ffc2dfbc342dff31bc5152919ddb609d4 Mon Sep 17 00:00:00 2001 From: Trecia Agoylo Date: Wed, 23 Aug 2023 14:27:55 +0800 Subject: [PATCH 19/23] Fix lint Signed-off-by: Trecia Agoylo --- JenkinsfileHW | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JenkinsfileHW b/JenkinsfileHW index 5b006df0d..97c6ce613 100644 --- a/JenkinsfileHW +++ b/JenkinsfileHW @@ -19,7 +19,7 @@ lock(label: 'adgt_test_harness_boards') { harness.set_env('netbox_token', TOKEN) } harness.set_env('netbox_devices_tag','active') - + //update first the agent with the required deps harness.set_required_agent(["sdg-nuc-01","sdg-nuc-02"]) harness.update_agents() From 00e1761458ee4f37943f3b8adb156677298f353f Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Wed, 23 Aug 2023 12:41:46 -0600 Subject: [PATCH 20/23] Change libiio CI branch to v0.25 Signed-off-by: Travis F. Collins --- .github/scripts/install_libiio.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/install_libiio.sh b/.github/scripts/install_libiio.sh index d1655d6e8..c4b0123f3 100755 --- a/.github/scripts/install_libiio.sh +++ b/.github/scripts/install_libiio.sh @@ -1,7 +1,7 @@ #!/bin/bash sudo apt-get -qq update sudo apt-get install -y git cmake graphviz libavahi-common-dev libavahi-client-dev libaio-dev libusb-1.0-0-dev libxml2-dev rpm tar bzip2 gzip flex bison git -git clone -b 'master' --single-branch --depth 1 https://github.com/analogdevicesinc/libiio.git +git clone -b 'v0.25' --single-branch --depth 1 https://github.com/analogdevicesinc/libiio.git cd libiio cmake . -DHAVE_DNS_SD=OFF make From b9b6366d6cf50d5a5b89f34cd47c0c48e7331dd6 Mon Sep 17 00:00:00 2001 From: Ramona Gradinariu Date: Thu, 24 Aug 2023 16:19:31 +0300 Subject: [PATCH 21/23] Add support for adis16475 driver Signed-off-by: Ramona Gradinariu --- adi/__init__.py | 1 + adi/adis16475.py | 71 ++++++++++++++++++++++++++++ doc/source/devices/adi.adis16475.rst | 7 +++ doc/source/devices/index.rst | 1 + examples/adis16475.py | 18 +++++++ supported_parts.md | 1 + tasks.py | 1 + test/emu/devices/adis16475.xml | 2 + test/emu/hardware_map.yml | 9 ++++ test/test_adis16475_p.py | 15 ++++++ 10 files changed, 126 insertions(+) create mode 100644 adi/adis16475.py create mode 100644 doc/source/devices/adi.adis16475.rst create mode 100644 examples/adis16475.py create mode 100644 test/emu/devices/adis16475.xml create mode 100644 test/test_adis16475_p.py diff --git a/adi/__init__.py b/adi/__init__.py index 1f1143935..411cf5e62 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -51,6 +51,7 @@ from adi.adf5610 import adf5610 from adi.adg2128 import adg2128 from adi.adis16460 import adis16460 +from adi.adis16475 import adis16475 from adi.adis16495 import adis16495 from adi.adis16507 import adis16507 from adi.adl5240 import adl5240 diff --git a/adi/adis16475.py b/adi/adis16475.py new file mode 100644 index 000000000..2b015c2eb --- /dev/null +++ b/adi/adis16475.py @@ -0,0 +1,71 @@ +# Copyright (C) 2019-2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from adi.context_manager import context_manager +from adi.rx_tx import rx + + +class adis16475(rx, context_manager): + """ADIS16475 Compact, Precision, Six Degrees of Freedom Inertial Sensor""" + + _complex_data = False + _rx_channel_names = [ + "anglvel_x", + "anglvel_y", + "anglvel_z", + "accel_x", + "accel_y", + "accel_z", + "temp0", + ] + _device_name = "" + + def __init__(self, uri="", device_name="adis16505-2"): + context_manager.__init__(self, uri, self._device_name) + + compatible_parts = [ + "adis16470", + "adis16475-1", + "adis16475-2", + "adis16475-3", + "adis16477-1", + "adis16477-2", + "adis16477-3", + "adis16465-1", + "adis16465-2", + "adis16465-3", + "adis16467-1", + "adis16467-2", + "adis16467-3", + "adis16500", + "adis16505-1", + "adis16505-2", + "adis16505-3", + "adis16507-1", + "adis16507-2", + "adis16507-3", + ] + + if device_name not in compatible_parts: + raise Exception( + "Not a compatible device:" + + str(device_name) + + ".Please select from:" + + str(compatible_parts) + ) + else: + self._ctrl = self._ctx.find_device(device_name) + self._rxadc = self._ctx.find_device(device_name) + + rx.__init__(self) + self.rx_buffer_size = 16 # Make default buffer smaller + + @property + def sample_rate(self): + """sample_rate: Sample rate in samples per second""" + return self._get_iio_dev_attr("sampling_frequency") + + @sample_rate.setter + def sample_rate(self, value): + self._set_iio_dev_attr_str("sampling_frequency", value) diff --git a/doc/source/devices/adi.adis16475.rst b/doc/source/devices/adi.adis16475.rst new file mode 100644 index 000000000..bb08ee017 --- /dev/null +++ b/doc/source/devices/adi.adis16475.rst @@ -0,0 +1,7 @@ +adis16475 +==================== + +.. automodule:: adi.adis16475 + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index b2339eef0..ea68d05fb 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -59,6 +59,7 @@ Supported Devices adi.adf5610 adi.adg2128 adi.adis16460 + adi.adis16475 adi.adis16495 adi.adis16507 adi.adl5240 diff --git a/examples/adis16475.py b/examples/adis16475.py new file mode 100644 index 000000000..4de4cc803 --- /dev/null +++ b/examples/adis16475.py @@ -0,0 +1,18 @@ +# Copyright (C) 2019 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import adi + +# Set up AD7124 +adis16475 = adi.adis16475() + +adis16475.rx_output_type = "raw" +adis16475.rx_enabled_channels = [0, 1, 2, 3, 4, 5, 6] +adis16475.sample_rate = 2000 +adis16475.rx_buffer_size = 100 + +data = adis16475.rx() + +# print Y and Z axis acceleration +print(data) diff --git a/supported_parts.md b/supported_parts.md index edbf4044d..e6267ba0f 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -106,6 +106,7 @@ - ADF5610 - ADG2128 - ADIS16460 +- ADIS16475 - ADIS16495 - ADIS16507 - ADL5240 diff --git a/tasks.py b/tasks.py index 213358030..60e9e960e 100644 --- a/tasks.py +++ b/tasks.py @@ -254,6 +254,7 @@ def checkemulation(c): "adf5610", "adg2128", "adis16460", + "adis16475", "adis16495", "adis16507", "adl5240", diff --git a/test/emu/devices/adis16475.xml b/test/emu/devices/adis16475.xml new file mode 100644 index 000000000..f78c74fb6 --- /dev/null +++ b/test/emu/devices/adis16475.xml @@ -0,0 +1,2 @@ +]> diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index d9b32a422..20eb040c4 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -397,3 +397,12 @@ ad4858: - data_devices: - iio:device0 - iio:device2 + +adis16475: + - adis16475 + - emulate: + - filename: adis16475.xml + - data_devices: + - iio:device0 + - pyadi_iio_class_support: + - adis16475 diff --git a/test/test_adis16475_p.py b/test/test_adis16475_p.py new file mode 100644 index 000000000..2a70e76f7 --- /dev/null +++ b/test/test_adis16475_p.py @@ -0,0 +1,15 @@ +import pytest + +hardware = "adis16475" +classname = "adi.adis16475" + + +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize( + "classname, attr, start, stop, step, tol", + [(classname, "sample_rate", 1000, 2000, 1000, 0)], +) +def test_adis16475_sample_rate( + test_attribute_single_value, iio_uri, classname, attr, start, stop, step, tol +): + test_attribute_single_value(iio_uri, classname, attr, start, stop, step, tol) From bb4c6aea682c54a8eb0f3ffcd767aaaac976d37f Mon Sep 17 00:00:00 2001 From: RibhuDP Date: Tue, 12 Sep 2023 13:57:57 +0530 Subject: [PATCH 22/23] adi:ad5754r: Add initial support Add initial attribute r/w support for ad5754r Signed-off-by: RibhuDP --- adi/__init__.py | 1 + adi/ad5754r.py | 209 +++++++++++++++++++++++++++++ doc/source/devices/adi.ad5754r.rst | 7 + doc/source/devices/index.rst | 1 + examples/ad5754r_example.py | 48 +++++++ supported_parts.md | 1 + test/emu/devices/ad5754r.xml | 1 + test/emu/hardware_map.yml | 9 ++ test/test_ad5754r.py | 51 +++++++ 9 files changed, 328 insertions(+) create mode 100644 adi/ad5754r.py create mode 100644 doc/source/devices/adi.ad5754r.rst create mode 100644 examples/ad5754r_example.py create mode 100644 test/emu/devices/ad5754r.xml create mode 100644 test/test_ad5754r.py diff --git a/adi/__init__.py b/adi/__init__.py index 0cb356152..5a02742c9 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -13,6 +13,7 @@ from adi.ad4630 import ad4630 from adi.ad5592r import ad5592r from adi.ad5686 import ad5686 +from adi.ad5754r import ad5754r from adi.ad5940 import ad5940 from adi.ad6676 import ad6676 from adi.ad7124 import ad7124 diff --git a/adi/ad5754r.py b/adi/ad5754r.py new file mode 100644 index 000000000..d1208c29b --- /dev/null +++ b/adi/ad5754r.py @@ -0,0 +1,209 @@ +# Copyright (C) 2020-2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +from decimal import Decimal + +from adi.attribute import attribute +from adi.context_manager import context_manager +from adi.rx_tx import tx + + +class ad5754r(tx, context_manager): + """ AD5754R DAC """ + + _complex_data = False + channel = [] + _device_name = "" + + def __init__(self, uri="", device_name=""): + """ Constructor for AD5754R driver class """ + + context_manager.__init__(self, uri, self._device_name) + + compatible_parts = ["ad5754r"] + self._ctrl = None + + if not device_name: + device_name = compatible_parts[0] + else: + if device_name not in compatible_parts: + raise Exception( + f"Not a compatible device: {device_name}. Supported device names " + f"are: {','.join(compatible_parts)}" + ) + + # Select the device matching device_name as working device + for device in self._ctx.devices: + if device.name == device_name: + self._ctrl = device + self._txdac = device + break + + if not self._ctrl: + raise Exception("Error in selecting matching device") + + if not self._txdac: + raise Exception("Error in selecting matching device") + + self.output_bits = [] + for ch in self._ctrl.channels: + name = ch.id + self.output_bits.append(ch.data_format.bits) + self._tx_channel_names.append(name) + self.channel.append(self._channel(self._ctrl, name)) + + tx.__init__(self) + + @property + def int_ref_powerup(self): + """Get internal reference powerup""" + return self._get_iio_dev_attr_str("int_ref_powerup") + + @property + def int_ref_powerup_available(self): + """Get list of all internal reference powerup settings""" + return self._get_iio_dev_attr_str("int_ref_powerup_available") + + @int_ref_powerup.setter + def int_ref_powerup(self, value): + """Set internal reference powerup""" + if value in self.int_ref_powerup_available: + self._set_iio_dev_attr_str("int_ref_powerup", value) + else: + raise ValueError( + "Error: internal reference powerup not supported \nUse one of: " + + str(self.int_ref_powerup_available) + ) + + @property + def clear_setting(self): + """Get clear code setting""" + return self._get_iio_dev_attr_str("clear_setting") + + @property + def clear_setting_available(self): + """Get list of all clear code settings""" + return self._get_iio_dev_attr_str("clear_setting_available") + + @clear_setting.setter + def clear_setting(self, value): + """Set clear setting""" + if value in self.clear_setting_available: + self._set_iio_dev_attr_str("clear_setting", value) + else: + raise ValueError( + "Error: clear setting not supported \nUse one of: " + + str(self.clear_setting_available) + ) + + @property + def sdo_disable(self): + """Get sdo disable""" + return self._get_iio_dev_attr_str("sdo_disable") + + @property + def sdo_disable_available(self): + """Get list of all sdo enable/disable settings""" + return self._get_iio_dev_attr_str("sdo_disable_available") + + @sdo_disable.setter + def sdo_disable(self, value): + """Set sdo enable/disable setting""" + if value in self.sdo_disable_available: + self._set_iio_dev_attr_str("sdo_disable", value) + else: + raise ValueError( + "Error: sdo setting not supported \nUse one of: " + + str(self.sdo_disable_available) + ) + + @property + def sampling_frequency(self): + """Get sampling frequency""" + return self._get_iio_dev_attr_str("sampling_frequency") + + @sampling_frequency.setter + def sampling_frequency(self, value): + """Set sampling frequency""" + self._set_iio_dev_attr_str("sampling_frequency", value) + + class _channel(attribute): + """AD5754R channel""" + + def __init__(self, ctrl, channel_name): + self.name = channel_name + self._ctrl = ctrl + + @property + def raw(self): + """Get channel raw value + DAC code in the range 0-65535""" + return self._get_iio_attr(self.name, "raw", True) + + @raw.setter + def raw(self, value): + """Set channel raw value""" + self._set_iio_attr(self.name, "raw", True, str(int(value))) + + @property + def offset(self): + """Get channel offset""" + return self._get_iio_attr_str(self.name, "offset", True) + + @offset.setter + def offset(self, value): + """Set channel offset""" + self._set_iio_attr(self.name, "offset", True, str(Decimal(value).real)) + + @property + def scale(self): + """Get channel scale""" + return float(self._get_iio_attr_str(self.name, "scale", True)) + + @scale.setter + def scale(self, value): + """Set channel scale""" + self._set_iio_attr(self.name, "scale", True, str(Decimal(value).real)) + + @property + def powerup(self): + """Get DAC chn powerup""" + return self._get_iio_attr_str(self.name, "powerup", True) + + @property + def powerup_available(self): + """Get list of DAC chn powerup settings""" + return self._get_iio_attr_str(self.name, "powerup_available", True) + + @powerup.setter + def powerup(self, value): + """Set DAC chn powerup""" + if value in self.powerup_available: + self._set_iio_attr(self.name, "powerup", True, value) + else: + raise ValueError( + "Error: powerup setting not supported \nUse one of: " + + str(self.powerup_available) + ) + + @property + def range(self): + """Get output range""" + return self._get_iio_attr_str(self.name, "range", True) + + @property + def range_available(self): + """Get list of all output ranges""" + return self._get_iio_attr_str(self.name, "range_available", True) + + @range.setter + def range(self, value): + """Set DAC chn range""" + if value in self.range_available: + self._set_iio_attr(self.name, "range", True, value) + else: + raise ValueError( + "Error: range setting not supported \nUse one of: " + + str(self.range_available) + ) diff --git a/doc/source/devices/adi.ad5754r.rst b/doc/source/devices/adi.ad5754r.rst new file mode 100644 index 000000000..d5325e9fa --- /dev/null +++ b/doc/source/devices/adi.ad5754r.rst @@ -0,0 +1,7 @@ +ad5754r +================= + +.. automodule:: adi.ad5754r + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 51ca275ea..1c88deb56 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -16,6 +16,7 @@ Supported Devices adi.ad5592r adi.ad5627 adi.ad5686 + adi.ad5754r adi.ad5940 adi.ad6676 adi.ad7124 diff --git a/examples/ad5754r_example.py b/examples/ad5754r_example.py new file mode 100644 index 000000000..0cc090350 --- /dev/null +++ b/examples/ad5754r_example.py @@ -0,0 +1,48 @@ +# Copyright (C) 2023 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 adi + +# Set up AD5754R +ad578x_dev = adi.ad5754r(uri="serial:COM46,230400,8n1n") +ad578x.int_ref_powerup = "powerup" + +# Configure channel 0 +chn_num = 0 +ad5754r_chan = ad5754r_dev.channel[chn_num] +ad5754r_chan.powerup = "powerup" + +raw = int(ad5754r_chan.raw) +scale = float(ad5754r_chan.scale) +offset = int(ad5754r_chan.offset) +print(f"Channel{chn_num} voltage: {(raw + offset) * scale}") diff --git a/supported_parts.md b/supported_parts.md index c4d430d98..ba969cd6a 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -55,6 +55,7 @@ - AD5695R - AD5696 - AD5696R +- AD5754R - AD5940 - AD6676 - AD7124 diff --git a/test/emu/devices/ad5754r.xml b/test/emu/devices/ad5754r.xml new file mode 100644 index 000000000..2bb4f8d03 --- /dev/null +++ b/test/emu/devices/ad5754r.xml @@ -0,0 +1 @@ +]> \ No newline at end of file diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index 43841da98..4ef7e4c76 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -350,6 +350,15 @@ ad5592r: - filename: ad5592r.xml - data_devices: - iio:device0 + +ad5754r: + - ad5754r + - pyadi_iio_class_support: + - ad5754r + - emulate: + - filename: ad5754r.xml + - data_devices: + - iio:device0 ad7291: - ad7291 diff --git a/test/test_ad5754r.py b/test/test_ad5754r.py new file mode 100644 index 000000000..45f860494 --- /dev/null +++ b/test/test_ad5754r.py @@ -0,0 +1,51 @@ +import adi +import pytest + +hardware = "ad5754r" +classname = "adi.ad5754r" + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize( + "attr, val", + [ + ("int_ref_powerup", ["powerdown", "powerup"],), + ("clear_setting", ["0v", "midscale_code"],), + ("sdo_disable", ["enable", "disable"],), + ], +) +def test_ad5754r_global_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("channel", [0, 1, 2, 3]) +@pytest.mark.parametrize( + "attr, val", + [ + ("powerup", ["powerdown", "powerup"],), + ( + "range", + [ + "0v_to_5v", + "0v_to_10v", + "0v_to_10v8", + "neg5v_to_5v", + "neg10v_to_10v", + "neg10v8_to_10v8", + ], + ), + ], +) +def test_ad5754r_channel_attr(iio_uri, classname, channel, attr, val): + dev = adi.ad5754r(iio_uri) + dev_chan = dev.channel[channel] + for value in val: + setattr(dev_chan, attr, value) + val_read = getattr(dev_chan, attr) + assert val_read == value From ec81a92c66540ebc271960d0005a1c0ec89a4417 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 2 Oct 2023 17:08:02 -0500 Subject: [PATCH 23/23] test: fix spelling of multiple This replaces the misspelled "multipe" with "multiple" in the tests. Signed-off-by: David Lechner --- test/attr_tests.py | 8 ++++---- test/conftest.py | 8 ++++---- test/test_ad4630.py | 4 ++-- test/test_ad5754r.py | 4 ++-- test/test_ad7768.py | 4 ++-- test/test_ad7768_4.py | 4 ++-- test/test_ad9081.py | 4 ++-- test/test_ad9084.py | 4 ++-- test/test_adl5240.py | 4 ++-- test/test_adrv9002_p.py | 16 ++++++++-------- test/test_cn0575.py | 4 ++-- test/test_cn0579.py | 4 ++-- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/test/attr_tests.py b/test/attr_tests.py index caff49cbc..f86cf6d9b 100644 --- a/test/attr_tests.py +++ b/test/attr_tests.py @@ -179,8 +179,8 @@ def attribute_single_value_pow2(uri, classname, attr, max_pow, tol, repeats=1): assert dev_interface(uri, classname, val, attr, tol) -def attribute_multipe_values(uri, classname, attr, values, tol, repeats=1, sleep=0): - """attribute_multipe_values: Write and read back multiple class properties +def attribute_multiple_values(uri, classname, attr, values, tol, repeats=1, sleep=0): + """attribute_multiple_values: Write and read back multiple class properties in a loop where all values are pre-defined. This is performed a defined number of times. @@ -206,10 +206,10 @@ def attribute_multipe_values(uri, classname, attr, values, tol, repeats=1, sleep assert dev_interface(uri, classname, val, attr, tol, sleep=sleep) -def attribute_multipe_values_with_depends( +def attribute_multiple_values_with_depends( uri, classname, attr, depends, values, tol, repeats=1 ): - """attribute_multipe_values_with_depends: Write and read back multiple class + """attribute_multiple_values_with_depends: Write and read back multiple class properties in a loop where all values are pre-defined, where a set of dependent attributes are written first. This is performed a defined number of times. diff --git a/test/conftest.py b/test/conftest.py index b9fe7cf16..fc6ead0b4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -155,13 +155,13 @@ def test_dcxo_calibration(request): @pytest.fixture() -def test_attribute_multipe_values(request): - yield attribute_multipe_values +def test_attribute_multiple_values(request): + yield attribute_multiple_values @pytest.fixture() -def test_attribute_multipe_values_with_depends(request): - yield attribute_multipe_values_with_depends +def test_attribute_multiple_values_with_depends(request): + yield attribute_multiple_values_with_depends @pytest.fixture() diff --git a/test/test_ad4630.py b/test/test_ad4630.py index 1373d3d35..04323ebf1 100644 --- a/test/test_ad4630.py +++ b/test/test_ad4630.py @@ -23,5 +23,5 @@ def test_ad4630_rx_data(test_dma_rx, iio_uri, classname, channel): ), ], ) -def test_ad4630_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_ad4630_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) diff --git a/test/test_ad5754r.py b/test/test_ad5754r.py index 45f860494..1478ffccd 100644 --- a/test/test_ad5754r.py +++ b/test/test_ad5754r.py @@ -16,9 +16,9 @@ ], ) def test_ad5754r_global_attr( - test_attribute_multipe_values, iio_uri, classname, attr, val + test_attribute_multiple_values, iio_uri, classname, attr, val ): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) ######################################### diff --git a/test/test_ad7768.py b/test/test_ad7768.py index 7b6f403ed..36942cf8e 100644 --- a/test/test_ad7768.py +++ b/test/test_ad7768.py @@ -36,5 +36,5 @@ def test_ad7768_rx_data(test_dma_rx, iio_uri, classname, channel): ("power_mode", ["MEDIAN_MODE", "FAST_MODE"],), ], ) -def test_ad4630_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_ad4630_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) diff --git a/test/test_ad7768_4.py b/test/test_ad7768_4.py index 09a2271ae..fc49fd80a 100644 --- a/test/test_ad7768_4.py +++ b/test/test_ad7768_4.py @@ -37,5 +37,5 @@ def test_ad7768_4_rx_data(test_dma_rx, iio_uri, classname, channel): ("sync_start_enable", ["arm"],), ], ) -def test_ad7768_4_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_ad7768_4_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) diff --git a/test/test_ad9081.py b/test/test_ad9081.py index f02da4824..83091e34c 100644 --- a/test/test_ad9081.py +++ b/test/test_ad9081.py @@ -51,8 +51,8 @@ def scale_field(param_set, iio_uri): ), ], ) -def test_ad9081_str_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_ad9081_str_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) ######################################### diff --git a/test/test_ad9084.py b/test/test_ad9084.py index 71e5d4d29..291b5e3b8 100644 --- a/test/test_ad9084.py +++ b/test/test_ad9084.py @@ -47,8 +47,8 @@ def scale_field(param_set, iio_uri): ), ], ) -def test_ad9084_str_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_ad9084_str_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) ######################################### diff --git a/test/test_adl5240.py b/test/test_adl5240.py index 3052bba26..94704df3a 100644 --- a/test/test_adl5240.py +++ b/test/test_adl5240.py @@ -12,5 +12,5 @@ @pytest.mark.parametrize( "attr, val", [("hardwaregain", [0.06, 0.12, 0.25, 0.5, 0.9],),], ) -def test_adl5240_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) +def test_adl5240_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) diff --git a/test/test_adrv9002_p.py b/test/test_adrv9002_p.py index bb94a491d..58cd77fa7 100644 --- a/test/test_adrv9002_p.py +++ b/test/test_adrv9002_p.py @@ -94,9 +94,9 @@ def test_adrv9002_float_attr( ], ) def test_adrv9002_hardware_gain( - test_attribute_multipe_values_with_depends, iio_uri, classname, attr, depends, val + test_attribute_multiple_values_with_depends, iio_uri, classname, attr, depends, val ): - test_attribute_multipe_values_with_depends( + test_attribute_multiple_values_with_depends( iio_uri, classname, attr, depends, val, 0 ) @@ -138,9 +138,9 @@ def test_adrv9002_hardware_gain( ], ) def test_adrv9002_boolean_attr( - test_attribute_multipe_values, iio_uri, classname, attr, val + test_attribute_multiple_values, iio_uri, classname, attr, val ): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) ######################################### @@ -166,10 +166,10 @@ def test_adrv9002_boolean_attr( ], ) def test_adrv9002_str_attr( - test_attribute_multipe_values, iio_uri, classname, attr, val + test_attribute_multiple_values, iio_uri, classname, attr, val ): sleep = 3 if "ensm_mode" in attr else 0 - test_attribute_multipe_values(iio_uri, classname, attr, val, 0, sleep=sleep) + test_attribute_multiple_values(iio_uri, classname, attr, val, 0, sleep=sleep) ######################################### @@ -198,7 +198,7 @@ def test_adrv9002_str_attr( ], ) def test_adrv9002_interface_gain_narrowband( - test_attribute_multipe_values_with_depends, iio_uri, classname, attr, depends, val + test_attribute_multiple_values_with_depends, iio_uri, classname, attr, depends, val ): from adi.adrv9002 import adrv9002 @@ -214,7 +214,7 @@ def test_adrv9002_interface_gain_narrowband( "Baseband RX2 Sample Rate should be less than 1MHz to run this test." ) - test_attribute_multipe_values_with_depends( + test_attribute_multiple_values_with_depends( iio_uri, classname, attr, depends, val, 0 ) diff --git a/test/test_cn0575.py b/test/test_cn0575.py index 099cbeaab..fc9268f2a 100644 --- a/test/test_cn0575.py +++ b/test/test_cn0575.py @@ -36,5 +36,5 @@ def test_adt75_attr( @pytest.mark.parametrize( "attr, val", [("led", [1, 0],),], ) -def test_led_attr(test_attribute_multipe_values, iio_uri, classname, attr, val): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0, repeats=10) +def test_led_attr(test_attribute_multiple_values, iio_uri, classname, attr, val): + test_attribute_multiple_values(iio_uri, classname, attr, val, 0, repeats=10) diff --git a/test/test_cn0579.py b/test/test_cn0579.py index 15465a29f..497668dc2 100644 --- a/test/test_cn0579.py +++ b/test/test_cn0579.py @@ -38,9 +38,9 @@ def test_cn0579_rx_data(test_dma_rx, iio_uri, classname, channel): ], ) def test_cn0579_attr_multiple( - test_attribute_multipe_values, iio_uri, classname, attr, val + test_attribute_multiple_values, iio_uri, classname, attr, val ): - test_attribute_multipe_values(iio_uri, classname, attr, val, 0) + test_attribute_multiple_values(iio_uri, classname, attr, val, 0) #########################################