FPGA の高速シリアルトランシーバ (4)

早いもので、この連載もいよいよ後半です。前回はちょっとディープな内容になってしまいましたが、今回はいよいよ Xilinx の Aurora 64B/66B コアと接続して通信する回路を作ってみます。前回あたりの記事は流し読み程度の理解でも大丈夫なように書いていきたいと思いますので、お付き合いいただければ幸いです。

まずは Aurora 64B/66B の仕様から

Aurora 64B/66B に関する公式のドキュメントは Xilinx のウェブサイトの IP コア関係資料の、Aurora 64B/66B のページにまとめられています。データシートやアプリケーションノートなど、さまざまなドキュメントがありますが、今回読み解いていくのはプロトコル仕様書である SP011 – Aurora 64B/66B Protocol Specification (v1.3) です。全部で60ページに満たない資料ですが、読み解くのはそれなりに大変なので、まず最初にポイントを押さえて説明していきたいと思います。

データのスクランブル処理

まず、連載の第1回でご紹介したように、高速シリアルリンクではデータをスクランブルしてビットの遷移が定常的に起きるようにすることでクロック復元を可能にしています。Aurora 64B/66B はその名前の通り、64B/66B コーディングでスクランブルを行いますが、Xilinx の高速シリアルトランシーバはこのスクランブル・デスクランブルのための専用回路を持っていません。これについてプロトコル仕様書を読んでいくと、スクランブル処理について section 5.3 に、

という式が “IEEE 802.3ae と同じだよ” というコメントと一緒に、しれっと出てきます。しかし、この式、いったいどういうことでしょうか。この記号の使い方はシリアル伝送界隈ではよく出てくるのですが、ちょっと独特で、64ビット単位ではなくシリアルなビット列について考えたときに、

いまのスクランブル出力 = いまの入力ビット(1) ⊕
39ビット前のスクランブル出力 (x39) ⊕
58ビット前のスクランブル出力 (x58)

といった具合の XOR 演算です。つまり、58ビット前までのスクランブル出力の結果を覚えておけばよい、ということなので、64ビット幅なら前のワードのスクランブル結果がわかっていれば OK です。したがって、HDL ではスクランブル結果を格納しておくレジスタを用意して、以下のように書くことができます。

module teng_sc // TEN-Gigabit ethernet scrambler
  ( input wire CLK, RST,
    input wire [65:128] D,
    output wire [ 0:63] S );

   wire [1:128]          Sc;     // S combinational
   reg [0:63]            Sr = 0; // S register

   assign Sc[ 1: 64] = S[ 0:63];
   assign Sc[65:128] = D[65:128] ^ Sc[26:89] ^ Sc[7:70];

   always @ (posedge CLK) begin
      if (RST) Sr <= 0;
      else     Sr <= Sc[65:128];
   end

   assign S = Sr;
endmodule

入力 D のビット番号を中間の信号 S に合わせているため、ちょっと直感的でないかもしれませんが、レジスタ 1 つとあとは XOR 演算だけなので、わりと簡単です。また、XOR 演算なので、デスクランブルして元に戻すのもだいたい同じ計算です。

スクランブルもデスクランブルもこのやり方ですので、当然、最初の 1 ワードは復元できませんが、高速シリアル伝送ではそもそもワード境界が確定するまでデータが正しく受信できないのは当然なので、これは特に問題ではないというか、それを前提にしたプロトコル設計が必要になるわけです。これについては、このあと Aurora 64B/66B がリンクアップするところまでの手順をご紹介するところでご理解いただけると思います。

データブロックと制御ブロック

スクランブルの方式について見てきましたので、次はデータブロックについて見ていくことにしましょう。64B/66B なので、64ビットのデータを66ビットに符号化して送受信するわけですが、スクランブルだけではビット数は増えも減りもしません。では残りの2ビットは、というと、ここは「同期ヘッダ」と呼ばれ、これに続く64ビットがデータなのか制御符号なのかを表すのに使われます。下の図のように、01 ならデータ、10 なら制御文字で、00 や 11 はエラーになります。また、前述のスクランブル処理が行われるのは、右側の 64bit の部分だけで、同期ヘッダはスクランブル処理の対象ではないことも重要なポイントです。

制御ブロックの先頭 8 ビットは BTF (Block Type Field) で、制御ブロックの種類を表しており、残りの 56 ビットは BTF の値によってそれぞれの意味を持ちます。BTF の一覧は仕様書の section 5.2.1 に、”Valid Block Type Field Values in Aurora 64B/66B” という表が掲載されていますが、このうち最低限必要なのは、

  • 0x78: Idle / Not Ready / Clock Compensation / Channel Bonding (Idle block)
  • 0x1e: Separator block

の 2 つくらいです。ここで、0x1e の Separator block は、AXI4-Stream インタフェイスの TLAST がアサートされてデータフレームが終了したことを示すブロックですので、連載第2回の記事のように Streaming interface を選択している場合には発生しません。一方、0x78 の Idle block はどんな設定の Aurora 64B/66B コアであれ、必須になるブロックです。

Idle block の構造と役割

それでは、どうやら大事そう、という Idle block についてもうちょっと詳しく見ていきましょう。これは仕様書の section 5.2.2 に解説されています。Idle block の構造は以下のようになっており、BTF の値は16進で78、BTF に続く 4bit に CC, CB, NR, SA という名前が付いています。

いずれにしても、Idle block は受信側では有効なデータではなく、捨てられてしまうのですが、これに CC, CB , NR, SA の 4 つの制御ビットが乗ってくる、というわけです。これらはそれぞれ 1 ビットで、以下のような役割を持っています:

  • CC (Clock Compensation): その名前の通りクロック補償のためのブロックで、送信側と受信側のクロック周波数の (わずかな) 差を吸収するためのものです。送信側のクロック周波数の方が高い場合、受信側ではバッファがあふれてしまいますから、定期的にこのブロックを送信し、受信側ではこれを破棄することでこれを防止します。仕様書の ssection 5.6 では、「10,000 クロックにつき3つの連続する CC ブロックを送信すること」と定められています。また、このビットが1である場合は残りの3ビットはすべて0でなければなりません。
  • CB (Channel Bonding): チャネルボンディングの同期を行うためのブロックです。今回の連載では使う機会はどうやらなさそうですが、実は Xilinx の Aurora 64B/66B コアではチャネルボンディングを行わない1レーンの Aurora 64B/66B コアでもこのビットが1になっています。これは、実装の効率化を考えてのことではないかと思います。詳しくは次回、みていきましょう。
  • NR (Not Ready): リセットの直後だったり、ケーブルが接続された直後だったり、相手からのデータを正しく受信できていない状態のときに送信するブロックです。つまり、初期化の際にこのブロックが正しく受信できればギアボックスの初期化が完了、ということでもあります。これは今回の記事の続きの部分でみていきます。
  • SA (Strict Alignment): Aurora 64B/66B のユーザ回路とのインタフェイスは64ビットです。ところで、この Idle Block が 48bit の未使用領域を持っているように、ほかの制御ブロックでもデータが入る余地となる未使用領域のあるブロックが存在します。ここにデータを入れれば実効バンド幅は高くなりますが、ユーザ回路とのインタフェイスとトランシーバとインタフェイスでデータがずれることになるので、実装上はやっかいです。Strict Alignment はこれをやらないことにして、ユーザ回路との 64bit とトランシーバとの 64bit をそのまま一致させますよ、ということを示すビットです。現在の Xilinx の Aurora 64B/66B コアでは、Strict Alignment が有効になっています。

以上の4つのビット、特に Not Ready はリンクの初期化の際に重要な役割を果たします。では次に、リンクがどのように初期化されるか、という点について仕様書を読み解いていきましょう。

リンクの初期化シーケンス

Aurora 64B/66B のコントローラが動作を開始してから、通信可能な状態になるまでの初期化シーケンスは仕様書の第4章、4.1 と 4.2 にまとめられています。実質的に Xilinx のコアしかコントローラの実装が存在しないプロトコルなので、実際のところこれだけを守っていればリンクアップする、というほど簡単でもないのですが、基本的には以下の4ステップです。 

  1. Lane initialization: トランシーバのワード境界が確定していない状態、リンクパートナーからの Not Ready ブロックが正しく受信できるようにギアボックスを調整する
  2. Channel bonding: レーン間の同期がとれていない状態、リンクパートナーからの Channel Bonding ブロックが全レーン同時に受信できるように FIFO などを調整する
  3. Wait for remote: 相手の準備が完了するのを待つ状態、Not Ready を受信しなくなるまで待機、Idle ブロックを最低16文字受信、64ブロック送信
  4. Channel ready: 通信が可能な状態。エラーを検出したり、Not ready を受信した場合は 1. の状態に戻る

この間なにを送信するか、ということはこの際置いておくことにしましょう。とりあえず、最初のステップでは Not ready の Idle ブロックを送れば間違いありません。

Aurora 64B/66B コアとトランシーバの接続

いよいよ自前の回路と Xilinx の Aurora 64B/66B コアを接続していきます。さすがにトランシーバのプリミティブから記述するのは大変なので、前回の記事と同様に Vivado 付属の Transceiver Wizard を使います。Ultrascale, Ultrascale+ のもの (GTH, GTY トランシーバ) を例として使いますが、7シリーズの FPGA (GTX トランシーバ) ではちょっと工夫が必要です。

Intel FPGA でも Transceiver PHY に ATX PLL と PHY Reset Controller を接続すればほぼ同様なのですが、Aurora 64B/66B との相互接続のシミュレーションは Vivado や Quartus に付属のシミュレータでは行えませんので、こちらは次回、できれば取り上げることにしたいと思います。

トランシーバコアを作る

基本的には前回の記事で説明した通りなのですが、今回はシミュレーションとはいえ実際に動かすので、もうちょっと細かいところまで設定が必要です。まず、設定項目から見ていきましょう。

Transceiver Wizard を開いたら、まず Basic タブでプリセットを選択します。Aurora_64B66B の文字があれば OK です。その他、ラインレート、ファレンスクロック、Encoding / Decoding (ギアボックス) と、ユーザデータ幅を設定しましょう。

続いて、Optional Features タブで、”Free-running and DRP clock frequency” を設定します。これは、トランシーバの外側にあるロジックからリセットなどの処理を行うためのクロックの周波数です。トランシーバブロックの PLL をリセットしたり、タイミングが重要な処理を行う回路なので、クロック周波数を正しく設定しておくことは重要です。今回は Aurora コアが使う 100MHz のクロックを共用するので、100に設定しておきます。

最後に、QPLL、クロック、リセットコントローラなど一式をコアに含める設定にしておきます。デフォルトでは一部が Example Design に入ることになっているので、注意が必要です。

Aurora 64B/66B コアと接続シミュレーション

では、いよいよ Xilinx のコアと接続していくことにします。前回のテストベンチを拡張する形でやっていきますが、全体としては長くなるので、要点だけを抜粋して掲載していきます。今回の記事の最後でテストベンチ全体が見られるようにしてあります。

まずは Aurora 64B/66B コアの入出力です。前回はこれをループバックで接続していましたが、今回はこれに上記の Transceiver Wizard で作ったモジュールがつながりますので、Tx と Rx のポートには差動信号 A と B を新たに接続し直します。その他は変わりません。

   aurora_64b66b_0 uut_aurora
     ( .txp(A_P), .txn(A_N),                       // O
       .rxp(B_P), .rxn(B_N),                       // I

Transceiver Wizard のインスタンス

つづいて Transceiver Wizard で生成したコアのインスタンスを宣言します。必ず接続しなければならない信号は以下の通りです (そのほか、0に固定しておかなければいけないポートもあります。)

  wire        TXUSERCLK, RXUSERCLK, TX_RDY, RX_RDY, RX_BITSLIP;
  wire [63:0] RXS, TXS, RXDATA, TXDATA;
  wire [5:0]  RXHDRi;
  reg [5:0]   TXHDRi;

  gtwizard_ultrascale_0 uut_gtwiz
     ( .gthrxp_in (A_P), .gthrxn_in (A_N),         // I
       .gthtxp_out(B_P), .gthtxn_out(B_N),         // O

       .gtwiz_reset_clk_freerun_in  (CLK100),      // I
       .gtrefclk00_in               (CLK156),      // I
       .gtwiz_userclk_tx_usrclk2_out(TXUSERCLK),   // O
       .gtwiz_userclk_rx_usrclk2_out(RXUSERCLK),   // O

       .gtwiz_reset_all_in          (RESET_PB),    // I
       .gtwiz_reset_tx_done_out     (TX_RDY),      // O
       .gtwiz_reset_rx_done_out     (RX_RDY),      // O
       .gtwiz_userdata_tx_in        (TXS),         // I [63:0]
       .gtwiz_userdata_rx_out       (RXS),         // O [63:0]
       .rxgearboxslip_in            (RX_BITSLIP),  // I
       .txheader_in                 (TXHDRi),      // I [5:0]
       .rxheader_out                (RXHDRi),      // O [5:0]

簡単にポートの役割を確認していきましょう。

  • gthrxp_in, gthrxn_in: Aurora コアからの受信信号
  • gthtxp_out, gthtxn_out: Aurora コアへの出力信号
  • gtwiz_reset_clk_freerun_in: さきほど設定した、初期化などに使うクロック
  • gtrefclk00_in: トランシーバの基準クロック
  • gtwiz_userclk_{tx,rx}_usrclk2_out: 送受信データに同期するクロック
    • Tx では QPLL から PMA に供給されるクロックから生成
    • Rx では PMA のリカバリクロックから生成
  • gtwiz_reset_all_in: トランシーバの全体をリセット
  • gtwiz_reset_{tx,rx}_done_out: Tx あるいは Rx の準備完了を通知
  • gtwiz_userdata_{tx,rx}_{in,out}: 64B/66B ギアボックスのデータ部分 64 bit (スクランブルあり)
  • rxgearslip_in: 64B/66B 受信ギアボックスの bit slip 信号
  • {tx,rx}header_{in,out}: 64B/66B ギアボックスのヘッダ部分 2bit
    • ただしポートは 5 bit 幅で、LSB 側 2bit だけを仕様

といった具合です。だいたいのことは前回までで説明してきた通りですが、64B/66B の信号はユーザロジック側からはヘッダの 2bit とデータの 64bit のポートに分割されていることと、送受信のクロックが別になっているあたりがポイントです。Aurora 64B/66B コアでは送受信のクロックは共通になっていますが、つまりそれはクロックを載せ替えるための回路 (具体的には FIFO) が入っている、ということでもあります。

また、Aurora 64B/66B では PMA_RESET と RESET_PB のふたつのリセットが必要ですが、こちらは gtwiz_reset_all_in をアサートすると、内部のリセットのステートマシンが順次各コンポーネントのリセットを行います。そのほか、内部の各コンポーネントを個別にリセットする信号も用意されており、エラー回復などの際に必要に応じて利用することがでいます。

64B/66B コーディング

今回の記事の最初で解説した通り、64B/66B コーディングのためには、64bit のデータのスクランブル・デスクランブルと、追加の 2bit のヘッダが必要です。スクランブルのための teng_sc モジュールのソースコードはそこでご説明した通りですが、同様にデスクランブルのための teng_desc も用意しました。どちらもポート D が非スクランブルのデータ、S がスクランブルされたデータです。

   teng_sc   sc ( .CLK(TXUSERCLK), .RST(~TX_RDY), .D(TXDATA), .S(TXS) );
   teng_desc ds ( .CLK(RXUSERCLK), .RST(~RX_RDY), .S(RXS), .D(RXDATA) );

   reg [1:0] RXHDR;
   wire [1:0] TXHDR;
   always @ (TXUSERCLK) RXHDR <= RXHDRi[1:0];
   always @ (RXUSERCLK) TXHDRi <= TXHDR;

トランシーバとやりとりするデータはスクランブルされていなければならないので、このための信号線が TXS, RXS で、非スクランブルのデータが TXDATA, RXDATA です。また、スクランブルとデスクランブルにはそれぞれ 1 クロックサイクルが必要なので、ヘッダもレジスタに入れて、非スクランブルなデータと同期しているのは受信から1クロック遅れた RXHDR と、送信の1クロック前の TXHDR です。

受信初期化ステートマシン

これでだいたい準備が整いました、ついに Aurora 64B/66B からのデータを受信する準備です。リンクが確立するまで、リンクパートナーからは Idle block しかこないことになっているので、ヘッダの 2bit は 10, データ 64bit の上位 8bit は 8’h78 のデータが受信できれば成功です。これを検出するのが次のリストの5行目で、デスクランブラの出力と、さきほどのコードで1クロック遅らせたヘッダを使います。

   reg         RX_LOCKED;
   reg [6:0]   RXSLIP_TIMER;
   reg [3:0]   RXMATCH_CNT;

   wire        RX_IS_IDLE = (RXHDR[1:0] == 2'b10 & RXDATA[63:56]==8'h78 );

   always @ (posedge RXUSERCLK) begin
      if (~RX_RDY) begin
         RX_LOCKED <= 0;
         RXSLIP_TIMER <= 0;
         RXMATCH_CNT  <= 0;
      end else begin
         if (~RX_LOCKED) begin
            RXMATCH_CNT  <= (RX_IS_IDLE) ? RXMATCH_CNT + 1 : 0;
            RX_LOCKED    <= &RXMATCH_CNT;
            RXSLIP_TIMER <= RXSLIP_TIMER + 1;
         end
      end
   end

  assign RX_BITSLIP = &RXSLIP_TIMER & ~RX_LOCKED;

トランシーバの初期状態では 64bit (というか 66bit ですね) のワード境界は確定していないわけですが、Idle block が連続して検出できるようになれば OK です。これを数えるのが RXMATCH_CNT (4bit, リストの 3 行目) です。これがうまく受信できない間は、 RXSLIP_TIMER (7bit) というカウンタを使って、一定間隔でギアボックスのビットスリップを行います。

Idle block は RXMATCH_CNT いっぱい、つまり16回受信できれたら受信成功、ということにします。このカウンタは Idle block でないデータがきたら0に戻るので、このカウンタが飽和したら受信ワードのアライメントが成功したと考えて差し支えない、という実装です。受信ギアボックスのビットスリップはそれと関係なく7ビットカウンタで128クロックに1度出力します。どこかのタイミングで Idle block が16回受信できたら初期化完了、となります。初期化が完了すると、RX_LOCKED を1にして、ビットスリップを停止します。

シミュレーションしてみる

では、シミュレーションを走らせてみましょう。リンクが確立するまでの間は、リンクパートナーからは Idle block しか届かない、ということは、そのあいだ同期ヘッダ (RXHDR) は 2 (2’b10) で固定、さらにデータ (RXDATA) の上位 bit は 8’h78ということでした。受信が安定すれば、上のリストの9行めにしたがって RX_LOCKED が1になります。実際に、シミュレーションを走らせてみると、次の図のような波形が得られます。

RX_BITSLIP が定期的に1になったあと、RX_LOCKED が1になって RX_BITSLIP は停止します。波形をみると、そのあと RXHDR (ヘッダ) は2で安定していますが、RXDATA はずっと動いているように見えます。ちょっと拡大してみましょう。

この図から、RX_LOCKED が上がるちょっと前の時点ですでにデータは正しく受信できており、その際の受信データは 64’h7840_0000_… です。さらにその直後にはしばらく 64’h7810_0000_… が受信されていそうです。前者 (‘h7840) は Channel Bonding、そのあとの値 (‘h7810) は Strict Alignment ですので、内容はともかく Idle block が正しく受信できるようになっていることがわかります。

これで受信側のワードアライメントは確定なので、何らかのエラーが起きない限りはずっと、連続して 64bit のワードを連続して受信することができます。初期化シーケンスの最初の、Link initialization のステップがこれで完了です。

おわりに

ちょっと駆け足気味ではありますが、これでリンクパートナーである Xilinx の Aurora 64B/66B コアからくる信号を、トランシーバに接続した自前の回路で受けられるようになりました。もちろん、これだけではリンクパートナーとの間でデータの送受信はできません。次回の最終回で、残りの初期化シーケンスと、データの送受信について解説していきます。

最後になりますが、下のリンクから今回の記事のテストベンチと、Aurora 64B/66B, Transceiver Wizard で生成したコアのファイル、ダウンロード可能ですので、ぜひお試しください。

琉球大学 長名保範

タイトルとURLをコピーしました