aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mpm/python/usrp_mpm/cores/CMakeLists.txt5
-rw-r--r--mpm/python/usrp_mpm/cores/eyescan.py839
-rw-r--r--mpm/python/usrp_mpm/cores/nijesdcore.py20
3 files changed, 860 insertions, 4 deletions
diff --git a/mpm/python/usrp_mpm/cores/CMakeLists.txt b/mpm/python/usrp_mpm/cores/CMakeLists.txt
index bbed68eb3..39a15c0ed 100644
--- a/mpm/python/usrp_mpm/cores/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/cores/CMakeLists.txt
@@ -1,7 +1,7 @@
#
-# Copyright 2017 Ettus Research, National Instruments Company
+# Copyright 2017-2018 Ettus Research, National Instruments Company
#
-# SPDX-License-Identifier: GPL-3.0
+# SPDX-License-Identifier: GPL-3.0-or-later
#
SET(USRP_MPM_FILES ${USRP_MPM_FILES})
@@ -9,6 +9,7 @@ SET(USRP_MPM_CORE_FILES
${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
${CMAKE_CURRENT_SOURCE_DIR}/tdc_sync.py
${CMAKE_CURRENT_SOURCE_DIR}/nijesdcore.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/eyescan.py
${CMAKE_CURRENT_SOURCE_DIR}/white_rabbit.py
)
LIST(APPEND USRP_MPM_FILES ${USRP_MPM_CORE_FILES})
diff --git a/mpm/python/usrp_mpm/cores/eyescan.py b/mpm/python/usrp_mpm/cores/eyescan.py
new file mode 100644
index 000000000..b6e24ef13
--- /dev/null
+++ b/mpm/python/usrp_mpm/cores/eyescan.py
@@ -0,0 +1,839 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+RX Eye Scan utility for GTX transceivers on 7-series FPGAs.
+
+Introduction:
+
+ A RX eye scan provides a mechanism to validate the receiver's eye margin after the
+ transceiver's equalizer in high-speed digital transmission lines.
+ The principle of operation of an eye scan is the comparison between the data sampled
+ at the nominal center of the eye, and the data sampled at an offset (horizontal and
+ vertical) from the said nominal center.
+ A miscomparison between the nominal data and the offset data yields a bit error, and
+ the Bit Error Rate (BER) is the ratio of bit errors to the total number of samples
+ compared. A 2-D statistical eye can be obtained by determining the BER at multiple
+ offset coordinates (horizontal, vertical) in a given 2-D range. Each offset coordinate
+ has two components: phase offset (horizontal), and voltage offset (vertical).
+
+ The 7-series FPGAs' transceivers include hardware that allows us to perfom parallel
+ sampling on the same signal: one at the nominal center, and one with certain offset.
+ The hardware consists on two separate samplers, one which signal can be offset.
+ Also, a sample counter, that keeps track of the number of samples compared at a given
+ offset; as well as an error counter that tracks the miscomparisons detected. These two
+ counters are accessible via the transceiver's Dynamic Reconfiguration Port (DRP).
+ Finally, a state machine, configurable through the DRP, controls the BER measurement
+ at a given offset.
+ The second sampler path can apply both a phase and a voltage offset to the signal.
+ Thanks to this parallel sampler, one may perform eye scan measurements without
+ affecting the link's integrity (i.e. the nominal sampled data remains untouched).
+
+
+Important considerations:
+
+ - This tool only supports 7-Series FPGAs (GTX only).
+ - The tool needs the NI's JESD core IP and MPM driver to access the DRP of the GTs.
+ - Currently, a .pes (Python Eye Scan) custom binary file is generated, which includes
+ metadata and the sample/error counters results at each offset for the scanned GT(s).
+ This file is further processed and visualized with an internal set of LabVIEW VIs.
+
+
+Using the Eye Scan tool:
+
+ 1. Determine the transceiver(s) configuration.
+ Some parameters are defined with the GT instantiation in the FPGA. These need to
+ be known by the Eye Scan tool in order to function properly:
+ Equalization mode.
+ Low-Power Mode (LPM) or Decision Feedback Equalization (DFE).
+ Please refer to UG476 (p. 184) for further details.
+ Valid values for this tool: 'LMP' or 'DFE'.
+ RXOUT_DIV.
+ This attribute controls the setting for the RX serial clock divider.
+ Please refer to UG476 (p. 213) for further details.
+ Valid values for this tool: 1, 2, 4, 8, 16.
+ RX_INT_DATAWIDTH.
+ Width of valid data on Rdata and Sdata buses is RX fabric data width.
+ Please refer to UG476 (p. 213) for further details.
+ Valid values for this tool: 16, 20, 32, 40.
+
+ 2. Determine the Eye Scan measurement configuration.
+ The eye scan measurement's confidence/resolution depends on some parameters,
+ depending upon their values, the time to perform the scan will change:
+ Prescale.
+ Controls the prescaling of the sample count to keep both sample count and
+ error count in reasonable precision within the 16-bit register range.
+ The higher the prescale value, the more time the eye scan takes, but also
+ the lower BER floor (i.e. best eye margin statistics).
+ Please refer to UG476 (p. 214) for further details.
+ Valid values for this tool: 0 to 31.
+ Horizontal range.
+ Defines the phase offset limits and step that the tool will use to iterate
+ in the horizontal axis during the measurement.
+ Valid range is -32 to 32 (full range), corresponding to -0.5 UI to 0.5 UI
+ Definition example: {'start':-32 , 'stop':32 , 'step': 1}
+ Vertical range.
+ Defines the voltage offset limits and step that the tool will use to iterate
+ in the vertical axis during the measurement.
+ Valid range is -127 to 127 (full range), corresponding to 0.39 %
+ increments.
+ Definition example: {'start':-127, 'stop':127, 'step': 2}
+
+ 3. Determine which GT(s) will be scanned.
+ The tool supports single scan and parallel multi-lane scan. The previous GT
+ configuration (from 1), and the measurement configuration (from 2) applies
+ for all the GTs scanned in a single run. Thus, if one requires a different scan
+ per GT, a EyeScanTool object must be created each time with different parameters.
+ The tool receives a 1-D array, which each element must be a valid GT number that
+ the NI JESD core has access to.
+ Examples: [0] or [0, 1] or [0, 1, 2, 3].
+
+ 4. Obtain a JESD core MPM object.
+ The Eye Scan tool relies on the JESD core IP in the FPGA to talk to the GTs
+ through their DRP, thus a MPM driver object for this core must be available
+ upon creation of the EyeScanTool object.
+ Please refer to nijesdcore.py for further documentation.
+
+ 5. Create an Eye Scan tool MPM object.
+ Once the configuration is defined and a MPM JESD core object is available, one
+ may proceed to create and initialize an EyeScanTool object.
+ The genrated .pes file will be saved to the location given by the SAVE_DIR, which
+ default value is set to a folder named "eyescan" in the home directory.
+ One may change the save location by including the SAVE_DIR attribute at init.
+ Assuming the EyeScanTool class has been imported to the calling file, here is
+ an object creation example:
+ args = {'rxout_div': 2, 'rx_int_datawidth': 20, 'eq_mode': 'LPM', 'prescale': 1,
+ 'SAVE_DIR': "/home/root/my_dir/"}
+ eyescan_tool = EyeScanTool(jesdcore=jesdcore_object,
+ slot_idx=0,
+ **args)
+
+ 6. Perform the Eye Scan measurement!
+ With the EyeScanTool object created and initialized, one may proceed to start
+ the measurement by calling the function eyescan_full_scan(...).
+ This function receives the array of GTs to scan, the horizontal range, and the
+ vertical range. It returns the name of the PES file (automatically generated).
+
+ Here is an example on how to start the scan:
+ scan_lanes = [0, 1, 2, 3]
+ hor_range = {'start':-32 , 'stop':32 , 'step': 2}
+ ver_range = {'start':-127, 'stop':127, 'step': 2}
+ pes_file_name = eyescan_tool.eyescan_full_scan(scan_lanes, hor_range, ver_range)
+
+ 7. Process and visualize the PES file.
+ The resulting .pes binary file must be manually copied to a known location for
+ LabVIEW access (i.e. a Windows machine running LV).
+ Currently, two different file post-processing methods are supported:
+ Single lane full scans.
+ When only one lane is measured (e.g. scan_lanes = [0]), it is recommended to
+ use the Single-Lane VI to process/visualize the results.
+ Multiple lane full scans.
+ When multiple lanes are measured (e.g. scan_lanes = [0, 1, 2, 3]), it is
+ recommended to use the Multi-Lane VI to process/visualize the results.
+ These VIs are for NI/Ettus internal use only. For further details, please contact
+ Humberto Jimenez at humberto.jimenez@ni.com.
+
+
+Theory of operation:
+
+ As explained in the introductory section, the main two elements to perform a rx
+ margin analysis are a sample counter and an error counter. These two are provided
+ by the GT instantiation at the FPGA. This tools implements the algorithm to control
+ the Eye Scan measurement state machine for the given GT(s) and retreive the counts.
+ This task is repetead over and over again through the vertical and horizontal ranges
+ specified by the user. The results for each offset coordinate are stored in a custom
+ binary file (.pes) that is then processed (BER calculation) and visualized.
+
+ When a EyeScanTool object is created (i.e. __init__ is called), the "fixed"
+ configuration parameters for the GT(s) instantiation is defined. Also, the provided
+ JESD core object is verified to make sure it contains the method implementations to
+ access the Dynamic Reconfiguration Port (DRP) of the desired GT(s).
+ With an EyeScanTool object created and initialized, the user only needs to call the
+ eyescan_full_scan(...) method; which handles the measurement configuration, the binary
+ file creation, the GT(s) configuration, and the measurement sweep across the ranges.
+
+
+Future work ideas:
+
+ 1. Generate the eye scan results in human-readable fashion (i.e. ascii encoded
+ instead of binary data).
+ 2. Add Bit Error Rate (BER) calculation for each offset within the tool, instead of
+ just spitting samples and errors counters.
+ 3. Develop a open-source data visualization tool to enable non-LabVIEW users to
+ process and visualize the eye scan results (pes file).
+"""
+
+import os
+import time
+import math
+import datetime
+from builtins import object
+from usrp_mpm.mpmlog import get_logger
+
+class EyeScanTool(object):
+ """
+ Provides a library to perform Eye Scan measurements using the NI JESD core.
+ """
+
+ MGT_TYPE = "GTX"
+ VER_MAJOR = "1"
+ VER_MINOR = "0"
+ SAVE_DIR = "/home/root/eyescan/"
+
+ # Set this value according to the status message printing rate desired. (Min=1)
+ # E.g. PRINT_STATUS_EVERY = 1 will print a status message every offset measurement.
+ PRINT_STATUS_EVERY = 10
+
+ lanes = None
+ # Array that defines the available lanes to measure.
+ lane_num = None
+ # Defines the currently global controled lane number.
+ def set_global_lane(self, lane_num=None):
+ """
+ This method sets the global lane number variable being accessed, as well as
+ configures the DRP to target the given lane number.
+ """
+ # Set the global variable and the DRP target with the given lane number.
+ if lane_num is not None:
+ self.log.trace("Setting lane %d as the global variable...", lane_num)
+ # Set the global variable.
+ self.lane_num = lane_num
+ # Set the DRP target in the JESD core to the given lane number.
+ self.jesdcore.set_drp_target('mgt', self.lane_num)
+ else:
+ self.log.trace("Unsetting the lane global variable...")
+ # Unset the global variable.
+ self.lane_num = None
+ # Disable DRP target for the given lane number.
+ self.jesdcore.disable_drp_target()
+
+
+ def __init__(self, jesdcore, slot_idx=0, **kwargs):
+ def validate_config():
+ """
+ This function validates the configuration parameters' ranges.
+ """
+ assert (0 <= self.prescale) and (self.prescale <= 31)
+ assert self.rxout_div in (1, 2, 4, 8, 16)
+ assert self.rx_int_datawidth in (16, 20, 32, 40)
+ assert self.eq_mode.upper() in ('LPM', 'DFE')
+ self.log.debug("Valid Eye Scan configuration: prescale=%d rxout_div=%d"
+ " rx_int_datawidth=%d eq_mode=%s",
+ self.prescale, self.rxout_div, self.rx_int_datawidth, self.eq_mode)
+ #
+ self.slot_idx = slot_idx
+ self.log = get_logger("EyeScanTool-{}".format(self.slot_idx))
+ self.log.info("Initializing Eye Scan Tool...")
+ self.jesdcore = jesdcore
+ assert hasattr(self.jesdcore, 'set_drp_target')
+ assert hasattr(self.jesdcore, 'disable_drp_target')
+ assert hasattr(self.jesdcore, 'drp_access')
+ # Some global parameters defined.
+ #
+ # Control the prescaling of the sample count to keep both sample
+ # count and error count in reasonable precision within the 16-bit
+ # register range.
+ # Valid values: from 0 to 31.
+ self.prescale = 0
+ #
+ # QPLL/CPLL output clock divider D for the RX datapath.
+ # Valid values: 1, 2, 4, 8, 16.
+ self.rxout_div = 1
+ #
+ # Defines the width of valid data on Rdata and Sdata buses.
+ # Valid values: 16, 20, 32, 40.
+ self.rx_int_datawidth = 16
+ #
+ # Equalizer mode: LPM linear eq. or DFE eq.
+ # When in DFE mode (RXLPMEN=0), due to the unrolled first DFE tap,
+ # two separate eye scan measurements are needed, one at +UT and
+ # one at -UT, to measure the TOTAL BER at a given vertical and
+ # horizontal offset.
+ # Valid values = 'LPM', 'DFE'.
+ self.eq_mode = 'LPM'
+ #
+ # Overwrite the default configuration parameters with the ones given
+ # by the user (host) through kwargs.
+ for key, new_val in list(kwargs.items()):
+ if hasattr(self, key) and (new_val != getattr(self, key)):
+ self.log.trace("Overwriting {0}... default:{1} user:{2}"
+ .format(key, getattr(self, key), new_val))
+ setattr(self, key, new_val)
+ # Validate configuration attributes' values.
+ validate_config()
+
+
+ def parse_ranges(self,
+ hor_range={'start':-32 , 'stop':32 , 'step': 1},
+ ver_range={'start':-127, 'stop':127, 'step': 2}):
+ """
+ This function extracts parameters from the Eye Scan phase and voltage ranges
+ used in measurement loops and height/width calculation.
+ The function returns a list with the following keys:
+ parsed_ranges = {'hor_start', 'hor_stop', 'hor_iterations', 'hor_step',
+ 'ver_start', 'ver_stop', 'ver_iterations', 'ver_step'}
+
+ Parameters:
+ hor_range -> Horizontal phase offset range.
+ This parameter is given as a list of three keyed elements:
+ 'start' -> Defines the first point of the range. [-32,32].
+ 'stop' -> Defines the last point of the range. [-32,32].
+ 'step' -> Defines the step at which the range is iterated. [1,2,4,8].
+ ver_range -> Vertical volateg offset range.
+ This parameter is given as a list of three keyed elements:
+ 'start' -> Defines the first point of the range. [-127,127].
+ 'stop' -> Defines the last point of the range. [-127,127].
+ 'step' -> Defines the step at which the range is iterated. [1,2,4,8].
+ """
+ self.log.trace("Parsing horizontal/vertical ranges...")
+ parsed_ranges = {}
+ # Do some input validation.
+ assert ('start' in hor_range) and ('stop' in hor_range) and ('step' in hor_range)
+ assert ('start' in ver_range) and ('stop' in ver_range) and ('step' in ver_range)
+ assert hor_range['step'] in (1, 2, 4, 8)
+ assert ver_range['step'] in (1, 2, 4, 8)
+ # Parse the horizontal and vertical ranges, and build the output lists.
+ parsed_ranges['hor_start'] = self.rxout_div * hor_range['start']
+ parsed_ranges['hor_stop' ] = self.rxout_div * hor_range['stop' ]
+ parsed_ranges['hor_step' ] = self.rxout_div * hor_range['step' ]
+ parsed_ranges['hor_iterations'] = math.ceil( \
+ (parsed_ranges['hor_stop'] - parsed_ranges['hor_start'] + 1) / \
+ parsed_ranges['hor_step'])
+ self.log.trace("hor_start=%d hor_stop=%d hor_step=%d hor_iterations=%d",
+ parsed_ranges['hor_start'], parsed_ranges['hor_stop'],
+ parsed_ranges['hor_step' ], parsed_ranges['hor_iterations'])
+ parsed_ranges['ver_start'] = ver_range['start']
+ parsed_ranges['ver_stop' ] = ver_range['stop']
+ parsed_ranges['ver_step' ] = ver_range['step']
+ parsed_ranges['ver_iterations'] = math.ceil( \
+ (ver_range['stop'] - ver_range['start'] + 1) / \
+ ver_range['step'])
+ self.log.trace("ver_start=%d ver_stop=%d ver_step=%d ver_iterations=%d",
+ parsed_ranges['ver_start'], parsed_ranges['ver_stop'],
+ parsed_ranges['ver_step' ], parsed_ranges['ver_iterations'])
+ # Return the list with the extracted parameters.
+ return parsed_ranges
+
+
+ def eyescan_config(self,
+ es_qualifier =[0x0000, 0x0000, 0x0000, 0x0000, 0x0000],
+ es_qual_mask =[0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
+ es_sdata_mask=[]):
+ """
+ This function configures the current GT number to enable Eye Scan.
+ The following attributes are configured for the given transceiver lane:
+ - ES_QUALIFIER
+ - ES_QUAL_MASK
+ - ES_SDATA_MASK
+ - ES_PRESCALE
+
+ Parameters:
+ es_qualifier -> This element must be a 5 elements (16-bit integers) 1D array,
+ containing the 80-bit ES_QUALIFIER value.
+ This element is optional, default values available.
+ es_qualifier[0] -> ES_QUALIFIER[15:0]
+ ...
+ es_qualifier[4] -> ES_QUALIFIER[79:64]
+ es_qual_mask -> This element must be a 5 elements (16-bit integers) 1D array,
+ containing the 80-bit ES_QUAL_MASK value.
+ This element is optional, default values available.
+ es_qual_mask[0] -> ES_QUAL_MASK[15:0]
+ ...
+ es_qual_mask[4] -> ES_QUAL_MASK[79:64]
+ es_sdata_mask -> This element must be a 5 elements (16-bit integers) 1D array,
+ containing the 80-bit ES_SDATA_MASK value.
+ If this mask is not given, then an internal default will be
+ assigned for the statistical eye application based on the
+ rx_int_datawidth global parameter value.
+ es_sdata_mask[0] -> ES_SDATA_MASK[15:0]
+ ...
+ es_sdata_mask[4] -> ES_SDATA_MASK[79:64]
+ """
+ #
+ # [15: 0] [31:16] [47:32] [63:48] [79:64]
+ ES_SDATA_MASK_DICT = {16 : [0xFFFF, 0x00FF, 0xFF00, 0xFFFF, 0xFFFF],
+ 20 : [0xFFFF, 0x000F, 0xFF00, 0xFFFF, 0xFFFF],
+ 32 : [0x00FF, 0x0000, 0xFF00, 0xFFFF, 0xFFFF],
+ 40 : [0x0000, 0x0000, 0xFF00, 0xFFFF, 0xFFFF]}
+ #
+ # Configure each lane in the global lanes array.
+ for current_lane in self.lanes:
+ self.set_global_lane(current_lane)
+ self.log.trace("Configuring GT #%d...", self.lane_num)
+ # Do some input validation.
+ assert len(es_qualifier) == 5
+ assert len(es_qual_mask) == 5
+ if len(es_sdata_mask) != 5:
+ self.log.trace("ES_SDATA_MASK defined based on rx_int_datawidth=%d",
+ self.rx_int_datawidth)
+ es_sdata_mask = ES_SDATA_MASK_DICT[self.rx_int_datawidth]
+ # Configure the ES_QUALIFIER attribute.
+ self.log.trace("Configuring the ES_QUALIFIER attribute for GT #%d...", self.lane_num)
+ ES_QUALIFIER_FIRST_ADDR = 0x02C
+ ES_QUALIFIER_LAST_ADDR = 0x030
+ index = 0
+ for drp_addr in range(ES_QUALIFIER_FIRST_ADDR, ES_QUALIFIER_LAST_ADDR + 0x1):
+ self.jesdcore.drp_access(rd=False, addr=drp_addr,
+ wr_data=es_qualifier[index]); index += 1
+ # Configure the ES_QUAL_MASK attribute.
+ # According to UG476 pg. 217, ES_QUAL_MASK for a statistical eye is 80 1's,
+ # so the sample counter and error counter accumulate on every cycle.
+ # Thus, we write this registers to the given GT number to configure the hardware.
+ self.log.trace("Configuring the ES_QUAL_MASK attribute for GT #%d...", self.lane_num)
+ ES_QUAL_MASK_FIRST_ADDR = 0x031
+ ES_QUAL_MASK_LAST_ADDR = 0x035
+ index = 0
+ for drp_addr in range(ES_QUAL_MASK_FIRST_ADDR, ES_QUAL_MASK_LAST_ADDR + 0x1):
+ self.jesdcore.drp_access(rd=False, addr=drp_addr,
+ wr_data=es_qual_mask[index]); index += 1
+ # Configure the ES_SDATA_MASK attribute.
+ self.log.trace("Configuring the ES_SDATA_MASK attribute for GT #%d...", self.lane_num)
+ ES_SDATA_MASK_FIRST_ADDR = 0x036
+ ES_SDATA_MASK_LAST_ADDR = 0x03A
+ index = 0
+ for drp_addr in range(ES_SDATA_MASK_FIRST_ADDR, ES_SDATA_MASK_LAST_ADDR + 0x1):
+ self.jesdcore.drp_access(rd=False, addr=drp_addr,
+ wr_data=es_sdata_mask[index]); index += 1
+ # Configure the ES_PRESCALE attribute.
+ ES_PRESCALE_ADDR = 0x03B
+ self.log.trace("Configuring the ES_PRESCALE attribute for GT #%d...", self.lane_num)
+ drp_x03B_rb = self.jesdcore.drp_access(rd=True, addr=ES_PRESCALE_ADDR)
+ es_prescale = self.prescale & 0x1F
+ drp_x03B_wr = (drp_x03B_rb & ~0xF800) | (es_prescale << 11)
+ self.jesdcore.drp_access(rd=False, addr=ES_PRESCALE_ADDR, wr_data=drp_x03B_wr)
+ # According to UG476 pg. 220, for a GTX xcvr PMA_RSV2[5] should always be
+ # asserted when using Eye Scan; otherwise, the Eye Scan circuitry in the PMA
+ # will be powered down.
+ PMA_RSV2_ADDR = 0x082
+ if self.MGT_TYPE == 'GTX':
+ self.log.trace("Asserting that PMA_RSV2[5] bit is high for GTX #%d...", self.lane_num)
+ drp_x082_rb = self.jesdcore.drp_access(rd=True, addr=PMA_RSV2_ADDR)
+ pma_rsv2_bit5 = (drp_x082_rb >> 5) & 0x1
+ if not pma_rsv2_bit5:
+ self.log.error("PMA_RSV2[5] is not asserted for GT#{}, enable it before link bringup."
+ .format(self.lane_num))
+ raise RuntimeError('Eye Scan cicuitry is powered down, see log for details.')
+ self.log.info("Configured GT #%d!", self.lane_num)
+ self.set_global_lane(None)
+ return
+
+
+ def eyescan_control(self, err_det_en=True, run=False, arm=False):
+ """
+ Configures the eye scan control state machine for the current XCVR lane.
+ This function returns True if there was an update in the register map.
+
+ Parameters:
+ err_det_en -> Enable error detection.
+ 1 -> statistical eye | 0 -> scope and waveform views.
+ run -> Asserting this parameter causes a state transition from the WAIT
+ state to the RESET state, initiating a BER measurement sequence.
+ arm -> Asserting this parameter causes a state transition from the WAIT
+ state to the RESET state, initiating a diagnostic sequence.
+ In the ARMED state, deasserting this bit causes a state transition
+ to the READ state if one of the states of bits x03D[5:2] below is
+ not met.
+ """
+ ARM_TRIGGER_ON = {"error_detected" : 0b0001,\
+ "qualifier_pattern": 0b0010,\
+ "es_trigger" : 0b0100,\
+ "immediate" : 0b1000}
+ EYE_SCAN_EN_VAL = 0b1
+ self.log.trace("Eyescan state machine control for MGT #%d", self.lane_num)
+ # Read the current register values.
+ drp_x03d_rb = self.jesdcore.drp_access(rd=True, addr=0x03D)
+ # Determine the GT Channel attributes to be changed.
+ es_errdet_en = int(err_det_en)
+ es_eye_scan_en = EYE_SCAN_EN_VAL
+ es_control = (int(run) << 0) | \
+ (int(arm) << 1) | \
+ (ARM_TRIGGER_ON["error_detected"] << 2)
+ self.log.trace("Control attributes... ES_ERRDET_EN:0b{0:b}"
+ " ES_EYE_SCAN_EN:0b{1:b} ES_CONTROL:0b{2:06b}"
+ .format(es_errdet_en, es_eye_scan_en, es_control))
+ # Build and write the new register values.
+ drp_x03d_wr = ((drp_x03d_rb & ~0x023F) << 0) | \
+ (es_errdet_en << 9) | \
+ (es_eye_scan_en << 8) | \
+ (es_control << 0)
+ self.jesdcore.drp_access(rd=False, addr=0x03D, wr_data=drp_x03d_wr)
+ return drp_x03d_rb != drp_x03d_wr # Return True when the register changed.
+
+
+ def eyescan_offset(self, hor_offset=0, ver_offset=0, ut_sign='+UT'):
+ """
+ Configures the eyescan horizontal phase offset and vertical voltage offset
+ for the current XCVR lane number.
+ This function returns true if at least one register was updated.
+
+ Parameters:
+ hor_offset -> Horizontal phase offset.
+ [-32, 32] corresponding to -0.5 UI to +0.5 UI.
+ ver_offset -> Vertical voltage offset.
+ [-127, 127] corresponding to 0.39% increments.
+ ut_sign -> UT tap sign: '+UT' or '-UT'.
+ """
+ UT_SIGN_BIT = {'+UT': 0b0, '-UT': 0b1}
+ self.log.trace("Offset configuration for MGT #{}:".format(self.lane_num))
+ # Do some input validation for the given parameters.
+ assert ut_sign.upper() in ('+UT', '-UT')
+ self.log.trace("GT #%d Horizontal offset: %d Vertical offset: %d Tap: %s",
+ self.lane_num, hor_offset, ver_offset, ut_sign)
+ # Read the current register values.
+ drp_x03b_rb = self.jesdcore.drp_access(rd=True, addr=0x03B)
+ drp_x03c_rb = self.jesdcore.drp_access(rd=True, addr=0x03C)
+ # Determine the GT channel attributes to be changed.
+ es_vert_offset = ((abs(ver_offset) & 0x007F) << 0) | \
+ ( int(ver_offset < 0) << 7) | \
+ ( UT_SIGN_BIT[ut_sign] << 8)
+ es_horz_offset = (hor_offset & 0x0FFF)
+ self.log.trace("Offset attributes... ES_HORZ_OFFSET:0b{0:012b} ES_VERT_OFFSET:0b{1:09b}"
+ .format(es_horz_offset, es_vert_offset))
+ # Build and write new register values.
+ drp_x03b_wr = (drp_x03b_rb & ~0x01FF) | (es_vert_offset & 0x01FF)
+ drp_x03c_wr = (drp_x03c_rb & ~0x0FFF) | (es_horz_offset & 0x0FFF)
+ self.jesdcore.drp_access(rd=False, addr=0x03B, wr_data=drp_x03b_wr)
+ self.jesdcore.drp_access(rd=False, addr=0x03C, wr_data=drp_x03c_wr)
+ # Return True when at least one of the two registers changed.
+ return (drp_x03b_rb != drp_x03b_wr) or (drp_x03c_rb != drp_x03c_wr)
+
+
+ def eyescan_wait(self, wait_for='END', exit_after=10000):
+ """
+ This function waits for the eye scan control FSM of the current lane
+ number, to transition to the given state (wait_for).
+ Returns True when the desired state is reached.
+
+ Parameters:
+ wait_for -> State which the function waits the FSM to transition to.
+ {'WAIT','RESET','COUNT','END','ARMED','READ'}
+ """
+ STATE_DECODE = {'WAIT': 0b000, 'RESET': 0b001, 'COUNT': 0b011, \
+ 'END' : 0b010, 'ARMED': 0b101, 'READ' : 0b100}
+ self.log.trace("Waiting for %s state at MGT #%d", wait_for, self.lane_num)
+ # Validate the state input parameter.
+ assert wait_for.upper() in ('WAIT', 'RESET', 'COUNT', 'END', 'ARMED', 'READ')
+ # Poll the es_control_status GT attribute until the FSM transistions to
+ # the given state.
+ state_reached = False
+ iterations = 0
+ delay = 2 ** (self.prescale - 13) if (self.prescale > 13) else 0
+ while not state_reached:
+ # Read the status register.
+ es_control_status = self.jesdcore.drp_access(rd=True, addr=0x151)
+ done = es_control_status & 0x0001
+ current_state = (es_control_status & 0x000E) >> 1
+ self.log.trace("Current state: 0b{0:03b} Status: %s"
+ .format(current_state), {0b0:'Not Done!', 0b1: 'Done!'}[done])
+ # Compare current state with expected state.
+ state_reached = (current_state == STATE_DECODE[wait_for])
+ if (iterations >= 100) and (not state_reached) and (iterations % 100 == 0):
+ self.log.debug("%s state has not been reached for GT #%d after %d iterations.",
+ wait_for, self.lane_num, iterations)
+ time.sleep(delay / 1000.0)
+ # Exit after so many iterations, prevneting the application to hang.
+ iterations += 1
+ if exit_after == iterations:
+ break
+ # Validate that the expected state was reached.
+ if not state_reached:
+ self.log.error("%s state was not reached at GT #%d after %d polls.",
+ wait_for, self.lane_num, iterations)
+ raise Exception("Eyescan status timed out, see log for details.")
+ self.log.trace("%s state reached at GT #%d", wait_for, self.lane_num)
+ return state_reached
+
+
+ def eyescan_counters(self):
+ """
+ This function reads the error and sample counters for the current lane number.
+ Returns a tuple with the error and sample count.
+ """
+ self.log.trace("Reading counters for GT #%d ...", self.lane_num)
+ counters = {'error_count': 0x0000, 'sample_count': 0x0000}
+ # Read the error counter.
+ counters['error_count' ] = self.jesdcore.drp_access(rd=True, addr=0x14F) & 0xFFFF
+ counters['sample_count'] = self.jesdcore.drp_access(rd=True, addr=0x150) & 0xFFFF
+ self.log.trace("es_error_count: 0x{:04X} es_sample_count: 0x{:04X}"
+ .format(counters['error_count'], counters['sample_count']))
+ return counters
+
+
+ def eyescan_acquisition(self, hor_offset=0, ver_offset=0):
+ """
+ This function performs an acquisition for each lane in the lanes global array.
+ Each GT is tested at the same "coordinate". Only after all GTs measurements
+ are completed, the function returns.
+
+ Parameters:
+ hor_offset -> Horizontal phase offset.
+ [-32, 32] corresponding to -0.5 UI to +0.5 UI.
+ ver_offset -> Vertical voltage offset.
+ [-127, 127] corresponding to 0.39% increments.
+ """
+ self.log.trace("Starting acquisition for GTs {}".format(self.lanes))
+ acq_counters = [] # Array that stores multiple sl_counters lists.
+ for _ in range(0, max(self.lanes) + 1):
+ acq_counters.append({})
+ #
+ # First eye scan measurement (LPM | DFE)
+ # Start the FSM on all the requested lanes.
+ for current_lane in self.lanes:
+ self.set_global_lane(current_lane)
+ self.log.trace("Starting +UT acquisition for GT #%d", self.lane_num)
+ # Clear run & arm bits in the Eyescan control.
+ self.eyescan_control(err_det_en=True, run=False, arm=False)
+ # Set offsets with +UT.
+ self.eyescan_offset(hor_offset, ver_offset, ut_sign='+UT')
+ # Start eyescan FSM: set run with ErrDet enabled.
+ self.eyescan_control(err_det_en=True, run=True, arm=False)
+ #
+ # Wait for the FSM to complete on each lane, read counters, and
+ # start second eye scan measurement (DFE eq. only).
+ for current_lane in self.lanes:
+ self.set_global_lane(current_lane)
+ # Wait for END state.
+ self.eyescan_wait(wait_for='END')
+ # Clear run & arm bits in the Eyescan control.
+ self.eyescan_control(err_det_en=True, run=False, arm=False)
+ # Read counters with +UT.
+ acq_counters[self.lane_num]['+UT'] = self.eyescan_counters()
+ self.log.trace("Results +UT GT #%d... Errors=%d Samples=%d.", self.lane_num,
+ acq_counters[self.lane_num]['+UT']['error_count'],
+ acq_counters[self.lane_num]['+UT']['sample_count'])
+ # Start second eye scan measurement (DFE eq. only).
+ if self.eq_mode == 'DFE':
+ # Set offsets with -UT.
+ self.eyescan_offset(hor_offset, ver_offset, ut_sign='-UT')
+ # Start eyescan FSM: set run with ErrDet enabled.
+ self.eyescan_control(err_det_en=True, run=True, arm=False)
+ else:
+ self.log.debug("Single measurement finalized for GT #%d (H=%d, V=%d, %s).",
+ self.lane_num, hor_offset, ver_offset, self.eq_mode)
+ #
+ # Wait for the FSM (-UT, DFE only) to complete on each lane, and read counters.
+ if self.eq_mode == 'DFE':
+ for current_lane in self.lanes:
+ self.set_global_lane(current_lane)
+ # Wait for END state.
+ self.eyescan_wait(wait_for='END')
+ # Clear run & arm bits in the Eyescan control.
+ self.eyescan_control(err_det_en=True, run=False, arm=False)
+ # Read counters with -UT.
+ acq_counters[self.lane_num]['-UT'] = self.eyescan_counters()
+ self.log.trace("Results -UT GT #%d... Errors=%d Samples=%d.", self.lane_num,
+ acq_counters[self.lane_num]['-UT']['error_count'],
+ acq_counters[self.lane_num]['-UT']['sample_count'])
+ self.log.debug("Single measurement finalized for GT #%d (H=%d, V=%d, %s).",
+ self.lane_num, hor_offset, ver_offset, self.eq_mode)
+ #
+ self.set_global_lane(None)
+ # Return the error and sample counters for all lanes, both +UT and -UT.
+ return acq_counters
+
+
+ def eyescan_sweep(self, bin_file, parsed_ranges):
+ """
+ Performs Eye Scan "measurement loop" (error counting) acquisitions across the
+ given phase and voltage offset ranges.
+
+ This function creates a binary file containing the sweep results.
+ The binary file is a set of bytes, where file[position] represents a byte
+ (8-bit) at the given position. The bytes are saved as follows:
+
+ If eq_mode = 'LPM'...
+ file[offset + 4*lanes*i + 4*curr_lane + 0] = sample_count[15:0] (+UT) (ith acquisition)
+ file[offset + 4*lanes*i + 4*curr_lane + 2] = error_count[15:0] (+UT) (ith acquisition)
+
+ If eq_mode = 'DFE'...
+ file[offset + 4*lanes*(i*2 + 0) + 4*curr_lane + 0] = sample_count[15:0] (+UT) (ith acquisition)
+ file[offset + 4*lanes*(i*2 + 0) + 4*curr_lane + 2] = error_count[15:0] (+UT) (ith acquisition)
+ file[offset + 4*lanes*(i*2 + 1) + 4*curr_lane + 0] = sample_count[15:0] (-UT) (ith acquisition)
+ file[offset + 4*lanes*(i*2 + 1) + 4*curr_lane + 2] = error_count[15:0] (-UT) (ith acquisition)
+
+ Where,
+ i -> single acquisition iteration number, ranging from 0 to
+ (hor_iterations * ver_iterations - 1).
+ offset -> set offset for metadata to be stored at the beginning of
+ the binary file.
+ lanes -> total number of lanes to be scanned. Defined as len(self.lanes).
+ curr_lane -> a given lane number. It should be any value in the lanes array.
+
+ Parameters:
+ bin_file -> Binary file reference to write data to. Passed from top level function.
+ parsed_ranges -> This is a keyed list with parsed parameters from parse_ranges().
+ """
+ def write_byte_counters(acq_counters):
+ """
+ This method writes the acquisition counters for each lane to the binary file.
+ """
+ # Write the sample and error counters for all given lanes.
+ for current_lane in self.lanes:
+ # Write the counters for the current lane.
+ sl_counters = acq_counters[current_lane]
+ self.log.debug("Writing +UT counters for GT #{0}: {1}"
+ .format(current_lane, sl_counters))
+ byte_number = (sl_counters['+UT']['sample_count']).to_bytes(2, 'little')
+ bin_file.write(byte_number)
+ byte_number = (sl_counters['+UT']['error_count' ]).to_bytes(2, 'little')
+ bin_file.write(byte_number)
+ # -UT results only exists when DFE eq. mode is used.
+ if '-UT' in sl_counters:
+ self.log.debug("Writing -UT counters for GT #%d...", current_lane)
+ byte_number = (sl_counters['-UT']['sample_count']).to_bytes(2, 'little')
+ bin_file.write(byte_number)
+ byte_number = (sl_counters['-UT']['error_count' ]).to_bytes(2, 'little')
+ bin_file.write(byte_number)
+ #
+ gts_string = "GTs {}".format(self.lanes)
+ self.log.trace("Starting sweep for %s ...", gts_string)
+ # Perform the Eye Scan sweep!
+ acq_counters = []
+ total_iterations = parsed_ranges['hor_iterations'] * parsed_ranges['ver_iterations']
+ iterations = 0
+ # Outer loop iterates horizontally.
+ for hor_offset in range(parsed_ranges['hor_start'], parsed_ranges['hor_stop'] + 1,
+ parsed_ranges['hor_step']):
+ # Inner loop iterates vertically.
+ for ver_offset in range(parsed_ranges['ver_start'], parsed_ranges['ver_stop'] + 1,
+ parsed_ranges['ver_step']):
+ # Perform a single acquisition at each "coordinate".
+ acq_counters = self.eyescan_acquisition(hor_offset, ver_offset)
+ # Write the data to a binary file.
+ write_byte_counters(acq_counters)
+ # Report Eye Scan progress.
+ iterations += 1
+ progress = iterations / total_iterations * 100
+ # Only print status messages every PRINT_STATUS_EVERY iterations.
+ if iterations % self.PRINT_STATUS_EVERY == 0:
+ self.log.info("Eye Scan progress for %s sweep: %.2f %%", gts_string, progress)
+
+
+ def create_pes_file(self, hor_range, ver_range):
+ """
+ This function creates a .pes file and writes the metadata header.
+ The file object is returned.
+
+ 0x00 -> 0x0F : "PythonEyeScanXpY" [16 bytes] (X -> Major, Y -> Minor)
+ --- Data offset ---
+ 0x10 -> 0x11 : data_offset [ 2 bytes]
+ --- Eye Scan run configuration ---
+ 0x12 -> 0x13 : prescale [ 2 bytes]
+ 0x14 -> 0x15 : rxout_div [ 2 bytes]
+ 0x16 -> 0x17 : rx_int_datawidth [ 2 bytes]
+ 0x18 -> 0x19 : eq_mode [ 2 bytes] (0 -> LPM / 1 -> DFE)
+ --- Phase ranges ---
+ 0x1A -> 0x1B : hor_start [ 2 bytes]
+ 0x1C -> 0x1D : hor_stop [ 2 bytes]
+ 0x1E -> 0x1F : hor_step [ 2 bytes]
+ 0x20 -> 0x21 : ver_start [ 2 bytes]
+ 0x22 -> 0x23 : ver_stop [ 2 bytes]
+ 0x24 -> 0x25 : ver_step [ 2 bytes]
+ --- Lane(s) information ---
+ 0x26 -> 0x27 : number of lanes [ 2 bytes]
+ 0x28 -> 0x29 : lane_num array [ 2 bytes] (Each nibble represents a lane)
+ """
+ def build_file_name():
+ """
+ This function builds the file name, which contains the scanned GTs and the
+ prescale value used.
+ """
+ file_name = "eyescan"
+ # Include a time stamp.
+ now = datetime.datetime.now()
+ file_name += ("_%04d%02d%02d%02d%02d" %
+ (now.year, now.month, now.day, now.hour, now.minute))
+ # Include the scanned slot.
+ file_name += ("_slot%d" % self.slot_idx)
+ # Include the scanned GTs.
+ file_name += "_gt"
+ for lane in self.lanes:
+ file_name += ("%d" % lane)
+ # Include the prescale value.
+ file_name += ("_pre%d" % self.prescale)
+ # Include extension.
+ file_name += ".pes"
+ return file_name
+ #
+ def write_byte_number(number=0, size=2, offset=None):
+ """
+ This function writes a number as bytes in the binary file.
+ When an offset is given, the data is written at that address, leaving the
+ pointer there.
+ """
+ if offset:
+ pes_file.seek(offset)
+ byte_number = (number).to_bytes(size, 'little', signed=True)
+ pes_file.write(byte_number)
+ #
+ # Create the directory to save the pes files if it does not exist.
+ if not os.path.isdir(self.SAVE_DIR):
+ self.log.trace("Creating directory: {}".format(self.SAVE_DIR))
+ os.makedirs(self.SAVE_DIR)
+ # Open the binary file which data will be saved to.
+ file_name = build_file_name()
+ pes_file = open(self.SAVE_DIR + file_name, "wb+")
+ # Write the metadata header.
+ byte_string = ("PythonEyeScan"+self.VER_MAJOR+"p"+self.VER_MINOR).encode('utf-8')
+ pes_file.write(byte_string) # 0x00: signature string.
+ write_byte_number(number=0x0000) # 0x10: data_offset (placeholder).
+ write_byte_number(number=self.prescale) # 0x12: prescale.
+ write_byte_number(number=self.rxout_div) # 0x14: rxout_div.
+ write_byte_number(number=self.rx_int_datawidth) # 0x16: rx_int_datawidth.
+ write_byte_number(number={'LPM':0, 'DFE':1}[self.eq_mode]) # 0x18: eq_mode.
+ write_byte_number(number=hor_range['start']) # 0x1A: hor_start.
+ write_byte_number(number=hor_range['stop']) # 0x1C: hor_stop.
+ write_byte_number(number=hor_range['step']) # 0x1E: hor_step.
+ write_byte_number(number=ver_range['start']) # 0x20: ver_start.
+ write_byte_number(number=ver_range['stop']) # 0x22: ver_stop.
+ write_byte_number(number=ver_range['step']) # 0x24: ver_step.
+ write_byte_number(number=len(self.lanes)) # 0x26: number of lanes.
+ nibble = 0x0000
+ for lane_index in range(0, len(self.lanes)):
+ nibble |= (self.lanes[lane_index] & 0xF) << lane_index*4
+ write_byte_number(number=nibble) # 0x28: lane_num array.
+ # Write data offset and set pointer ready for data writing.
+ data_offset = pes_file.tell()
+ write_byte_number(number=data_offset, offset=0x10)
+ pes_file.seek(data_offset)
+ # Return the opened file.
+ return (file_name, pes_file)
+
+
+ def eyescan_full_scan(self,
+ scan_lanes=[0],
+ hor_range={'start':-32 , 'stop':32 , 'step': 1},
+ ver_range={'start':-127, 'stop':127, 'step': 2}):
+ """
+ This function performs all the GT configuration and starts the eye scan sweep.
+ The binary file should be open here.
+
+ Parameters:
+ scan_lanes -> Array that represents which GTs will be scanned. The maximum size
+ of the array is 4, and the valid values for each item are 0,1,2,3.
+ hor_range -> Horizontal phase offset range.
+ This parameter is given as a list of three keyed elements:
+ 'start' -> Defines the first point of the range. [-32,32].
+ 'stop' -> Defines the last point of the range. [-32,32].
+ 'step' -> Defines the step at which the range is iterated. [1,2,4,8].
+ ver_range -> Vertical volateg offset range.
+ This parameter is given as a list of three keyed elements:
+ 'start' -> Defines the first point of the range. [-127,127].
+ 'stop' -> Defines the last point of the range. [-127,127].
+ 'step' -> Defines the step at which the range is iterated. [1,2,4,8].
+ """
+ # Set the global lanes variable that defines which lanes will be scanned.
+ self.lanes = scan_lanes
+ # Extract the needed parameters from the given ranges.
+ parsed_ranges = self.parse_ranges(hor_range, ver_range)
+ # Create the .pes binary file.
+ file_name, pes_file = self.create_pes_file(hor_range, ver_range)
+ # Configure the requested lanes.
+ self.eyescan_config()
+ # Perform the sweep on the requested lanes.
+ self.eyescan_sweep(pes_file, parsed_ranges)
+ # Close the binary file.
+ pes_file.close()
+ return file_name
diff --git a/mpm/python/usrp_mpm/cores/nijesdcore.py b/mpm/python/usrp_mpm/cores/nijesdcore.py
index 90b45430f..a63da8bc1 100644
--- a/mpm/python/usrp_mpm/cores/nijesdcore.py
+++ b/mpm/python/usrp_mpm/cores/nijesdcore.py
@@ -61,7 +61,8 @@ class NIJESDCore(object):
"tx_sysref_delay" : 10, # Cycles of delay added to TX SYSREF
"tx_driver_swing" : 0b1111, # See UG476, TXDIFFCTRL
"tx_precursor" : 0b00000, # See UG476, TXPRECURSOR
- "tx_postcursor" : 0b00000} # See UG476, TXPOSTCURSOR
+ "tx_postcursor" : 0b00000, # See UG476, TXPOSTCURSOR
+ "enable_rx_eyescan" : False} # Enable the PMA Eye Scan circuitry.
def __init__(self, regs, slot_idx=0, **kwargs):
self.regs = regs
@@ -210,6 +211,7 @@ class NIJESDCore(object):
Initializes the core. Must happen after the reference clock is stable.
"""
self.log.trace("Initializing JESD204B FPGA core(s)...")
+ self._gt_pma_eyescan(self.enable_rx_eyescan)
self._gt_pll_power_control(self.qplls_used, self.cplls_used)
self._gt_reset('tx', reset_only=True)
self._gt_reset('rx', reset_only=True)
@@ -326,6 +328,21 @@ class NIJESDCore(object):
raise RuntimeError("One or more GT QPLLs failed to lock!")
self.log.trace("QPLL(s) reporting locked!")
+ def _gt_pma_eyescan(self, enable=False):
+ # According to UG476 pg. 220, for a GTX xcvr PMA_RSV2[5] should always be
+ # asserted when using Eye Scan; otherwise, the Eye Scan circuitry in the PMA
+ # will be powered down.
+ PMA_RSV2_DRP_ADDR = 0x082
+ self.log.debug("{} the eye scan circuitry in the PMA for the GTXs..."
+ .format({True: "Enabling", False: "Disabling"}[enable]))
+ for gt_num in range(0, self.rx_lanes):
+ self.set_drp_target('mgt', gt_num)
+ drp_x082_rb = self.drp_access(rd=True, addr=PMA_RSV2_DRP_ADDR)
+ pma_rsv2_bit5 = int(enable)
+ drp_x082_wr = (drp_x082_rb & ~(0b1 << 5)) | (pma_rsv2_bit5 << 5)
+ self.drp_access(rd=False, addr=PMA_RSV2_DRP_ADDR, wr_data=drp_x082_wr)
+ self.disable_drp_target()
+
def set_drp_target(self, mgt_or_qpll, dev_num):
"""
Sets up access to the specified MGT or QPLL. This must be called
@@ -382,4 +399,3 @@ class NIJESDCore(object):
self.log.error("DRP read after write failed to match!")
return rd_data
-