diff --git a/adi/ad9081.py b/adi/ad9081.py index a9bed2afa..a494a1fa6 100644 --- a/adi/ad9081.py +++ b/adi/ad9081.py @@ -5,6 +5,7 @@ from typing import Dict, List from adi.context_manager import context_manager +from adi.jesd import jesd_eye_scan from adi.rx_tx import rx_tx from adi.sync_start import sync_start @@ -66,7 +67,9 @@ class ad9081(rx_tx, context_manager, sync_start): _path_map: Dict[str, Dict[str, Dict[str, List[str]]]] = {} - def __init__(self, uri=""): + def __init__( + self, uri="", username="root", password="analog", disable_jesd_control=True + ): # Reset default channel names self._rx_channel_names = [] @@ -85,6 +88,9 @@ def __init__(self, uri=""): self._rxadc = self._ctx.find_device("axi-ad9081-rx-hpc") self._txdac = self._ctx.find_device("axi-ad9081-tx-hpc") + if not disable_jesd_control and jesd_eye_scan: + self._jesd = jesd_eye_scan(self, uri, username=username, password=password) + # Get DDC and DUC mappings paths = {} diff --git a/adi/jesd.py b/adi/jesd.py index 0ef9cd5d9..cb39909cf 100644 --- a/adi/jesd.py +++ b/adi/jesd.py @@ -6,6 +6,6 @@ try: from .sshfs import sshfs - from .jesd_internal import jesd + from .jesd_internal import jesd, jesd_eye_scan except ImportError: jesd = None diff --git a/adi/jesd_internal.py b/adi/jesd_internal.py index 349133f8d..a471fe554 100644 --- a/adi/jesd_internal.py +++ b/adi/jesd_internal.py @@ -5,7 +5,7 @@ from .sshfs import sshfs -class jesd: +class jesd(object): """JESD Monitoring""" def __init__(self, address, username="root", password="analog"): @@ -24,17 +24,16 @@ def __init__(self, address, username="root", password="analog"): def find_lanes(self): self.lanes = {} + if len(self.dirs) == 0: + raise Exception("No JESD links found") for dr in self.dirs: if "-rx" in dr: self.lanes[dr] = [] - lanIndx = 0 - while 1: - li = "/lane{}_info".format(lanIndx) - if self.fs.isfile(self.rootdir + dr + li): - self.lanes[dr].append(li) - lanIndx += 1 - else: - break + subdirs = self.fs.listdir(f"{self.rootdir}{dr}") + for subdir in subdirs: + if "lane" in subdir and "info" in subdir: + if self.fs.isfile(f"{self.rootdir}{dr}/{subdir}"): + self.lanes[dr].append(subdir) def find_jesd_dir(self): dirs = self.fs.listdir(self.rootdir) @@ -76,3 +75,168 @@ def get_all_link_statuses(self): def get_all_statuses(self): return {dr: self.decode_status(self.get_status(dr)) for dr in self.dirs} + + +class jesd_eye_scan(jesd): + _jesd_es_duration_ms = 10 + _jesd_prbs = 7 + _max_possible_lanes_index = 24 + + _half_rate = {"mode": "Half Rate", "scale": 1} + _quarter_rate = {"mode": "Quarter Rate", "scale": 4} + + lanes = {} + + def __init__(self, parent, address, username="root", password="analog"): + """JESD204 Eye Scan + + Args: + parent (adi.ad9081): Parent AD9081 instance + address (str): IP address of the device + username (str, optional): Username. Defaults to "root". + password (str, optional): Password. Defaults to "analog". + """ + super().__init__(address, username, password) + self._parent = parent + self._actual_lane_numbers = {} + for device in self.lanes.keys(): + self._actual_lane_numbers[device] = self._get_actual_lane_numbers(device) + + def _get_actual_lane_numbers(self, device: str): + """Get actual lane numbers from device + + The sysfs lanes always go 0-(N-1) where N is the number of lanes. But these + are not always the actual lane numbers. This function gets the actual lane + numbers from the device. + """ + # Check if supported + if "bist_2d_eyescan_jrx" not in self._parent._ctrl.debug_attrs: + raise Exception("2D eye scan not supported on platform") + + if device not in self.lanes.keys(): + raise Exception(f"Device {device} not found.") + num_lanes = len(self.lanes[device]) + + actual_lane_numbers = [] + for lane_index in range(self._max_possible_lanes_index): + try: + self._parent._set_iio_debug_attr_str( + "bist_2d_eyescan_jrx", + f"{lane_index} {self._jesd_prbs} {self._jesd_es_duration_ms}", + ) + actual_lane_numbers.append(str(lane_index)) + if len(actual_lane_numbers) == num_lanes: + break + except OSError: + continue + + if len(actual_lane_numbers) != num_lanes: + raise Exception( + f"Could not find all lanes for device {device}. Expected {num_lanes}, found {len(actual_lane_numbers)}." + ) + + return actual_lane_numbers + + def get_eye_data(self, device=None, lanes=None): + """Get JESD204 eye scan data + + Args: + device (str, optional): Device to get data for. Defaults to None which will get data for the first device found. + lanes (list, optional): List of lanes to get data for. Defaults to None which will get data for all lanes. + + Returns: + dict: Dictionary of lane data. Keys are lane numbers, values are dictionaries with keys "x", "y1", "y2", and "mode". + where "x" is the x-axis data SPO, "y1" is the y-axis data for the first eye, "y2" is the y-axis data for the second eye, + in volts + + """ + # Check if supported + if "bist_2d_eyescan_jrx" not in self._parent._ctrl.debug_attrs: + raise Exception("2D eye scan not supported on platform") + + if device is None: + device = list(self._actual_lane_numbers.keys())[0] + if device not in self._actual_lane_numbers.keys(): + raise Exception(f"Device {device} not found.") + + available_lanes = self._actual_lane_numbers[device] + + if not isinstance(lanes, list) and lanes is not None: + lanes = [lanes] + if lanes is None: + if len(available_lanes) == 0: + raise Exception("No lanes found. Please run find_lanes() first") + lanes = available_lanes + + # Check if lanes are valid + for lane in lanes: + if lane not in available_lanes: + raise Exception(f"Lane {lane} not found for device {device}.") + + # Enable PRBS on TX side + devices_root = "/sys/bus/platform/devices/" + dev_list = self.fs.listdir(devices_root) + tx_dev = next((dev for dev in dev_list if "adxcvr-tx" in dev), None) + if not tx_dev: + raise Exception("No adxcvr-tx device found. Cannot enable PRBS.") + + self.fs.echo_to_fd("7", f"{devices_root}/{tx_dev}/prbs_select") + + lane_eye_data = {} + + print("Hold tight while we get the eye data...") + + for lane in lanes: + # Configure BIST + print(f"Getting eye data for lane {lane}") + + self._parent._set_iio_debug_attr_str( + "bist_2d_eyescan_jrx", + f"{lane} {self._jesd_prbs} {self._jesd_es_duration_ms}", + ) + + eye_data = self._parent._get_iio_debug_attr_str("bist_2d_eyescan_jrx") + + x = [] + y1 = [] + y2 = [] + + for eye_line in eye_data.splitlines(): + if "#" in eye_line: + info = [int(s) for s in eye_line.split() if s.isdigit()] + if info[1] == 64: + mode = self._half_rate["mode"] + scale = self._half_rate["scale"] + else: + mode = self._quarter_rate["mode"] + scale = self._quarter_rate["scale"] + if info[0] != int(lane): + print("Invalid lane number for eye data") + print(f"Expected {lane}, got {info[0]}") + else: + spo = [float(x) for x in eye_line.split(",")] + x.append(spo[0]) + y1.append(spo[1] * scale) + y2.append(spo[2] * scale) + + if len(x) == 0: + raise Exception(f"No eye data found for lane {lane}.") + + graph_helpers = { + "xlim": [-info[1] / 2, info[1] / 2 - 1], + "ylim": [-256, 256], + "xlabel": "SPO", + "ylabel": "EYE Voltage (mV)", + "title": "JESD204 2D Eye Scan", + "rate_gbps": info[2] / 1000000, + } + + lane_eye_data[lane] = { + "x": x, + "y1": y1, + "y2": y2, + "mode": mode, + "graph_helpers": graph_helpers, + } + + return lane_eye_data diff --git a/adi/sshfs.py b/adi/sshfs.py index af47d3560..752d64388 100644 --- a/adi/sshfs.py +++ b/adi/sshfs.py @@ -51,3 +51,8 @@ def listdir(self, path): def gettext(self, path, *kargs, **kwargs): stdout, _ = self._run(f"cat {path}") return stdout + + def echo_to_fd(self, data, path): + if not self.isfile(path): + raise FileNotFoundError(f"No such file: {path}") + self._run(f"echo '{data}' > {path}") diff --git a/examples/ad9081_jesd_eye_diagram.py b/examples/ad9081_jesd_eye_diagram.py new file mode 100644 index 000000000..0ce1800ee --- /dev/null +++ b/examples/ad9081_jesd_eye_diagram.py @@ -0,0 +1,52 @@ +import time + +import adi +import matplotlib.pyplot as plt +from scipy import signal + +dev = adi.ad9081("ip:10.44.3.92", disable_jesd_control=False) + +# Configure properties +print("--Setting up chip") + +dev._ctx.set_timeout(90000) + +fig = plt.figure() + +eye_data_per_lane = dev._jesd.get_eye_data() + +num_lanes = len(eye_data_per_lane.keys()) + +for i, lane in enumerate(eye_data_per_lane): + + x = eye_data_per_lane[lane]["x"] + y1 = eye_data_per_lane[lane]["y1"] + y2 = eye_data_per_lane[lane]["y2"] + + ax1 = plt.subplot(int(num_lanes / 2), 2, int(i) + 1) + plt.scatter(x, y1, marker="+", color="blue") + plt.scatter(x, y2, marker="+", color="red") + plt.xlim(eye_data_per_lane[lane]["graph_helpers"]["xlim"]) + plt.xlabel(eye_data_per_lane[lane]["graph_helpers"]["xlabel"]) + plt.ylabel(eye_data_per_lane[lane]["graph_helpers"]["ylabel"]) + plt.rcParams["axes.titley"] = 1.0 # y is in axes-relative coordinates. + plt.rcParams["axes.titlepad"] = -14 # pad is in points... + plt.title(f" Lane {lane}", loc="left", fontweight="bold") + fig.suptitle( + f"JESD204 MxFE 2D Eye Scan ({eye_data_per_lane[lane]['mode']}) Rate {eye_data_per_lane[lane]['graph_helpers']['rate_gbps']} Gbps" + ) + plt.axvline(0, color="black") # vertical + plt.axhline(0, color="black") # horizontal + plt.grid(True) + # Add secondary x-axis + x_norm = [round(n * 0.1, 2) for n in range(11)] + x.sort() + x = np.linspace(min(x), max(x), 11) + + ax2 = ax1.twiny() + ax2.set_xlim(ax1.get_xlim()) + ax2.set_xticks(x) + ax2.set_xticklabels(x_norm) + ax2.set_xlabel("Unit Interval (UI)") + +plt.show() diff --git a/test/conftest.py b/test/conftest.py index 6e56b2e2d..73d85d24a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -14,6 +14,7 @@ from test.generics import iio_attribute_single_value from test.globals import * from test.html import pytest_html_report_title, pytest_runtest_makereport +from test.jesd import check_jesd_links import adi import numpy as np @@ -202,3 +203,8 @@ def test_verify_overflow(request): @pytest.fixture() def test_verify_underflow(request): yield verify_underflow + + +@pytest.fixture() +def test_check_jesd_links(request): + yield check_jesd_links diff --git a/test/jesd.py b/test/jesd.py new file mode 100644 index 000000000..8c6202949 --- /dev/null +++ b/test/jesd.py @@ -0,0 +1,26 @@ +import time + +import adi +import pytest + + +def check_jesd_links(classname, uri, iterations=4): + """Check that the JESD links are up and in DATA mode + + Args: + classname (str): The name of the class to instantiate + uri (str): The URI of the device to connect to + iterations (int): The number of times to check the JESD links + """ + + sdr = eval(f"{classname}(uri='{uri}', disable_jesd_control=False)") + + for _ in range(iterations): + # Check that the JESD links are up + links = sdr._jesd.get_all_statuses() + for link in links: + print(f"Link {link} status: \n{links[link]}") + assert links[link]["enabled"] == "enabled", f"Link {link} is down" + assert links[link]["Link status"] == "DATA", f"Link {link} not in DATA mode" + + time.sleep(1) diff --git a/test/test_ad9081.py b/test/test_ad9081.py index 83091e34c..ddfb034bd 100644 --- a/test/test_ad9081.py +++ b/test/test_ad9081.py @@ -19,6 +19,12 @@ def scale_field(param_set, iio_uri): return param_set +######################################### +@pytest.mark.iio_hardware(hardware) +def test_ad9081_jesd_links(test_check_jesd_links, iio_uri): + test_check_jesd_links(classname, iio_uri) + + ######################################### @pytest.mark.iio_hardware(hardware) @pytest.mark.parametrize("classname", [(classname)])