#目次
#1.はじめに
筆者は、Verilog HDL を拡張したSysteVerilogというHDL言語に関心を持ち始めています。
その中でもC言語と連携を行う DPI-C というインターフェースに着目しています。
筆者の場合、Windows環境の方に慣れ親しんでいる為、Linux環境には幾分の敷居の高さ
を感じています。
そのためWindowsの環境で、且つ、費用的にも敷居の低い無償のツールで動かせるか
調査を行い、インテル® 社が提供するFPGA開発ツールQuartus® Primeに同梱されて
いる無償のシミュレーション・ツールだけで実現を試みました。
とりあえず「動かす」ことに重点を置き、稚拙ながら簡単な自作コードを用いて、
言語間でインターフェースが行われている「動き」について投稿してみました。
本稿の執筆にあたり、参考にした情報も文中に掲載しました。
#2.環境
ModelSim® インテル® FPGA Starter Edition 10.5b (Windows版)
- このシミュレーション・ツールは、Quartus® Prime Lite Edition (無償版)の
バージョン17.1 ~19.1に同梱されています。 - Quartus® Prime Standard Edition (有償版)でも同梱されています。
#3.サンプル・コード
本稿で使用するサンプル・コードは、次の①~③を目的としています。
-
① SystemVerilog コードからC言語を呼び出す
- 応用例:ハード・ソフトの協調検証
- ハードウェアで受信したデータをホスト側のソフトウェアに渡して
高度なアルゴリズムでエラー・チェック
- ハードウェアで受信したデータをホスト側のソフトウェアに渡して
- 応用例:ハード・ソフトの協調検証
-
② C 言語からSystemVerilog コード(ファンクションを使用)を呼び出す
- 応用例:ハードウェア・アクセラレーター
- ホスト側のソフトウェアで処理時間を要する機能をハードウェアで処理
- 応用例:ハードウェア・アクセラレーター
-
③ C 言語からSystemVerilog コード(タスクを使用)を呼び出す
- 応用例:ハード・ソフトの協調検証
- ハードウェアのテスト・プログラムをC言語で記述
- 応用例:ハード・ソフトの協調検証
上記①~③では、SystemVerilogとC言語の他に Tcl スクリプトも使用します。
##3-1. ① SystemVerilog コードからC言語を呼び出す
SystemVerilog コードからC言語を呼び出す場合、SytemVerilogコード内にimport "DPI-C"
を記述し、その後に、C言語の関数名と、その関数名に関連する情報(型や引数)を指定し
ます。
SytemVerilogコード内に記述するので、SystemVerilogの文法に従って関数を指定します。import
の場合、指定する関数は、pure
とcontext
に分類されます。
サンプル・コードでは、C言語からSystemVerilogコードに参照するような記述では無いので、pure
の方を使用しています。
import "DPI-C"
後に、pure function int add(input int value_1, int value_2)
という二つの引数を加算する関数を指定しています。
関数の実体はC言語側にあるので、加算部分はC言語側で記述しています。
引数に値を代入して、initial
文の中で20ns後に関数が呼び出され、C言語側で処理した
加算結果が、戻り値としてResult
に代入しています。
`timescale 1 ns/ 1 ps
module tb_dpi;
import "DPI-C" pure function int add(input int value_1, int value_2);
int Result_val = 0;
int val_a = 13;
int val_b = 21;
initial
begin
$display("--- SystemVerilog to C function using 'DPI-C' ---");
#20
$display("Call to C function from System Verilog");
Result_val = add(val_a, val_b);
#20
$display("Get Value from C function: Result_val = %d", Result_val);
$display("Finish");
#1000
$finish;
end
endmodule
個人的には、import
という意味合いから、SystemVerilogコードからC言語に処理を分岐する
とよりも、SystemVerilogコードの中に、C言語の関数を組み込まれているような印象を持って
います。
参考:
・ModelSim DEサンプル・コード
###3-1-2. C コード
本コードでは、加算処理を行い、その結果をメッセージに表示後、呼び出し側に戻り値として
返しています。
ヘッダ・ファイルもC言語で標準的に用意されているものをインクルードしています。
#include <stdio.h>
#include <stdlib.h>
extern int add(int val_1, int val_2) {
int accm = 0;
accm = val_1 + val_2;
printf(">>> Run C function result = %d\n", accm);
return accm;
}
###3-1-3. Tcl スクリプト
ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに
重点を置きました。
vlib work
vlog -sv -dpiheader tb_dpi.sv CFunc1.c
vlog -sv tb_dpi.sv
vsim -gui work.tb_dpi
add wave -r /*
参考:
・ModelSim DEサンプル・コード
###3-1-5. Modelsim の起動
- 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。
本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。
- ModelSim インテル® FPGA Starter Edition を起動します。
- Transcript ウィンドウで次のコマンドをタイプして、上記 1) のディレクトリに移動します。
【例】 cd C:/work/test_sv2c
カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。
###3-1-6. Tclスクリプトの実行
次のコマンドで Tclスクリプトを実行します。
do ./dpic_sv2c.tcl
Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
###3-1-7. Modelsim によるシミュレーション
Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
Simulation メニューから Run -> Run -All を選択します。
次のような画面が起動したら、[ いいえ(N) ] をクリックします。
[ Zoom Full (F) ] アイコンをクリックします。
###3-1-8. シミュレーション結果
変化点をズームアップすると、SystemVerilog 側で用意した、引数 13 と 21 とが、20ns 地点でC の関数に渡された後、即座に加算結果が返される動きを把握できます。
このときのメッセージでも、C言語側の関数を呼び出して、加算結果 34 が返ってきたことを示しています。
参考:
・ModelSim DEサンプル・コード
・DPI-CでSystemVerilogからC言語の関数呼び出し
・ModelSimASE+clangでSystemVerilog DPI-Cをシミュレーションする
##3-2. ② C 言語からSystemVerilogコード(ファンクションを使用)を呼び出す
CコードからSystemVerilog コードを呼び出す場合、SystemVerilogコード内に、export "DPI-C"
を記述します。import
のようにSystemVerilog の中にC言語を取り込むの
とは逆の方向になるので、SystemVerilogのコードを外部に吐き出す意味合いでexport
が使われるようです。
export "DPI-C"
の後には、C言語にエクスポートされるSystemVerilogの関数やタスクを指定します。
本サンプル・コードでは、SystemVerilogのファンクションをエクスポートするので、export "DPI-C"
の後ろに、ファンクション名と、それに関連した情報(型や引数)を使用しています。
export
の場合、import
とは異なり、pure
や context
は不要になります。
###3-2-1. C コード
本コードは、C言語のmain関数では無く、add_main()
というサブ関数になります。
また、その下にはadd_func()
というサブ関数を定義しています。
ヘッダ・ファイルには、C言語側で標準的に用意されているライブラリに加えて、dpiheader.h
を明示的にインクルードして、DPI-C経由で処理を分岐できるようにしています。
サブ関数add_func()
の引数に値を指定した後、サブ関数add_func()
を呼び出して、
SystemVeriligで定義したadd_func
というfunction
に加算の処理を委ねています。
SystemVerilog側からの加算結果は、戻り値として変数result
に格納後、printf()
を使用
してメッセージに表示できるように記述しています。
ここでは同時に、変数 num
の値もメッセージに表示させています。
後術しますが、この値はadd_maion()
の引数として外部から指定されます。
#include <stdio.h>
#include <stdlib.h>
#include "dpiheader.h"
int add_func(int, int);
int add_main(int num){
int a = 35;
int b = 11;
int result;
result = add_func(a, b);
printf("************************************************************************\n");
printf("Mesage from C function\n");
printf(">>> Culcurate System Verilog function result = %d <- %d\n", result, num);
printf("************************************************************************\n");
return (0);
}
###3-2-2. SystemVerilog コード
本コードでは、sub_moduleというモジュール名を定義しています。実装を想定し、トップ階層よりも下位の階層にインスタンスされるサブ・モジュールを意識し、敢えてポート名を定義しています。
このモジュール内には、SystemVerilog の function
としてadd_func
を定義していますが、これがC言語側から呼び出される対象になります。SystemVerilog側から見れば、外部にエクスポートする対象となります。
add_func
では 入力ポートとしてint
型で宣言されたf_da
と f_db
を仮引数とし、C言語側から与えられた引数と対応しています。
仮引数の型をintから wireに切り替えば、assign 文で記述した接続に従い、
モジュール名の直後に指定した入力ポートda_iとdb_iに各々接続され、
function で記述した加算結果が出力ポートq_oと接続され、トップ階層と
接続する加算器として実装されます。
VerilogやSystemVerilogではfunctionは、組み合わせロジックの回路記述
に該当します。
f_da
と f_db
を加算した結果が、add_func
の戻り値となり、DPI-Cによって、C言語側に送られます。DPI-Cに処理をエクスポートする為、export "DPI-C"
の後ろに function add_func
の記述を追加しています。
また、本コードでは、initial
文を使用して、シミュレーション波形で時間的な動きを視覚的に把握できるようにしています。
シミュレーション直後、つまり0nsでnum_val
に3を代入した後、20ns経過した後に、C言語側のサブ関数add_main()
に引数3を与えて実行しています。組込みシステムのように、CPUのリセットを解除して、ROMからCコードをブートする動きに似ているかもしれません。
この処理によって、C言語側のadd_main()
が実行後、その中にあるサブ関数add_func()
が実行されるフローがシミュレートされます。
このフローは、結果的にSystemVerilogのinitial
文からC言語を呼び出す仕組みが無いと実現できないので、本コード内では、import “DPI-C”
も併用しています。
SystemVerilogから呼び出されるC言語のadd_main()
の中では、サブ関数add_func()
からSystemVerilogを参照するので、本コードではimport “DPI-C”
の後ろに context
を付加して、int
型のfunction
としてadd_main
の呼び出せるように記述しています。
`timescale 1ns/1ps
module sub_module(
input wire [31:0] da_i, // Input Data
input wire [31:0] db_i, // Input Data
output wire [31:0] q_o // Output Data
);
import "DPI-C" context function int add_main(input int num);
export "DPI-C" function add_func;
int num_val = 0;
// Function
function int add_func;
input int f_da;
input int f_db;
begin
add_func = f_da + f_db;
end
endfunction
// Connect
assign q_o = add_func(da_i, db_i);
// Monitoring
initial
begin
num_val = 3;
#20
add_main(num_val);
#20 $finish;
end
endmodule
参考:
・ModelSim DEサンプル・コード
###3-2-3. Tclスクリプト
ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに重点を置きました。
vlib work
vlog -sv -dpiheader dpiheader.h sub_module.sv add_main.c -ccflags "-g"
vsim -c sub_module
add wave -r /*
参考:
・ModelSim DEサンプル・コード
###3-2-5. Modelsim の起動
- 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。
本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。
2) ModelSim インテル® FPGA Starter Edition を起動します。
3) Transcript ウィンドウで次のコマンドをタイプして上記 1) のディレクトリに移動します。
【例】 cd C:/work/test_c2sv
カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。
###3-2-6. Tclスクリプトの実行
次のコマンドで Tclスクリプトを実行します。
do ./dpic_c2sv.tcl
Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
###3-2-7. Modelsim によるシミュレーション
Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
その際、信号の順番も適宜入れ替えます。
Simulation メニューから Run -> Run -All を選択します。
次のような画面が起動したら、[ いいえ(N) ] をクリックします。
[ Zoom Full (F) ] アイコンをクリックします。
###3-2-8. シミュレーション結果
シミュレーターで時系列の動きを追うため、SystemVerilog コードの initial
文に記述した内容に従い、nmu_val
に値3を指定してから20 ns 経過後にnum
が0から3に変化していることから、この時点でC言語側のadd_main()
が実行開始していることが把握できます。
同時に、add_main()
関数内のサブ関数 add_func()
が実行され、add_func()
の実体であるSystemVerilog コードで定義した関数add_func
で即座に加算が行われ、その結果がC言語側に返された後、次のようにprintf()
によってModelSimのTranscriptウィンドウ内にメッセージとして表示されます。
また、add_func
はトップ階層では無く、DPI-Cとのインターフェースによって、C言語側と接続していることから、モジュール名の後ろに定義した入力ポートには何も値が表示されず、出力ポートにも加算結果が一切反映されていないこともシミュレーション波形からも把握できます。
##3-3. ③ C 言語からSystemVerilogコード(タスクを使用)を呼び出す
CコードからSystemVerilog コードを呼び出す場合、SystemVerilogコード内にexport "DPI-C"
を記述します。import
とは異なりSystemVerilog の中にC言語を取り込む
のとは逆の方向になるので、SystemVerilogを外部言語に吐き出す意味合いのexport
がDPI-C
では使われます。
export "DPI-C"
の後には、C言語側にエクスポートされるSystemVerilogの関数(ファンク
ションやタスク)を指定します。
本デザインでは、SystemVerilogのタスクをエクスポートするので、export "DPI-C"
の
後ろに、タスク名と、それに関連した情報(型や引数)を使用しています。
export
の場合、import
でfunction
を指定するケースとは異なり、pure
やcontext
は
不要です。また、import
の場合でもtask
を指定する際には必ずcontext
を使用します。
###3-3-1. C コード
本コードは、C言語のmain関数では無く、add_main()
というサブ関数になります。加えて、
その下にはadd_task()
というサブ関数を定義しています。
ヘッダ・ファイルには、C言語側で標準的に用意されているライブラリに加えて、dpiheader.h
を明示的にインクルードして、DPI-C経由で処理が分岐されます。
サブ関数add_task()
の引数に値を指定した後、add_task()
の呼び出しを行い、
SystemVeriligで定義したadd_task
というタスクに加算処理を渡しています。
SystemVerilog側からの加算結果は、戻り値として変数result
に格納後、printf()
を使用
してメッセージに表示できるように記述しています。
ここでは同時に、変数 num
の値もメッセージに表示させています。後術しますが、この値はadd_maion()
の引数として外部から指定されます。
#include <stdio.h>
#include <stdlib.h>
#include "dpiheader.h"
int add_task(int, int, int*);
int add_main(int num){
int a = 35;
int b = 11;
int result;
add_task(a, b, &result);
printf("************************************************************************\n");
printf("Mesage from C function\n");
printf(">>> Culcurate System Verilog task result = %d <- %d\n", result, num);
printf("************************************************************************\n");
return (0);
}
本コードでは、tb_module
というモジュール名を定義しています。タスクはテストベンチで
利用するケースが多いので、モジュール名の後ろに定義するポート名は何も定義していません。あくまでC言語と連携した検証システムのようなものをイメージしています。
このモジュール内には、SystemVerilog のタスクadd_task
を定義していますが、これが
C言語側から呼び出される対象になります。
SystemVerilog側から見れば、外部にエクスポートする対象となります。
add_task
では 入力ポートとしてint
型で宣言された仮引数t_da
と t_db
が、C言語側の
引数に対応します。
t_da
と t_db
を加算した結果は、出力ポートとしてint
型で宣言された仮引数t_q
に
送られて、add_task
の第三引数に反映されることで、外部から参照できるようになります。
DPI-Cに処理をエクスポートする為、export "DPI-C"
の後ろに task add_task
の記述を
追加しています。
このタスクには、時間的変化を与えています。対象のadd_task
は、開始して20ns後に
即値99をt_q
に出力し、さらに20ns後に加算結果がt_q
に出力するように記述しています。
本コードでは、initial
文を使用して、シミュレーション波形で時間的な動きを視覚的に
把握できるようにしています。
シミュレーション直後、つまり0nsでnum_val
に3を代入した後、20ns経過した後に、
C言語側のサブ関数add_main()
に引数3を与えて実行しています。組込みシステムのように、
CPUのリセットを解除して、ROMからCコードをブートする動きに似ているかもしれません。
この処理によって、Cコードadd_main()
が実行後、その中にあるサブ関数add_task()
が
実行されるフローがシミュレートされます。
このフローは、結果的にSystemVerilogのinitial
文からC言語を呼び出す仕組みが無いと
実現できないので、本コード内では、import “DPI-C”
も併用しています。
SystemVerilogから呼び出されるCコードadd_main()
の中では、サブ関数add_func()
が
SystemVerilogに参照するので、本コードではimport “DPI-C”
の後ろに context
を付加
して、int
型のfunction
としてadd_main
を呼び出すように記述しています。
`timescale 1ns/1ps
module tb_module();
import "DPI-C" context task add_main(input int num);
export "DPI-C" task add_task;
int num_val = 0;
// Task
task add_task;
input int t_da;
input int t_db;
output int t_q;
begin
#20 t_q = 99;
#20 t_q = t_da + t_db;
end
endtask
// Monitoring
initial
begin
num_val = 3;
#20
add_main(num_val);
#20 $finish;
end
endmodule
参考:
・ModelSim DEサンプル・コード
###3-3-3. Tcl スクリプト
ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに
重点を置きました。
vlib work
vlog -sv -dpiheader dpiheader.h tb_module.sv add_main_tsk.c -ccflags "-g"
vsim -c tb_module
add wave -r /*
参考:
・ModelSim DEサンプル・コード
###3-3-5. Modelsim の起動
- 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。
本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。
- ModelSim インテル® FPGA Starter Edition を起動します。
- Transcript ウィンドウで次のコマンドをタイプして、上記 1) のディレクトリに移動します。
【例】 cd C:/work/test_c2sv_tsk
カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。
###3-3-6. Tclスクリプトの実行
次のコマンドで Tclスクリプトを実行します。
do ./dpic_c2sv_tsk.tcl
Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
###3-3-7. ModelSim によるシミュレーション
Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
その際、信号の順番も適宜入れ替えます。
Simulation メニューから Run -> Run -All を選択します。
次のような画面が起動したら、[ いいえ(N) ] をクリックします。
[ Zoom Full (F) ] アイコンをクリックします。
###3-3-8. シミュレーション結果
シミュレーターで時系列の動きを追うため、SystemVerilog コードの initial
文に記述した
内容に従い、nmu_val
に値3を指定してから20 ns 経過後にnum
が0から3に変化している
ことから、この時点でCコードadd_main()
が実行開始していることが把握できます。
同時に、add_main()
関数内のサブ関数 add_task()
が参照されて、SystemVerilog コードで
定義したタスクadd_task
に加算する引数が渡されていることが確認できます。add_task
内では出力に与える値を時間に変えるように記述しています。
その為、add_task
が呼び出された地点から20ns後に即値99を出力し、さらに20ns後に加算が
行われている動きがシミュレーション波形でも確認できます。
加算結果がCコードに返された後、次のようにprintf()
によってModelSimの
Transcriptウィンドウ内にメッセージとして表示されます。
#4. 【考察】 DPI-CとCコンパイラとの関係
コンパイル・メッセージからの推察の範囲になりますが、ModelSimにはGCCが内包されているように見受けられます。
どのバージョンからGCCが内包されたかのは未調査
また、コンパイル・オプションでは、次のように -dpiheaderオプションの後ろにdpiheader.hと追記しています。
vlog -sv -dpiheader dpiheader.h (ファイル名).sv (ファイル名).c -ccflags "-g"
このときツールが自動でdpiheader.hをプロジェクト・ディレクトリに生成していました。
さらに、このdpiheader.hでは、#include "svdpi.h"
と記述されており、ファイル名から
推察して、svdpi.hがDPI-Cとの関連付けを行っているように感じています。
また、このdpiheader.hでは、DPI-Cでインポート/エクスポートを行う為の関数プロトタイプ
も再定義されていました。
その他、Load時に幾つか専用のDLLファイルも生成されていることから、シミュレーション時
に、シミュレーターがこれらのDLLを参照しているように思われます。
この辺りの仕組みは、シミュレーション・ツールの作りに依存するので、筆者のような
一般ユーザーでは、ここまでが限界と言えます。
参考:
・[DPI-CでSystemVerilogからC言語の関数呼び出し]
(https://happyfield.hatenablog.com/entry/2015/11/22/025736)
-
DPI-Cを使用して、SystemVerilog コードからC言語を呼び出す場合、System Verilogコード
にはimport "DPI-C"
を記述し、その後にはSystemVerilogコード内に記述している関数
(ファンクションもしくはタスク)の名前を記述 -
指定する関数は、
pure
とcontext
に分類され、次のルールに基づいて関数名の
前に追記 -
ファンクションを呼び出す場合:
- C言語からSystemVerilogコードに参照しない場合、pure
を追記
- C言語からSystemVerilogコードに参照する場合、context
を追記 -
タスクを呼び出す場合:常に
context
を追記 -
DPI-Cを使用して、C言語からSystemVerilogコード内に記述している関数(ファンクション
やタスク)を呼び出す場合、export “DPI-C”
を記述し、その後にはSystemVerilogコード
に記述している関数の名前を記述
(export
の場合、import
とは異なり、pure
やcontext
は不要 ) -
DPI-Cを使用する場合、コマンド・オプション -dpiheaderを追加してコンパイル
-
dpiheader.h内にDPI-Cを使用するためのインクルード・ファイルsvdpi.hが定義
-
dpiheader.hは、ModelSimが自動的に生成
-
ModelSimに内包されているGCCを使用
どのバージョンからGCCが内包されたかのは未調査
#6. リファレンス
この記事は以下の情報を参考にして執筆しました。
・ SystemVerilog設計スタートアップ Design Wave Magazine 編集部 編(CQ出版社)
・ DPI-CでSystemVerilogからC言語の関数呼び出し
・ModelSimASE+clangでSystemVerilog DPI-Cをシミュレーションする
・16.2 DPI
・[無償ツールで実践する「ハード・ソフト協調検証」(1) ―― SystemVerilogのDPI-C機能]
(http://www.kumikomi.net/archives/2009/12/_1systemverilogdpi-c.php?page=1)
・[無償ツールで実践する「ハード・ソフト協調検証」(2) ―― テスト・プログラムはC言語で書く(Tech Village)]
(http://www.kumikomi.net/archives/2009/12/_2c.php?page=2)
・ModelSim DEサンプル・コード