ModelSim XE を使った SystemVerilog DPI-C テスト

(978d) 更新

公開メモ

Xilinx が ModelSim の無償版&廉価版の配布を中止との情報

http://marsee101.blog19.fc2.com/blog-entry-1570.html

Xilinx 向けには ModelSim は敷居の高い環境になってしまいました。

(2016/02 追記)
個人的には未確認なのですが、Xilinx の Vivado 付属の ISim でも DPI-C が使えるようになっているようですね。

Vivado Design Suite ユーザーガイド - ロジックシミュレーション
付録E Vivado シミュレータのダイレクトプログラミングインターフェイス(DPI)
http://japan.xilinx.com/support/documentation/sw_manuals_j/xilinx2015_2/ug900-vivado-logic-simulation.pdf

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

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

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

通常、Verilog のテストをするには Verilog でテストベンチを書きます。
しかし IP とソフトウェアとのインタフェースなど、複雑なテストを Verilog で 記述するのは、言語が非力なためにかなり骨が折れます。

ISE は Verilog 2001 までしか対応していないのに対して、ModelSim は上位互換の SystemVerilog を使うことができるので、テストベンチの記述がいくらか楽になります。

さらに、SystemVerilog の DPI-C という機能を使うと、テストの一部を C や C++ のコードを使って記述でき、非常に複雑なテストも書けるようになります。

この DPI-C を Xilinx から入手可能な無料の開発環境上で利用する方法を調べてみました。
→ Xilinx が ModelSim の無償配布をやめてしまったので、今は無料ではなくなっています

情報源

2010/05/05 (コメントを受けて追記)
以下の記事は、その多くが上記の情報源に記載の記事をまとめ直したものになっています。

特に「簡単な例で確かめる」の部分は、ModelSim のマニュアル、および、4番目の EDA Blog - [Verilog] ModelSimでDPIを使う の内容に多少の説明を加えただけのものです。

有用な情報を提供下さった皆様に感謝いたします。

オリジナルの記事の方が詳しく書かれた部分も多いので、ぜひリンク先もご参照下さい。

DPI-C でできること

DPI-C を使うことで、

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

ようになります。

C から Verilog コードを呼ぶ準備として、Verilog の task や function を export しておきます。

逆に Verilog コードから C の関数を呼ぶには、Verilog 側で C の関数を import しておきます。

import / export される Verilog task/function の、C 側でのインターフェースを ヘッダファイル(下では dpiheader.h)として自動生成できるので、それに合わせて C 側のコードを記述します。

Verilog 側:

LANG:verilog
// Verilog の task を実装する
task verilog_task;
   begin
      // 何らかの処理
   end
endtask

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

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

// C の関数を呼び出す
initial
   c_task();

上のような Verilog ソース(実際には引数などをちゃんと記述します)から、C 用のヘッダファイル dpiheader.h を自動生成できます。
このヘッダファイルには Verilog の task/function に対応する C 関数のプロトタイプが宣言されています。

C 側:

LANG:C
#include "svdpi.h"
#include "dpiheader.h"  // Verilog task/function に対応するプロトタイプを読み込む

int c_task() // import される C 側の関数を実装する
{
  verilog_task(); // export された Verilog の task を呼び出す
}

このようなテストが無償のソフトウェアだけで実現できます。
→ Xilinx が ModelSim の無償配布をやめたので・・・(略

必要なもの

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

  • Xilinx ISE WebPack
    統合開発環境

  • ModelSim Xilinx Edition-III (MXE-III)
    Xilinx 向けの ModelSim 無償版 → 今は手に入らなくなってしまいました

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

  • MinGW
    MS-Windows 上の ModelSim で DPI-C を使うには Windows の DLL を生成できる開発環境が必要です。
    Visual C++ の他、大抵の Windows 用開発環境であれば何でも使えるのだと思いますが、 ここでは Windows 用に gcc や g++ を提供する MinGW というシステムを使いました。
    MinGW-5.1.6.exe などという名前のファイルがインストーラなので、 それを落として起動すると、サブコンポーネントとして何を入れるかを聞かれます。 C++ や make を使うのであれば、ここで忘れずに選択します。

  • cygwin
    MinGW とは異なる系統の Windows 上の GNU 環境。
    Windows のコマンドプロンプトが使いにくすぎるので、cygwin を入れて bash 上でコマンドを入力しています。
    cygwin にも gcc や g++ といった開発環境が揃っているのですが、 それらを使うと cygwin 独自の共有ライブラリができてしまい、 ModelSim からは使えないようなので、シェルその他のコマンドラインツールを使うためだけに入れています。
    同様なものに、MinGW 上で bash などを提供する MSYS というのがあるらしいのですが、 cygwin の X-Window や sshd などを他の用途に使っているので、敢えて cygwin で行きます。
    以下の作業は、恐らく MSYS 上でもほとんど何も変わらずできるはず?!

前提条件

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

環境変数等の整備

ここでは筆者の都合で cygwin を使っているので面倒な手順が必要になっています。 始めから MinGW 上で MSYS を使う場合には、make の名称変更以外の手順は必要ないのだと思います。

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

また、MinGW の make は mingw32-make.exe という名前になっているので、これを make という名前で使えるようにします。cygwin の 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

make の alias 指定について

上記のように make という名前の alias を作っておく方法では、 Makefile (後述) の中からサブディレクトリの make を起動する場合に、 うまく行きませんでした。

これは1段目の make の各コマンドは、make が起動した別のシェル内で実行されるためで、 alias 指定が引き継がれないためです。

そういったケースを想定すると、 素直にシンボリックリンクを張ってしまうのが正しい解のようでした。

LANG:console
ln -s /cygdrive/c/MinGW/bin/mingw32-make.exe /cygdrive/c/MinGW/bin/make

簡単な例で確かめる

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

  1. Verilog コードを記述する
    Verilog と C との相互呼び出しのための import / export を含んだコードを書きます

  2. C 用のヘッダファイルを自動生成する
    Verilog コードをコンパイルすることで、import / export される task / function の
    C 言語におけるプロトタイプを記述したヘッダファイル dpiheader.h を自動生成します。

  3. C から Verilog を呼び出すためのオブジェクトコードを自動生成する
    Verilog の export 記述を元に、C から Verilog を呼び出すためのコードを自動生成します

  4. 生成したヘッダファイルを使って C コードを記述する
    Verilog の要求するプロトタイプ通りに C コードを記述します

  5. C コードをコンパイルする

  6. 3. と 5. のオブジェクトコードをリンクして dll を作る

  7. dll を指定して ModelSim を起動する

詳細手順は以下の通りです。

Verilog コードを記述する

Verilog と C との相互呼び出しのための import / export を含んだコードを書きます

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(%d)", i);
        *o = 2 * i;
    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 begin
        c_task(1, ret);  // c_task は verilog_task を呼び出す
        $display("ret = %d", ret);
    end
 
 endmodule

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 というファイル名にしておけば必要ありません。

できあがった dpiheader.h は次のようになりました。

LANG:C
/* MTI_DPI */

/*
 * Copyright 2002-2008 Mentor Graphics Corporation.
 *
 * Note:
 *   This file is automatically generated.
 *   Please do not edit this file - you will lose your edits.
 *
 * Settings when this file was generated:
 *   PLATFORM = 'win32pe'
 */
#ifndef INCLUDED_DPIHEADER
#define INCLUDED_DPIHEADER

#ifdef __cplusplus
#define DPI_LINK_DECL  extern "C" 
#else
#define DPI_LINK_DECL 
#endif

#include "svdpi.h"


DPI_LINK_DECL DPI_DLLESPEC
int
c_task(
    int i,
    int* o);

DPI_LINK_DECL int
verilog_task(
    int i,
    int* o);

#endif 

中を見てみると、

LANG:Verilog
export "DPI-C" task verilog_task;

に対応する、

LANG:C
DPI_LINK_DECL int verilog_task(int i, int* o);

および、

LANG:Verilog
import "DPI-C" context task c_task(input int i, output int o);

に対応する、

LANG:C
DPI_LINK_DECL DPI_DLLESPEC int c_task(int i, int* o);

が宣言されているのが分かります。

DPI_LINK_DECL は C / C++ の両方で使えるライブラリを作成するときの常套手段で、 関数名が C++ で マングリング されないようにするための記述です。

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 ファイル名

ここでできあがった cexports.obj に、DPI_LINK_DECL int verilog_task(int i, int* o); の実体が定義されています。

生成したヘッダファイルを使って C コードを記述する

dpiheader.h のプロトタイプに合わせて、c_task を記述し、verilog_task を呼び出します。

LANG:console
$ cat > hello.c
 #include "svdpi.h"
 #include "dpiheader.h"
 
 int c_task(int i, int *o)
 {
     printf("Hello from c_task(%d)\n", i);
     verilog_task(i, o); /* Call back into Verilog */
     *o = i;
     return(0); /* Return success (required by tasks) */
 }

C コードをコンパイルする

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

LANG:console
$ gcc -c -g -I'C:\Modeltech_xe_starter\include' hello.c -o hello.obj

hello.o と cexports.obj とをリンクして dll を作成する

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

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

dll を指定して ModelSim を起動する

dll を指定して ModelSim を起動すると、 シミュレータが 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 -c -sv_lib cimports hello_top -do "run -all; quit -f"
 # Loading sv_std.std
 # Loading work.hello_top
 # Loading .\cimports.dll
 # run -all
 # Hello from c_task(1)
 # Hello from verilog_task(1)
 # ret = 2
 # quit -f

-c はバッチモードで(コマンドラインで)起動を意味します

-c が無ければ gui が起動します

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++ の規則で マングル されることはありません。

したがって、ユーザーが用意する 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()
 # ret = 2
 # quit -f

実用的な環境を構築

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

make と Makefile について

ここで、make に詳しくない方のために少し説明を加えると、Makefile というのは make と呼ばれるプログラムへの入力ファイルです。

例えば、上の例では hello.v と hello.c から work/, dpiheader.h, cexports.obj, hello.o, cimports.dll などの種々のファイルを生成し、最後にできあがった dll を指定して vsim を呼び出しました。

その後、もし hello.c に手を加えたとすると、work/, dpiheader.h, cexports.obj に手を加える必要はありませんが、hello.o, cimports.dll を作り直さなければなりません。これに対して、hello.v に手を加えた場合には、work/ を再生成する必要は無いですが、他の全てのファイルを作り直す必要があります(hello.c は hello.v に依存している dpiheader.h をインクルードしているので、念のため hello.o も再生成が必要です)。

このように、コンパイルやリンクによって目的のファイル(ここではcimports.dll)を生成するには、さまざまな中間ファイルを決まった順で生成しなければなりませんし、さらに、ソースファイルが変更されたときには、必要なものだけを再生成できると効率がよいです。しかしそれを毎回手作業で行うのは大変です。

特に、数多くの Verilog ファイルと C ファイル、インクルードファイル、PicoBlaze用のアセンブリファイル、その他の .mem ファイルや .mem ファイルを生成するためのソースファイルなどがある場合に、その中のいくつかのファイルを編集した後、必要なファイルのみを正しい順序で再生成することを考えると気が遠くなります。

こういった作業を自動化するためのプログラムとして make が存在します。長い歴史のあるプログラムなので、参考書や Web 上の how to も見つけやすいと思います。

作成した Makefile

下記の内容を Makefile という名前で用意しておき、そのディレクトリで make コマンドを起動すると、make が Makefile を読み込み、また、更新されたソースファイルを調べて、必要なファイルだけを必要な順序で再生成し、最後に ModelSim を起動してくれます。

Makefile

LANG:makefile
# デフォルトターゲット

default: simulate

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

.PHONY : cleanup
.PHONY : rebuild

# 設定

V_INCS          = 
V_SRC_TOP       = hello.v
V_SRCS          = $(V_SRC_TOP)
C_HDRS          = 
C_OBJS	         = hello.o
TOP_MODULE      = hello_top
VLOG_OPTIONS    = 
VSIM_LIBS       = 
VSIM_OPTIONS    = -do "run -all;quit -f"

MTI_HOME        = /Modeltech_xe_starter

# 個別の依存関係を記述

hello.o: hello.c dpiheader.h

# 共有の依存関係

.SUFFIXES:	.o .c .cpp .obj

work: $(V_SRCS) $(V_INCS)
	vlib work
	vlog $(VLOG_OPTIONS) -incr -sv $(V_SRCS)

dpiheader.h: work $(V_SRC_TOP) $(V_INCS)
	vlog $(VLOG_OPTIONS) -sv -dpiheader dpiheader.h $(V_SRC_TOP)

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: cexports.obj $(C_OBJS)
	g++ -shared $(C_OBJS) $(wildcard cexports.obj) -o $@ -L$(MTI_HOME)/win32xoem -lmtipli

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

clean:
	-rm -rf work $(C_OBJS) cimports.dll cexports.obj dpiheader.h

rebuild: cleanup simulate

使い方は以下の通りで、make というプログラム名に、 必要に応じてターゲット名を指定します。

  • make :必要最低限の手順で cimports.dll を更新し vsim を起動する
  • make clean :自動生成されるファイルを消去する
  • make rebuild :一旦全てを消してから cimports.dll を再構築して vsim を起動する

通常のプロジェクトで使うには

先の Makefile は上記の非常に単純化された例のために書いたものです。

実際に ISE 上で開発中のコードをデバッグするためには、Makefile の始めの方の変数設定と C ソースファイルの依存関係にかなり手を入れなければなりません。

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

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

LANG:makefile
V_INCS        = global.inc utils.inc
V_SRC_TOP     = mymodule_test.v
V_SRCS        = $(V_SRC_TOP) 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 dpiheader.h
mycpp_sub1.o: mycpp_sub1.cpp mycpp_sub1.h global.h
mycpp_sub2.o: mycpp_sub2.cpp mycpp_sub2.h global.h

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

解説

以下、注意が必要な点に少々解説を加えます。

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

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

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

-incr と -dpiheader を同時に使うのは NG

上記 Makefile では work を作るのに vlog を -incr 付きで呼び出して、
dpiheader.h を作るのにもう一度 vlog を呼び出しています。

これらを1つにまとめて、

LANG:makefile
	vlog -incr -sv -dpiheader dpiheader.h $(V_SRCS)

とすると、初回はうまく行ったように見えるものの、 2度目から dpiheader.h の中身が空になってしまいます。

どうやら、-incr でスキップされた Verilog ソース中で定義される import / export は dpiheader.h に出力されないようなのです。

-dpiheader 付きで vlog を呼ぶときには -incr を付けてはいけない、 と覚えておく必要があります。

verilog から export された task / function が1つも無いとき

cimports.dll を作るための以下の記述を、

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

$(wildcard ... ) を使わずに、

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

と書いていると、Verilog から export された task / function が1つも無いときにおかしな動作をします。

これは、export された task / function が無いとき、vsim は cexports.obj を作らないためです。

そのため、

  • cexports.obj が無ければエラーで止まります。
  • 古い cexports.obj があればそれが使われます。

$(wildcard ... ) を使うことで、cexports.obj が無いときにはコマンド中に cexports.obj が現れないようになります。

同時に、

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

とすることで、export が無いときに古い cexports.obj が残らないようになっています。

work の更新日時

考えてみると、上記 Makefile の以下の行:

LANG:makefile
work: $(V_SRCS) $(V_INCS)
	vlib work
	vlog $(VLOG_OPTIONS) -incr -sv $(V_SRCS)

これはちょっとうまくないですね。

  • "vlib work" は、work/ が古いかどうか、ではなく、work/ が存在するかどうか、で判定して実行すべきです
  • "vlog $(VLOG_OPTIONS) -incr -sv $(V_SRCS)" で work/ の内容が最新の状態になっても、更新日時自体は変化しない場合が多いので、内容が最新かどうかにかかわらず、ほぼ毎回コマンドが実行されてしまいます

という2点に問題があります。

まあ、どちらもエラーにはなりませんし、
それほど時間も掛からないので、目くじらを立てるほどでもないのですが・・・

ちょっと考えて思いつく回避策は、

LANG:makefile
work: $(V_SRCS) $(V_INCS)
	test -d work || vlib work
	vlog $(VLOG_OPTIONS) -incr -sv $(V_SRCS) && touch work

で、

  • work というディレクトリが存在しない場合のみ vlib work を実行
  • vlog $(VLOG_OPTIONS) -incr -sv $(V_SRCS) が成功したら work/ の更新日時を現在時刻に変更

という内容です。

シェルから make を起動している限りはこれでうまく行くようですが、 ModelSim 上から vlog を直接呼び出す際にはそもそも更新日時のチェックが行われませんので 毎回必ず vlog が実行されてしまいます。

通常の開発ではシミュレーションを走らせては、必要なところに手を加えて、 もう一度シミュレーションを走らせる、の繰り返しで、シェルから make を起動するのは始めの一回だけ、という場合が多いですので、 上記改善は結局あまり効果がないような気もします。

依存関係の自動生成 #1:ModelSim の vmake

ModelSim に vmake というコマンドがあって、Makefile を自動生成する機能があると書いてあるのですが、まだ試したことがありません。

あまり有用な情報ではないですが、一応ここにメモらせて下さい。

(追記)試してみたのですが、、、あまり役に立ちません? 使いどころがよく分からないツールでした。

代替案として、

LANG:make
V_SRCS		= $(shell grep "^vlog" main_test.fdo | sed -e 's/.* "//g' -e 's/"//g')

というような書き方ができそうです。

これなら、ソースが追加されても半自動で追いかけられます。

なぜ「半」かというと、main_test.fdo が更新されても work/ が更新されるわけではないので、 一旦 make rebuild しなければならないからです。何かもう少し工夫すると良いのでしょうけれど、 それほどの手間ではないのでそのまま使っています。

ただ、.fdo ファイルを生成するには、 うまく行かないのを承知で一旦 ISE から ModelSim を立ち上げる“ふり”をしなければならないので、 そのあたりが何なのですが。

依存関係の自動生成 #2:C++ ファイルの依存関係を自動生成

LANG:makefile
mycpp_main.o: mycpp_main.cpp mycpp_main.h mycpp_sub1.h mycpp_sub2.h global.h dpiheader.h
mycpp_sub1.o: mycpp_sub1.cpp mycpp_sub1.h global.h
mycpp_sub2.o: mycpp_sub2.cpp mycpp_sub2.h global.h

のような依存関係を手動で書くのは、ファイルが少ない内はよいのですが、 ファイル数が増えてくると手に負えなくなります。

http://www.ecoop.net/coop/translated/GNUMake3.77/make_4.jp.html#SEC42
によれば、このような C++ ファイルの依存関係を gcc や g++ に自動生成させることもできるそうです。

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 されました。

2010.07.21 追記

Verification Engineerの戯言 で紹介されていた SystemVerilog DPI-C Tutorial http://www.testbench.in/DP_00_INDEX.html によれば、 呼び出し元のスレッドが disable されたかどうかは int svIsDisabledState() コマンドを呼ばなくても C から呼び出した verilog task/function の返り値を見れば良いようです。

DPI-C では、C だけで書かれた task/function 内でシミュレーション時刻が進むことはありません。 時刻が進むのは必ず verilog から export された task/function 内部で、 ということになります。

で、時刻が進まないのだから C のコードを実行中に ステートが not disable から disable に変化することはありません。

ですから、C から verilog コードを呼び出したときに、 その返り値が 1 であるかどうかを見ることで、 int svIsDisabledState() コマンドを呼ばなくても、 disable されたかどうかを確実に判別できるのだと思います。

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

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

もしそうだとすると、C 側で使うグローバル変数等に mutex 等を使った同期処理が必要になりそうですが・・・

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

LANG:verilog
   // 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

すると結果は、

LANG:console
# 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

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

ModelSim 起動中のリコンパイル

http://marsee101.blog19.fc2.com/blog-entry-1392.html で触れられているように、 上記の方法で cimports.dll をリンクして ModelSim を立ち上げると cimports.dll が使用中になるため、ModelSim を終了しない限り cimports.dll のコンパイルができません。

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

1点注意として、ModelSim から exec で Makefile を呼び出し、 その Makefile から vsim が起動されると、 「vsim を2重起動できない」というエラーになってしまいます。

そこで、Verilog 関連の更新を ModelSim 上で行ってから、 Makefile を呼び出す形にしました。 Verilog 関連が最新の状態になっていれば Makefile から vsim が呼び出されることはないのでうまくいきます。

Makefile の後半部分を以下のようにします。

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

recompile.do: Makefile
	echo "quit -sim" > $@
	echo "vlog $(VLOG_OPPTIONS) -incr -sv $(V_SRCS)" >> $@
	echo "vlog $(VLOG_OPPTIONS) -sv -dpiheader dpiheader.h trimac_test.v" >> $@
	echo "vsim $(VSIM_OPTIONS) -c $(VSIM_LIBS) -lib work $(TOP_MODULE) -dpiexportobj cexports" >> $@
	echo 'exec c:/MinGW/bin/mingw32-make.exe compile' >> $@
	echo "do simulate.do" >> $@

compile: cimports.dll simulate.do recompile.do

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

console: compile
	vsim -do "do simulate.do" -c

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

rebuild: cleanup simulate

ここで、VSIM_OPTIONS には -do は必要なくなります。 代わりにワーニング回避のためのオプションなどを指定することができます。

上記変更により、シミュレーション前に simulate.do と recompile.do というファイルができます。

ModelSim の Transcript ウィンドウから

VSIM> do recompile.do

とすれば、C ソース側で変更された部分のみ反映して、再度シミュレータを起動できます。

こういったコマンドを ModelSim のメニューかボタンに割り付けられると操作が 楽になりそうなのですが、残念ながら ModelSim は tk の menu コマンドなどを 受け付けてくれないみたいですね。

コマンドの alias

独自のコマンドをメニューに割り当てる方法は分かりませんでしたが、 長いコマンドに短い名前を割り当てる alias が便利に使えることが分かりました。

alias re "do recompile.do"

としておくことで、re [Enter] とタイプするだけでリコンパイル用のスクリプトを呼び出せます。

他にも例えば、

alias re "do recompile.do"                          # リコンパイル
alias rs "restart -force; run 100us"                # リスタート(wave を追加した後などに)
alias g  "run 100us"                                # 100 us 進める
alias sv "write format wave mymodule_test_wave.fdo" # wave 設定を保存する

などとしておくと、これらのコマンドをマウス等でメニューからちまちま選ぶのに比べて シミュレーションが手軽に行えます。

LANG:makefile
simulate.do: Makefile
	echo "vsim -t 1ps $(VSIM_LIBS) $(TOP_MODULE) -sv_lib cimports $(VSIM_OPTIONS)" > $@
	echo "do {mymodule_test_wave.fdo}" >> $@
	echo "view wave" >> $@
	echo "view structure" >> $@
	echo "view signals" >> $@
	echo "run 10us" >> $@
	echo "do {mymodule_test.udo}" >> $@
	echo 'alias re "do recompile.do"' >> $@
	echo 'alias sv "write format wave C:/working_dir/mymodule_test_wave.fdo"' >> $@
	echo 'alias g "run 300us"' >> $@
	echo 'alias rs "restart -force; run 400us"' >> $@

のようにして simulate.do に入れておけば、いつでも使うことができます。 もちろん .udo に入れておいても良いです。

Warning: (vsim-WLF-5000) WLF file currently in use: vsim.wlf

上記 simulate.do や recompile.do を実行したところ、 下記のような警告が出て、古い wlf ファイルがどんどん溜まっていくという 現象に遭遇しました。

LANG:console
# ** Warning: (vsim-WLF-5000) WLF file currently in use: vsim.wlf
#           File in use by: osamu  Hostname: XXXXXXXX  ProcessID: 7100
#           Attempting to use alternate WLF file "./wlftcvr91n".
# ** Warning: (vsim-WLF-5001) Could not open WLF file: vsim.wlf
#           Using alternate file: ./wlftcvr91n
# ** Warning: (vsim-WLF-5001) Could not open WLF file: vsim.wlf
#           Using alternate file: ./wlftcvr91n

http://www.edaboard.com/ftopic40567.html によると、 ライセンスファイルの問題が疑われるとのことでした。

そこで自身の環境を調べたところ、 環境変数 LM_LICENSE_FILE が以前使っていた古い ModelSim 用のライセンスファイルを指していました。

これを正しい値に直し、古い vsim.wlf を削除したら問題が 解消された様子です。

Testfixture 内でのプルアップ指定について

ビヘイビャーレベルのシミュレーションでは

LANG:verilog
PULLUP pullup_inst (pulluped_pin);

と書いても、

LANG:verilog
pullup pullup_inst (pulluped_pin);

と書いてもうまく行きますが、Post-Place & Route では後者でないとエラーが出ました。

コメント

  • http://blogs.yahoo.co.jp/verification_engineer -- [Verification Engineerの戯言]
  • どうもこんにちは。この書き込みがご本人かどうか分からないのですが、Verification Engineerさんの、All of SystemVerilog の記事も、無償ツールで実践する「ハード・ソフト協調検証」(全8回)、も大いに参考にさせていただきました。この場を借りて感謝いたします。また、本文中で謝辞を書き落としていたため追記しました。 -- [武内(管理人)]
  • 本人です。昨日、Googleで見つけました。参考にしていただき、ありがとうございます。 -- [Verification Engineerの戯言]
  • ご本人が書き込みして下さるとは光栄です。重ねて、貴重な情報をありがとうございました。今後ともどうぞよろしくお願いいたします。 -- [武内(管理人)]
  • わかりやすい記事で色々勉強させていただき、感謝しております。 "作成したMakefile"のコードをそのままコピーして実行してみたのですが、Modelsimでcimport.dll.dllがないとerrorになりました。上の説明でも-sv_libの後は.dllが含まれていないので試しに$<をcimportsに 変更すると正常に実行できました。5年経ってますので、modelsimの仕様が変わってたりするのかもしれませんが(^^; -- GUEE?

Counter: 21001 (from 2010/06/03), today: 7, yesterday: 0