ModelSim XE を使った SystemVerilog DPI-C テスト のバックアップ(No.2)

更新


公開メモ

Verilog HDL のテストに C++ コードを用いたい

最近、Verilog を使って FPGA 内部の回路設計をしています。

開発環境としては Xilinx の ISE WebPack で Verilog コードを書き、
ModelSim XE (Xilinx Edition) Free でテストベンチを動かしています。

通常、Verilog のテストをするには Verilog でテストベンチを書きます。

ISE は Verilog 2001 までしか対応していませんが、
ModelSim は SystemVerilog を使うことができます。

さらに SystemVerilog では DPI-C という機能を使って C や C++ のコードを使って
テストを行えるそうで、複雑なテストをするのに便利です。

この DPI-C を、Xilinx から入手可能な無料の開発環境上で実行する方法について調べてみました。

情報源

できること

verilog コードで

  • verilog の task や function を export
  • C の関数を import

しておくと、

  • verilog から C の関数を呼び出せる
  • verilog から呼び出された C の関数から verilog の task / function を呼び出せる

というもの。

LANGUAGE:verilog
// verilog の task
task verilog_task;
   begin
      // 何らかの処理
   end
endtask

// エクスポートする
export "DPI-C" task verilog_task;

// C の c_task() という関数をインポートする
import "DPI-C" context task c_task();

// C の関数を呼び出す
initial
   c_task(); // C 側から verilog_task() を呼び出せる

このようなテストが無償のソフトウェアだけで実現できます。

必要なもの

Xilinx の FPGA 開発に使える無償環境として以下を揃えました。

  • Xilinx ISE WebPack
    統合開発環境

  • ModelSim Xilinx Edition-III (MXE-III)
    Xilinx 向けの ModelSim 無償版

  • ModelSim Xilinx Edition Libraries Update
    Xilinx でダウンロード可能な ModelSim XE は古いので、 そのライブラリを最新版に置き換える必要があります

  • MinGW
    C/C++ のコンパイル・リンクを行うための gcc や g++ といった開発環境
    MinGW-5.1.6.exe などという名前のファイルがインストーラなので、 それを落として起動すると、サブコンポーネントとして何を入れるかを聞かれます。 c++ や make を使うのであれば、ここで忘れずに選択します。

  • cygwin
    MinGW とは異なる系統の Windows 上の GNU 環境。
    Windows のコマンドプロンプトが使いにくすぎるので、 cygwin を入れて bash 上でコマンドを入力しています。
    同様なものに、MinGW 上で bash などを提供する MSYS というのがあるらしいのだけれど、 cygwin の X-Window や sshd などを他の用途に使っているので、敢えて cygwin で。
    本当は cygwin の gcc で開発ができれば言うことがないのだけれど、 どうやら ModelSim の DPI-C は cygwin の gcc と合わせて使うことはできないようです。
    MSYS でもほとんど何も変わらずできるはず?!

前提条件

  • ISE と ModelSim で通常の開発ができる事を確認

環境変数等の整備

cygwin の bash を通常通り起動すると、cygwin 側の gcc や g++ にパスが通っているために MinGW の開発環境を使うことができません。そこで、PATH 環境変数の先頭に MinGW のパスを入れます。

また、MinGW の make は mingw32-make.exe という名前になっているので、 これを make という名前で使えるようにします。

最後に、ModelSim へのパスも通しておきます。

LANG:console
$ export PATH=/cygdrive/c/MinGW/bin:$PATH
$ which gcc
 /cygdrive/c/MinGW/bin/gcc
$ which make
$ alias make='/cygdrive/c/MinGW/bin/mingw32-make.exe'
$ make
 mingw32-make: *** No targets specified and no makefile found.  Stop.
$ export PATH=/cygdrive/c/Modeltech_xe_starter/win32xoem:$PATH

毎回これを入力するのは面倒なので、mingw-environment.sh というスクリプトを作っておき、次回からはこれを起動することにします。

LANG:console
$ cat > mingw-environment.sh
 #/usr/bin/bash
 export PATH=/cygdrive/c/MinGW/bin:$PATH
 alias make=/cygdrive/c/MinGW/bin/mingw32-make.exe
 export PATH=/cygdrive/c/Modeltech_xe_starter/win32xoem:$PATH
$ chmod u+x mingw-environment.sh
$ ./mingw-environment.sh

簡単な例で確かめる

verilog から C を、C から verilog を呼び出せることを確認するため、 以下の手順を踏みました。

サンプルコード

LANG:console
$ mkdir test_dpi-c
$ cd test_dpi-c
$ cat > hello.v
 `timescale 1ns / 1ps
 
 // トップモジュール
 module hello_top;
 
    // verilog で書いた task
    task verilog_task(input int i, output int o);
        $display("Hello from verilog_task()");
    endtask
 
    // C から使うためにエクスポートする
    export "DPI-C" task verilog_task;
 
    // C で書かれた関数をインポートする
    import "DPI-C" context task c_task(input int i, output int o);
 
    // C で書かれた関数を呼び出す
    int ret;
    initial
        c_task(1, ret);  // c_task は verilog_task を呼び出す
 
 endmodule
$ cat > hello.c
 #include "svdpi.h"
 #include "dpiheader.h"
 int c_task(int i, int *o)
 {
     printf("Hello from c_task()\n");
     verilog_task(i, o); /* Call back into Verilog */
     *o = i;
     return(0); /* Return success (required by tasks) */
 }

verilog ファイルから C 言語様のヘッダファイルを作成する

verilog ソースには、import / export される関数やタスクがすべて書かれているはずです。

これをコンパイルすることで、C 言語側で使うヘッダファイル dpiheader.h を作成します。

LANG:console
$ vlib work
$ vlog -sv -novopt -dpiheader dpiheader.h hello.v
 Model Technology ModelSim XE III vlog 6.4b Compiler 2008.11 Nov 15 2008
 -- Compiling module hello_top
 
 Top level modules:
         hello_top

-sv は .v ファイルを SystemVerilog として解釈させるためのオプションで、 始めから hello.sv というファイル名にしておけば必要ありません。

verilog ソースを元に、C から verilog を呼び出すためのコードを作成する

C から verilog task/function を呼び出すためのコード cexports.obj を作成します。

LANG:console
$ vsim hello_top -dpiexportobj cexports -c
 Reading C:/Modeltech_xe_starter/tcl/vsim/pref.tcl
 
 # 6.4b
 
 # vsim -dpiexportobj cexports -c hello_top
 # Loading sv_std.std
 # Loading work.hello_top
 # Compiling c:\...\work\_dpi\win32pe_gcc-3.4.5\exportwrapper.c
 # Successfully generated DPI export object 'cexports.obj'.

vsim の直後に与えるのは トップモジュール名
cexports は出力される .obj ファイル名

C コードをコンパイルして dll を作成

C コードをコンパイルして hello.o を作ります。

さらに、hello.o と cexports.obj とをリンクして、cimports.dll という dll を作成します。

LANG:console
$ gcc -c -g -I'C:\Modeltech_xe_starter\include' hello.c -o hello.obj
$ gcc -shared -o cimports.dll hello.obj cexports.obj -L'C:\Modeltech_xe_starter\win32xoem' -lmtipli

dll を指定してシミュレーションを起動

シミュレータが dll 内のコードを呼び出すことで C 側の関数が起動されます。

LANG:console
$ vsim -c -sv_lib cimports hello_top -do "run -all;quit -f"
 Reading C:/Modeltech_xe_starter/tcl/vsim/pref.tcl
 
 # 6.4b
 
 # vsim -do {run -all; quit -f} -c -sv_lib cimports hello_top
 # Loading sv_std.std
 # Loading work.hello_top
 # Loading .\cimports.dll
 # run -all
 # Hello from c_task()
 # Hello from verilog_task()
 # quit -f

Verilog から C の呼び出し、
C から Verilog の呼び出し、
共にうまく行っている様子です。

C++ を使うには

dpiheader.h には

LANG:C++
#ifdef __cplusplus
#define DPI_LINK_DECL  extern "C" 
#else
#define DPI_LINK_DECL 
#endif

という定義が含まれており、
import / export された関数のプロトタイプには DPI_LINK_DECL が付いていますので、 dpiheader.h で宣言される関数名がマングルされることはありません。

ユーザーが用意する C++ のソースでしなければならないのは、 関数の定義に DPI_LINK_DECL を付けることだけです。

hello.cpp

LANG:C++
 #include "svdpi.h"
 #include "dpiheader.h"
 #include <iostream>
 
 DPI_LINK_DECL
 int c_task(int i, int *o)
 {
     std::cout << "Hello from c_task() in cpp" << std::endl;
     verilog_task(i, o); /* Call back into Verilog */
     *o = i;
     return(0); /* Return success (required by tasks) */
 }

後は、コンパイルに gcc ではなく g++ を使えばそのまま行けます。

LANG:console
$ g++ -c -g -I'C:\Modeltech_xe_starter\include' hello.cpp -o hello.obj
$ g++ -shared -o cimports.dll hello.obj cexports.obj -L'C:\Modeltech_xe_starter\win32xoem' -lmtipli
$ vsim -c -sv_lib cimports hello_top -do "run -all;quit -f"
 Reading C:/Modeltech_xe_starter/tcl/vsim/pref.tcl
 
 # 6.4b
 
 # vsim -do {run -all;quit -f} -c -sv_lib cimports hello_top
 # Loading sv_std.std
 # Loading work.hello_top
 # Loading .\cimports.dll
 # run -all
 # Hello from c_task() in cpp
 # Hello from verilog_task()
 # quit -f

実用的な環境を構築

毎回上のような手順を手作業で行うのは手間なので、 テストベンチ起動用の Makefile を作ります。

Makefile

# デフォルトターゲット

default: simulate

# コマンド (PHONY ターゲット)

.PHONY : cleanup
.PHONY : rebuild

# 設定

V_INCS          = 
V_SRCS          = hello.v
C_HDRS          = 
C_OBJS	         = hello.o
TOP_MODULE      = hello_top
VSIM_LIBS       = -L xilinxcorelib_ver -L unisims_ver -L unimacro_ver -lib work
VSIM_OPTIONS    = -do "run -all;quit -f"

MTI_HOME        = /Modeltech_xe_starter

# 個別の依存関係を記述

hello.o: hello.c

# 共有の依存関係

.SUFFIXES:	.o .c .cpp .obj

work:
	vlib work

dpiheader.h: $(V_SRCS) $(V_INCS) work
	vlog -incr -sv -dpiheader dpiheader.h $(V_SRCS)

cexports.obj: dpiheader.h
	vsim -c $(VSIM_LIBS) -lib work $(TOP_MODULE) -dpiexportobj cexports

.c.o:
	gcc -c -g -I$(MTI_HOME)/include $< -o $@

.cpp.o:
	g++ -c -g -I$(MTI_HOME)/include $< -o $@

cimports.dll: $(C_OBJS) cexports.obj
	g++ -shared -o $@ $(C_OBJS) cexports.obj -L$(MTI_HOME)/win32xoem -lmtipli

simulate: cimports.dll
	vsim -t 1ps $(VSIM_LIBS) $(TOP_MODULE) -sv_lib cimports $(VSIM_OPTIONS)

cleanup:
	-rm -rf work *.o cimports.dll cexports.obj dpiheader.h

rebuild: cleanup simulate

先の例だとこれでうまく行きますが、本当に ISE 上で開発中のコードをデバッグするためには 始めの方の変数設定にかなり手を入れなければなりません。

このとき参考になるのが、ISE の作る .fdo ファイルです。

.fdo ファイルを参考に、vsim に与えるライブラリや -do オプションを記述します。

V_INCS        = utils.inc
V_SRCS        = mymodule_test.v mymodule.v mymodule_sub1.v mymodule_sub2.v
C_HDRS        = mycpp_main.h mycpp_sub1.h mycpp_sub2.h global.h
C_OBJS        = mycpp_main.o mycpp_sub1.o mycpp_sub2.o
TOP_MODULE    = mymodule_test glbl
VSIM_LIBS     = -L xilinxcorelib_ver -L unisims_ver -L unimacro_ver 
VSIM_OPTIONS  = -do "do {mymodule_test_wave.fdo};view wave; view structure; view signals; run 100us;do {mymodule_test.udo};"

...

# 個々の依存関係を記述

mycpp_main.o: mycpp_main.cpp mycpp_main.h mycpp_sub1.h mycpp_sub2.h global.h
mycpp_sub1.o: mycpp_sub1.cpp mycpp_sub1.h global.h
mycpp_sub2.o: mycpp_sub2.cpp mycpp_sub2.h global.h

こんな感じになると思います。

disable への対応

cpp 側でシミュレーション時間を進める処理を行う場合、 disable コマンドに対応しなければなりません。

呼び出し元のスレッドが disable されたかどうかは int svIsDisabledState() コマンドで判断します。

これが 1 を返したら disable されているので、void svAckDisabledState() を読んだ後、 すぐに return しなければなりません。

このとき、返り値は 0 ではなく 1 とします。

hello.v

LANG:verilog
`timescale 1ns / 1ps

// トップモジュール
module hello_top;

    // #10 だけ進めるタスク
    task verilog_task(input int i, output int o);
        begin
            #10;
            $display("proceed #10 - %d", i);
        end
    endtask
    export "DPI-C" task verilog_task;

    // C の関数をインポート
    import "DPI-C" context task c_task(input int i, output int o);

    int ret;
    initial 
        begin:main
            c_task(1, ret);  // verilog_task を 10 回呼び出すはずだが
        end

    // #35 つまり3回呼び出し後に disable する
    initial
        #35 disable main;

endmodule

hello.cpp

LANG:c++
#include "svdpi.h"
#include "dpiheader.h"
#include <iostream>

DPI_LINK_DECL
int c_task(int i, int *o)
{
    std::cout << "c_task(" << i << ") called" << std::endl;

    // 10 回呼び出すはずが、途中で disable される
    for (int j=0; j<10; j++) {
        if (svIsDisabledState()) {
            std::cout << "disabled" << std::endl;
            svAckDisabledState();
            return 1;
        }
        verilog_task(i, o);
    }
    
    std::cout << "done - " << i << std::endl;
    return 0;
}

結果:

LANG:console
$ vsim ...
 # run -all
 # c_task() called
 # proceed #10 - 1
 # proceed #10 - 1
 # proceed #10 - 1
 # disabled
 # quit -f

ちゃんと3回目終了後に disable されました。

マルチスレッド対応について

シミュレーションのタイミングを考えると、 cpp コードの関数は複数スレッドから同時に起動される可能性があるんでしょうか。

もしそうだとすると、mutex 等を使った同期処理が必要になりそうですが・・・

上記コードの c_task() 呼び出し部分を、次のように変更してみました。

   // C で書かれた関数を呼び出す
   int ret;
   initial 
       fork:main
           c_task(1, ret);
           c_task(2, ret);
           c_task(3, ret);
           c_task(4, ret);
           c_task(5, ret);
           c_task(6, ret);
           c_task(7, ret);
           c_task(8, ret);
           c_task(9, ret);
           c_task(10, ret);
           c_task(11, ret);
           c_task(12, ret);
           c_task(13, ret);
           c_task(14, ret);
           c_task(15, ret);
           c_task(16, ret);
           c_task(17, ret);
           c_task(18, ret);
           c_task(19, ret);
           c_task(20, ret);
       join

すると結果は、

# run -all
# c_task(1) called
# c_task(2) called
# c_task(3) called
# c_task(4) called
# c_task(5) called
# c_task(6) called
# c_task(7) called
# c_task(8) called
# c_task(9) called
# c_task(10) called
# c_task(11) called
# c_task(12) called
# c_task(13) called
# c_task(14) called
# c_task(15) called
# c_task(16) called
# c_task(17) called
# c_task(18) called
# c_task(19) called
# c_task(20) called
# proceed #10 -          20
# proceed #10 -           1
# proceed #10 -           2
# proceed #10 -           3
# proceed #10 -           4
# proceed #10 -           5
# proceed #10 -           6
# proceed #10 -           7
# proceed #10 -           8
# proceed #10 -           9
# proceed #10 -          10
# proceed #10 -          11
# proceed #10 -          12
# proceed #10 -          13
# proceed #10 -          14
# proceed #10 -          15
# proceed #10 -          16
# proceed #10 -          17
# proceed #10 -          18
# proceed #10 -          19
# proceed #10 -          20
# proceed #10 -           1
# proceed #10 -           2
# proceed #10 -           3
# proceed #10 -           4
# proceed #10 -           5
# proceed #10 -           6
# proceed #10 -           7
# proceed #10 -           8
# proceed #10 -           9
# proceed #10 -          10
# proceed #10 -          11
# proceed #10 -          12
# proceed #10 -          13
# proceed #10 -          14
# proceed #10 -          15
# proceed #10 -          16
# proceed #10 -          17
# proceed #10 -          18
# proceed #10 -          19
# proceed #10 -          20
# proceed #10 -           1
# proceed #10 -           2
# proceed #10 -           3
# proceed #10 -           4
# proceed #10 -           5
# proceed #10 -           6
# proceed #10 -           7
# proceed #10 -           8
# proceed #10 -           9
# proceed #10 -          10
# proceed #10 -          11
# proceed #10 -          12
# proceed #10 -          13
# proceed #10 -          14
# proceed #10 -          15
# proceed #10 -          16
# proceed #10 -          17
# proceed #10 -          18
# proceed #10 -          19
# disabled - 20
# disabled - 19
# disabled - 18
# disabled - 17
# disabled - 16
# disabled - 15
# disabled - 14
# disabled - 13
# disabled - 12
# disabled - 11
# disabled - 10
# disabled - 9
# disabled - 8
# disabled - 7
# disabled - 6
# disabled - 5
# disabled - 4
# disabled - 3
# disabled - 2
# disabled - 1
# quit -f

どうやら個々のスレッドは本当に並列に動いているわけでは無いようなので、 同期処理など、複雑なことを考える必要は無いみたい・・・ですかね。

必要なファイルだけをコンパイル

上記 Makefile で vlog への引数に -incr が無いと .v ファイルが多いときに毎回全てをコンパイルするため非常に時間が掛かってしまいます。

ModelSim のマニュアルを見ると、vlog を呼び出す際に -incr というオプションを付けておけば、 必要なファイルのみがコンパイルされるようですので、このオプションを加えてあります。

ModelSim 起動中のリコンパイル

http://marsee101.blog19.fc2.com/blog-entry-1392.html で触れられているように、 上記の様に cimports.dll をリンクして ModelSim を立ち上げると、 シミュレータを落とさない限り cimports.dll のコンパイルができない。

これを回避するには、quit -sim コマンドで一旦シミュレータを落としてから dll をコンパイルして、再度シミュレータを起動すればいい。

simulate.do: cimports.dll
	echo "vsim -t 1ps $(VSIM_LIBS) $(TOP_MODULE) -sv_lib cimports $(VSIM_OPTIONS)" > $@
	echo "do {trimac_test_wave.fdo}" >> $@
	echo "view wave" >> $@
	echo "view structure" >> $@
	echo "view signals" >> $@
	echo "run 10us" >> $@
	echo "do {trimac_test.udo}" >> $@

recompile.do:
	echo "quit -sim" > $@
	echo 'exec c:/MinGW/bin/mingw32-make.exe compile' >> $@
	echo "do simulate.do" >> $@

simulate: simulate.do recompile.do
	vsim -do "do simulate.do" &

compile: cimports.dll

としておくと、シミュレーション前に simulate.do と recompile.do ができあがる。

ModelSim のコマンドラインから do recompile.do とすれば、 変更された部分のみ反映して、再度シミュレータを起動することができる。

コメント


Counter: 31768 (from 2010/06/03), today: 1, yesterday: 0