非同期式回路と FPGA 〜「作ってみる編」(3)

みなさん、こんにちは。このコースでは、ある簡単な例題を非同期式回路として FPGA 上に実装し、ツールを適切に使うことで、正しく動作するものを作る、ということ目指しています。前回は、非同期式回路の実行制御のかなめとなるハンドシェイクについて少し詳しく紹介しました。

今回は、いよいよ例題回路を非同期式回路として設計していきたいと思います。

例題回路

まずは、例題として取り上げる回路を説明します。例題なので、簡単で完全なもの、ということで、4ビットの整数 x の2乗 x2 を8ビット整数として出力する回路を考えることとします。ただし、入力側のデータ供給スピードと出力側のデータ消費スピードが異なる場合も正しく動作するような制御を行い、また、入力レジスタと出力レジスタを備え、それらがパイプライン動作をするものを考えることにします。

単項演算の方が、重要なポイントを簡潔に説明できると考え、関数の内容的にはあまり意味がありませんが、x2 を求める回路としました。回路の概要は下図のようになります。

同期式回路による設計

まず、比較のために、この例題回路を一般的な設計方法である、同期式回路として設計してみます。制御方式としては、入力側を AXI スレーブ、出力側を AXI マスタの簡易版とします。具体的には下図のような信号線を考えます。

基本的に、valid はデータが有効であることを表し、ready はデータを取り込むことが可能であることを表します。ですので、入力側 (スレーブ動作) は、ready_o を1として待っており、valid_i が1となったらデータを入力レジスタに取り込みます。valid_iready_o の両方が1であるクロックタイミングでデータの取り込みが行われることに注意してください。出力レジスタが空いている、あるいは、埋まっていても ready_iが1のため次のクロックで出力レジスタが空になる場合は ready_o は1のままとします。そうでない場合は ready_oを0として次のクロックでの入力データ受け取りを一時停止します。

一方、出力側 (マスタ動作) は、ready_i が1であるなら演算結果を出力レジスタに書き込み、valid_o を1とするとともに、そのデータが取り込まれたら (valid_oready_i の両方が1となったら) valid_o を0に戻す、という動作を行います。出力データの取り込みが遅い場合の動作例を図示すると次のようになります。

この同期式回路のトップレベル Verilog 記述は、例えば以下のようになります。

module main(data_i, data_o,
            valid_i, ready_o, 
            valid_o, ready_i, 
            clk, rstb);
   input [3:0]  data_i;
   output [7:0] data_o;
   input        valid_i;
   output       ready_o;
   output       valid_o;
   input        ready_i;
   input        clk;
   input        rstb;

   reg [3:0] ireg;
   reg [7:0] oreg;

   controller U_cont(.valid_i(valid_i), .ready_o(ready_o), 
                     .valid_o(valid_o), .ready_i(ready_i), 
                     .ireg_en(ireg_en), .oreg_en(oreg_en), 
                     .clk(clk), .rstb(rstb));

   always @(posedge clk) begin
      if (rstb == 1'b0) begin
         ireg <= 0;
         oreg <= 0;
      end
      else begin
         if (ireg_en == 1'b1) ireg <= data_i;
         if (oreg_en == 1'b1) oreg <= ireg * ireg;
      end
   end

   assign data_o = oreg;
endmodule // main

制御回路 (module controller) は、入出力レジスタが埋まっているかどうかを判定するために、それぞれに対するステートマシンを設けることで、入出力レジスタに対するイネーブル信号 (ireg_en, oreg_en) および制御出力 (ready_o, valid_o) を以下のように求めることで実現できます。

module controller(valid_i, ready_o, 
                  valid_o, ready_i, 
                  ireg_en, oreg_en, 
                  clk, rstb);
   input valid_i;
   output ready_o;
   output valid_o;
   input  ready_i;
   output ireg_en;
   output oreg_en;
   input  clk;
   input  rstb;

   reg ireg_full;
   reg oreg_full;
   wire oreg_writable;
   wire ireg_clk;
   wire oreg_clk;
   wire valid_o;

   // 入力レジスタのステートマシン
   always @(posedge clk) begin
      if (rstb == 1'b0) begin
         ireg_full <= 1'b0;
      end
      else begin
         if (ireg_full == 1'b0) begin
            if (valid_i == 1'b1) 
              ireg_full <= 1'b1;
         end
         else begin
            if (oreg_writable && ~valid_i) 
              ireg_full <= 1'b0;
         end
      end
   end

   // 出力レジスタのステートマシン
   always @(posedge clk) begin
      if (rstb == 1'b0) begin
         oreg_full <= 1'b0;
      end
      else begin
         if (oreg_full == 1'b0) begin
            if (ireg_full == 1'b1) 
              oreg_full <= 1'b1;
         end
         else begin
            if (ready_i && ~ireg_full) 
              oreg_full <= 1'b0;
         end
      end
   end

   // 出力レジスタにデータを書き込める                                                                            
   assign oreg_writable = ~oreg_full || ready_i;

   // 入力レジスタにデータを書き込める                                                                            
   assign ireg_writable = ~ireg_full || oreg_writable;

   // 入力レジスタのイネーブル信号                                                                                
   assign ireg_en = valid_i && ireg_writable;

   // 出力レジスタのイネーブル信号                                                                                
   assign oreg_en = ireg_full && oreg_writable;

   assign ready_o = ireg_writable;
   assign valid_o = oreg_full;

endmodule // controller

同期式回路の RTL シミュレーション

上に示した動作例の信号波形は、次のようなテストベンチと組み合わせ、RTL シミュレーション (Vivadoではビヘイビアシミュレーションと呼んでいます) した結果となります。

`timescale 1ns/1ps
module testmain;

   reg [3:0] data_i;
   wire [7:0] data_o;
   reg valid_i;
   reg ready_i;
   reg clk;
   reg rstb;
   wire ready_o;
   wire valid_o;
   
   main U_main(.data_i(data_i), .data_o(data_o),
               .valid_i(valid_i), .ready_o(ready_o), 
               .valid_o(valid_o), .ready_i(ready_i), 
               .clk(clk), .rstb(rstb));

   initial begin
      clk = 1;
      forever #50 clk = ~clk;
   end

   initial begin
      #0 rstb = 0;
      valid_i = 0;
      ready_i = 1;
      data_i = 0;

      #200 rstb = 1;
      #110 valid_i = 1;
           data_i = 3;
      #100 data_i = 2;
      #100 data_i = 5;
      #100 data_i = 6;
           ready_i = 0;
      #300 ready_i = 1;
      #100 data_i = 7;
      #100 valid_i = 0;
      #400 $finish;
   end

   initial begin
      $dumpfile("wave.vcd");
      $dumpvars(0, U_main);
   end

endmodule

なお、このシミュレーションは macOS 上で

iverilog test_sync_main.v sync_main.v sync_cont.v
vvp a.out

により行い、gtkwave にて波形出力しています。

非同期式回路による設計

非同期式回路による方式では、前回検討したハンドシェイクコントローラ2つを、それぞれのレジスタの部分に置き、

  • 前段のコントローラの oreq を、演算回路に相当する遅延素子を介して、後段のコントローラの ireq に接続
  • 後段のコントローラの oack を、直接前段のコントローラの iack に接続

することで目的の回路を実現します。すなわち、下図のようになります。赤点線で囲った部分が、前回のハンドシェイクコントローラです。

こちらについても、Verilog で記述し、RTL シミュレーションしてみます。まず、ハンドシェイクコントローラ単体を記述します。

module hs_cont(ireq, oack,
               oreq, iack,
               clk, rstb);
   input  ireq;
   output oack;
   output oreq;
   input  iack;
   output clk;
   input  rstb;

   wire G1_out;
   wire G2_out;

   assign #5 G1_out = (ireq & G1_out) | (ireq & ~G2_out & ~iack);
   assign #5 G2_out = (G2_out & ~iack & rstb) | G1_out;
   assign oack = G1_out;
   assign clk = G1_out;
   assign oreq = G2_out;

endmodule // hs_cont

非同期式回路では、ゲートにはある程度の遅延があるものとして設計されます。そうでないと、組み合わせループの部分が想定したように動作しません。実際の遅延は、回路素子の物理的特性により決まりますが、RTLシミュレーションでは正確な値がわかりませんので、ゲート遅延を仮に 5ns として入れておきます (14, 15行目)。厳密なシミュレーションは、レイアウト後にゲートおよび配線の遅延をバックアノテートして行う必要があります。

次に、このコントローラを二つつないで、制御回路全体を作ります。ここでも、遅延素子の遅延として、10ns という仮の値を使っています (22行目)。この遅延素子は、実際にはバッファやインバータなどを何段か直列に接続して構成します。また、その遅延値は、データパスの遅延に合わせる必要があります。これらの構成方法や、遅延値の検証・調整については次回以降に詳しく説明したいと思います。

module controller(ireq, oack,
                  oreq, iack,
                  ireg_clk, oreg_clk,
                  rstb);
   input  ireq;
   output oack;
   output oreq;
   input  iack;
   output ireg_clk;
   output oreg_clk;
   input  rstb;

   wire   in_req1, in_req2;
   wire   in_ack;

   hs_cont U_ireg_cont(.ireq(ireq), .oack(oack),
                       .oreq(in_req1), .iack(in_ack),
                       .clk(ireg_clk), .rstb(rstb));
   hs_cont U_oreg_cont(.ireq(in_req2), .oack(in_ack),
                       .oreq(oreq), .iack(iack),
                       .clk(oreg_clk), .rstb(rstb));
   assign #10 in_req2 = in_req1;

endmodule // controller

最後に、データパスを含む全体の記述です。

module main(data_i, data_o,
            ireq, oack,
            oreq, iack,
            rstb);
   input [3:0]  data_i;
   output [7:0] data_o;
   input        ireq;
   output       oack;
   output       oreq;
   input        iack;
   input        rstb;

   reg [3:0] ireg;
   reg [7:0] oreg;

   wire      ireg_clk;
   wire      oreg_clk;

   controller U_cont(.ireq(ireq), .oack(oack),
                     .oreq(oreq), .iack(iack),
                     .ireg_clk(ireg_clk), .oreg_clk(oreg_clk),
                     .rstb(rstb));

   always @(posedge ireg_clk, negedge rstb) begin
      if (rstb == 1'b0)
        ireg <= 0;
      else
        ireg <= data_i;
   end
   always @(posedge oreg_clk, negedge rstb) begin
      if (rstb == 1'b0)
        oreg <= 0;
      else
        oreg <= ireg * ireg;
   end
   assign data_o = oreg;
endmodule // main

これは、同期式版とほとんど同じです。異なるのは、制御部分から各レジスタへ異なるクロック信号 (ireg_clk, oreg_clk) を供給している点です。それぞれのレジスタに異なるクロックが与えられていることから、これらをローカルクロックと呼ぶことがあります。

これに対して、同期式回路では、回路全体で単一のクロック信号を使っているので、グローバルクロックと呼ばれます。グローバルクロックは、各素子のクロック入力に同時に到達することを前提にしていますので、そのタイミングに差が生じる (クロックスキューと言います) と回路は正しく動作しません (ただし、クロックスキューを積極的に利用する設計方法もあります)。ですので、第一回で少し述べたように、クロックスキューが発生しないような実装上の工夫が必要になります。

ローカルクロックでは、そもそもクロックスキューの概念はありませんが、レジスタのセットアップ・ホールドの時間制約は満たす必要があります。これは、ハンドシェイクコントローラ間の遅延素子を調整することにより実現します。

非同期式回路の RTL シミュレーション

同期式版と同じような状況をシミュレーションするために次のようなテストベンチを用います。

`timescale 1ns/1ps
module testmain;

   reg [3:0] data_i;
   wire [7:0] data_o;
   reg ireq;
   reg iack;
   reg rstb;
   wire oreq;
   wire oack;
   
   main U_main(.data_i(data_i), .data_o(data_o),
               .ireq(ireq), .oack(oack),
               .oreq(oreq), .iack(iack),
               .rstb(rstb));

   always @(posedge oack)
     #5 ireq <= 0;
   
   always @(negedge oreq)
     #5 iack <= 0;

   initial begin
      #0 rstb = 0;
      ireq = 0;
      iack = 0;
      data_i = 0;

      #50 rstb = 1;
      #50 data_i = 3;
      ireq = 1;
      #50 iack = 1;
      #50 data_i = 2;
      ireq = 1;
      #50 iack = 1;
      #50 data_i = 5;
      ireq = 1;
      #100 data_i = 6;
      ireq = 1;
      #100 data_i = 7;
      ireq = 1;
      #50 iack = 1;
      #100 iack = 1;
      #100 iack = 1;
      #50 $finish;
   end

   initial begin
      $dumpfile("wave.vcd");
      $dumpvars(0, U_main);
   end

endmodule

これにより次のようなシミュレーション結果が得られます。

設計スタイルの違い

これら二つの設計スタイルは大きく異なります。非同期式回路では、単体のコントローラを、「あるイベントが起こったら、次にどのイベントを発生させるか」という点に着目して回路設計し、それらを単純に接続していくことで複雑な回路を形作っていきます。第二回のコントローラ設計はやや難しく感じられた方もいるかもしれませんが、単体のコントローラができてしまえば、それをブロックのように組み合わせるだけで複雑な回路が構成できる点は、わかりやすいのではないでしょうか。

一方、同期式回路の制御回路では、最終的な回路の全体の信号を見て、各段の制御を行うため、段数が多くなってくると設計が複雑になるかもしれません。ただし、逆に言えばこれが同期式回路の動作効率の高さに繋がっています。ずっと後段の動作も見通して、各段の制御が適切に行えるからです。

それに対して、(ブロックのように組合わせて構成した) 非同期式回路では、前後の段の制御信号を見るだけで自分の動作を決めるので、ずっと後段で起こった動作が前の方に伝わってくるには少し時間がかかります。これが、特に (データが各段に詰まって) 連続的な動作を行なっている場合の性能オーバヘッドに繋がります。

まとめ

今回は、例題回路を決め、それを同期式回路と非同期式回路として実現しました。また、それらの RTL シミュレーションを行い、動作のようすを紹介しました。

ここで示した同期式回路版はそのまま合成可能で、FPGA に問題なく実装できます。一方、非同期式回路版は、FPGA 実装する上でいろいろと注意することがあります。まずは、遅延素子をどのように FPGA 上で実装するか、そして、それを所望の遅延値に調整するにはどうするかについて、次回で具体的に説明していきたいと思っています。

国立情報学研究所 米田友洋

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