SystemC によるテストベンチ のバックアップソース(No.3)

更新

[[公開メモ]]

#contents

* Verilator を使って SystemC でテストベンチを書く [#lb00ad13]

SystemC はごく最近勉強し始めたので、分からないことだらけです。

忘れそうなことをここにメモろうと思います。

まだこれからです。

* Verilator を使ったテストの流れ [#u5cb90ff]

Verilator は Verilog のモジュール(ただし合成可能なものに限る)を
SystemC 互換の C++ コードに変換するためのツールです。

同じ verilog モジュールでも、テストベンチのように論理合成ができないものは
変換できないため、Verilator を使った検証では、テストベンチは SystemC 
で書かれるのが一般的なようです。

SystemC は、C++ を使って論理回路を表現するためのライブラリです。
フリーウェアとして開発されています。

SystemC で書かれたコードを通常の C++ コンパイラでコンパイルすることで、
実行可能なファイルを作成することができ、それ実行することで論理回路の
動作をシミュレートできます。

市販の検証ツールと違って検証可能な回路規模に制限などはありませんので、
メモリサイズの許す限り、大規模な回路の検証も高速に行うことができるそうです。

また、テストベンチのコード中では C++ の機能をすべて使うことができますので、
ソフトウェアとの整合性を検証したり、検証結果をレポートする方法を工夫したり、
非常に自由度が高いという利点もあります。

Verilator を使ったテストは、

+ verilog モジュールを Verilator で C++ コードに変換する
+ テストベンチコードおよび SystemC ライブラリと一緒にコンパイルする
+ コンパイル結果を実行し、検証結果を得る

という流れで行います。

* SystemC でのテストベンチ [#a7e84d2f]

テストベンチを書くには、最低限以下の操作が必要になります。

- テスト対象モジュールをインスタンス化する
- 入出力線を宣言する
- 入出力線をモジュールに結合する
- 入力線に値を設定する
- 時刻を進める
- 出力を読み取る

[[電気回路/HDL/SystemC の導入]] で紹介した、
次のテストベンチコードを例に使って、
それぞれのやり方を書いておきます。

 LANG:cpp
 #include "Vbin2bcd.h"
 int sc_main(int argc, char **argv) {
 
     Verilated::commandArgs(argc, argv);
 
     // モジュールのインスタンス化 
     Vbin2bcd top("top");
 
     // 信号ネットの定義
     sc_clock            clk ("clk", 10, SC_NS);
     sc_signal<bool>     rst, start, busy;
     sc_signal<uint32_t> bin;
     sc_signal<uint32_t> bcd;
 
     // モジュールに結線
     top.clk(clk);
     top.rst(rst);
     top.start(start);
     top.busy(busy);
     top.bin(bin);
     top.bcd(bcd);
 
     // 検証開始
     rst   = 1;
     start = 0;
 
     sc_start(100, SC_NS);
     rst = 0;
     sc_start(100, SC_NS);
 
     for(int i=0; i<1024; i++) {
         
         bin = i;
 
         start = 1;
         sc_start(10, SC_NS);
         start = 0;
         
         sc_start(10, SC_NS);
         while(busy)
             sc_start(10, SC_NS);
 
         std::ostringstream os_bin;
         std::ostringstream os_dec;
         
         os_bin << dec << i;     // 元の数値を10進数で表したもの
         os_dec << hex << bcd;   // 変換結果を16進数で表したもの
         sc_assert(os_bin.str()==os_dec.str()); // 等しいことを検証
         
         sc_start(40, SC_NS);
     }
     
     cout << "done";
 
     exit(0);
 }

** テスト対象モジュールをインスタンス化する [#oeed7be7]

SystemC において、モジュールは C++ のクラスに対応しており、
そのクラスのインスタンスを作成することが、
モジュールをインスタンス化することに相当します。

Verilator を使った場合、verilog のモジュール名の先頭に大文字の V 
がついた名前のクラスができます。上記の例では、verilog 
で bin2bcd という名前のモジュールが、SystemC では Vbin2bcd 
という名前になっています。

クラス定義が Vbin2bcd.h というヘッダファイルに宣言されるので、
テストベンチファイルの先頭部分でこれを #include しておきます。

 LANG:cpp
 #include "Vbin2bcd.h"

インスタンス化する際には、
コンストラクタにインスタンス名を文字列で与えるようになっています。

C++ でクラスのインスタンスを作るには、
+ 自動変数として作成する
+ new で作成してポインタ変数で保持する

の2つの方法がありますが、SystemC でもどちらを使っても構いません。

*** 自動変数として作成する [#oc788251]

上でやっているように、

 LANG:cpp
 Vbin2bcd top("top");

とすれば、そのスコープ内のみで有効な自動変数としてインスタンスが作成されます。
このインスタンスはスコープを抜けると自動的に破棄されます。

コンストラクタに与えている "top" がインスタンス名になります。

この場合、インスタンスのメンバーへのアクセスは top.foo 
のように行います。

*** new で作成してポインタ変数に代入する [#gecfb87a]

ヒープ領域にインスタンスを確保するには new を使います。
new した結果はポインタが返るので、ポインタが他の変数に値を保持します。

 LANG:cpp
 Vbin2bcd *top = new Vbin2bcd("top");

ポインタ変数がスコープを抜けても、
new で作成したインスタンスは自動的には削除されないので、
必要なくなった時点で delete を使って明示的に破棄する必要があります。

 LANG:cpp
 delete top;

コンストラクタに与えている "top" がインスタンス名になります。

この場合、インスタンスのメンバーへのアクセスは top->foo 
のように行います。

*** どちらがいい? [#s0a0d73f]

上の例では自動変数としたほうが楽チンと思ってそうしてますが、
SystemC の世界でどちらが一般的かはまだこれから勉強するところです。

もしかするとモジュールインスタンスのサイズが大きくなる場合に備えて、
テストベンチではヒープに確保するほうが良いのかも知れません。

std::auto_ptr なんかをうまく使うことになるんですかね。

** 入出力ネットを定義する [#tcf82ff1]

信号線ネット、すなわち verilog でいうところの wire に相当するのは、
sc_signal というクラスのインスタンスです。

sc_signal はテンプレートクラスとして定義されていて、
後ろに < > で括ってビット数を指定します。

原理的には sc_signal も new で作成してポインタ変数で扱ってもよいはずですが、
面倒なので自動変数として定義されることが多いのだと思います。

 LANG:cpp
 sc_signal<bool>        sig1;    //  1 bit
 sc_signal<sc_uint<8> > sig2;    //  8 bit unsigned
 sc_signal<sc_int<8> >  sig3;    //  8 bit signed
 sc_signal<uint32_t>    sig4;    // 32 bit unsigned
 sc_signal<int32_t>     sig5;    // 32 bit signed
 sc_signal<uint64_t>    sig6;    // 64 bit unsigned
 sc_signal<int64_t>     sig7;    // 64 bit signed

sc_signal<sc_uint<8> > を sc_signal<sc_uint<8>> 
と書いてしまうと文法エラーになることに注意してください。
~> と > との間には必ずスペースを入れるようにします。
これは、C++ では >> が独立の演算子として定義されているためです。

また、verilog で定義した複数ビットの信号線は、
verilator では必ずしも同じ幅の信号線として表現されず、
32ビットあるいは64ビットに切り上げられてしまうようです。

bin2bcd で宣言した 10 ビットの入力線 bin に sc_signal<uint32_t> 
として宣言した信号線を結合しているのはこのためです。

これはシミュレーション時の速度を上げるための配慮だそうですが、
オーバーフロー時の動作などに違いが出る可能性もあるため、
注意が必要です。

追記:

bool や sc_uint、uint32_t などは、0, 1 のみの2進数しか表せないのに対して、

VHDL や Verilog のビットは X や Z など、0, 1 以外の状態も取り得ます。

そこで、Verilog のビットのように 0,1,X,Z を表現できる4値ビットを表すために、

 LANG:cpp
 sc_signal<sc_logic>  sig_1bit;    // 1 bit
 sc_signal<sc_lv<4> > sig_4bits;   // 4 bits

のように、sc_logic や sc_lv のような型も定義されているそうです。

Verilator ではこのような多値ビットを使わない形で、
なおかつ上で見たとおりビット数すらワード単位にまとめた形で、
信号線が定義されます。これはシミュレーションの速度を上げるためのようです。

*** クロック線について [#de7989ac]

クロック線を簡単に定義するため、sc_clock というクラスが用意されています。

このインスタンスは、sc_signal<bool> と同じ1ビットの信号線ですが、
コンストラクタで指定された周期で自動的にクロックを刻みます。

 LANG:cpp
 sc_clock clk("clk", 10, SC_NS);

コンストラクタへの引数は、
- "clk" : クロックソースのインスタンス名
- 10 : クロック周期
- SC_NS : クロック周期の単位 (SC_NS は ns, SC_US は us を意味します)

となっています。

このほかにもオプションの引数を渡すことでデューティー比などを変更できるそうです。

** 入出力線をモジュールに結合する [#x79bda8f]

モジュールへの結合は、
モジュールのメンバとして定義されている入出力ピンに信号線ネットを渡すことで行われます。

 LANG:cpp
     // モジュールに結線
     top.clk(clk);
     top.rst(rst);
     top.start(start);
     top.busy(busy);
     top.bin(bin);
     top.bcd(bcd);

top.clk などの .clk の部分がモジュールピンの名前、カッコ内が信号ネットのインスタンスです。

top をポインタ変数で保持している場合には、top.clk(clk) の代わりに top->clk(clk) となります。

** 入力線に値を設定する [#ia5f9455]

単純に sc_sig のインスタンスに数値を代入します。

 LANG:cpp
     rst   = 1;
     start = 0;

代入する値を計算するためには C++ で利用可能な任意の関数や演算子を使えますので、
verilog などでテストベンチを書く際に苦労するちょっとした計算などを手短に書くことができます。

** 時刻を進める [#z33d32f3]

他にも色々あるのですが、もっとも手軽な方法として、
verilog の #123 のように一定時間だけ時刻を進める方法として、
sc_start に数値を指定するという方法があります。

 LANG:cpp
    sc_start(10.0, SC_NS);

SC_NS は与える数値に付ける単位で、SC_NS ならば ns を表します。

したがって、この例では 10ns 時刻を進めるという意味になります。

数値部分は必ずしも整数でなくても、任意の浮動小数点数を指定できるようです。

** 出力を確認する [#n8fe542f]

sc_sig は暗黙的に整数値に変換されるようなので、
任意の整数と比較したり、整数値を要求する関数の引数として与えることが可能です。

** 基本は以上で OK ? [#ha8134e1]

と、ここまで分かっていれば、上記のような簡単なテストベンチを書くことができるようになります。

が、より複雑なテストをするには SystemC のモジュールを書いたり、
イベントを使ったりしたくなるので、以下ではそのあたりを勉強しようと思います。

* 気になっている点 [#n11e3ce6]

** verilog モジュールにパラメータを指定する [#h5c583fc]

パラメータを含む verilog モジュールを verilator + SystemC で検証するのは難しそう?

http://www.veripool.org/boards/2/topics/show/276-Verilator-Efficient-Usage-of-Verilog-Parameters

この記事の 2010-04-14 の時点では、
- parameter を verilator から設定する方法は無い
- 今後も実装する気はない

との回答でした。

これは結構困ったことで、parameter を含む verilog モジュールをテストするには、
ラッパーとなる verilog モジュールをテストしたいパラメータの数だけ作り、
それらを verilate して SystemC とリンクすることになるのだと思います。

Counter: 21344 (from 2010/06/03), today: 3, yesterday: 0