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
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.
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.
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.
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
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().
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.
- 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.
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.
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.
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.
- Run the SERDES Init calibration and keep SERDES tracking disabled.
- Send PRBS pattern from the FPGA/BBIC. Enable the PRBS checker on the ADRV904x.
- Run a horizontal eyeSweep using RunEyeSweep_v2(), and clear any PRBS errors using DfrmPrbsCountReset().
- Enable SERDES tracking
- 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.
- 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.
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. |
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 |