AN-2594: ADRV904x のSERDES チューニングに関するアプリケーション・ノート

はじめに

ADRV904x デバイスは、JESD204B/JESD204C 規格に基づくSERDES 高速シリアル・インターフェースを使用して、トランシーバ・デバイスとベースバンドIC(BBIC)との間、またはフィールド・プログラマブル・ゲート・アレイ(FPGA)との間で、A/D コンバータ(ADC)とD/A コンバータ(DAC)のサンプルを転送します。ADRV904x のデシリアライザJRx 側にあるPHY ブロックは、時間連続リニア・イコライザ(CTLE)とデシジョン・フィードバック・イコライザ(DFE)を使用して、損失の大きいチャンネルを通過した後のシリアル・ビットを正しくデコードします。

様々なレーン・レートで最大限の性能を引き出すには、CTLEとDFE を慎重にキャリブレーションする必要がありますが、これはADRV904x のファームウェアによって自動的に行われます。ユーザは、シリアライザJTx の振幅とプリエンファシス設定、およびデシリアライザJRx のCTLE フィルタ設定をチューニングして、図1 に示すように、そのチャンネルの特性に応じてリンクを最適化することができます。このアプリケーション・ノートでは、ADRV904x デバイスの特性評価を行う際に推奨される、SERDES チューニングのプロセスについて説明します。

SERDES リンクのブロック図

図1. BBIC からADRV904x へのSERDES リンクの簡略ブロック図
図1. BBIC からADRV904x へのSERDES リンクの簡略ブロック図

DFE の基本

ADRV904x のSERDES レシーバは、CTLE とDFE の両方を1 個ずつ内蔵しています。CTLEはいくつかのフィルタリング段で構成されています。これらの段はチャンネル応答をイコライズするために使われ、そのほとんどは自動的に調整されますが、以降のセクションに示すようにローパス・フィルタ(LPF)段はユーザが設定できます。DFE は、隣接するサンプル点同士のシンボル間干渉(ISI)を計算し、これを除去することによって機能します。DFE の係数はADRV904x のSERDES トラッキング・キャリブレーションによって計算され、自動的に調整されます。更にDFE は、ビットが1 か0 かを判定するデータ・コンパレータのセットと、エラー情報を取得してサンプリング点を最適化するコンパレータも内蔵しています。これらのコンパレータ閾値とその他のDFE キャリブレーション情報は、以降のセクションで説明するSERDES ヘルス・メトリクス・アプリケーション・プロセッサ・インターフェース(API)によってリードバックできます。

SERDES リンク・チューニング

ADRV904x デバイスは、内部CTLE 帯域幅設定、コンパレータDC オフセット、閾値などのパラメータを調整することによって、SERDES PHY ブロックの初期キャリブレーションを実行し、CTLE およびDFE ブロックの適切な設定を決定します。これは、デバイス個々の変動を調整して、ISI のある状態で受信したアイを最適化する助けとなります。

トラッキング・キャリブレーションは、温度や電圧が変動しても性能を維持できるように、通常動作時にも実行されます。トラッキング・キャリブレーションはバックグラウンドで動作し、CTLE とDFE のビット・エラー・レート性能を維持しながら、そのパラメータを微調整します。

キャリブレーションの設定は、使用するSERDES のレーン・レートに応じ、ソフトウェアにより主にデフォルトで行われますが、例えば16G 動作と24G 動作ではデフォルトのパラメータが異なります。これらのキャリブレーション設定のデフォルト値は、アナログ・デバイセズ内部で行われる特性評価に基づいて選ばれます。しかし、配線パターン長とそれに伴う挿入損失の変動はそのプリント回路基板(PCB)固有のSERDES レーン・レイアウトによって決まるので、これらの変動に対応できるように設定を変更できる余地を残しておく必要があります。この調整をできるだけ容易にするために、ADRV904x シリーズのデバイスにはそれぞれのレーン・レートにおける挿入損失に応じて調整が必要なパラメータが1 つあります。

これをイネーブルするには、基本的なJESDの分析、制御、評価(ACE)用グラフィック・ユーザ・インターフェース(GUI)にあるJESD デフレーマ・セクションでEnable ETM (Enhanced Tuning Mode)を選択して、レーンごとに挿入損失範囲を入力します。この挿入損失のプルダウン・リストを図2 に示します。対象レーンの挿入損失範囲が2 つのセクションにまたがる場合、例えば4dB~7dBの場合は、最も近い値の範囲を先に入力します。SERDES リンク・チューニングのセクションに示すように、挿入損失範囲の他の値をテストして比較することができます。更にこの挿入損失範囲は、表1 に示すように、SERDES レーンのレートに応じてlpfMask 値に変換されます。

図2. ADRV904x GUI のJESD デフレーマの「イネーブルETM」モード
図2. ADRV904x GUI のJESD デフレーマの「イネーブルETM」モード
表1. lpfMask の設定
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

表2 は、与えられたlpfMask に対して設定される10 進値です。この設定はレーンごとに行えます。例えば、いくつかのレーンをlpfMask = Low とし、いくつかのレーンをlpfMask = Mediumとする必要があるような場合です。

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

この値はGUI/コンフィギュレータによって自動的にJSONファイルに書き込まれ、レーンごとにJSON のdeser_lane_cfg セクションにあるconfigOption1が編集されます。例えば、16Gbps時の全レーンの挿入損失範囲が0dB~5dBに設定されている場合、コンフィギュレータは8 つのレーンに"configOption1": 64 を書き込みます。highBoost パラメータもコンフィギュレータによって1 に設定する必要があります。アナログ・デバイセズによって構成が設定されている場合を除き、highBoostconfigOptionsの設定を手動で行う必要はありません。

図3. ADRV904x JSON ファイルのdeser_lane_cfg セクション
図3. ADRV904x JSON ファイルのdeser_lane_cfg セクション

レーン・レートが16Gbps 未満の場合のJSONファイル設定


SERDES レーン・レートを16Gbps 未満に設定する場合は、より単純なCTLE およびDFE キャリブレーションが使われます。この場合、SERDES 拡張チューニングは適用されず、すべてのhighboostconfigOptions の各値を0 に設定して、Enable ETM(Enhanced Tuning Mode)のチェック・マークを外したままにしておくことができます。その場合でも、SERDES 起動シーケンスの一部として、また、この設定での通常動作として、SERDES InitCal を実行し、SERDES TrackingCal をイネーブルする必要があります。

例外の1 つはチャンネルが短い場合で、この場合は、挿入損失が、表3 に示す所定のSERDES レーン・レートに対してユーザ・ガイドに示された最小値未満になります。チャンネルの挿入損失が最小値未満の場合、例えば12Gbps での挿入損失が4dB未満の場合は、以下の設定を手動でJSON ファイルに適用できます。これは、使用中のレーンの一番下のレーンに加えます。例えば、SERDINレーン2/レーン3/レーン4/レーン5/レーン6/レーン7 を使用している場合は、以下のように、JSONファイルのdeser_lane_cfg セクションにあるレーン2 の位置にこれらの値を加えます。

  • highBoost:19
  • configOption1:78185384

表3. デフォルト設定時に16Gbps 未満のSERDES レートに対してサポートされている挿入損失範囲
SERDES Lane Rate (Gbps) Min Return Loss (dB) Max Insertion Loss (dB)
8 3 15
12 4 17
14 4.5 17

16Gbps 未満で低挿入損失のチャンネルについて示した表3 のチューニング・オプションに加えて、SERDES トランスミッタ側のチューニングも実行できます。例えばFPGA で、最良のアイが得られるよう振幅およびプリエンファシスの設定を最適化するには、自動化されたeyeSweep ルーチンを実行し、RunVerticalEyeSweep_v2() APIを使ってeyeHeightおよびeyeWidthデータを得ることができます。


レーン・レートが16GBPS を超えるSERDESキャリブレーション・メトリクス・リードバック用にリンク・チューニングを実行するためのシーケンス


SERDES リンクの確立後、つまりSERDES のInitCal が問題なく実行されてトラッキング・キャリブレーションがイネーブルされた後は、以下のヘルス・メトリクスAPI を通じてキャリブレーション・パラメータをリードバックできます。

  • SerdesInitCalStatusGet()
  • SerdesTrackingCalStatusGet()

どちらのAPI も様々なキャリブレーション・メトリクスを含む構造を返し、これらはシステム上のファイルに保存したり書き込んだりすることができます。対象となるパラメータを表4 に、返されるメトリクスのすべてのリストをAPI 機能のセクションの表6 と表7 に示します。リンクを介してUserData を送信するときは、SERDES メトリクスをリードバックできます。なお、リンク・チューニングを行うときは、疑似乱数バイナリ・シーケンス(PRBS)データの使用を推奨します。これは、自動化されたeyeSweep ルーチンを実行し、API のRunVerticalEyeSweep_v2()を使ってeyeHeight とeyeWidth のデータを得ることもできるからです。

表4. 対象となるSERDES InitCal およびTrackingCal ステータス・メトリクス・フィールド
InitCal StatusGet フィールド 基本的な説明
InitCalStatus.temperature initCal を実行する温度
InitCalStatus.spoLeft アイ開口部の左側にある位相の数。spoLeft + spoRight は水平方向のeyeWidth を与えます。
InitCalStatus.spoRight アイ開口部の右側にある位相の数。spoLeft + spoRight は水平方向のeyeWidth を与えます。
InitCalStatus.innerUp 値(1 − b1)。この値はフェーズ・ディテクタの値に近くなければなりません。
InitCalStatus.innerDown 値−(1 − b1)。
InitCalStatus.outerUp 値(1 + b1)。この値は最大信号振幅に相当します(これらb の値がすべて正の場合)。
InitCalStatus.outerDown 値(1 + b1)。
InitCalStatus.b[7:0] DFE が計算したカーソル前後のb の値。これらは、ISI を除去するためにデシジョン・フィードバック・イコライザで使われます。

SERDES メトリクス・リミットによる疑似コードのチェック


数多くのパラメータがAPI を介して返されますが、ユーザはその一部を使えば、アイ・メトリクスが良好かどうかを迅速に判断できます。これらは、リンクが良好な状態か、あるいは再チューニングが必要かを判定するために追加する必要のある推奨リミット・チェックです。

このチューニング・プロセスを示すフロー図を図4 に示します。

図4. チューニング・プロセスのフローチャート
図4. チューニング・プロセスのフローチャート
  • チェック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')
  • チェック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')
  • チェック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') 
  • チェック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)
  • チェック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')
  • 判定チェック
    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"
        

出力データの例を図5 に示します。ここでは16G SERDES の使用事例が選択されており、挿入損失のドロップダウン範囲を調整することにより4 つのlpfMask 設定を通じてスイープが実行されます。lpfMask 設定はC 列に記録されます。4 つのチェックは「CALSTATUS B[3]_B[6] ≥ 8」列に表示され、合格の場合は緑、不合格の場合は赤でハイライトされます。この測定ではチェック5 は省略されています。チェック結果の論理OR(例えば判定チェック)は最後の「Need Tuning?」列に示され、0 は合格、1 は不合格を示します。eyeHeight とeyeWidth だけを合否基準として考えた場合は、4 つのlpfMask 設定のすべてが合格となります。しかし、他の3 つのチェックを考慮した場合、2 と4のlpfMask 設定はすべてのレーンで不合格となります。つまり、該当する挿入損失を使ってアナログ・デバイセズの評価用ボード上で行うこの測定では、lpfMask の値を64(非常に低い)または1(低い)とすることが推奨されます。

図5. 16G SERDES レーン・レートでの使用事例におけるSERDES 初期CalStatus データとIpfMask の関係
図5. 16G SERDES レーン・レートでの使用事例におけるSERDES 初期CalStatus データとIpfMask の関係

様々な温度でのリンク・チューニングの実行


安定したリンクが実現され、そのリンクが良好な状態でチューニングは必要ないことを示すメトリクスが得られたら、温度変化に対するこのリンクの安定性を確認するためのテストを行う必要があります。通常は、一方の温度限界値でSERDES InitCalを実行してからもう一方の温度限界値へ移行し、その後室温に戻りますが、各温度点でデバイスをその温度に馴染ませる時間を取れるようにしてください温度プロファイルの例を図6 に示します。SerdesTrackingCalStatusGet() API を使用し、細かい温度間隔(例えば10ºC)でトラッキング・キャリブレーション・メトリクスを読み取ってファイルに保存し、SERDES メトリクス・リミットによる疑似コードのチェックのセクションに示すリミットに対してポストプロセシングを行うことができます。SERDES メトリクス・リミットによる疑似コードのチェックのセクションに示したものと同じリミット・チェックを使用できます。

図6. SERDES テストのための温度ランプ・プロファイル例
図6. SERDES テストのための温度ランプ・プロファイル例

SERDES の初期キャリブレーション時間


比較的単純なチューニング手順に加え、SERDES の初期キャリブレーション時間に対する改善点がいくつかあります。これらは、主に24.3G のレーン・レートに対するものです。相違の概要については表5 を参照してください。

表5. SERDES 初期キャリブレーション時間の概要
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

SERDES 問題分析のためのARM ダンプ・シーケンス


リンク・チューニング・プロセスは、異なる温度と様々なSERDES レーン・レートで安定したSERDES 性能が得られるように、テストされた上で示されています。最初に水平方向のeyeSweep を実行することによって追加的なSERDES テレメトリ・データを保存し、高性能の縮小命令セット(RISC)コンピュータ(ARM)でダンプすることができます。しかし、まだ問題が残っていて、ADRV904x サポート・チームによる分析が更に必要なときは、以下のシーケンスに従ってテストを行ってください。

  1. SERDES の初期キャリブレーションを実行して、SERDES トラッキングをディスエーブルします。
  2. FPGA/BBICからPRBS パターンを送信します。ADRV904x のPRBS チェッカーをイネーブルします。
  3. RunEyeSweep_v2() を使って水平eyeSweep を実行し、DfrmPrbsCountReset()を使ってPRBS エラーをすべてクリアします。
  4. SERDES トラッキングをイネーブルします。
  5. 通常のテスト(例えば温度変化に対するテスト)を実行してPRBS エラーの有無を定期的にチェックし、エラーがある場合は以下を実行します。
    • 垂直eyeSweep と水平eyeSweep を実行します。
    • ARM ダンプを行います。
  6. ステップ5 に示すテストで一定時間PRBS エラーがない場合は、以下を実行します。
    • 垂直eyeSweep と水平eyeSweep を実行します。
    • ARM ダンプを行います。

ARM ダンプ分析は、少なくとも1 温度サイクル実行することを推奨します。これは、温度領域テレメトリが記録されるようにするためです。


PYTHON スクリプト:SERDES ヘルス・マトリックス


    '''
    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 機能

InitCal ステータスおよびTrackingCalStatus ヘルス・メトリクスAPI によって返されるすべてのパラメータの概要を、表6 と表7に示します。なお、それ以上のリンク・チューニングが必要かどうかを判断するためにチェックする必要があるのは、これらのパラメータの一部だけです。

表6. SERDES InitCal ステータス・メトリクス・フィールド
InitCal ステータス取得フィールド 基本的な説明
InitCalStatus.temperature initCal を実行する温度。
InitCalStatus.lpfIndex LPF テーブルで選択したLPF エントリのインデックス。
InitCalStatus.ctleIndex CTLE テーブルで選択したCTLE エントリのインデックス。
InitCalStatus.numEyes アイを開くことができるCTLE エントリの数。
InitCalStatus.bsum Bsum は信号のピーク値を反映します。
InitCalStatus.bsum_dc Bsum_dc は信号のDC 値です。
InitCalStatus.spoLeft アイ開口部の左側にある位相の数。spoLeft + spoRight は水平方向のeyeWidth を与えます。
InitCalStatus.spoRight アイ開口部の右側にある位相の数。spoLeft + spoRight は水平方向のeyeWidth を与えます。
InitCalStatus.eom アイの開口状態を示すメトリクスで、アイの面積を表します。
InitCalStatus.eomMax アイの開口状態を示すメトリクスの最大値。
InitCalStatus.pd ロック点におけるフェーズ・ディテクタの値。
InitCalStatus.innerUp 値(1 − b1)。この値はフェーズ・ディテクタの値に近くなければなりません。
InitCalStatus.innerDown 値−(1 − b1)。
InitCalStatus.outerUp 値(1 + b1)。この値は最大信号振幅に相当します(b の値がすべて正の場合)。
InitCalStatus.outerDown 値(1 + b1)。
InitCalStatus.b[7:0] DFE が計算したカーソル前後のb の値。これらは、ISI を除去するためにデシジョン・フィードバック・イコライザで使われます。
 表7. SERDES TrackingCal ステータス・メトリクス・フィールド
TrackingCal ステータス取得フィールド 基本的な説明
TrackingCalStatus.temperature 最後のトラッキング・キャリブレーションが行われた温度
TrackingCalStatus.pd[0] レーン・スライス0 のフェーズ・ディテクタ値
TrackingCalStatus.pd[1] レーン・スライス1 のフェーズ・ディテクタ値
TrackingCalStatus.dllPeakPd[0] レーンのスライス0 の遅延ロック・ループ(DLL)曲線のピーク値
TrackingCalStatus.dllPeakPd[1] レーンのスライス1 のDLL 曲線のピーク値
TrackingCalStatus.dllPeakPdDelta[0] レーンのスライス0 のロック点におけるDLL ピーク値とDLL フェーズ・ディテクタ値の差
TrackingCalStatus.dllPeakPdDelta[1] レーンのスライス1 のロック点におけるDLL ピーク値とDLL フェーズ・ディテクタ値の差
TrackingCalStatus.innerUp 表6 のinitCalStatus を参照
TrackingCalStatus.innerDown 表6 のinitCalStatus を参照
TrackingCalStatus.outerUp 表6 のinitCalStatus を参照
TrackingCalStatus.outerDown 表6 のinitCalStatus を参照
TrackingCalStatus.ps[0] 不等間隔サンプリングの最初のサンプリング点における位相オフセット
TrackingCalStatus.ps[1] 不等間隔サンプリングの2 番目のサンプリング点における位相オフセット
TrackingCalStatus.b[7:0] DFE が計算したカーソル前後のb の値。これらは、ISI を除去するためにデシジョン・フィードバック・イコライザで使われます。
TrackingCalStatus.yVector[15:0] 不等間隔サンプリングの2 つのサンプリング点に相当するb の値