AN-2594: ADRV904x SERDES Tuning Application Note

Introduction

The ADRV904x devices use a SERDES high-speed serial interface based on the JESD204B/JESD204C standards to transfer analog-to-digital converter (ADC) and digital-to-analog converter (DAC) samples between the transceiver device and a baseband integrated chip (BBIC) or field-programmable gate array (FPGA). The PHY block in the ADRV904x deserializer JRx side uses a continuous time linear equalizer (CTLE) plus a decision feedback equalizer (DFE) to decode the serial bits correctly after passing through the lossy channel.

The CTLE and DFE requires careful calibration to get optimal peformance at the various lane rates, which is handled automatically by the ADRV904x firmware. The user can tune the serializer JTx amplitude and preemphasis settings as well as the deserializer JRx CTLE filter settings to optimize the link depending on the channel characteristics as shown in Figure 1. This document describes the recommended SERDES tuning process when evaluating the ADRV904x devices.

Block Diagram of SERDES Link

Figure 1. BBIC to ADRV904x SERDES Link Simplified Block Diagram.

DFE Basic Description

The SERDES receiver on the ADRV904x contains both a CTLE and a DFE. The CTLE consists of several filtering stages, which are used to equalize the channel response and is mostly auto adapted, but the low-pass filter (LPF) stage can be set by the user as described in the following sections. The DFE works by calculating and removing the inter symbol interference (ISI) between adjacent sample points. The coefficients of the DFE are calculated and auto adapted by the SERDES tracking calibration on the ADRV904x. In addition, the DFE contains a set of data comparators to determine if the bit is a 1 or a 0, and also comparators to obtain error information and optimize the sampling point. These comparator thresholds along with other DFE calibration information can be readback by the SERDES health metrics application processor interface (API) that is discussed in the next sections.

SERDES Link Tuning

The ADRV904x device runs an initial calibration on the SERDES PHY block to determine the appropriate settings for the CTLE and DFE block by adjusting parameters like the internal CTLE bandwidth settings and comparator DC offset and thresholds. This helps adjust for any part to part variation and optimize the received eye in the presence of ISI.

A tracking calibration is also run during normal operation to maintain performance over any temperature or supply voltage variation. The tracking calibration operates in the background and makes small adjustments to the CTLE and DFE parameters while maintaining bit error rate performance.

The settings for the calibrations are mostly default in software depending on the SERDES lane rates used, for example the default parameters are different for 16 G vs. 24 G operation. These calibration settings defaults are chosen based on internal Analog Devices, Inc. characterization. However, there is still some programmability needed to cater for trace length and associated insertion loss variations, as this is determined by the unique printed circuit board (PCB) SERDES lane layout. To keep this adjustment as simple as possible, the ADRV904x parts have a single parameter that needs to be adjusted depending on the insertion loss at a respective lane rate.

To enable this, select Enable ETM (Enhanced Tuning Mode) in the JESD deframer section under basic JESD in analysis, control, evaluation (ACE) graphic user interface (GUI), and enter in an insertion loss range per lane in the GUI. This insertion loss pulldown is shown in Figure 2. Note, if the insertion loss range for a given lane straddles across two selections, for example 4 dB to 7 dB, then enter the closest range value at first. You can test and compare the other insertion loss range values as described in the SERDES Link Tuning section. This insertion loss range then gets translated to an lpfMask value depending on the SERDES lane rate as shown in Table 1.

Figure 2. ADRV904x GUI JESD Deframer Enable ETM Mode.
Table 1. lpfMask Settings
SERDES Lane Insertion Loss 16 G 19 G 24 G 32 G
0 dB to 5 dB Super Low Low Low High
5 dB to 10 dB Low Low Low High
10 dB to 15 dB Low Med Med N/A
15 dB to 20 dB Med High High N/A
20 dB+ High High High N/A

Table 2 gives the decimal value that gets programmed for a given lpfMask. This can be done on a per lane basis. For example, if some lanes require lpfMask = Low, and some lanes require lpfMask = Medium.

Table 2. lpfMask Values
CTLE Bandwidth Setting LPFMask Value to Program (Decimal)
Super Low 64
Low 1
Medium 2
High 4

This value gets written in the JSON file by the GUI/configurator automatically, which edits configOption1 in the deser_lane_cfg section of the JSON per lane. For example, if 0 dB to 5 dB insertion loss range for all lanes at 16 Gbps is set, then configurator writes “configOption1”: 64 for the eight lanes. The highBoost parameter must also be set to a 1 by the configurator. You must not manually set the highBoost or configOptions settings unless given a configuration setting by Analog Devices.

Figure 3. ADRV904x JSON File deser_lane_cfg Section.

Json File Settings for Lane Rates < 16Gbps


If you are running a configuration with SERDES lane rates that are less than 16 Gbps, then a simpler CTLE and DFE calibration is used. In this case, the SERDES enhanced tuning is not applicable and you can set all highboost and configOptions values in the JSON to 0, and leave the Enable ETM (Enhanced Tuning Mode) unchecked. You must still run the SERDES InitCal and enable SERDES TrackingCal as normal for this configuration as part of the SERDES bring up sequence.

One exception is for short channels, where the insertion loss is lower than the minimum given in the user guide for a given SERDES lane rate as shown in Table 3. If your channel insertion loss is lower than the minimum, for example at 12 Gbps your insertion loss is < 4 dB, then you can manually apply the following settings to the JSON file. This must be added to the lowest lane in use. For example, if you are using SERDIN Lane 2/Lane 3/Lane 4/Lane 5/Lane 6/Lane 7, then add these values in the Lane 2 position in the deser_lane_cfg section of the JSON file, which are as follows:

  • highBoost:19
  • configOption1:78185384

Table 3. Insertion Loss Range Supported for SERDES Rate < 16 Gbps with Default Configuration
SERDES Lane Rate (Gbps) Min Return Loss (dB) Max Insertion Loss (dB)
8 3 15
12 4 17
14 4.5 17

In addition to the tuning options in Table 3 for low insertion loss channels at < 16 Gbps, tuning on the SERDES transmitter side can still be run. For example on the FPGA, to optimize the swing and preemphasis settings to achieve the optimum eye, you can run the automated eyeSweep routine to get the eyeHeight and eyeWidth data using the API, RunVerticalEyeSweep_v2().


Sequence for Running Link Tuning For Lane Rates >16 Gbps SERDES Calibration Metrics Readback


After the SERDES links have been brought up, which means both the SERDES InitCal has run successfully and the tracking calibration is enabled, you can readback calibration parameters through the following health metric APIs:

  • SerdesInitCalStatusGet()
  • SerdesTrackingCalStatusGet()

Both API return a structure that contains various calibration metrics, which can be saved off or written to a file on your system. The parameters of interest are listed in Table 4, and the full list of metrics returned are given in the API Functions section in Table 6 and Table 7. When sending UserData over the links, the SERDES metrics can be readback. Note that when performing link tuning, it is recommended to use pseudo random binary sequence (PRBS) data. This is so you can also run the automated eyeSweep routine to get the eyeHeight and eyeWidth data using the API, RunVerticalEyeSweep_v2().

Table 4. Serdes InitCal and TrackingCal Status Metric Fields of Interest
InitCal StatusGet Fields Basic Description
InitCalStatus.temperature The temperature at which the initCal is done
InitCalStatus.spoLeft Number of phases on the left side of the opened eye. spoLeft + spoRight gives the horizontal eyeWidth
InitCalStatus.spoRight Number of phases on the right side of the opened eye. spoLeft + spoRight gives the horizontal eyeWidth.
InitCalStatus.innerUp The value (1 − b1), which should be close to phase detector value.
InitCalStatus.innerDown The value −(1 − b1)
InitCalStatus.outerUp The value (1 + b1), which corresponds to the maximum signal amplitude (if all these b values are all positive).
InitCalStatus.outerDown The value of (1 + b1)
InitCalStatus.b[7:0] The DFE computed pre and post cursor b-values. These are used in the used in the decision feedback equalizer to cancel the ISI.

SERDES Metrics Limit Checks Pseudocode


A lot of parameters are returned via the API call, but the user can use a subset to quickly tell if the eye metrics are good or not. These are the recommended limit checks you must add to determine if the link is good or requires retuning.

A visual flow of this tuning process is given in Figure 4.

Figure 4. Flowchart of Tuning Process.
  • Check 1
    if (abs(InitcalStatus.outer►
        Up) >= 63 AND abs(InitcalStatus.outer►
        Down) >= 63):
        then check_sat=1
        print('DFE is near saturation, consid►
        er lowering LPF bandwidth or incident sig►
        nal swing')
  • Check 2
    if (abs(float(calStatus.b[1])/float(calSta►
        tus.b[0])) < 0.1):
        then check_isi=1
        print ('ISI on channel is too low, consid►
        er increasing ISI by lowering CTLE LPF band►
        width')
  • Check 3
    if ((calStatus.spoLeft < 5) OR (calSta►
        tus.spoRight < 5)):
        then check_horizontal = 1
        print('Horizontal eye open►
        ing is too low, consider rais►
        ing FPGA JTx signal swing or increas►
        ing CTLE LPF bandwidth') 
  • Check 4
    If (runEyeVerticalSweepResp.eyeHeight[i] -
        runEyeVerticalSweepResp.eyeHeight[i+1]) < 20:
       Then check_eyeHeight = 1
       print(‘Vertical eye opening is too low, con►
       sider raising FPGA JTx signal swing or in►
       creasing CTLE LPF bandwidth)
  • Check 5
    If (abs(calStatus.b[3]) | abs(calSta►
        tus.b[4]) | abs(calStatus.b[5]) | abs(calSta►
        tus.b[6]))>= 8:
        check_postCursors=1
        print ('Post cursor on channel is higher 
        than expected, consider lowering CTLE LPF 
        bandwidth')
  • Summary Check
    If (check_sat OR check_isi OR check_hori►
        zontal OR check_eyeHeight OR check_postCur►
        sors) == 1:
        Then print "Tuning needed"
        Else print "No tuning needed"
        

An example of the output data is given in Figure 5. In this case, a 16 G SERDES use case is selected and a sweep done through the 4 × lpfMask settings by adjusting the insertion loss dropdown ranges. The lpfMask setting is recorded in Column C. The four checks are shown in the CALSTATUS B[3]_B[6] ≧ 8 column that are highlighted green if they pass and highlighted red if they fail. Note that Check 5 was omitted in this measurement. The logical OR of the check results, for example the summary check, is shown n the last column, which is the Need Tuning? column, where a 0 indicates a pass and a 1 indicates a fail. If only the eyeHeight and eyeWidth are considered as the pass/fail criteria, then all 4 × lpfMask settings are given a pass result. However, if the other three checks are considered, then the lpfMask settings of two and four fail on all lanes. That means for this measurement on the Analog Devices evaluation board with its associated insertion loss, an lpfMask value of 64 (super low) or 1 (low) is recommended.

Figure 5. SERDES Init CalStatus Data vs. IpfMask for a 16 G SERDES Lane Rate Use Case.

Running the Link Tuning Over Temperature


Once a stable link is achieved along with the metrics indicating the link is good and with no tuning needed, you must test this over temperature to ensure link stability across temperature. Typically, you must run the SERDES InitCal at one temperature extreme, drift to the other temperature extreme, and back to room temperature, which allows soak time at each temperature point. An example temperature profile is shown in Figure 6. Using the SerdesTrackingCalStatusGet() API, you can read the tracking calibration metrics at frequent temperature intervals, for example 10°C, and store to a file for post processing vs. the limits given in the SERDES Metrics Limit Checks Pseudocode section. Note, the same limit checks given in the SERDES Metrics Limit Checks Pseudocode section can be used.

Figure 6. Example Temperature Ramp Profile for SERDES Testing.

SERDES Init Calibration Time


In addition to the simpler tuning procedure, there are some improvements to the SERDES Init calibration time, mainly for the 24.3 G lane rate. A summary of the differences are given in Table 5.

Table 5. SERDES Init Calibration Time Summary
Use Case Calibration Time ≧ with Enhanced Tuning Feature
32 G N/A N/A
24 G 7.7 sec 12.5 sec
19 G 10.2 sec 11.5 sec
16 G 6.4 sec 12.1 sec

Arm Dump Sequence for SERDES Issue Analysis


The link tuning process is tested and shown to give stable SERDES performance over temperature at various SERDES lane rates. By running the horizontal eyeSweep initially, additional SERDES telemetry data is stored and available in the advanced reduced instruction set computer (RISC) machines (ARM) dump. However, if there are still issues and further analysis is requested by the ADRV904x support team, follow the below sequence to do the tests.

  1. Run the SERDES Init calibration and keep SERDES tracking disabled.
  2. Send PRBS pattern from the FPGA/BBIC. Enable the PRBS checker on the ADRV904x.
  3. Run a horizontal eyeSweep using RunEyeSweep_v2(), and clear any PRBS errors using DfrmPrbsCountReset().
  4. Enable SERDES tracking
  5. Run your regular test, for example over temperature, and check the PRBS error periodically. If there are errors, perform the following:
    • Run the vertical eyeSweep and horizontal eyeSweep.
    • Do an ARM dump.
  6. If there are no PRBS errors in a certain time as stated in Step 5, perform the following:
    • Run the vertical eyeSweep and horizontal eyeSweep.
    • Do an ARM dump.

It is recommended to run at least one temperature cycle for an ARM dump analysis. This is so that the temperature zone telemetry is recorded.


Python Script: SERDES Health Matrix


    '''
    Koror IronPython Programming Template Version 0.2
    Generated with:
    ACE GUI Version: 1.26.3240.1417
    ACE Plug-in Vesion: 1.2023.42300
    API Client Version: 2.9.0.4
    API Server Version: 0.0.0.0
    FPGA Version: 0.0.0.0
    ARM Version: 0.0.0.0
    StreamVersion: 2.9.0.4
    Note: ACE does not need to be disconnected from ADRV9040 command server before running this script 
    anymore.
    '''
    
    import clr
    import System
    from System import Array, Boolean, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64
    import sys
    import os
    import glob # for finding plug-in's installation path
    import json, getpass, time
    ACE_DIRECTORY = os.environ['ACEEXEDIR'] or os.getcwd()
    """ str: path to ACE
    Example: 'C:\Program Files\Analog Devices\ACE'
    """
    CLIENT_DIRECTORY = os.environ['ADRV9040LIBPATH'] or (glob.glob(r'C:\ProgramData\Analog Devices\ACE\Plu►
    gins\Board.ADRV904*')[0] + '\lib')
    """ str: path to client DLLs included in ADRV9040 Board plugin's installation
    Example: 'C:\ProgramData\Analog Devices\ACE\Plugins\Board.ADRV9040.1.2021.24400\lib'
    """
    D0FAULT_IPADDRESS = '192.168.1.12'
    DEFAULT_PORT = '5000'
    # Add ACE and client to path
    sys.path.append(ACE_DIRECTORY)
    sys.path.append(CLIENT_DIRECTORY)
    # Verify ADRV9040 plugin version used
    print 'Loading client DLLs from', CLIENT_DIRECTORY
    clr.AddReference("AnalogDevices.EvalClient")
    clr.AddReference("AnalogDevices.EvalClient.Installers")
    clr.AddReference("AnalogDevices.EvalClient.Adrvgen6.Ad9528.Device")
    clr.AddReference("AnalogDevices.EvalClient.Adrvgen6.Board")
    clr.AddReference("AnalogDevices.EvalClient.Adrvgen6.Device")
    clr.AddReference("AnalogDevices.EvalClient.Adrvgen6.Fpga.Device")
    clr.AddReference("AnalogDevices.EvalClient.Adrvgen6.Platform")
    clr.AddReference("Adrv904x.Configurator")
    clr.AddReference("StreamGen")
    from AnalogDevices.EvalClient import *
    from AnalogDevices.EvalClient.Installers import *
    from AnalogDevices.EvalClient.Adrvgen6.Board import *
    from AnalogDevices.EvalClient.Adrvgen6.Board.BaseClasses import *
    from AnalogDevices.EvalClient.AD9528 import *
    from AnalogDevices.EvalClient.Adrvgen6.Ad9528.Device import *
    from AnalogDevices.EvalClient.Adrvgen6.Fpga.Device import *
    from AnalogDevices.EvalClient.FPGAGEN6 import *
    from AnalogDevices.EvalClient.Device import *
    from AnalogDevices.EvalClient.AdiCommon import *
    from AnalogDevices.EvalClient.Adrvgen6 import *
    from AnalogDevices.Adrvgen6.Platform import *
    from AnalogDevices.Adrv904x.Configurator import *
    from Adi.Design import *
    def connect(ipAddress="", portNumber=""):
    """
    Connect IronPython to the command server.
    If TCP connection exceptions are seen, please verify that 
    ACE is disconnected from the ADRV9040 command server (see header).
    
    """
    ipAddress = ipAddress or DEFAULT_IPADDRESS
    portNumber = portNumber or DEFAULT_PORT
    print 'Attempting to connect to {}:{}...'.format(ipAddress, portNumber)
    EvalClientManager.Instance.Initialize(CLIENT_DIRECTORY)
    transport = Transports.CreateDefaultTcpTransport(ipAddress + ':' + portNumber)
    context = ExecutionContext(transport)
    context.ErrorRetriever = ErrorRetriever()
    platform = EvalClientManager.Instance.PlatformBuilder.CreatePlatform('', context)
    platform.Timeout = 60000
    platform.OutputFilesBasePath = '.'
    configGen = ConfigGen()
    return platform
    def spiWriteByte(address, data):
    """
    >>> spiWriteByte(0xA, 0x55)
    spiWriteByte 0x000A 0x55
    """
    data = Array[Byte]([data])
    numBytes = len(data)
    adrv904x.hal.RegistersByteWrite(None, address, data, numBytes)
    print 'spiWriteByte\t0x{:04X}\t0x{:02X}'.format(address, int(data[0]))
    def spiReadByte(address, numBytes=1):
    """
    >>> spiWriteByte(0xA, 0x55);
    >>> spiReadByte(0xA)
    spiReadByte 0x000A 0x55
    85
    """
    data = Array[Byte]([0])
    adrv904x.hal.RegistersByteRead(None, address, data, None, numBytes)
    data = int(data[0]) # get first element of returned data array
    print 'spiReadByte\t0x{:04X}\t0x{:02X}'.format(address, data)
    return data
    def csvFileCreate(InitCal_outputFile, TrackingCal_outputFile):
    titles_init = ["laneNumber", "lpfMask", "needTuning", "eyeWidth", "eyeHeight", "isTrackingCal",
    "calStatus.temperature",
    "calStatus.lpfIndex", "calStatus.ctleIndex", "calStatus.numEyes", "calStatus.bsum",
    "calStatus.bsum_dc",
    "calStatus.spoLeft", "calStatus.spoRight", "calStatus.eom", "calStatus.eomMax", "calStatus.pd",
    "calStatus.innerUp", "calStatus.innerDown", "calStatus.outerUp", "calStatus.outerDown"]
    for _i in range(8):
    titles_init.append("calStatus.b[{0}]".format(_i))
    with open(InitCal_outputFile, 'w') as f:
    f.write(','.join(titles_init) + '\n')
    titles_tracking = ["laneNumber", "lpfMask", "isTrackingCal", "calStatus.temperature", "calSta►
    tus.pd[0]",
    "calStatus.pd[1]",
    "calStatus.dllPeakPd[0]", "calStatus.dllPeakPdDelta[0]", "calStatus.dllPeakPdDelta[1]",
    "calStatus.innerUp", "calStatus.innerDown", "calStatus.outerUp", "calStatus.outerDown",
    "calStatus.ps[0]",
    "calStatus.ps[1]"]
    
for _i in range(8):
    titles_tracking.append("calStatus.b[{0}]".format(_i))
    for _i in range(16):
    titles_tracking.append("calStatus.yVector[{0}]".format(_i))
    with open(TrackingCal_outputFile, 'w') as f:
    f.write(','.join(titles_tracking) + '\n')
    def RunVerticalEyeSweep(lane, printLog=0):
    print('**** Running vertical Eye Sweep V2 for Lane{0} ****'.format(lane))
    runEyeVerticalSweep = adi_adrvgen6_CpuCmd_RunVertEyeSweep_t()
    runEyeVerticalSweepResp = adi_adrvgen6_CpuCmd_RunVertEyeSweepResp_t()
    runEyeVerticalSweep.lane = System.Enum.GetValues(adi_adrvgen6_CpuCmd_DeserializerLane_e)[lane]
    adrv904x.dataInterface.RunVerticalEyeSweep_v2(runEyeVerticalSweep, runEyeVerticalSweepResp)
    spo_p_list = runEyeVerticalSweepResp.eyeHeightsAtSpo[0:64:2]
    spo_n_list = runEyeVerticalSweepResp.eyeHeightsAtSpo[1:64:2]
    eyeHeightMax = 0
    eyeWidth = 0
    for spo_idx, spo_p, spo_n in zip(range(-15, 17), spo_p_list, spo_n_list):
    eyeHeight = spo_p - spo_n
    if (abs(spo_p) != 0x7F) | (abs(spo_n) != 0x7F):
    if eyeHeight > eyeHeightMax:
    eyeHeightMax = eyeHeight
    if eyeHeight > 0:
    eyeWidth += 1
    if printLog:
    print('{0}\t{1}\t{2}'.format(spo_idx, spo_p, spo_n))
    if printLog:
    print('\n### Printing Eye Diagram ###')
    eye_graph = [' # ']*33
    for y in range(127, -128, -1):
    if y >= 0:
    spo_list = spo_p_list
    else:
    spo_list = spo_n_list
    for idx, val in enumerate(spo_list):
    if abs(val) == 0x7F:
    eye_graph[idx+1] = ' # '
    elif abs(y) <= abs(val):
    eye_graph[idx+1] = ' '
    else:
    eye_graph[idx+1] = ' # '
    eye_graph[0] = '{: >4} |'.format(y)
    print('\t'.join(eye_graph))
    print('---- {0}'.format(''.join(['----']*32)))
    print('spo \t{}\n'.format('\t'.join(['{:^3}'.format(x) for x in range(-15,17)])))
    return eyeWidth, eyeHeightMax
    #
    def InitCalStatusGet(calget, channel, printLog=0):
    CALGET = {
    0x0001: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_RX_DC_OFFSET,
    0x0002: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_ADC_RX,
    0x0004: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_ADC_ORX,
    0x0008: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_ADC_TXLB,
    0x0010: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXDAC,
    
    0x0020: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXBBF,
    0x0040: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXLB_FILTER,
    0x0080: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXLB_PATH_DLY,
    0x0100: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TX_ATTEN_CAL,
    0x0200: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_HRM,
    0x0400: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXQEC,
    0x0800: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_TXLOL,
    0x1000: adi_adrvgen6_InitCalibrations_e.ADI_ADRVGEN6_IC_SERDES
    }
    CHANNEL = {
    0x00: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CHOFF, # /*!< No channels are enabled */
    0x01: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH0, # /*!< Channel 0 enabled */
    0x02: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH1, # /*!< Channel 1 enabled */
    0x04: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH2, # /*!< Channel 2 enabled */
    0x08: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH3, # /*!< Channel 3 enabled */
    0x10: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH4, # /*!< Channel 4 enabled */
    0x20: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH5, # /*!< Channel 5 enabled */
    0x40: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH6, # /*!< Channel 6 enabled */
    0x80: adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH7 # /*!< Channel 7 enabled */
    }
    calStatus = adi_adrvgen6_CalStatus_t()
    adrv904x.cals.InitCalStatusGet(CALGET.get(calget), CHANNEL.get(channel), calStatus)
    if (printLog):
    print('### InitCalStatusGet() ###')
    print("InitCal percentComplete = %d" % calStatus.percentComplete)
    print("InitCal performanceMetric = %d" % calStatus.performanceMetric)
    print("InitCal iterCount = %d" % calStatus.iterCount)
    print("InitCal updateCount = %d" % calStatus.updateCount)
    print("InitCal errorCode = %d\n" % calStatus.errorCode)
    def cals_TrackingCalStatusGet(calId, channel, printLog):
    TrackCalID = {
    1: adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_RX_QEC_MASK ,
    2: adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_TX_LOL_MASK ,
    4: adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_TX_QEC_MASK ,
    8: adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_TX_SERDES_MASK ,
    16:adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_RX_ADC_MASK ,
    32:adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_TX_LB_ADC_MASK ,
    64:adi_adrvgen6_TrackingCalibrationMask_e.ADI_ADRVGEN6_TC_ORX_ADC_MASK
    }
    CHANNEL = {
    0x00 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CHOFF ,# /*!< No channels are enabled */
    0x01 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH0 ,# /*!< Channel 0 enabled */
    0x02 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH1 ,# /*!< Channel 1 enabled */
    0x04 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH2 ,# /*!< Channel 2 enabled */
    0x08 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH3 ,# /*!< Channel 3 enabled */
    0x10 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH4 ,# /*!< Channel 4 enabled */
    0x20 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH5 ,# /*!< Channel 5 enabled */
    0x40 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH6 ,# /*!< Channel 6 enabled */
    0x80 : adi_adrvgen6_Channels_e.ADI_ADRVGEN6_CH7 # /*!< Channel 7 enabled */
    }
    calStatus = adi_adrvgen6_CalStatus_t()
    adrv904x.cals.TrackingCalStatusGet(TrackCalID.get(calId), CHANNEL.get(channel), calStatus)
    
if (printLog):
    print('### TrackingCalStatusGet() ###')
    print("TrackingCal percentComplete = %d" % calStatus.percentComplete)
    print("TrackingCal performanceMetric = %d" % calStatus.performanceMetric)
    print("TrackingCal iterCount = %d" % calStatus.iterCount)
    print("TrackingCal updateCount = %d" % calStatus.updateCount)
    print("TrackingCal errorCode = %d\n" % calStatus.errorCode)
    def SerdesInitCalStatusGet(laneNumber, printLog=0, writeToCsvFile=0, csvFileName='', lpfMask=0, eye►
    Width=0, eyeHeight=0):
    filepath = adi_adrvgen6_GenericStrBuf_t()
    filename = "init_cal_status_log.txt"
    for _i in range(len(filename)):
    filepath.c_str[_i] = ord(filename[_i])
    msg = adi_adrvgen6_GenericStrBuf_t()
    msgName = "output"
    for _i in range(len(msgName)):
    msg.c_str[_i] = ord(msgName[_i])
    calStatus = adi_adrvgen6_SerdesInitCalStatus_t()
    adrv904x.dataInterface.SerdesInitCalStatusGet(filepath, laneNumber, msg, calStatus)
    needTuning = Serdes_health_check(calStatus) # run the health check by running limit checks on some of 
    the parameters.
    if (printLog):
    # log the return value from above API function call to the output dictionary - optional
    print('### SerdesInitCalStatusGet() ###')
    print('Serdes InitCal data for Lane{}'.format(laneNumber))
    print('calStatus.temperature = {}'.format(calStatus.temperature))
    print('calStatus.lpfIndex = {}'.format(calStatus.lpfIndex))
    print('calStatus.ctleIndex = {}'.format(calStatus.ctleIndex))
    print('calStatus.numEyes = {}'.format(calStatus.numEyes))
    print('calStatus.bsum = {}'.format(calStatus.bsum))
    print('calStatus.bsum_dc = {}'.format(calStatus.bsum_dc))
    print('calStatus.spoLeft = {}'.format(calStatus.spoLeft))
    print('calStatus.spoRight = {}'.format(calStatus.spoRight))
    print('calStatus.eom = {}'.format(calStatus.eom))
    print('calStatus.eomMax = {}'.format(calStatus.eomMax))
    print('calStatus.pd = {}'.format(calStatus.pd))
    print('calStatus.innerUp = {}'.format(calStatus.innerUp))
    print('calStatus.innerDown = {}'.format(calStatus.innerDown))
    print('calStatus.outerUp = {}'.format(calStatus.outerUp))
    print('calStatus.outerDown = {}'.format(calStatus.outerDown))
    for idx, val in enumerate(calStatus.b):
    print('HC_IC_b{} = {}'.format(idx, val))
    print('')
    if (writeToCsvFile):
    isTrackingCal = 0 # just a flag to denote if writing tracking cal status or InitCal status
    metricData = [laneNumber, lpfMask, needTuning, eyeWidth, eyeHeight, isTrackingCal, calStatus.tempera►
    ture,
    calStatus.lpfIndex, calStatus.ctleIndex, calStatus.numEyes, calStatus.bsum, calStatus.bsum_dc,
    calStatus.spoLeft, calStatus.spoRight, calStatus.eom, calStatus.eomMax, calStatus.pd,
    calStatus.innerUp, calStatus.innerDown, calStatus.outerUp, calStatus.outerDown]
    for val in calStatus.b:
    metricData.append(val)
    
    with open(csvFileName, 'a') as f:
    f.write(','.join([str(x) for x in metricData])+'\n')
    return calStatus, needTuning
    def SerdesTrackingCalStatusGet(laneNumber, printLog=0, writeToCsvFile=0, csvFileName='', lpfMask=0):
    filepath = adi_adrvgen6_GenericStrBuf_t()
    filename = "tracking_cal_status_log.txt"
    for _i in range(len(filename)):
    filepath.c_str[_i] = ord(filename[_i])
    msg = adi_adrvgen6_GenericStrBuf_t()
    msgName = "output"
    for _i in range(len(msgName)):
    msg.c_str[_i] = ord(msgName[_i])
    calStatus = adi_adrvgen6_SerdesTrackingCalStatus_t()
    adrv904x.dataInterface.SerdesTrackingCalStatusGet(filepath, laneNumber, msg, calStatus)
    if printLog:
    print('### SerdesTrackingCalStatusGet() ###')
    print('Serdes TrackingCal data for Lane{}'.format(laneNumber))
    print('calStatus.temperature = {}'.format(calStatus.temperature))
    print('calStatus.pd[0] = {}'.format(calStatus.pd[0]))
    print('calStatus.pd[1] = {}'.format(calStatus.pd[1]))
    print('calStatus.dllPeakPd[0] = {}'.format(calStatus.dllPeakPd[0]))
    print('calStatus.dllPeakPd[1] = {}'.format(calStatus.dllPeakPd[1]))
    print('calStatus.dllPeakPdDelta[0] = {}'.format(calStatus.dllPeakPdDelta[0]))
    print('calStatus.dllPeakPdDelta[1] = {}'.format(calStatus.dllPeakPdDelta[1]))
    print('calStatus.innerUp = {}'.format(calStatus.innerUp))
    print('calStatus.innerDown = {}'.format(calStatus.innerDown))
    print('calStatus.outerUp = {}'.format(calStatus.outerUp))
    print('calStatus.outerDown = {}'.format(calStatus.outerDown))
    print('calStatus.ps[0] = {}'.format(calStatus.ps[0]))
    print('calStatus.ps[1] = {}'.format(calStatus.ps[1]))
    for idx, val in enumerate(calStatus.b):
    print('HC_TC_b{} = {}'.format(idx, val))
    for idx, val in enumerate(calStatus.yVector):
    print('HC_TC_yVector{} = {}'.format(idx, val))
    print('')
    if writeToCsvFile:
    isTrackingCal = 1 # just a flag to denote if writing tracking cal status or InitCal status
    metricData = [laneNumber, lpfMask, isTrackingCal, calStatus.temperature, calStatus.pd[0], calSta►
    tus.pd[1],
    calStatus.dllPeakPd[0], calStatus.dllPeakPdDelta[0], calStatus.dllPeakPdDelta[1],
    calStatus.innerUp, calStatus.innerDown, calStatus.outerUp, calStatus.outerDown, calStatus.ps[0],
    calStatus.ps[1]]
    for val in calStatus.b:
    metricData.append(val)
    for val in calStatus.yVector:
    metricData.append(val)
    with open(csvFileName, 'a') as f:
    f.write(','.join([str(x) for x in metricData])+'\n')
    return calStatus
    
    def Serdes_health_check(calStatus):
    check_sat = check_isi = check_horizontal = check_postCursors = 0
    if (abs(calStatus.outerUp) >= 63 | abs(calStatus.outerDown) >= 63):
    check_sat = 1
    print('DFE is near saturation, consider lowering LPF bandwidth or incident signal swing')
    if (abs(float(calStatus.b[1]) / float(calStatus.b[0])) < 0.1):
    check_isi = 1
    print('ISI on channel is too low, consider increasing ISI by lowering CTLE LPF bandwidth')
    if ((calStatus.spoLeft < 5) | (calStatus.spoRight < 5)):
    check_horizontal = 1
    print('Horizontal eye opening is too low, consider raising FPGA JTx signal swing or increasing CTLE 
    LPF bandwidth')
    if (abs(calStatus.b[3]) | abs(calStatus.b[4]) | abs(calStatus.b[5]) | abs(calStatus.b[6])) >= 8:
    check_postCursors=1
    print ('Post cursor on channel is higher than expected, consider lowering CTLE LPF bandwidth')
    if (check_sat | check_isi | check_horizontal | check_postCursors) == 1:
    print('Tuning needed\n')
    else:
    print('no tuning needed\n')
    return (check_sat | check_isi | check_horizontal)
    def programBoard(filepath, rsFilePath=''):
    resourceFolder = r'C:\Users\{}\AppData\Local\Analog Devices\ACE\PluginFiles\Board.ADRV9040\Local'.for►
    mat(
    getpass.getuser())
    print('Loading profile from {}...'.format(filepath))
    initStruct = AdrvGen6ProgramSupport()
    initStruct = initStruct.ProgramSupportRead(filepath)
    syncSettings = AdrvGen6FileSyncSettings()
    syncSettings.host = DEFAULT_IPADDRESS
    syncSettings.username = 'root'
    syncSettings.password = 'analog'
    syncSettings.srcDir = resourceFolder
    syncSettings.dstDir = '/home/analog/adrv9040_server/data'
    # Set resource file generation options
    programOptions = AdrvGen6ProgramOptions()
    programOptions.GenerateCStructure = False
    programOptions.GenerateCpuProfile = True
    programOptions.GenerateStream = True
    programOptions.GenerateRadioSequencer = False
    if rsFilePath != '':
    programOptions.GenerateRadioSequencer = True
    initStruct.rsGeneratorSettings = rsFilePath
    print("Programming...please wait...")
    START_TIME = time.time()
    board.Program(initStruct, AdrvGen6ProgramActionMask.ALL_PHASES, syncSettings, programOptions)
    DURATION = time.time() - START_TIME
    print("Device Fully Programmed successfully in {:.2f}s!\n".format(DURATION))
    if __name__ == '__main__':
    # Connect IronPython to command server
    platform = connect()
    
# Get device objects
    board = platform.Boards[0]
    ad9528Dev = board.DeviceGet('ad9528', 0)
    fpgaDev = board.DeviceGet('fpga', 0)
    try:
    adrv904x = board.DeviceGet('adrvgen6', 0)
    except SystemError: # SW0.5 onwards
    adrv904x = board.Adrvgen6DeviceGet(0)
    # API documentation: System Explorer -> ADRV904X -> Open Help File 
    # Check SPI functionality by writing/reading from scratchpad SPI register
    address = 0xA
    data = 0x55
    spiWriteByte(address, data)
    spiReadByte(address)
    # Your code goes here!
    print 'Hello World!'
    # *********************************** INPUT VARIABLES 
    **************************************************
    uc_json_inputFile = r'C:\Temp\input.json'
    RS_json_inputFile = r'' # If RS in not enabled, use blank string
    SerdesInitCalStatusGet_outputFile = r'C:\temp\JESD_Enhanced_Tuning\_init.csv'
    SerdesTrackingCalStatusGet_outputFile = r'C:\temp\JESD_Enhanced_Tuning\_tracking.csv'
    printLog = 0
    writeToCsvFile = 1
    # 
    *******************************************************************************************************
    needTuning = [0]*8
    # Create the labels for the output files
    if writeToCsvFile:
    csvFileCreate(SerdesInitCalStatusGet_outputFile, SerdesTrackingCalStatusGet_outputFile)
    # Program the board and load the JSON file
    programBoard(uc_json_inputFile, RS_json_inputFile)
    time.sleep(20) # Need to wait some time before calling the APIs, otherwise it gives errors
    with open(uc_json_inputFile, 'r') as f:
    fileData = f.readlines()
    for idx, line in enumerate(fileData): # Loop to remove comments in JSON file
    if line.__contains__('//'):
    fileData[idx] = line.split('//')[0]
    json_file = json.loads(''.join(fileData))
    # Check which deframer lanes are enable
    deframerStatus = adi_adrvgen6_DeframerStatus_v2_t()
    adrv904x.dataInterface.DeframerStatusGet_v2(adi_adrvgen6_DeframerSel_e.ADI_ADRVGEN6_DEFRAMER_0, defra►
    merStatus )
    phyLaneMask = deframerStatus.phyLaneMask
    adrv904x.dataInterface.DeframerStatusGet_v2(adi_adrvgen6_DeframerSel_e.ADI_ADRVGEN6_DEFRAMER_1, defra►
    merStatus )
    phyLaneMask = phyLaneMask | deframerStatus.phyLaneMask
    print('phyLaneMask = {}'.format(hex(int(phyLaneMask))))
    deframerLanesEnable = [(phyLaneMask >> bit) & 0x1 for bit in range(8)]
    
print 'deframerLanesEnable',deframerLanesEnable
    # Check the configuration of the selected deframer lanes
    for lane, enbl in enumerate(deframerLanesEnable):
    if enbl:
    if json_file["configSettings"]["Jesd204"]["deser_lane_cfg"][lane]["highBoost"] != 1:
    print('highBoost should be set to "1" but it is "{}" in the json file'.format(json_file["configSet►
    tings"]["Jesd204"]["deser_lane_cfg"][lane]["highBoost"]))
    lpfMask = json_file["configSettings"]["Jesd204"]["deser_lane_cfg"][lane]["configOption1"]
    print 'lpfMask',lpfMask
    laneMask = 1 << lane
    (eyeWidth, eyeHeight) = RunVerticalEyeSweep(lane, printLog)
    InitCalStatusGet(0x1000, laneMask, printLog) # will see InitCal updateCount increment each time you 
    run Serdes InitCal unless you mask off a particular lane
    cals_TrackingCalStatusGet(0x8, laneMask, printLog)
    InitcalStatus, needTuning[lane] = SerdesInitCalStatusGet(lane, printLog, writeToCsvFile, SerdesInitCal►
    StatusGet_outputFile, lpfMask, eyeWidth, eyeHeight)
    TrackingcalStatus = SerdesTrackingCalStatusGet(lane, printLog, writeToCsvFile, SerdesTrackingCalStatus►
    Get_outputFile, lpfMask) # could monitor tracking metrics vs. temp and log periodically
    print('Re-tuning needed per lane: {}'.format(needTuning))
    ymore.
    '''

API Functions

A summary description of all the parameters returned by the InitCalStatus and TrackingCalStatus health metric API is provided in Table 6 and Table 7. Note that only a subset of the parameters must to be checked to determine whether further link tuning is required or not.

Table 6. SERDES InitCal Status Metric Fields
InitCal Status Get Fields Basic Description
InitCalStatus.temperature The temperature at which the initCal is done.
InitCalStatus.lpfIndex The index of the selected LPF entry in the LPF table.
InitCalStatus.ctleIndex The index of the selected CTLE entry in the CTLE table.
InitCalStatus.numEyes Number of CTLE entries that can open the eye.
InitCalStatus.bsum Bsum reflects the peak value of the signal.
InitCalStatus.bsum_dc Bsum_dc is the DC value of the signal.
InitCalStatus.spoLeft Number of phases on the left side of the opened eye. spoLeft + spoRight gives the horizontal eyeWidth.
InitCalStatus.spoRight Number of phases on the right side of the opened eye. spoLeft + spoRight gives the horizontal eyeWidth.
InitCalStatus.eom Eye opening metric, which is representative of the area of the eye.
InitCalStatus.eomMax The maximum value of eye opening metrics.
InitCalStatus.pd Phase detector value at the lock point.
InitCalStatus.innerUp The value (1 − b1), which must be close to phase detector value.
InitCalStatus.innerDown The value −(1 − b1)
InitCalStatus.outerUp The value (1 + b1), which corresponds to the maximum signal amplitude (if all b values are positive).
InitCalStatus.outerDown The value of (1 + b1).
InitCalStatus.b[7:0] The DFE computed pre post cursor b values. These are used in the decision feedback equalizer to cancel the ISI.
 Table 7. SERDES TrackingCal Status Metric Fields
TrackingCal Status Get Fields Basic Description
TrackingCalStatus.temperature The temperature at which last tracking cal was run
TrackingCalStatus.pd[0] Phase detector value for a lane Slice 0
TrackingCalStatus.pd[1] Phase detector value for a lane Slice 1
TrackingCalStatus.dllPeakPd[0] Peak value of delay locked loop (DLL) curve of Slice 0 of a lane
TrackingCalStatus.dllPeakPd[1] Peak value of DLL curve of Slice 1 of a lane
TrackingCalStatus.dllPeakPdDelta[0] The delta between DLL peak value and the DLL phase detector value at lock point for Slice 0 of a lane
TrackingCalStatus.dllPeakPdDelta[1] The delta between DLL peak value and the DLL phase detector value at lock point for Slice 1 of a lane
TrackingCalStatus.innerUp See initCalStatus in Table 6
TrackingCalStatus.innerDown See initCalStatus in Table 6
TrackingCalStatus.outerUp See initCalStatus in Table 6
TrackingCalStatus.outerDown See initCalStatus in Table 6
TrackingCalStatus.ps[0] Phase offset of first sampling point for nonuniform sampling
TrackingCalStatus.ps[1] Phase offset of second sampling point for nonuniform sampling
TrackingCalStatus.b[7:0] The DFE computed pre post cursor b values. These are used in the decision feedback equalizer to cancel the ISI.
TrackingCalStatus.yVector[15:0] The b values corresponding to the two sampling points for nonuniform sampling