From 0f5985c80ae3a04d7e149a39c6c9f3e75203bfb1 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 18 Jan 2024 10:14:35 -0700 Subject: [PATCH 1/6] Add JESD eye scan helper and example Signed-off-by: Travis F. Collins --- adi/jesd_internal.py | 91 ++++++++++++++++++++++++++++- examples/ad9081_jesd_eye_diagram.py | 40 +++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 examples/ad9081_jesd_eye_diagram.py diff --git a/adi/jesd_internal.py b/adi/jesd_internal.py index 349133f8d..84f74706c 100644 --- a/adi/jesd_internal.py +++ b/adi/jesd_internal.py @@ -2,10 +2,99 @@ # # SPDX short identifier: ADIBSD +from abc import ABCMeta, abstractmethod + from .sshfs import sshfs -class jesd: +class eye_scan(metaclass=ABCMeta): + _jesd_es_duration_ms = 100 + _jesd_prbs = 7 + + _half_rate = {"mode": "Half Fate", "scale": 0.004} + _quarter_rate = {"mode": "Quarter Rate", "scale": 0.001} + + lanes = {} + + def get_eye_data(self, lanes=None): + """Get JESD204 eye scan data + + Args: + 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._ctrl.debug_attrs: + raise Exception("2D eye scan not supported on platform") + + if not isinstance(lanes, list) and lanes is not None: + lanes = [lanes] + if lanes is None: + if len(self.lanes) == 0: + raise Exception("No lanes found. Please run find_lanes() first") + lanes = list(self.lanes.keys()) + for lane in lanes: + if lane not in self.lanes.keys(): + raise Exception(f"Lane {lane} not found.") + + lane_eye_data = {} + + for lane in lanes: + # Configure BIST + self._set_iio_debug_attr_str( + "bist_2d_eyescan_jrx", + f"{lane} {self._jesd_prbs} {self._jesd_es_duration_ms}", + ) + + eye_data = self._get_iio_debug_attr_str("bist_2d_eyescan_jrx_data") + + 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] != lane: + print("Invalid lane number for eye data") + print(f"Expected {lane}, got {info[0]}") + else: + spo = [int(x) for x in eye_line.split(",")] + x.append(spo[0]) + y1.append(spo[1] * scale) + y2.append(spo[2] * scale) + + graph_helpers = { + "xlim": [-info[1] / 2, info[1] / 2 - 1], + "xlabel": "SPO", + "ylabel": "EYE Voltage (V)", + "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 + + +class jesd(eye_scan): """JESD Monitoring""" def __init__(self, address, username="root", password="analog"): diff --git a/examples/ad9081_jesd_eye_diagram.py b/examples/ad9081_jesd_eye_diagram.py new file mode 100644 index 000000000..6dea9ab3b --- /dev/null +++ b/examples/ad9081_jesd_eye_diagram.py @@ -0,0 +1,40 @@ +import time + +import adi +import matplotlib.pyplot as plt +from scipy import signal + +dev = adi.ad9081("ip:10.44.3.92") + +# Configure properties +print("--Setting up chip") + +dev._ctx.set_timeout(90000) + +fig = plt.figure() + +eye_data_per_lane = dev.get_eye_data() +num_lanes = len(eye_data_per_lane.keys()) + +for lane in 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"] + + plt.subplot(int(num_lanes / 2), 2, int(lane) + 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.show() From 3b96e82d4d909168d6045ac32d3d3cd5c7baa26b Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 18 Jan 2024 10:44:10 -0700 Subject: [PATCH 2/6] Add JESD debug support to AD9081 Signed-off-by: Travis F. Collins --- adi/ad9081.py | 8 +++++++- adi/jesd_internal.py | 4 +--- examples/ad9081_jesd_eye_diagram.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/adi/ad9081.py b/adi/ad9081.py index a9bed2afa..7293b0157 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 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: + self._jesd = jesd(uri, username=username, password=password) + # Get DDC and DUC mappings paths = {} diff --git a/adi/jesd_internal.py b/adi/jesd_internal.py index 84f74706c..69c4e4f44 100644 --- a/adi/jesd_internal.py +++ b/adi/jesd_internal.py @@ -2,12 +2,10 @@ # # SPDX short identifier: ADIBSD -from abc import ABCMeta, abstractmethod - from .sshfs import sshfs -class eye_scan(metaclass=ABCMeta): +class eye_scan(object): _jesd_es_duration_ms = 100 _jesd_prbs = 7 diff --git a/examples/ad9081_jesd_eye_diagram.py b/examples/ad9081_jesd_eye_diagram.py index 6dea9ab3b..554167670 100644 --- a/examples/ad9081_jesd_eye_diagram.py +++ b/examples/ad9081_jesd_eye_diagram.py @@ -4,7 +4,7 @@ import matplotlib.pyplot as plt from scipy import signal -dev = adi.ad9081("ip:10.44.3.92") +dev = adi.ad9081("ip:10.44.3.92", disable_jesd_control=False) # Configure properties print("--Setting up chip") @@ -13,7 +13,7 @@ fig = plt.figure() -eye_data_per_lane = dev.get_eye_data() +eye_data_per_lane = dev._jesd.get_eye_data() num_lanes = len(eye_data_per_lane.keys()) for lane in eye_data_per_lane: From b44ead5e1a11a5c10fe7aca450fb4a2f07c0e706 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 18 Jan 2024 10:44:49 -0700 Subject: [PATCH 3/6] Add JESD test fixture and add AD9081 test for JESD Signed-off-by: Travis F. Collins --- test/conftest.py | 6 ++++++ test/jesd.py | 26 ++++++++++++++++++++++++++ test/test_ad9081.py | 6 ++++++ 3 files changed, 38 insertions(+) create mode 100644 test/jesd.py 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)]) From d268e40e7a50345fc0a411ca7197fbe70dca3340 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 26 Jan 2024 09:40:36 -0700 Subject: [PATCH 4/6] Fix JESD204 eyescan to handle lanes better Signed-off-by: Travis F. Collins --- adi/ad9081.py | 6 +- adi/jesd.py | 2 +- adi/jesd_internal.py | 241 +++++++++++++++++++++++++++---------------- 3 files changed, 157 insertions(+), 92 deletions(-) diff --git a/adi/ad9081.py b/adi/ad9081.py index 7293b0157..a494a1fa6 100644 --- a/adi/ad9081.py +++ b/adi/ad9081.py @@ -5,7 +5,7 @@ from typing import Dict, List from adi.context_manager import context_manager -from adi.jesd import jesd +from adi.jesd import jesd_eye_scan from adi.rx_tx import rx_tx from adi.sync_start import sync_start @@ -88,8 +88,8 @@ def __init__( 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: - self._jesd = jesd(uri, username=username, password=password) + 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 69c4e4f44..d26b20abf 100644 --- a/adi/jesd_internal.py +++ b/adi/jesd_internal.py @@ -5,19 +5,143 @@ from .sshfs import sshfs -class eye_scan(object): +class jesd(object): + """JESD Monitoring""" + + def __init__(self, address, username="root", password="analog"): + if "ip:" in address: + address = address[3:] + self.address = address + self.username = username + self.password = password + self.rootdir = "/sys/bus/platform/devices/" + + # Connect + self.fs = sshfs(address, username, password) + + self.find_jesd_dir() + self.find_lanes() + + 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] = [] + 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) + self.dirs = [] + for dr in dirs: + if "jesd" in dr: + self.dirs.append(dr) + + def decode_status(self, status): + status = status.replace(",", "\n") + status = status.split("\n") + link_status = {} + for s in status: + if ":" in s: + o = s.split(":") + link_status[o[0].strip().replace("/", "")] = o[1].strip() + if "Link is" in s: + link_status["enabled"] = s.split(" ")[-1].strip() + + return link_status + + def get_status(self, dr): + return self.fs.gettext(self.rootdir + dr + "/status") + + def get_dev_lane_info(self, dr): + return { + ldir.replace("/", ""): self.decode_status( + self.fs.gettext(self.rootdir + dr + "/" + ldir) + ) + for ldir in self.lanes[dr] + } + + def get_all_link_statuses(self): + statuses = dict() + for dr in self.dirs: + if "-rx" in dr: + statuses[dr] = self.get_dev_lane_info(dr) + return statuses + + 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 = 100 _jesd_prbs = 7 + _max_possible_lanes_index = 24 _half_rate = {"mode": "Half Fate", "scale": 0.004} - _quarter_rate = {"mode": "Quarter Rate", "scale": 0.001} + _quarter_rate = {"mode": "Quarter Rate", "scale": 4} lanes = {} - def get_eye_data(self, lanes=None): + 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: @@ -27,29 +151,40 @@ def get_eye_data(self, lanes=None): """ # Check if supported - if "bist_2d_eyescan_jrx" not in self._ctrl.debug_attrs: + 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(self.lanes) == 0: + if len(available_lanes) == 0: raise Exception("No lanes found. Please run find_lanes() first") - lanes = list(self.lanes.keys()) + lanes = available_lanes + + # Check if lanes are valid for lane in lanes: - if lane not in self.lanes.keys(): - raise Exception(f"Lane {lane} not found.") + if lane not in available_lanes: + raise Exception(f"Lane {lane} not found for device {device}.") lane_eye_data = {} for lane in lanes: # Configure BIST - self._set_iio_debug_attr_str( + 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._get_iio_debug_attr_str("bist_2d_eyescan_jrx_data") + eye_data = self._parent._get_iio_debug_attr_str("bist_2d_eyescan_jrx") x = [] y1 = [] @@ -64,19 +199,22 @@ def get_eye_data(self, lanes=None): else: mode = self._quarter_rate["mode"] scale = self._quarter_rate["scale"] - if info[0] != lane: + if info[0] != int(lane): print("Invalid lane number for eye data") print(f"Expected {lane}, got {info[0]}") else: - spo = [int(x) for x in eye_line.split(",")] + spo = [float(x) for x in eye_line.split(",")] x.append(spo[0]) - y1.append(spo[1] * scale) - y2.append(spo[2] * scale) + y1.append(spo[1] * scale / 1000) + y2.append(spo[2] * scale / 1000) + + if len(x) == 0: + raise Exception(f"No eye data found for lane {lane}.") graph_helpers = { "xlim": [-info[1] / 2, info[1] / 2 - 1], "xlabel": "SPO", - "ylabel": "EYE Voltage (V)", + "ylabel": "EYE Voltage (mV)", "title": "JESD204 2D Eye Scan", "rate_gbps": info[2] / 1000000, } @@ -90,76 +228,3 @@ def get_eye_data(self, lanes=None): } return lane_eye_data - - -class jesd(eye_scan): - """JESD Monitoring""" - - def __init__(self, address, username="root", password="analog"): - if "ip:" in address: - address = address[3:] - self.address = address - self.username = username - self.password = password - self.rootdir = "/sys/bus/platform/devices/" - - # Connect - self.fs = sshfs(address, username, password) - - self.find_jesd_dir() - self.find_lanes() - - def find_lanes(self): - self.lanes = {} - 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 - - def find_jesd_dir(self): - dirs = self.fs.listdir(self.rootdir) - self.dirs = [] - for dr in dirs: - if "jesd" in dr: - self.dirs.append(dr) - - def decode_status(self, status): - status = status.replace(",", "\n") - status = status.split("\n") - link_status = {} - for s in status: - if ":" in s: - o = s.split(":") - link_status[o[0].strip().replace("/", "")] = o[1].strip() - if "Link is" in s: - link_status["enabled"] = s.split(" ")[-1].strip() - - return link_status - - def get_status(self, dr): - return self.fs.gettext(self.rootdir + dr + "/status") - - def get_dev_lane_info(self, dr): - return { - ldir.replace("/", ""): self.decode_status( - self.fs.gettext(self.rootdir + dr + "/" + ldir) - ) - for ldir in self.lanes[dr] - } - - def get_all_link_statuses(self): - statuses = dict() - for dr in self.dirs: - if "-rx" in dr: - statuses[dr] = self.get_dev_lane_info(dr) - return statuses - - def get_all_statuses(self): - return {dr: self.decode_status(self.get_status(dr)) for dr in self.dirs} From 8e7a6f682d36271a27471570b624e45dfd0a387f Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 26 Jan 2024 09:44:06 -0700 Subject: [PATCH 5/6] Fix scaling Signed-off-by: Travis F. Collins --- adi/jesd_internal.py | 20 ++++++++++++++++---- adi/sshfs.py | 5 +++++ examples/ad9081_jesd_eye_diagram.py | 5 +++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/adi/jesd_internal.py b/adi/jesd_internal.py index d26b20abf..a471fe554 100644 --- a/adi/jesd_internal.py +++ b/adi/jesd_internal.py @@ -78,11 +78,11 @@ def get_all_statuses(self): class jesd_eye_scan(jesd): - _jesd_es_duration_ms = 100 + _jesd_es_duration_ms = 10 _jesd_prbs = 7 _max_possible_lanes_index = 24 - _half_rate = {"mode": "Half Fate", "scale": 0.004} + _half_rate = {"mode": "Half Rate", "scale": 1} _quarter_rate = {"mode": "Quarter Rate", "scale": 4} lanes = {} @@ -173,8 +173,19 @@ def get_eye_data(self, device=None, lanes=None): 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}") @@ -205,14 +216,15 @@ def get_eye_data(self, device=None, lanes=None): else: spo = [float(x) for x in eye_line.split(",")] x.append(spo[0]) - y1.append(spo[1] * scale / 1000) - y2.append(spo[2] * scale / 1000) + 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", 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 index 554167670..da36d2d5f 100644 --- a/examples/ad9081_jesd_eye_diagram.py +++ b/examples/ad9081_jesd_eye_diagram.py @@ -16,13 +16,13 @@ eye_data_per_lane = dev._jesd.get_eye_data() num_lanes = len(eye_data_per_lane.keys()) -for lane in eye_data_per_lane: +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"] - plt.subplot(int(num_lanes / 2), 2, int(lane) + 1) + 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"]) @@ -36,5 +36,6 @@ ) plt.axvline(0, color="black") # vertical plt.axhline(0, color="black") # horizontal + plt.grid(True) plt.show() From 54c2a104abc8eead69ffaf537031ec4bd012d2a0 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Mon, 12 Feb 2024 17:44:20 -0700 Subject: [PATCH 6/6] Add Time UI secondary axis Signed-off-by: Travis F. Collins --- examples/ad9081_jesd_eye_diagram.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/ad9081_jesd_eye_diagram.py b/examples/ad9081_jesd_eye_diagram.py index da36d2d5f..0ce1800ee 100644 --- a/examples/ad9081_jesd_eye_diagram.py +++ b/examples/ad9081_jesd_eye_diagram.py @@ -14,6 +14,7 @@ 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): @@ -22,7 +23,7 @@ y1 = eye_data_per_lane[lane]["y1"] y2 = eye_data_per_lane[lane]["y2"] - plt.subplot(int(num_lanes / 2), 2, int(i) + 1) + 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"]) @@ -37,5 +38,15 @@ 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()