diff options
-rw-r--r-- | mpm/python/usrp_mpm/cores/CMakeLists.txt | 5 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/cores/eyescan.py | 839 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/cores/nijesdcore.py | 20 |
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 - |