diff --git a/adi/__init__.py b/adi/__init__.py index 8b3ae6f885..78938491f7 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -57,6 +57,7 @@ from adi.ada4961 import ada4961 from adi.adaq8092 import adaq8092 from adi.adar1000 import adar1000, adar1000_array +from adi.adf4030 import adf4030 from adi.adf4159 import adf4159 from adi.adf4355 import adf4355 from adi.adf4371 import adf4371 @@ -98,6 +99,7 @@ from adi.adxl355 import adxl355 from adi.adxl380 import adxl380 from adi.adxrs290 import adxrs290 +from adi.axi_aion_trig import axi_aion_trig from adi.cn0511 import cn0511 from adi.cn0532 import cn0532 from adi.cn0554 import cn0554 @@ -115,6 +117,7 @@ from adi.fmcomms5 import FMComms5 from adi.fmcomms11 import FMComms11 from adi.gen_mux import genmux +from adi.hmc7044 import hmc7044 from adi.lm75 import lm75 from adi.ltc2314_14 import ltc2314_14 from adi.ltc2387 import ltc2387 diff --git a/adi/ad9084.py b/adi/ad9084.py index e1d08fe8cc..daf0ba7144 100644 --- a/adi/ad9084.py +++ b/adi/ad9084.py @@ -8,7 +8,7 @@ from adi.context_manager import context_manager from adi.obs import obs, remap, tx_two from adi.rx_tx import rx_tx -from adi.sync_start import sync_start +from adi.sync_start import sync_start, sync_start_b def _map_to_dict(paths, ch): @@ -56,7 +56,7 @@ def ignorealt(w): return chans_names_out -class ad9084(rx_tx, context_manager, sync_start): +class ad9084(rx_tx, context_manager, sync_start, sync_start_b): """AD9084 Mixed-Signal Front End (MxFE)""" _complex_data = True @@ -188,6 +188,7 @@ def __init__( rx_tx.__init__(self) sync_start.__init__(self) + sync_start_b.__init__(self) self.rx_buffer_size = 2 ** 16 def _get_iio_attr_str_single(self, channel_name, attr, output): @@ -281,6 +282,22 @@ def rx_main_nco_phases(self, value): self._rx_coarse_ddc_channel_names, "main_nco_phase", False, value, ) + @property + def rx_main_tb1_6db_digital_gain_en(self): + """main_tb1_6db_digital_gain_en: Receive path coarse DDC NCO phases""" + return self._get_iio_attr_vec( + self._rx_coarse_ddc_channel_names, "main_tb1_6db_digital_gain_en", False + ) + + @rx_main_tb1_6db_digital_gain_en.setter + def rx_main_tb1_6db_digital_gain_en(self, value): + self._set_iio_attr_int_vec( + self._rx_coarse_ddc_channel_names, + "main_tb1_6db_digital_gain_en", + False, + value, + ) + @property def rx_test_mode(self): """rx_test_mode: NCO Test Mode""" @@ -459,6 +476,21 @@ def tx_ddr_offload(self): def tx_ddr_offload(self, value): self._set_iio_debug_attr_str("pl_ddr_fifo_enable", str(value * 1), self._txdac) + @property + def tx_b_ddr_offload(self): + """tx_b_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._txdac2) + + @tx_b_ddr_offload.setter + def tx_b_ddr_offload(self, value): + self._set_iio_debug_attr_str("pl_ddr_fifo_enable", str(value * 1), self._txdac2) + @property def rx_sample_rate(self): """rx_sampling_frequency: Sample rate after decimation""" diff --git a/adi/adf4030.py b/adi/adf4030.py new file mode 100644 index 0000000000..9941f1767d --- /dev/null +++ b/adi/adf4030.py @@ -0,0 +1,110 @@ +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + + +from adi.attribute import attribute +from adi.context_manager import context_manager + + +class adf4030(context_manager, attribute): + """ + This class provides an interface to the ADF4030 device via the IIO framework. + + Args: + uri (str, optional): URI of the IIO context. Defaults to "". + + Attributes: + _device_name (str): Name of the IIO device ("adf4030"). + _ctrl: Reference to the IIO device controller. + _attrs (list): List of supported channel attributes. + + Properties: + in_temp0_input (float): Returns the temperature sensor input value. + + Dynamic Channel Properties: + For each channel with an ID starting with "altvoltage", the following + properties are dynamically created (where is the channel label + or ID): + + _duty_cycle (float): Duty cycle of the channel. + _frequency (float): Frequency of the channel. + _label (str): Label of the channel. + _output_enable (bool): Output enable status of the channel. + _phase (float): Phase of the channel. + _reference_channel (int): Reference channel index. + _autoalign_iteration (int): Auto-align iteration value. + _autoalign_threshold (float): Auto-align threshold value. + _autoalign_threshold_en (bool): Enable/disable auto-align threshold. + _background_serial_alignment_en (bool): Enable/disable background serial alignment. + _oversampling_ratio (int): Oversampling ratio. + _oversampling_ratio_available (list): List of available oversampling ratios. + + Raises: + Exception: If the ADF4030 device is not found in the IIO context. + + Methods: + _make_channel_property(channel, attr): Creates a property for a given channel and attribute. + _add_channel_properties(): Adds dynamic properties for each supported channel and attribute. + + Example: + >>> from adi.hmc7044 import adf4030 + >>> dev = adf4030(uri="ip:192.168.2.1") + >>> print(dev.in_temp0_input) + >>> dev.altvoltage0_frequency = 10000000 + >>> print(dev.altvoltage0_frequency) + """ + + _device_name = "adf4030" + + def __init__(self, uri=""): + context_manager.__init__(self, uri, self._device_name) + self._ctrl = self._ctx.find_device(self._device_name) + if not self._ctrl: + raise Exception("ADF4030 device not found") + self._add_channel_properties() + + @property + def in_temp0_input(self): + return self._get_iio_attr("temp0", "input", False, self._ctrl) + + def _make_channel_property(self, channel, attr): + def getter(self): + return self._get_iio_attr(channel, attr, True, self._ctrl) + + def setter(self, value): + self._set_iio_attr(channel, attr, True, value, self._ctrl) + + return property(getter, setter) + + # List of channels and their attributes + _attrs = [ + "duty_cycle", + "frequency", + "label", + "output_enable", + "phase", + "reference_channel", + "autoalign_iteration", + "autoalign_threshold", + "autoalign_threshold_en", + "background_serial_alignment_en", + "oversampling_ratio", + "oversampling_ratio_available", + ] + + def _add_channel_properties(self): + for ch in self._ctrl.channels: + if not ch._id.startswith("altvoltage"): + continue + + if "label" in ch.attrs: + name = ch.attrs["label"].value + else: + name = ch._id + + for attr in self._attrs: + prop_name = f"{name}_{attr}" + setattr( + self.__class__, prop_name, self._make_channel_property(ch._id, attr) + ) diff --git a/adi/axi_aion_trig.py b/adi/axi_aion_trig.py new file mode 100644 index 0000000000..ed1cee917c --- /dev/null +++ b/adi/axi_aion_trig.py @@ -0,0 +1,95 @@ +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + + +from adi.attribute import attribute +from adi.context_manager import context_manager + + +class axi_aion_trig(context_manager, attribute): + """axi_aion_trig IIO Device Interface + + This class provides an interface to the 'axi_aion_trig' IIO device, allowing + control and monitoring of its voltage channels and associated attributes. + + Parameters + ---------- + uri : str, optional + URI of the IIO context to connect to. Defaults to an empty string. + + Attributes (per voltage channel) + -------------------------------- + For each voltage channel (e.g., voltage0, voltage1, ...), the following + properties are dynamically created: + + - trig{N}_en : bool + Enable or disable the trigger for channel N. + - trig{N}_phase : int + Phase setting for channel N. + - trig{N}_status : int + Status of the trigger for channel N. + - trig{N}_frequency : int + Frequency setting for channel N. + - trig{N}_internal_bsync_enable : bool + Enable or disable internal bsync for channel N. + - trig{N}_output_enable : bool + Enable or disable output for channel N. + - trig{N}_trigger_now : bool + Trigger an immediate event on channel N. + - trig{N}_trigger_select_gpio_enable : bool + Enable or disable GPIO selection for trigger on channel N. + + Raises + ------ + Exception + If the 'axi_aion_trig' device is not found in the IIO context. + + Examples + -------- + >>> trig = axi_aion_trig("ip:192.168.1.100") + >>> trig.trig0_en = True + >>> print(trig.trig0_status) + """ + + _device_name = "axi_aion_trig" + + def __init__(self, uri=""): + context_manager.__init__(self, uri, self._device_name) + self._ctrl = self._ctx.find_device(self._device_name) + if not self._ctrl: + raise Exception("axi_aion_trig device not found") + self._add_channel_properties() + + def _make_channel_property(self, channel, attr): + def getter(self): + return self._get_iio_attr(channel, attr, False, self._ctrl) + + def setter(self, value): + self._set_iio_attr(channel, attr, False, value, self._ctrl) + + return property(getter, setter) + + # Attributes for in_voltage channels and device properties + _attrs = [ + "en", + "phase", + "status", + "frequency", + "internal_bsync_enable", + "output_enable", + "trigger_now", + "trigger_select_gpio_enable", + ] + + def _add_channel_properties(self): + for ch in self._ctrl.channels: + if not ch._id.startswith("voltage"): + continue + name = ch._id.replace("voltage", "") + + for attr in self._attrs: + prop_name = f"trig{name}_{attr}" + setattr( + self.__class__, prop_name, self._make_channel_property(ch._id, attr) + ) diff --git a/adi/hmc7044.py b/adi/hmc7044.py new file mode 100644 index 0000000000..0f8957e351 --- /dev/null +++ b/adi/hmc7044.py @@ -0,0 +1,139 @@ +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + + +from adi.attribute import attribute +from adi.context_manager import context_manager + + +class hmc7044(context_manager, attribute): + """ + hmc7044 IIO Device Interface + This class provides a Python interface for interacting with the HMC7044 + device via the Industrial I/O (IIO) framework. + It allows users to access and modify device and channel attributes, as well + as perform device-specific operations. + + Attributes: + mute_request (str): Get or set the mute request state of the device. + reseed_request (str): Get or set the reseed request state of the device. + reset_dividers_request (str): Get or set the reset dividers request state of the device. + sleep_request (str): Get or set the sleep request state of the device. + sync_pin_mode (str): Get or set the synchronization pin mode. + sync_pin_mode_available (str): Get the available synchronization pin modes. + sysref_request (str): Get or set the SYSREF request state of the device. + status (str): Get the debug status of the device. + + Channel Attributes (dynamically added per channel): + _frequency (int): Get or set the frequency for the specified channel. + _label (str): Get or set the label for the specified channel. + _phase (int): Get or set the phase for the specified channel. + + Args: + uri (str, optional): URI of the IIO context. Defaults to "". + + Raises: + Exception: If the HMC7044 device is not found in the IIO context. + + Usage Example: + dev = hmc7044("ip:192.168.2.1") + print(dev.status) + dev.mute_request = "1" + print(dev.altvoltage0_frequency) + """ + + _device_name = "hmc7044" + + def __init__(self, uri=""): + context_manager.__init__(self, uri, self._device_name) + self._ctrl = self._ctx.find_device(self._device_name) + if not self._ctrl: + raise Exception("hmc7044 device not found") + self._add_channel_properties() + + def _make_channel_property(self, channel, attr): + def getter(self): + return self._get_iio_attr(channel, attr, True, self._ctrl) + + def setter(self, value): + self._set_iio_attr(channel, attr, True, value, self._ctrl) + + return property(getter, setter) + + # Device attributes + @property + def mute_request(self): + return self._get_iio_dev_attr("mute_request", self._ctrl) + + @mute_request.setter + def mute_request(self, value): + self._set_iio_dev_attr("mute_request", value, self._ctrl) + + @property + def reseed_request(self): + return self._get_iio_dev_attr("reseed_request", self._ctrl) + + @reseed_request.setter + def reseed_request(self, value): + self._set_iio_dev_attr("reseed_request", value, self._ctrl) + + @property + def reset_dividers_request(self): + return self._get_iio_dev_attr("reset_dividers_request", self._ctrl) + + @reset_dividers_request.setter + def reset_dividers_request(self, value): + self._set_iio_dev_attr("reset_dividers_request", value, self._ctrl) + + @property + def sleep_request(self): + return self._get_iio_dev_attr("sleep_request", self._ctrl) + + @sleep_request.setter + def sleep_request(self, value): + self._set_iio_dev_attr("sleep_request", value, self._ctrl) + + @property + def sync_pin_mode(self): + return self._get_iio_dev_attr_str("sync_pin_mode", self._ctrl) + + @sync_pin_mode.setter + def sync_pin_mode(self, value): + self._set_iio_dev_attr_str("sync_pin_mode", value, self._ctrl) + + @property + def sync_pin_mode_available(self): + return self._get_iio_dev_attr_str("sync_pin_mode_available", self._ctrl) + + @property + def sysref_request(self): + return self._get_iio_dev_attr("sysref_request", self._ctrl) + + @sysref_request.setter + def sysref_request(self, value): + self._set_iio_dev_attr("sysref_request", value, self._ctrl) + + # Debug attribute + @property + def status(self): + return self._get_iio_debug_attr_str("status", self._ctrl) + + # Channel attributes + _channel_attrs = ["frequency", "label", "phase"] + + def _add_channel_properties(self): + for ch in self._ctrl.channels: + if not ch._id.startswith("altvoltage"): + continue + + if "label" in ch.attrs: + name = ch.attrs["label"].value + else: + name = ch._id + + for attr in self._channel_attrs: + prop_name = f"{name}_{attr}" + setattr( + self.__class__, prop_name, self._make_channel_property(ch._id, attr) + ) diff --git a/adi/sync_start.py b/adi/sync_start.py index be5a488e61..dd408b6159 100644 --- a/adi/sync_start.py +++ b/adi/sync_start.py @@ -3,6 +3,8 @@ # SPDX short identifier: ADIBSD +import time + from adi.attribute import attribute @@ -29,9 +31,9 @@ def tx_sync_start(self): only the EXT_SYNC synthesis parameter is set. This key self clears. """ - try: + if "sync_start_enable" in self._txdac.attrs: return self._get_iio_dev_attr_str("sync_start_enable", _ctrl=self._txdac) - except: # noqa: E722 + else: if self._txdac.reg_read(0x80000068) & 0x1 == 0x1: return "arm" else: @@ -39,20 +41,20 @@ def tx_sync_start(self): @tx_sync_start.setter def tx_sync_start(self, value): - try: + if "sync_start_enable" in self._txdac.attrs: self._set_iio_dev_attr_str("sync_start_enable", value, _ctrl=self._txdac) - except: # noqa: E722 + else: chan = self._txdac.find_channel("altvoltage0", True) chan.attrs["raw"].value = "1" @property def tx_sync_start_available(self): """ tx_sync_start_available: Returns a list of possible keys used for tx_sync_start """ - try: + if "sync_start_enable_available" in self._txdac.attrs: return self._get_iio_dev_attr_str( "sync_start_enable_available", _ctrl=self._txdac ) - except: # noqa: E722 + else: return "arm" @property @@ -73,9 +75,9 @@ def rx_sync_start(self): only the EXT_SYNC synthesis parameter is set. This key self clears. """ - try: + if "sync_start_enable" in self._rxadc.attrs: return self._get_iio_dev_attr_str("sync_start_enable", _ctrl=self._rxadc) - except: # noqa: E722 + else: if self._rxadc.reg_read(0x80000068) & 1 == 1: return "arm" else: @@ -83,19 +85,120 @@ def rx_sync_start(self): @rx_sync_start.setter def rx_sync_start(self, value): - try: + if "sync_start_enable" in self._rxadc.attrs: self._set_iio_dev_attr_str("sync_start_enable", value, _ctrl=self._rxadc) - except: # noqa: E722 + else: + start_time = time.time() self._rxadc.reg_write(0x80000044, 0x8) while self._rxadc.reg_read(0x80000068) == 0: + if time.time() - start_time > 5: + raise Exception("Timeout reached. Sync start failed") self._rxadc.reg_write(0x80000044, 0x8) @property def rx_sync_start_available(self): """ rx_sync_start_available: Returns a list of possible keys used for rx_sync_start """ - try: + if "sync_start_enable_available" in self._rxadc.attrs: return self._get_iio_dev_attr_str( "sync_start_enable_available", _ctrl=self._rxadc ) - except: # noqa: E722 + else: + return "arm" + + +class sync_start_b(attribute): + """ Synchronization Control: This class allows for synchronous transfers + between transmit and receive data movement or captures. + """ + + @property + def tx_b_sync_start(self): + """ tx_b_sync_start: Issue a synchronisation request + + Possible values are: + - **arm**: Writing this key will arm the trigger mechanism sensitive to an + external sync signal. Once the external sync signal goes high + it synchronizes channels within a DAC, and across multiple + instances. This bit has an effect only the EXT_SYNC + synthesis parameter is set. + - **disarm**: Writing this key will disarm the trigger mechanism + sensitive to an external sync signal. This bit has an + effect only the EXT_SYNC synthesis parameter is set. + - **trigger_manual**: Writing this key will issue an external sync event + if it is hooked up inside the fabric. This key has an effect + only the EXT_SYNC synthesis parameter is set. + This key self clears. + """ + if "sync_start_enable" in self._txdac2.attrs: + return self._get_iio_dev_attr_str("sync_start_enable", _ctrl=self._txdac2) + else: + if self._txdac2.reg_read(0x80000068) & 0x1 == 0x1: + return "arm" + else: + return "disarm" + + @tx_b_sync_start.setter + def tx_b_sync_start(self, value): + if "sync_start_enable" in self._txdac2.attrs: + self._set_iio_dev_attr_str("sync_start_enable", value, _ctrl=self._txdac2) + else: + chan = self._txdac2.find_channel("altvoltage0", True) + chan.attrs["raw"].value = "1" + + @property + def tx_b_sync_start_available(self): + """ tx_sync_start_available: Returns a list of possible keys used for tx_sync_start """ + if "sync_start_enable_available" in self._txdac2.attrs: + return self._get_iio_dev_attr_str( + "sync_start_enable_available", _ctrl=self._txdac2 + ) + else: + return "arm" + + @property + def rx_b_sync_start(self): + """ rx_b_sync_start: Issue a synchronisation request + + Possible values are: + - **arm**: Writing this key will arm the trigger mechanism sensitive to an + external sync signal. Once the external sync signal goes high + it synchronizes channels within a ADC, and across multiple + instances. This bit has an effect only the EXT_SYNC + synthesis parameter is set. + - **disarm**: Writing this key will disarm the trigger mechanism + sensitive to an external sync signal. This bit has an + effect only the EXT_SYNC synthesis parameter is set. + - **trigger_manual**: Writing this key will issue an external sync event + if it is hooked up inside the fabric. This key has an effect + only the EXT_SYNC synthesis parameter is set. + This key self clears. + """ + if "sync_start_enable" in self._rxadc2.attrs: + return self._get_iio_dev_attr_str("sync_start_enable", _ctrl=self._rxadc2) + else: + if self._rxadc2.reg_read(0x68) & 1 == 1: + return "arm" + else: + return "disarm" + + @rx_b_sync_start.setter + def rx_b_sync_start(self, value): + if "sync_start_enable" in self._rxadc2.attrs: + self._set_iio_dev_attr_str("sync_start_enable", value, _ctrl=self._rxadc2) + else: + start_time = time.time() + self._rxadc2.reg_write(0x48, 0x2) + while self._rxadc2.reg_read(0x68) == 0: + if time.time() - start_time > 5: + raise Exception("Timeout reached. Sync start failed") + self._rxadc2.reg_write(0x48, 0x2) + + @property + def rx_b_sync_start_available(self): + """ rx_sync_start_available: Returns a list of possible keys used for rx_sync_start """ + if "sync_start_enable_available" in self._rxadc2.attrs: + return self._get_iio_dev_attr_str( + "sync_start_enable_available", _ctrl=self._rxadc2 + ) + else: return "arm" diff --git a/doc/source/devices/adi.adf4030.rst b/doc/source/devices/adi.adf4030.rst new file mode 100644 index 0000000000..57ed47ac4f --- /dev/null +++ b/doc/source/devices/adi.adf4030.rst @@ -0,0 +1,7 @@ +adi.adf4030 module +================== + +.. automodule:: adi.adf4030 + :members: + :show-inheritance: + :undoc-members: diff --git a/doc/source/devices/adi.axi_aion_trig.rst b/doc/source/devices/adi.axi_aion_trig.rst new file mode 100644 index 0000000000..92264f5e7b --- /dev/null +++ b/doc/source/devices/adi.axi_aion_trig.rst @@ -0,0 +1,7 @@ +adi.axi\_aion\_trig module +========================== + +.. automodule:: adi.axi_aion_trig + :members: + :show-inheritance: + :undoc-members: diff --git a/doc/source/devices/adi.hmc7044.rst b/doc/source/devices/adi.hmc7044.rst new file mode 100644 index 0000000000..9d6d70f54c --- /dev/null +++ b/doc/source/devices/adi.hmc7044.rst @@ -0,0 +1,7 @@ +adi.hmc7044 module +================== + +.. automodule:: adi.hmc7044 + :members: + :show-inheritance: + :undoc-members: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 88195a6de2..7aa03f7ca8 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -65,6 +65,7 @@ Supported Devices adi.ada4961 adi.adaq8092 adi.adar1000 + adi.adf4030 adi.adf4159 adi.adf4355 adi.adf4371 @@ -104,6 +105,7 @@ Supported Devices adi.adxl355 adi.adxl380 adi.adxrs290 + adi.axi_aion_trig adi.cn0511 adi.cn0532 adi.cn0540 @@ -122,6 +124,7 @@ Supported Devices adi.fmcomms5 adi.fmcomms11 adi.gen_mux + adi.hmc7044 adi.jesd adi.lm75 adi.ltc2314_14 diff --git a/examples/ad9084_ad9082_sync_start_example.py b/examples/ad9084_ad9082_sync_start_example.py new file mode 100644 index 0000000000..8868266915 --- /dev/null +++ b/examples/ad9084_ad9082_sync_start_example.py @@ -0,0 +1,385 @@ +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import time +from typing import Tuple + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy.signal import hilbert + +import adi + +# === Constants === +AD9084_URI = "ip:10.44.3.52" +AD9082_URI = "ip:10.44.3.54" +SYNCHRONA_URI = "ip:10.44.3.68" +NUM_RUNS = 10 +BUFFER_SIZE = 2 ** 14 +PLOT_RESULTS = True +USE_DMA = True +USE_AION_TRIGGER = True +APOLLO_SIDE_A = True + + +def measure_phase_and_delay( + sig1: np.ndarray, sig2: np.ndarray, window: int = None, step: int = None +) -> Tuple[float, float]: + """ + Calculate the average phase offset (in degrees) and sample delay between two signals. + + This function computes the analytic signal (via Hilbert transform) if the input is real, + then uses cross-correlation in sliding windows to estimate the phase and delay between the two signals. + + Args: + sig1 (np.ndarray): First input signal (real or complex). + sig2 (np.ndarray): Second input signal (real or complex), must be the same length as sig1. + window (int, optional): Size of the analysis window. Defaults to full signal length. + step (int, optional): Step size between windows. Defaults to non-overlapping. + + Returns: + Tuple[float, float]: (mean_phase_deg, mean_delay) - average phase offset in degrees and average sample delay. + """ + sig1 = np.asarray(sig1) + sig2 = np.asarray(sig2) + assert sig1.shape == sig2.shape, "Signals must be the same length" + + N = len(sig1) + if window is None: + window = N + if step is None: + step = window + + if not np.iscomplexobj(sig1): + sig1 = hilbert(sig1) + if not np.iscomplexobj(sig2): + sig2 = hilbert(sig2) + + phases = [] + delays = [] + + for start in range(0, N - window + 1, step): + s1 = sig1[start : start + window] + s2 = sig2[start : start + window] + cor = np.correlate(s1, s2, mode="full") + i = np.argmax(np.abs(cor)) + m = cor[i] + sample_delay = window - i - 1 + phase_deg = np.angle(m, deg=True) + phases.append(phase_deg) + delays.append(sample_delay) + + return float(np.mean(phases)), float(np.mean(delays)) + + +def remove_dc_offset(samples: np.ndarray) -> Tuple[np.ndarray, float]: + """ + Remove the DC offset from a numpy array of samples. + + Args: + samples (np.ndarray): Input array of samples. + + Returns: + Tuple[np.ndarray, float]: (adjusted_samples, dc_offset) - samples with DC removed and the computed DC offset. + """ + dc_offset = np.mean(samples) + adjusted_samples = samples - dc_offset + return adjusted_samples, dc_offset + + +def generate_tone(freq: float, sample_rate: float, num_samples: int) -> np.ndarray: + """ + Generate a complex sinusoidal tone for a given frequency, sample rate, and number of samples. + + Args: + freq (float): Desired tone frequency in Hz. + sample_rate (float): Sample rate in Hz. + num_samples (int): Number of samples to generate. + + Returns: + np.ndarray: Complex-valued tone signal. + """ + N = num_samples // 8 + freq = int(freq / (sample_rate / N)) * (sample_rate / N) + ts = 1 / float(sample_rate) + t = np.arange(0, N * ts, ts) + i = np.cos(2 * np.pi * t * freq) * 2 ** 15 + q = np.sin(2 * np.pi * t * freq) * 2 ** 15 + Xn = i + 1j * q + Xn = np.pad(Xn, (0, int(num_samples - N)), "constant") + return Xn + + +def zero_imag(samples: np.ndarray) -> np.ndarray: + """ + Zero out the imaginary part of complex samples, preserving only the real part. + + Args: + samples (np.ndarray): Input array of complex samples. + + Returns: + np.ndarray: Array with imaginary part set to zero. + """ + return np.real(samples) + 0j + + +def main(): + """ + Main synchronization and measurement routine for AD9084/AD9082 devices. + + This function performs the following steps: + 1. Initializes device handles for AD9084, AD9081, HMC7044, ADF4030, and AION trigger modules. + 2. Configures PLL and clocking delays, and prints relevant status information. + 3. Sets up JESD204 FSM state machines and resumes operation if paused. + 4. Configures buffer sizes, NCO frequencies, enabled channels, and Nyquist zones for both AD9084 and AD9081. + 5. Generates or loads a transmit waveform, depending on the DMA usage flag. + 6. Optionally enables and configures the AION trigger. + 7. Prints chip and API version information. + 8. Prints SYNC START availability for both AD9084 and AD9081, depending on the selected Apollo side. + 9. Runs a measurement loop for a specified number of runs: + - Arms SYNC START on both devices. + - Destroys and reinitializes RX/TX buffers and channels. + - Transmits waveform if DMA is used. + - Triggers synchronization via AION or manual trigger. + - Checks SYNC status after triggering. + - Receives data from both devices, removes DC offset, and downsamples AD9081 data. + - Measures and records phase and sample delays between TX and RX signals. + - Optionally plots results for each run. + 10. Cleans up device buffers after measurement. + 11. Prints statistical summaries of measured sample and phase offsets if DMA is used. + 12. Optionally plots the transmitted waveform. + + Raises: + Exception: If unexpected SYNC status is encountered during arming or after triggering. + """ + # Device handles + ad9084 = adi.ad9084(AD9084_URI) + hmc7044_main = adi.hmc7044(AD9084_URI) + aion_pll = adi.adf4030(AD9084_URI) + aion_trigger = adi.axi_aion_trig(AD9084_URI) + hmc7044_ad9082 = adi.hmc7044(AD9082_URI) + hmc7044_ad9084 = adi.hmc7044(AD9084_URI) + hmc7044_synchrona = adi.hmc7044(SYNCHRONA_URI) + ad9081 = adi.ad9081(AD9082_URI) + + print("--Setting up chips--") + aion_pll.BSYNC2_DEBUG_P19_reference_channel = 0 + delay_ps = aion_pll.BSYNC2_DEBUG_P19_phase / 1000 + print(f"Delay AD9084_HMC7044 <-> AD9082_HMC7044: {delay_ps} ps") + + if ad9084.jesd204_fsm_paused == 1 and ad9081.jesd204_fsm_paused == 1: + print("\nStatus HMC7044 Synchrona14:") + print(hmc7044_synchrona.status) + print("\nStatus HMC7044 AD9082:") + print(hmc7044_ad9082.status) + print("\nStatus HMC7044 AD9084:") + print(hmc7044_ad9084.status) + print("AD9084 and AD9081 JESD204 FSM is paused, resuming...") + ad9084._ctx.set_timeout(0) + ad9081._ctx.set_timeout(30000) + aion_pll.BSYNC2_reference_channel = 2 + hmc7044_synchrona.sysref_request = 1 + print("HMC7044 Synchrona14 sysref_request set to 1") + print("Resuming JESD204 FSM on AD9082") + ad9081.jesd204_fsm_resume = 1 + print("Resuming JESD204 FSM on AD9084") + ad9084.jesd204_fsm_resume = 1 + + aion_pll.BSYNC2_DEBUG_P19_reference_channel = 2 + delay_ps = aion_pll.BSYNC2_DEBUG_P19_phase / 1000 + print(f"Delay AD9084_SYSREF <-> AD9082_SYSREF: {delay_ps} ps") + + ad9084._ctx.set_timeout(5000) + ad9081._ctx.set_timeout(5000) + + ad9084._rxadc.set_kernel_buffers_count(1) + ad9084._txdac.set_kernel_buffers_count(2) + ad9084.rx_main_nco_frequencies = [0] * 4 + ad9084.tx_main_nco_frequencies = [0] * 4 + ad9084.rx_channel_nco_frequencies = [0] * 4 + ad9084.tx_channel_nco_frequencies = [0] * 4 + ad9084.rx_enabled_channels = [0] + ad9084.tx_enabled_channels = [0] + ad9084.rx_nyquist_zone = ["odd"] * 4 + ad9084.rx_main_tb1_6db_digital_gain_en = [1] * 4 + + ad9081._rxadc.set_kernel_buffers_count(1) + ad9081._txdac.set_kernel_buffers_count(1) + ad9081.rx_enabled_channels = [0] + ad9081.rx_nyquist_zone = ["odd"] + + ad9084.rx_buffer_size = BUFFER_SIZE + ad9084.tx_cyclic_buffer = False + ad9081.rx_buffer_size = 2 * BUFFER_SIZE + ad9081.tx_cyclic_buffer = False + + fs = int(ad9084.tx_sample_rate) + ad9084.dds_single_tone(fs / 10, 0.5, channel=0) + + if USE_DMA: + tx_wave = generate_tone(fs / 19, fs, BUFFER_SIZE) + else: + ad9084.dds_single_tone(fs / 10, 0.5, channel=0) + + ad9084.tx_ddr_offload = 0 + ad9084.tx_b_ddr_offload = 0 + + if USE_AION_TRIGGER: + aion_trigger.trig1_en = 1 + aion_trigger.trig1_trigger_select_gpio_enable = 0 + + print("CHIP Version:", ad9084.chip_version) + print("API Version:", ad9084.api_version) + + if APOLLO_SIDE_A: + print("AD9084 TX SYNC START AVAILABLE:", ad9084.tx_sync_start_available) + print("AD9084 RX SYNC START AVAILABLE:", ad9084.rx_sync_start_available) + else: + print("AD9084 TX_B SYNC START AVAILABLE:", ad9084.tx_b_sync_start_available) + print("AD9084 RX_B SYNC START AVAILABLE:", ad9084.rx_b_sync_start_available) + + print("AD9081 TX_B SYNC START AVAILABLE:", ad9081.tx_sync_start_available) + print("AD9081 RX_B SYNC START AVAILABLE:", ad9081.rx_sync_start_available) + + sample_offsets_9084 = [] + phase_offsets_9084 = [] + sample_offsets_9081 = [] + phase_offsets_9081 = [] + sample_offsets_diff = [] + phase_offsets_diff = [] + + try: + for run_idx in range(NUM_RUNS): + if APOLLO_SIDE_A: + ad9084.rx_sync_start = "arm" + ad9084.tx_sync_start = "arm" + if not ("arm" == ad9084.tx_sync_start == ad9084.rx_sync_start): + raise Exception( + f"AD9084 Unexpected SYNC status: TX {ad9084.tx_sync_start} RX: {ad9084.rx_sync_start}" + ) + else: + ad9084.rx_b_sync_start = "arm" + ad9084.tx_b_sync_start = "arm" + if not ("arm" == ad9084.tx_b_sync_start == ad9084.rx_b_sync_start): + raise Exception( + f"AD9084 Unexpected SYNC status: TX_B {ad9084.tx_b_sync_start} RX_B: {ad9084.rx_b_sync_start}" + ) + + ad9081.rx_sync_start = "arm" + ad9081.tx_sync_start = "arm" + if not ("arm" == ad9081.tx_sync_start == ad9081.rx_sync_start): + raise Exception( + f"AD9081 Unexpected SYNC status: TX {ad9081.tx_sync_start} RX: {ad9081.rx_sync_start}" + ) + + ad9084.tx_destroy_buffer() + ad9084.rx_destroy_buffer() + ad9084._rx_init_channels() + ad9081.rx_destroy_buffer() + ad9081._rx_init_channels() + + if USE_DMA: + ad9084.tx(tx_wave) + + if USE_AION_TRIGGER: + aion_trigger.trig1_trigger_now = 1 + else: + if APOLLO_SIDE_A: + ad9084.tx_sync_start = "trigger_manual" + else: + ad9084.tx_b_sync_start = "trigger_manual" + + if APOLLO_SIDE_A: + if not ("disarm" == ad9084.tx_sync_start == ad9084.rx_sync_start): + raise Exception( + f"Unexpected SYNC status: TX {ad9084.tx_sync_start} RX: {ad9084.rx_sync_start}" + ) + else: + if not ("disarm" == ad9084.tx_b_sync_start == ad9084.rx_b_sync_start): + raise Exception( + f"Unexpected SYNC status: TX_B {ad9084.tx_b_sync_start} RX_B: {ad9084.rx_b_sync_start}" + ) + + if not ("disarm" == ad9081.tx_sync_start == ad9081.rx_sync_start): + raise Exception( + f"AD9081 Unexpected SYNC status: TX {ad9081.tx_sync_start} RX: {ad9081.rx_sync_start}" + ) + + try: + rx_9084 = ad9084.rx() + rx_9084, offset_9084 = remove_dc_offset(rx_9084) + rx_9081 = ad9081.rx() + rx_9081 = rx_9081[::2] + except Exception as e: + print(f"Run# {run_idx} ----------------------------- FAILED: {e}") + continue + + if USE_DMA: + phase_9084, sample_9084 = measure_phase_and_delay( + tx_wave.real, rx_9084.real + ) + phase_9081, sample_9081 = measure_phase_and_delay( + tx_wave.real, rx_9081.real + ) + phase_diff, sample_diff = measure_phase_and_delay( + rx_9084.real, rx_9081.real + ) + sample_offsets_9084.append(sample_9084) + phase_offsets_9084.append(phase_9084) + sample_offsets_9081.append(sample_9081) + phase_offsets_9081.append(phase_9081) + sample_offsets_diff.append(sample_diff) + phase_offsets_diff.append(phase_diff) + print( + f"Run# {run_idx} AD9084 Sample Delay {int(sample_9084)} Phase {phase_9084:.2f}, " + f"AD9081 Sample Delay {int(sample_9081)} Phase {phase_9081:.2f}, " + f"AD9084-AD9082 Sample Delay {int(sample_diff)} Phase {phase_diff:.2f}" + ) + + if PLOT_RESULTS: + plt.xlim(0, 5000) + plt.plot( + np.real(rx_9084), + label=f"AD9084 #{run_idx} {phase_9084:.1f}°;{sample_9084:.0f}", + alpha=0.7, + ) + plt.plot( + np.real(rx_9081), + label=f"AD9081 #{run_idx} {phase_9081:.1f}°;{sample_9081:.0f}", + alpha=0.7, + ) + plt.legend() + plt.title(f"AD9084/AD9082 Phase Sync @ {fs / 1e6} MSPS") + plt.draw() + plt.pause(0.05) + time.sleep(0.1) + finally: + # Ensure device buffers are cleaned up + ad9084.tx_destroy_buffer() + ad9084.rx_destroy_buffer() + ad9081.rx_destroy_buffer() + + if USE_DMA: + print("\nAD9084 Sample Offsets") + print(pd.DataFrame(sample_offsets_9084).describe()) + print("\nAD9084 Phase Offsets\n") + print(pd.DataFrame(phase_offsets_9084).describe()) + print("\nAD9081 Sample Offsets") + print(pd.DataFrame(sample_offsets_9081).describe()) + print("\nAD9081 Phase Offsets\n") + print(pd.DataFrame(phase_offsets_9081).describe()) + print("\nAD9084-AD9082 Sample Offsets") + print(pd.DataFrame(sample_offsets_diff).describe()) + print("\nAD9084-AD9082 Phase Offsets\n") + print(pd.DataFrame(phase_offsets_diff).describe()) + + if PLOT_RESULTS: + plt.plot(2 ** 11 / 2 ** 15 * np.real(tx_wave), label="TX wave", alpha=0.1) + plt.legend() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/examples/ad9084_sync_start_example.py b/examples/ad9084_sync_start_example.py new file mode 100644 index 0000000000..d2eea39ba0 --- /dev/null +++ b/examples/ad9084_sync_start_example.py @@ -0,0 +1,218 @@ +# Copyright (C) 2023 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import time + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy import signal + +import adi + +uri = "ip:10.48.65.219" +dev = adi.ad9084(uri) +aion_trig = adi.axi_aion_trig(uri) + + +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 gen_tone(fc, fs, NN): + N = NN / 8 + fc = int(fc / (fs / N)) * (fs / N) + ts = 1 / float(fs) + t = np.arange(0, N * ts, ts) + i = np.cos(2 * np.pi * t * fc) * 2 ** 15 + q = np.sin(2 * np.pi * t * fc) * 2 ** 15 + Xn = i + 1j * q + # Xn = np.pad(Xn, int(NN - N)) + Xn = np.pad(Xn, (0, int(NN - N)), "constant") + return Xn + + +# Configure properties +print("--Setting up chip") + +# dev._ctx.set_timeout(30000) +dev._rxadc.set_kernel_buffers_count(1) +dev._txdac.set_kernel_buffers_count(2) + +# Set NCOs +dev.rx_channel_nco_frequencies = [0] * 4 +dev.tx_channel_nco_frequencies = [0] * 4 + +dev.rx_main_nco_frequencies = [1000000000] * 4 +dev.tx_main_nco_frequencies = [1000000000] * 4 + +# dev.jesd204_fsm_ctrl = "1" # This will resync the NCOs and all links + +dev.rx_enabled_channels = [0] +dev.tx_enabled_channels = [0] +dev.rx_nyquist_zone = ["odd"] * 4 + +run_plot = False +tx_use_dma = True +use_aion_trig = True +apollo_side_A = True + +RUNS = 10 +N = 2 ** 14 + +dev.rx_buffer_size = N +dev.tx_cyclic_buffer = False + +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) + +if tx_use_dma: + iq1 = gen_tone(fs / 20, fs, N) +else: + # Set single DDS tone for TX on one transmitter + dev.dds_single_tone(fs / 10, 0.5, channel=0) + +# Disable BRAM offload in FPGA +dev.tx_ddr_offload = 0 +dev.tx_b_ddr_offload = 0 + +if use_aion_trig: + aion_trig.trig1_en = 1 + aion_trig.trig1_trigger_select_gpio_enable = 0 + +print("CHIP Version:", dev.chip_version) +print("API Version:", dev.api_version) + +if apollo_side_A: + print("TX SYNC START AVAILABLE:", dev.tx_sync_start_available) + print("RX SYNC START AVAILABLE:", dev.rx_sync_start_available) +else: + print("TX_B SYNC START AVAILABLE:", dev.tx_b_sync_start_available) + print("RX_B SYNC START AVAILABLE:", dev.rx_b_sync_start_available) + +so = [] +po = [] + +# Collect data +for r in range(RUNS): + + # dev.jesd204_fsm_ctrl = "1" + + # if dev.jesd204_device_status_check: + # print(dev.jesd204_device_status) + + if apollo_side_A: + dev.rx_sync_start = "arm" + dev.tx_sync_start = "arm" + + if not ("arm" == dev.tx_sync_start == dev.rx_sync_start): + raise Exception( + "Unexpected SYNC status: TX " + + dev.tx_sync_start + + " RX: " + + dev.rx_sync_start + ) + else: + dev.rx_b_sync_start = "arm" + dev.tx_b_sync_start = "arm" + + if not ("arm" == dev.tx_b_sync_start == dev.rx_b_sync_start): + raise Exception( + "Unexpected SYNC status: TX_B " + + dev.tx_b_sync_start + + " RX_B: " + + dev.rx_b_sync_start + ) + + dev.tx_destroy_buffer() + dev.rx_destroy_buffer() + dev._rx_init_channels() + + if tx_use_dma: + dev.tx(iq1) + + if use_aion_trig: + aion_trig.trig1_trigger_now = 1 + else: + if apollo_side_A: + dev.tx_sync_start = ( + "trigger_manual" # Do an internal DAC TPL manual trigger + ) + else: + dev.tx_b_sync_start = "trigger_manual" + + if apollo_side_A: + if not ("disarm" == dev.tx_sync_start == dev.rx_sync_start): + raise Exception( + "Unexpected SYNC status: TX " + + dev.tx_sync_start + + " RX: " + + dev.rx_sync_start + ) + else: + if not ("disarm" == dev.tx_b_sync_start == dev.rx_b_sync_start): + raise Exception( + "Unexpected SYNC status: TX_B " + + dev.tx_b_sync_start + + " RX_B: " + + dev.rx_b_sync_start + ) + + try: + x = dev.rx() + except Exception as e: + print("Run#", r, " ----------------------------- FAILED:", e) + continue + + if tx_use_dma: + (p, s) = measure_phase_and_delay(iq1, x) + print("Run#", r, "Sample Delay", int(s), "Phase", f"{p:.2f}") + so.append(s) + po.append(p) + + if run_plot == True: + # plt.xlim(350, 450) + plt.plot(np.real(x), label=str(r), alpha=0.7) + plt.legend() + plt.title("MxFE Phase Sync @ " + str(fs / 1000000) + " MSPS") + plt.draw() + plt.pause(0.05) + time.sleep(0.1) + +if tx_use_dma and run_plot == True: + plt.plot(np.real(iq1), label="TX wave", alpha=0.5) + plt.legend() + plt.draw() + +if tx_use_dma: + print("\nSample Offsets") + print(pd.DataFrame(so).describe()) + print("\nPhase Offsets\n") + print(pd.DataFrame(po).describe()) + +if run_plot == True: + plt.show() diff --git a/supported_parts.md b/supported_parts.md index a96cf6e4bd..d6e337ec2f 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -126,6 +126,7 @@ - ADAQ4224 - ADAQ8092 - ADAR1000 +- ADF4030 - ADF4159 - ADF4355 - ADF4371 @@ -189,6 +190,7 @@ - FMCLIDAR1 - FMComms8 - FMComms11 +- HMC7044 - LTC2314-14 - LTC2387-18 - LTC2499 diff --git a/test/emu/devices/ad9084-fmca-ebz.xml b/test/emu/devices/ad9084-fmca-ebz.xml new file mode 100644 index 0000000000..9a05f11692 --- /dev/null +++ b/test/emu/devices/ad9084-fmca-ebz.xml @@ -0,0 +1,34 @@ +]> \ No newline at end of file diff --git a/test/emu/hardware_map.yml b/test/emu/hardware_map.yml index 20f776e0e3..e81752bdbd 100644 --- a/test/emu/hardware_map.yml +++ b/test/emu/hardware_map.yml @@ -747,4 +747,30 @@ ad738x: - filename: ad7381.xml - data_devices: - iio:device0 +hmc7044: + - hmc7044 + - pyadi_iio_class_support: + - hmc7044 + - emulate: + - filename: ad9084-fmca-ebz.xml + - data_devices: + - iio:device1 + +adf4030: + - adf4030 + - pyadi_iio_class_support: + - adf4030 + - emulate: + - filename: ad9084-fmca-ebz.xml + - data_devices: + - iio:device10 + +axi_aion_trig: + - axi_aion_trig + - pyadi_iio_class_support: + - axi_aion_trig + - emulate: + - filename: ad9084-fmca-ebz.xml + - data_devices: + - iio:device11