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

この連載もついに最終回です。前回は Xilinx の Transceiver Wizard で Aurora 64B/66B から送られてくる初期化シーケンスを受信するところまで見てきました。今回は初期化シーケンスを最後まで進め、データを送受信するところまでシミュレーションしてみます。

今回はデータの送信機能は実装せず、Xilinx のコアからのデータを受信するだけにとどめますが、入出力インタフェイスやクロック周りなどをしっかり実装すれば合成可能な実装に応用することもできます。前回の記事同様、最後のところからシミュレーション用のソースコードをダウンロードできますので、ぜひお試しください。

前回のポイント

それでは前回のシミュレーション波形の一部をみてみましょう。CH_UP と LANE_UP は Xilinx の Aurora 64B/66B コアの、そのほかの信号は Transceiver Wizard に接続した制御回路のものです。

20,000,000ps (20us) をすこし過ぎたあたりで Aurora 64B/66B コアの LANE_UP が high になり、50,000,000ps (50us) を過ぎたあたりで Transceiver Wizard のビットスリップ操作も完了します。

この状態でシリアルリンクの両側ではお互いのワードアライメントが正しく確立し、64bit のワードをやりとりできるようになっていますが、まだデータのやりとりを行うための初期化シーケンスは完了していません。リンク確立までの初期化シーケンスは以下の4ステップでした。

  1. Lane initialization:リンクパートナーとのワードアライメント
  2. Channel bonding: レーン間の同期
  3. Wait for remote: リンクパートナーとの最終的な同期
  4. Channel ready: リンク確立状態

このうち 1. はすでに完了していますので、今回の記事では 2. と 3. のところを実装して、最終的にリンクが確立するところまでを解説していきたいと思います。

初期化シーケンスと基本的な実装

初期化シーケンスは上記の4ステップですが、ここでは改めて、プロトコル仕様書から、初期化中の3ステップで送信するブロックについて確認してから、各ステップの実装に進んでいくことにします。

  1. Lane initialization: Not ready (と channel bonding) を送信
  2. Channel bonding: 同じく Not ready と channel bonding を送信
  3. Wait for remote: Idle と channel bonding を送信 (最低64ブロック送信、16ブロック受信)

なお、筆者らがシミュレーションで観察した限りにおいて、Lane initialization のステップは単に Not ready を送信するだけでもよさそうです。また、双方向 (duplex) モードの Aurora 64B/66B コアはレーン数が 1 でも複数でも、いずれも channel bonding を行うマルチレーンモードの動作になっているようです。

このあたり、プロトコルが公開されているとはいえ、実質的に Xilinx の実装しかないので、ある程度は「現物合わせ」でやっていくのがよい、と思います。

サンプルコードの構成

今回もシミュレーションで動作を確認していきます。サンプルのコードをまとめた zip ファイルがダウンロードできますので、Vivado シミュレータなどでぜひお試しください。モジュールの構成は前回のものを拡張しており、下の図のようになっています。

前回から追加になったのは、リンク確立までの手順を送受信のそれぞれで行うための Tx engine と Rx engine のモジュールです。これらは連動していなければいけませんが、送受信のクロックは別々なので、最低限のステータスシグナルだけを受信側から送信側へ接続しています。送信は初期化のためのシーケンスだけにしてデータ送信の機能は実装せず、受信データの確認はシミュレーションで Rx engine 内部の信号を直接観測することで行うことにします。ダウンロードのリンクはこの記事の最後にあります。

まずは受信の Lane Initialization

ここは前回実装が完了した部分です。Not Ready Idle ブロックを連続送信し、相手からの Idle ブロックが正しく受信できるまでビットスリップ操作を行います。

画像に alt 属性が指定されていません。ファイル名: image-25.png

シミュレーションでは前述のように先に Aurora 64B/66B コア側が LANE_UP になって、どうやらこのステップを抜けて、Wait for remote のステップまで進んでしまっているようです。これは、ビットスリップ操作が終了した時点で受け取るブロックが ‘h7840 (Channel Bonding) と’h7810 (Strict Alignment) であることからも窺えます。

Strict alignment については以前の記事でも解説していますが、現在 Xilinx が出している Aurora 64B/66B コアでは基本的に、制御ブロックの空きビット部分にデータを含めることはしない実装になっており、これを strict alignment と呼びます。Strict alignment を使用する場合、Idle ブロックでは strict alignment のビットを 1 にしておきます。

さて、何はともあれワードアライメントが確立したので Lane initialiation のステップは終了です。しかし、単に Not ready だけを送っているだけではこの先に進むことができません。ここから先は、何を受信しているかでどのステップにいるかを確認しながら先に進む、つまり、受信するブロックを参照しながら送信するブロックの内容も変更していく必要があります。

ここでやっかいなのは、受信側は受信データから復元した (リンクパートナーに同期している) リカバリクロックで、送信側は送信に使っている (こちら側のFPGAの) クロックで動いている、ということです。次は、このあたりも解決しながら Channel bonding のステップを抜けていきましょう。

先に送信のところを実装する

ではいよいよ Lane initialization を抜けて先にいきましょう。Lane initialization のところでは受信のほうから話を始めましたが、なにぶんトランシーバは相手のあることなので、受信だけのことだけを考えているわけにもいきません。そこで、先に送信のところを最後まで作ってしまうことにします。ただ、基本的には相手からなにを受信しているかで初期化シーケンスの状態が遷移していきますから、そのステートを管理する仕掛けは受信側に持たせる、ということにしておきましょう。また、今回のシミュレーションでは 1 レーン構成でチャネルボンディングは行いませんが、Xilinx のコアはどうやら 1 レーンでも複数レーンでも同じ動作をするようなので、仕様書は multi lane のところを読んでいくことにします。

この場合、リンクが確立するまでの間に送信するブロックは前述の「Aurora 64B/66B の初期化シーケンス」のところで説明した通りです。リンクが確立 (channel up の状態) するまでは Channel bonding ブロックが、確立後は Clock compensation ブロックがそれぞれ必要で、Channel bonding の間は 4 ブロック以上あける、Clock compensation は最低で 10,000 ブロックごとに連続する 3 以上のブロックを、ということが仕様に定められています。そこで、まずはこの要求をクリアするためのタイマーを作ることにしましょう。

仕様に定められている数字ぴったりではありませんが、Chanel bonding (CB) は 16 ブロックに 1 回、Clock compensation (CC) は 512 クロックに 7 回送るようにします。CC は 10,000 ブロックおきだと、シミュレーションで見るのが大変、ということで間隔を縮めていますが、実際にはもっと長くて大丈夫です。

   // CB timer for Not ready - Wait for remote : Once in 16 clk
   reg [3:0]           CB_TIMER;
   wire                SEND_CB = &CB_TIMER;
   always @ (posedge CLK) CB_TIMER <= RST ? 0 : CB_TIMER+1;

   // CC timer for Channel up : 7 in 512 clk (too frequent but OK)
   reg [8:0]           CC_TIMER;
   reg                 SEND_CC;
   always @ (posedge CLK) begin
      CC_TIMER <= RST ? 0 : CC_TIMER+1;
      SEND_CC  <= (CC_TIMER==1) ? 1 : (CC_TIMER==7) ? 0 : SEND_CC; end

これで、リンク確立までは SEND_CB が 1 のときに CB、確立後は SEND_CC が 1 のときに CC を送れば OK です。あとは、受信側でもっている初期化シーケンスのステートマシンに従って送信する文字を切り換えていけば送信側の実装はおわりです。送信する 64bit ブロックを D[63:0]、受信側からやってくるステートを RXSTAT_TX[3:0] とすると、以下のように書くことができます。

   assign D = RXSTAT_TX[0] ? {  16'h7830, 48'h0} :   // Not ready: NR+SA
              RXSTAT_TX[1] ? ( SEND_CB ?             // Channel bonding:
                               {16'h7840, 48'h0} :   //   CB
                               {16'h7830, 48'h0} ) : //   NR+SA
              RXSTAT_TX[2] ? ( SEND_CB ?             // Wait for remote
                               {16'h7840, 48'h0} :   //   CB
                               {16'h7810, 48'h0} ) : //   SA
              RXSTAT_TX[3] ? ( SEND_CC ?             // Channel up
                               {16'h7880, 48'h0} :   //   CC
                               {16'h7810, 48'h0} ) : //   SA
              0;                                     // ここにはこないはず

RXSTAT_TX[0] は Not ready, [1] は Channel bonding… といった具合に、4 ビットのそれぞれが初期化シーケンスの各ステップに対応しています。いまのところデータを送る機能はないので、同期ヘッダは 2’b10 (制御ブロック) に固定して差し支えありません。それから、ほんとうは Not ready のステートでは CB も送るべきなのですが、ここでは行数を減らすために省略しています。いちおう、これでもちゃんと動きます。

リンクが確立するまで

では、次は受信側で残りのステップをみていきましょう。初期化シーケンスで使われるのはすべて Idle ブロックなので、CC, CB に加えて NR (Not ready), SA (Strict alignment) の 4 種類を識別するための信号を先に作っておきます。これさえあれば、64 bit のブロックの中身をみなくても初期化シーケンスを進めていけます。

   wire        RX_IS_IDLE = (H[1:0] == 2'b10 & D[63:56]==8'h78 );
   wire        RX_IS_CC = RX_IS_IDLE & D[55]; // Clock comp
   wire        RX_IS_CB = RX_IS_IDLE & D[54]; // Ch bond
   wire        RX_IS_NR = RX_IS_IDLE & D[53]; // Not ready
   wire        RX_IS_SA = RX_IS_IDLE & D[52]; // Strict alignment

受信のステートマシンを設計する

それでは、各ステップでの基本的な戦略について考えていきましょう。

  • Not ready:
    なんらかの Idle ブロックが連続して正しく受信できるまで定期的にビットスリップ操作をします (これは前回実装済み。) ここでの最低受信回数はとくに規定されていませんが、16 回連続して受信したら次のステップへ進むことにしましょう。
  • Channel bonding:
    CB を一定数受信したら次のステップへ進む (今回のサンプルコードでは 50 回としています。) 相手がすでに Wait for remote のステップに進んでいる場合には SA だけの Idle ブロックが送られてくるので、 NR, CC, CB 以外のブロックを受信した場合はただちに次のステップへ進みます。
  • Wait for remote:
    NR を含まない Idle ブロックを一定数受信したら次のステップへ進みます。ここでは最低 16 の Idle ブロックを受信、64 の Idle ブロックを送信、と定められています。ちょっと余裕をみて 200 の Idle ブロックを受信したら、ということにしましょう。受信している間は当然こちらから同じような Idle ブロックを送信していますので、数は大丈夫です。
  • Channel up:
    ここにたどりつけば完了です。本当は NR を受信したら最初に戻る、などが規定されているのですが、今回は考えないことにしましょう。

ある程度 RTL を書き慣れている方なら、そんなに難しいことはなさそう、と思っていただけるのではないでしょうか。実際のところ、以下のような簡単なステートマシンでできてしまいます。

   reg [6:0]   RXSLIP_TIMER;
   reg [9:0]   CNT;
   wire [3:0]  RXSTAT_NEXT = {RXSTAT[2:0], RXSTAT[3]};

   always @ (posedge CLK) begin
      if (RST) begin
         RXSTAT <= 4'b0001;
         RXSLIP_TIMER <= 0;
         CNT  <= 0;
      end else begin
         case (RXSTAT)
           'b0001: begin // Not ready
              CNT  <= (RX_IS_IDLE) ? CNT + 1 : 0;
              RXSLIP_TIMER <= RXSLIP_TIMER + 1;
              if (CNT==16) begin
                 CNT <= 0;
                 RXSTAT <= RXSTAT_NEXT;  end end

           'b0010: begin // Channel bonding: but no CB check for single-lane
              if (~(RX_IS_CC | RX_IS_CB | RX_IS_NR) | CNT == 50 ) begin
                 RXSTAT <= RXSTAT_NEXT;
                 CNT <= 0;
              end else begin
                 if (RX_IS_CB) CNT <= CNT+1; end end

           'b0100: begin // Wait for remote
              if (CNT==200) begin
                 RXSTAT <= RXSTAT_NEXT;
                 CNT <= 0;
              end else begin
                if (~RX_IS_NR & RX_IS_IDLE) CNT <= CNT+1;
                else CNT <= 0; end end

           'b1000:  // Channel up
             RXSTAT <= RXSTAT;

           default: RXSTAT <= 'b0001;
         endcase
      end
   end

   assign BITSLIP = &RXSLIP_TIMER & RXSTAT[0];

送信ステートマシンのステートである RXSTAT は、前述のように送信の制御にもそのまま使われていますので、相手から送られてくるブロックを受信側でモニタしながら状態遷移していけばそれにつれて送信するブロックも変化し、最終的にリンクが確立できる、というわけです。

シミュレーション結果

今回の記事の最初にも載っている前回のシミュレーション結果では、Aurora 64B/66B コア側の LANE_UP は 1 になっていましたが、こちらからは Not ready を送信しているだけなのでリンクは確立せず、CH_UP は 0 のままでした。

今回の記事のここまでの部分で、リンク確立までのひと通りの手順をご紹介してきました。そのシミュレーションをした結果の一部が次の波形です。

最初は RXSTAT が 1 、つまり Not ready の状態でビットスリップ操作が行われています。そのあと RXSTAT は 2 になっている、のですが、下の波形に見るように 1 クロックサイクルだけです。このステートでは、相手から SA がくるとすぐ次のステップに進んでしまいます。RXSTAT = 4 が Wait for remote で、この間には CB が定期的に届いて、最終的に RXSTAT = 8 になってリンクが確立する様子がわかります。また、こちらが Wait for remote の状態にあるうちに相手の Aurora 64B/66B 側では CH_UP が 1 になって、リンクが確立した状態になっています。

データを受信してみる

ここまででリンクが確立、つまりリンクが上がった状態なので、最後に Aurora コア 64B/66B からのデータを受信するところをみてみましょう。

実は、前回の記事の時点から、Aurora 64B/66B コアに受信 tx_gen というモジュールが入っており、データが送信可能な状態になると自動的に、64bit の符号なし整数で 1, 2, 3, … というデータを送信します。これが受信できているかどうかを、シミュレーション結果から確認してみましょう。

印刷される記事ではないので、紙面の都合、というわけではないのですが、答えから先にいってしまうと、そのままではうまくデータが見えません。これは、受信される 64bit のデータを構成する 8 バイトの順番が逆順になっているためです。そこで、今回実装した受信モジュールには、以下のようなコードが入っています。

   // Rx data
   wire        RX_IS_DATA = (H[1:0] == 2'b01) & RXSTAT[3];

   wire [63:0]        RX_DATA;
   genvar             i;
   for (i=0; i<8; i=i+1) begin : rx_data_gen
      assign RX_DATA[i*8+7 : i*8] = D[63-i*8 : 56-i*8];
   end

ここで、D はデスクランブラの出力、RX_DATA はバイトの並び順を修正したあとの受信データです。この処理は rx_data_gen ブロックに書かれているとおり、単に 0, 1, 2, …, 7のバイト順になっている 64bit のデータを 7, 6, 5, …, 0 の順番に直すだけです。また、RX_IS_DATA は単に、リンクが上がった状態で、かつ、データブロックが到着していることを示します。

データブロックと制御ブロックの識別は上のコードにでているように、2ビットの同期ヘッダの値ででくるので、簡単です。あとはこの RX_IS_DATA や RX_DATA をモジュールの出力に接続すれば、データの受信が可能になる、というわけです。ただ、受信したデータは Rx PMA のリカバリクロックに同期していますからその点は注意が必要です。

送信については今回は記事の長さの都合で割愛しましたが、いまのコードで SA Idle ブロックを送信しているところで、かわりにデータブロックを送信すればリンクパートナーで受信できます。CC は定期的に送らなければならないので、CC を送るタイミングではデータブロックは送れないという点には注意が必要です。

おわりに

5回の連載でリンクが確立してデータが届くまで、というのを目標に Aurora 64B/66B 互換コアの実装について駆け足で説明してきました。あまり長くなりすぎても、ということで、実機動作のことは書かずにシミュレーションだけに絞ったり、細かいところの説明はいろいろと省略している部分もありますが、実際に動かすための基本的なポイントは押さえていますので、実機動作まではここからわりとすぐ、だと思っていただいて大丈夫です。

Intel FPGA のトランシーバについてはあまり説明できませんでしたが、PMA や PCS の構成は基本的には同じです。64bit 幅の入出力インタフェイスでは Xilinx と Intel でビット順が逆になっているので、その点だけ注意すれば移植も容易です。

シリアルトランシーバを直接操作する場合、あまり資料がなかったり、シミュレーションしてもシリアルラインのところの信号は何が流れているかわからなかったり、ということでいろいろと敷居が高い感じもあるのですが、実際にやってみると案外簡単です。筆者らのグループで最初にシリアルトランシーバを直接操作したのは、高速 AD コンバータからの信号 (高速 AD/DA コンバータのインタフェイスには JESD204 というシリアルトランシーバを使った規格があります) を受信することでした。一度使ってみるとなんとかなるものですし、通信のコントローラとデータ処理を直結できるのは FPGA の大きなメリットです。応用範囲も広がると思いますので、ぜひチャレンジしていただきたいと思います。

今回のソースコード一式 (テストベンチ + IP コアのファイル) はこちらです:

琉球大学 長名保範

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