SystemVerilog Tutorial
Last
Update Jul.8.2010
下記は、SystemVerilog Tutorial(リリース前の予稿)です。 LRM2005-P1800を元にしていますが、改訂VersionであるLRM2010では、変更されている箇所があります。(Draft8で確認。全てにサンプル記述とVeritakSVによる結果がありますが、VeritakSVは、未だリリースの予定をお話しできる段階ではありません。 )
16.SystemVerilogの新機能
16.1 fork join / fork join_any/ fork join_none
16.1.1 平行プロセスjoin_anyとjoin_noneの追加
verilogでは、fork joinで挟まれたステートメントは、平行プロセスになります。
たとえば、my_taskを起動するのに、
と書く代わりに、二つの平行プロセスを同時に起動するforkを使い、
と書くことができます。joinは、合流するという意味合いです。二つのプロセスの終了を待ってjoin以下が始まります。
SVでは、さらにjoin_anyとjoin_noneが追加されています。
ベンチです。
module fork_test2;
task my_task;
$display("Hi %d ",$time);
endtask
initial begin
fork//:Fork_Join
#5 my_task;//process1
#10 my_task;//process2
join
$display("Join Any Time=%d",$time);
end
initial begin
fork//:Fork_Any
#5 my_task;//process1
#10 my_task;//process2
join_any
$display("Join Any Time=%d",$time);
end
initial begin
fork//:Fork_None
#5 my_task;//process1
#10 my_task;//process2
join_none
$display("Join None Time=%d",$time);
end
endmodule
結果です。join_noneの方は、すぐに fork- join_noneを抜けていることが分かります。しかし、タスクの起動は、時刻#5と#10でしっかり行われています。この記述は、なにかtaskを起動しておいて、main()
を処理する形の定石かもしれません。
join_anyの方は、#5と#10のスレッドの内、早いほうのスレッドが終了した時点で抜けます。
fork-join は、fork-join内の全てのスレッドの完了を待って抜けます。
Loading vpi.dll Load Done. ***** Veritak SV Engine Version 0.021 Build Dec.30.2007 ***** Join None Time= 0 Hi 5 Hi 5 Join Any Time= 5 Hi 5 Hi 10 Join Any Time= 10 Hi 10 Hi 10 **** Test Done. Total 13.00[msec] ****
join_noneは、子プロセスの終了を待たずに、起動します。スルーしているかのように見えますが。親プロセス、子プロセス共、平行に走ります。(正確には、親プロセスのブロッキング文の実行後に子プロセスはスタートします。)いわば、”ノンブロキング”なプロセス形態です。
join_anyは、子プロセスのうち最初に終了したプロセスの後(どれでもよい)、親プロセスが始まります。従い、終了していない子プロセスと親プロセスとで平行に走ります。使い道としては、たとえばタイムアウト処理に使えそうです。
fork join_xx を使うと、複雑なプロセスが記述できます。
ベンチで少し詳しく動作を見ていきましょう。
module top;
initial begin
fork
do begin
#1;
$display("1st thread running time=%d",$time);
end while($time<10);
join_none // join_non なので、do while を起動してすぐに下に行く
fork
begin
#3;
$display("2nd thread finished time=%d",$time);
end
begin
#4;
$display("3rd thread finished time=%d",$time);
end
join_any //join_any なので、一つ起動したら下に行く
// disable fork;
end
endmodule
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** 1st thread running time= 1 1st thread running time= 2 2nd thread finished time= 3 1st thread running time= 3 3rd thread finished time= 4 1st thread running time= 4 1st thread running time= 5 1st thread running time= 6 1st thread running time= 7 1st thread running time= 8 1st thread running time= 9 1st thread running time= 10 sim_finished **** Test Done. Total 23.00[msec] ****
このベンチでは、3つのスレッドが起動していることが分かりますね。
最後の コメントアウトしているdisable fork をイネーブルしてみましょう。
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** 1st thread running time= 1 1st thread running time= 2 2nd thread finished time= 3 sim_finished **** Test Done. Total 10.00[msec] ****
disable forkは、現在のスレッドが起動した全ての子スレッドをdisable します。disable fork の行にやってくるのは、fork -any 中の早く終了するスレッド#3が終わった時点になります。その時点で、現在のinitial
スレッドが起動した全てのスレッドを disable します。 disable されたスレッドは、消滅します。従って、予約イベントも取り消され、終了するという訳です。従って、1st
2nd 3rd スレッド全てが終了されます。ここでdisable という意味は、停止というよりは、消滅です。消えてなくなると思って構いません。
ここで問題です。特定の一つのスレッドをdisable するには、どうしたらよいでしょうか?
それには、ラベルをつけて disable すればよいですね。下では、THREAD1というラベルをつけて それを disable
しています。
module top;
initial begin
fork :THREAD1
do begin
#1;
$display("1st thread running time=%d",$time);
end while($time<10);
join_none : THREAD1
fork
begin
#3;
$display("2nd thread finished time=%d",$time);
end
begin
#4;
$display("3rd thread finished time=%d",$time);
end
join_any
disable THREAD1;
$display("THEAD1 Disabled");
end
endmodule
16.1.3 disable と disable fork の違い
結果です。disable THREAD1を行うことで、THREAD1は、意図通りdisableされました。
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** 1st thread running time= 1 1st thread running time= 2 2nd thread finished time= 3 THEAD1 Disabled 3rd thread finished time= 4 sim_finished **** Test Done. Total 12.00[msec] ****
ところで、このやり方は注意することがあります。disable は、Staticなスコープ中の全スレッドに対して作用してしまいます。
次のベンチでその辺を見てみましょう。
同じタスクを平行プロセスで起動しています。
(このとき引数が意味を持つためには、task は、automatic である必要がありました。さもないと引数は、Static変数で上書きされてしまうのでしたね。)
module top1;
initial begin
fork
fork_processes(4);
fork_processes(7);
join_none
end
task automatic fork_processes(int delay);
fork : a
begin
#delay;
$display("Thread finished");
end
join_any : a
//disable a;
endtask
endmodule
結果です。
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** Thread finished Thread finished sim_finished **** Test Done. Total 11.00[msec] ****
コメントアウトしている disable a; をイネーブルしてみましょう。
すると、
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** Thread finished sim_finished **** Test Done. Total 7.00[msec] ****
disable a; で、aスレッドを disable しています。このとき、同じスコープ名のスレッドが2個起動していることに注意してください。 disable
は、この2個のスレッドに対して作用します。つまり、どちらのスレッドもdisable
されてしまいます。したがって、Thread finished は、2個でません。2個目が出る前に消されてしまいます。
そこで、disable の代わりにdisable fork を使ってみます。
module top1;
initial begin
fork
fork_processes(4);
fork_processes(7);
join_none
end
task automatic fork_processes(int delay);
fork : a
begin
#delay;
$display("Thread finished");
end
join_any : a
disable fork;
endtask
endmodule
disable fork は、現在のスレッドから生まれた子供、孫..に対してのみ作用します。同じスコープであっても自分が起動していないスレッドのことは関知しません。
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 ***** Thread finished Thread finished sim_finished **** Test Done. Total 11.00[msec] ****
16.1.4 wait fork
wait fork は、現在のスレッドが生成した全てのスレッドの完了を待ちます。
下の fork_blocks タスクでは、4つのtask を起動しています。wait fork は、現在のスレッドfork_block
が起動した4つのスレッドの完了を待ちます(ブロックします)。
全てが完了すると下に行きfork_blocks を抜けます。
i
module top1;
initial begin
fork
fork_blocks(1);
fork_blocks(4);
join
$display("All Done. time=%d",$time);
end
task automatic fork_blocks(int a);
fork
fork_processes(4);
fork_processes(7);
join_none
fork
fork_processes(14);
fork_processes(17);
join_none
$display("Join None a=%d time=%d",a,$time);
// if (a>3) begin
wait fork;
$display("wait fork time=%d",$time);
// end
$display("Fork_blocks end Time=%d",$time);
endtask
task automatic fork_processes(int delay);
fork : a
begin
#delay;
$display("Thread finished Time=%d",$time);
end
join_any : a
endtask
endmodule
***** Veritak SV Engine Version 0.031 Build Jun.23.2008 *****
Join None a= 1 time= 0
Join None a= 4 time= 0
Thread finished Time= 4
Thread finished Time= 4
Thread finished Time= 7
Thread finished Time= 7
Thread finished Time= 14
Thread finished Time= 14
Thread finished Time= 17
wait folk time= 17
Fork_blocks end Time= 17
Thread finished Time= 17
wait folk time= 17
Fork_blocks end Time= 17
All Done. time= 17
sim_finished
**** Test Done. Total 59.00[msec] ****
なお、引数を指定して、特定のスレッドだけ、wait fork をイネーブルさせることもできます。 作用は、同じスコープでも現在のスレッドのみに働きます。
16.1.2 fork 中の automatic 変数
次の例は、難しいです。ここでの問題はmが不定(undetermined,シミュレータ依存)になることです。
initial
for( int j = 1; j <= 3; ++j )
fork
automatic int k = j; // local copy, k, for each value of j
begin
#(k);
$display( "time=%d k=%d ",$time,k );
end
begin
automatic int m =j;// the value of m is undetermined
$display("time=%d m=%d",$time,m);
end
join_none
1)for loop内のint
jは、automatic変数で、下位スコープから参照可能です。スコープとは、可視範囲のことで、SVのautomatic変数はC++のそれに準じた扱いになります。ここで、intは、2値integerです。
2)for loop下のfork -join_noneは、ノンブロッキングプロセスですから、イタレーション3回分の3つのプロセスが時刻0で起動します。つまり時刻0時点では3つのダイナミックインスタンスプロセスが存在します。このイタレーションループ中、fork 下のautomatic
内部変数kは、jで初期化されます。kは、起動したインスタンスプロセスNoと言ってもよいでしょう。
3)fork下のステートメントは、独立した各々プロセスとして起動されますが、その実行は、スケジュールされます。スケジュールは(時刻0には違いないのですが、)実装依存です。なので、参照するjがいつの時点のjなのかは、不定になります。しかし、kは、プロセス起動時にイニシャライズされるので、子プロセスからは、自分のプロセスのkを参照することができます。
結果です。
Loading vpi.dll
Load Done.
***** Veritak SV Engine Version 0.021 Build Dec.30.2007 *****
time= 0 m= 4
time= 0 m= 4
time= 0 m= 4
time= 1 k= 1
time= 2 k= 2
time= 3 k= 3
**** Test Done. Total 10.00[msec] ****
16.1.3 Function中のfork/join_none
次のソースのように、fork-join_none中では、
verilog HDLでのfunction制限事項
1)イベントは書けない
2)ディレイは書けない
3)taskは、起動できない
4)ノンブロッキング文は使用できない
という制限が、fork-join_none内ではなくなります。function自体が、時間0で戻る事(時間を消費しない)は、従来と同じです。fork-join_noneは、いわば、ノンブロッキングなプロセスになりますが、そこで参照される変数については、気をつける必要があります。例えば、下のfunction
引き数a1は、automaticであり、
その生存期間は、function returnで返るまでです。functionが、return で抜けた後も、fork-join_noneプロセスは、独立プロセスとして生き続けることに注意してください。fork-join_noneのプロセス生成と、block_itemsの初期化は、functionが生きている間に行われますが。起動は、function
return の後、イベント文(#delay,@(x)等)のブロッキング文に出会ってからです。つまり、fork
-join_none内に、a1を参照するコードがあると、dangling参照になってしまいます。
下のように、block_itemの宣言で、a1をa2に初期化コピーすることで、dangling参照を防止するようにしてください。
結果です。
***** Veritak SV Engine Version 0.27 Build no.27.2009 ***** function a1=12345678 HiHi2 Hi2 time= 0 a2=12345678 a changed 0 a=1 0 Hi4 time= 1 a changed 10 a=0 10 Hi Task 20 Hi task2 30 Hi3 A 30 Hi3 B 40 **** Test Done. Total 7.00[msec] ****
16.2 DPI
16.2.1. VPI/DPIとの違い
16.2.1.1 SystemVerilot taskを直接呼べる
PLI/VPIとの大きな違いは、CからSVのtask/functionを”直接に”呼べることです。PLI/VPIの場合は、イベントの発生を待ってCALLBACKで呼んでもらう方法しかなく、Cから主体的に呼ぶ方法はありません。ところが、DPIを使うとC側から、SystemVerilogのtaskを直接に呼びます。これにより、時間を消費するDelayやイベントを含むtask を呼べるのでCとの同期通信が、全く簡単になります。
いままでそのようなIFが無いこと自体が不思議と思うかもしれません。そのためには、OBJECTレベルのリンクが不可避で、逆に言うと、いままでは、PLI/VPIでそれを巧妙に避けてきた、ということだと思います。
16.2.1.2 速度重視
2番目の違いは速度重視です。OBJECTレベルのリンクなので、オーバヘッドは0 または、VPIに比べると小さいです。
逆にいうとチェックがありません。Cで配列のアクセスチェックがないのと同様に、定義したOBJECTに対してなんらチェックは働きません。定義して、定義したとおりに実装するのはユーザの責任です。リンクは、CレベルのリンクがLRMで定義されています。しかしCレベルのリンケージというのは、型や引数は定義されていなくて、リンカでチェックは働きません。Cリンケージで外部にエキスポートされたシンボルというのは、変数なのか関数なのかさえ区別がありません。ですから間違うと簡単にクラッシュします。
<DLLの互換性 >
リンカが文句を言うのは、C++のリンクだからです。同一C++コンパイラ内のリンクでは、引き数や型のチェックが働きます。しかし、C++コンパイラ間の互換はありませんし、Cだけではなく、他の言語とのインターフェースの為に、DPIでは、Cリンケージになっています。厳密に言えば、Cリンケージにしても、レジスタ規約や、構造体のアライメントが同じでないと、各Cコンパイラで生成したDLL間の互換性が保証されません。X86については、ほぼ互換性はあると言ってよいと思いますが、X64では、GCC-VC++間のレジスタ規約が違うのでDLL間の互換性はない筈です。
ここまでする背景には、Cとのtransaction level 通信を徹底的に速くしたい、という要求があるのだと思います。
試しにVPIを使った場合に比べてどれ位になるか次のソースでやってみました。
DPI C ソースは、svdpi.h をインクルードし
一方、VPIについては、 vpi_user.hをインクルードして次のようなCソースになります。
このように、単にインクリメント値を返すだけのファンクションですが、VPIでは、本題に行くまでにかなりのコードを書く必要があります。一方DPIの方は、ダイレクトの記述でOKです。
結果:
VeritakSV ver0.25 Q9550 Vista 32bit ,VC8++ release モードでコンパイル
| 項 | 時間 | ifdef 定義 |
| インライン記述 | 0.26sec | USE_INLINE |
| DPI | 0.42sec | USE_DPI |
| VPI | 21sec | USE_VPI |
| SV関数 | 0.8sec | USE_SV_FUNCTION |
DPIとVPIでは、50倍近い差となりました。この例は、単純にインクリメントだけですので、やや極端な結果になっています。
16.2.1.3 DPI-SC
C/C++とsystemverilogの通信を掌るのが、"DPI-C" で宣言されたfunctionや、taskです。では、systemc とのやり取りも同じでよいかというと、そこはまた別で、
"DPI-SC" として宣言します。 これは、ベンダユニークでLRM化されてはいませんが、他のベンダも多分同じです。VeritakSVでは、systemcもシングルカーネルで動きますが、内部スレディングの扱いが違います。そのために必要な宣言です。 なお、"DPI-C"と"DPI-SC" を間違えるとクラッシュします。
16.2.2 インポート
SystemVerilogからCの関数・タスクを呼び出すことをインポートすると言います。
インポートの手順は、
SV側
1)インポート(関数またはタスク)を宣言する
C側
3)インポート(関数またはタスク)を定義する
のようになります。
<Cランタイムは直接呼べる>
ところで、上の例の場合、C側の関数でCのランタイムを呼び出しています。ユーザのSin関数は、結局のところCのランタイムを呼び出しているだけで少し冗長ですね。このようなCのランタイム関数呼び出しについては、SystemVerilogから直接呼び出すことが可能です。以下のように インポート定義を書くだけで、DLL作成は必要です。
16.2.3 エクスポート
SystemVerilogの関数やタスクがCから呼ばれることをエクスポートすると言います。エクスポートするには、まず、インポートすることから始める必要があります。
という順序が必然です。システムスケジューラ(シミュレーションカーネル)は、SystemVerilog側にあるので、必ずこのような形態になります。
SV側:
1)インポート宣言
2)インポートファンクション・タスクを起動
3)エクスポート宣言
4)エクスポートファンクション・タスクの中身
C側:
5)Cインポートファンクション
printfの代わりにVPIファンクションvpi_printfを使っているのは、SV上のコンソールにSV/C両方の結果が出るからです。
SV側全体です
実行結果です。
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
私は、C のimport_func()です。
私は sv_inc 関数です。
私はCです。 sv_incを呼び出した結果=101が返ってきました。
**** Test Done. Total 3.00[msec] ****
SV-Cを行ったり来たりしていますが、Cのファンクションの中身と結果は、直線的で分かりやすいと思います。
<functionとtaskの違い>
この例では、SV・C共にfunctionだけを使っていますが、functionからtask を呼べないのは、DPIでも同じです。ディレイや、イベント待ちを処理するのは、verilogと同様にtask
しか書けませんので、そういう場合には、taskにします。逆に、ディレイやイベント待ちがないのなら、functionを使うのが速度的に有利です。functionの実装は、多くの実装の場合Cのスタックフレームをそのまま使うと思います。スレッド処理(コンテキスト保存)が不要なためtaskより高速です。 SystemVerilogでは、上の例のように、値を返さない void
funtion が使用可能になっているので、 ディレイや、イベント待ちがない場合は、functionを使うとよいでしょう。
<pure と context >
pureは、ここは、オプティマイズが可能だよ、とシミュレータに教えてやるためのものです。結果が引き数だけで決まるものの場合、pureをつけることができます。inoutや、output
ポートを持つものは、pureにすることはできません。高度なコンパイラでは意味があるかもしれませんが、現状VeritakSVでは使っていませんのであってもなくても変りません。他のコンパイラならば、影響する可能性はあるかもしれません。安全サイドは、pureなしです。
しかし、contextは、違います。あるべきところにcontextが附加されていないと簡単にクラッシュします。SVコンパイラは、インポートの中身(Cの中身)について知ることはできないので、ユーザサイド側からコンパイラに教えてやるべきことになります。逆に、contextがなくてもよいところにcontextがあるとどうなるかというと、挙動には影響がなく速度低下だけです。ですから安全サイドは、contextをつけることになります。
T.B.D.
<インポートタスクの例>
SV側記述
C側記述:
結果:
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
私は、C のimport_task()です。
私はCです。 現在時刻は10です。
**** Test Done. Total 1.00[msec] ****
今度は、インポートタスク内で、ディレイを作りたいと思います。しかし、C言語では、Verilogのような並列プロセスを記述する機構がありません。そこで、ディレイやイベント類は、SV側で面倒を見てもらうことにします。時間を消費するtaskを呼び出します。
SV側:
C側:
実行結果です。fork join 内のステートメントは平行に走ります。上のC記述が、SystemVerilogタスクを記述するように書けることに注目してください。
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
私はCです。 現在時刻は1です。これから#10後に戻ってきます。
..............SV実行中です。時刻= 3
..............SV実行中です。時刻= 5
..............SV実行中です。時刻= 7
..............SV実行中です。時刻= 9
私はCです。 戻ってきました。現在時刻は11です。
..............SV実行中です。時刻= 11
..............SV実行中です。時刻= 13
join しました。
**** Test Done. Total 5.00[msec] ****
これにより、SVとC/C++間の同期通信が全く簡単になることがお分かりでしょう。CでSVタスクを呼んだ後でもCのコンテキストは、何事もなかったのごとく保存されていますので、ユーザは、自身の仕事に専心できるでしょう。
下の図は、筆者のイメージするSV-C++モデルです。Mainは、従来どおり、SVスケジューラになります。Initail Import TaskでC++オブジェクトモデルを生成します。この場合のImport Taskは、SIMの全期間を通じて生きるイメージです。ただし、ImportTaskはDynamicに生成され、ImportTaskを抜けたところでDeleteされますので、あくまでDynamicなObjectです。一旦、Import Taskが呼び出されれば、後は、SVとの同期通信は、Export Taskを使用してC++
側に立って書くことが出来ます。

<インポートとエクスポートの例>
下の例では、fork join 間でスレッドを4つ平行に起動しています。内一つは、インポートtask
で他の3つのスレッドと平行して動きます。
このCプログラムです。
結果です。時刻30でイベントをトリガ、時刻40でレベルをHighにしてトリガしています。C上では、それらのイベントにより、次のステップ行にいくのが分かると思います。
この例のようにVerilogは、イベントコントロールが豊富です。これらを駆使することにより、Cモデルとの通信が容易になるのではないかと思います。
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
私はCです。 現在時刻は1です。これから#10後に戻ってきます。
..............SV実行中です。時刻= 3
..............SV実行中です。時刻= 5
..............SV実行中です。時刻= 7
..............SV実行中です。時刻= 9
私はCです。 戻ってきました。現在時刻は11です。5CLK待ちます。
..............SV実行中です。時刻= 11
..............SV実行中です。時刻= 13
..............SV実行中です。時刻= 15
..............SV実行中です。時刻= 17
..............SV実行中です。時刻= 19
私はCです。 戻ってきました。現在時刻は20です。トリガを待ちます。
..............SV実行中です。時刻= 21
..............SV実行中です。時刻= 23
..............SV実行中です。時刻= 25
..............SV実行中です。時刻= 27
..............SV実行中です。時刻= 29
..............SV実行中です。時刻= 31
私はCです。 戻ってきました。現在時刻は31です。レベルがHighになるのを待ちます。
..............SV実行中です。時刻= 33
..............SV実行中です。時刻= 35
..............SV実行中です。時刻= 37
..............SV実行中です。時刻= 39
私はCです。 戻ってきました。現在時刻は41です。C importを抜けます。
..............SV実行中です。時刻= 41
..............SV実行中です。時刻= 43
join しました。 43
**** Test Done. Total 12.00[msec] ****
16.2.4 C++モデルとの接続
<Cポインタは、chandleで受ける>
Cポインタ専用の型としてchandleが用意されています。 しかし、これは、ポインタを一時的に受けるための便宜的な型であって、その演算の種類は、極めて限定されています。下の例はchandleに関する可能な演算の殆ど全てです。++とか、--とかは、言語仕様的に(意図的に)できないようになっています。 ですので、chandle
は、DPIを使うときだけ存在意義があります。
<C++の仮想関数を使う例>
C++側で、テストベンチを組みたいとします。往々にして、C++クラスのメンバー関数を直接呼び出したくなりますが、DPIは、Cリンケージで接続されるためにそれは、できません。 次善の策として、C側でラッパー関数を定義してC++メンバー関数を呼び出しています。functionで呼び出せば、C++に比しても殆どオーバヘッドはなく高速に処理することができます。
結果です。
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
私はベースクラスです。
私は子クラスです。
私は孫クラスです。
私はベースクラスです。
私は子クラスです。
私は孫クラスです。
**** Test Done. Total 2.00[msec] ****
SVコードです。chandleは、C++の型を全く理解していません。単なるホルダーにすぎないことに注意してください。
C++コードです。
16.2.5 DPIのコンパイル・リンク手順
VeritakSVのコンパイル・リンク手順については、こちらをご参照ください。
ベンダにより、違うと思いますが、概ね下図の手順です。
1)SVソースを読み込み、SVコンパイラは、ヘッダファイルを生成する。エクスポートがあった場合は、ラッパCファイルを生成する
2)C++コンパイラによりエクスポートDLLを生成する(エクスポートがない場合は、このステップは不要です。)
3)C++コンパイラによりインポートDLL(ユーザC++用)を生成する
4)シミュレータ本体と上記DLLをリンクしてシミュレーションを実行する

16.2.6 DPIのデバッグ
DPI周りは、間違えると簡単にクラッシュします。また、一見動いているように見えてもおかしな動作をすることも考えられます。DLLは、シミュレータプロセスにマッピングされ同一プロセスです。従いシミュレータ内のオブジェクトに対して保護は働きません。そこで、VC++デバッガの登場になるのですが、Cのスタックフレームや、Cの呼び出し規約に関する知識が必要になる場合があるので、下で簡単に触れておきます。
16.2.6.1 Cのスタックフレーム
下は、Cの関数の定石です。プロログ、中身、エピログの3つに分けられます。プロログの目的は、関数内で使うローカルスタックの確保及び、ebpの設定、エピログは、確保したスタックの廃棄とebpの値を元に戻すことです。
//プロログコード push ebp mov ebp, esp sub esp, ローカルスタック確保量 ~中身 //エピログコード leave ret
leave というのは複合命令で、
mov esp,ebp pop ebp
と等価です。
こうすると n番目の引き数は[ebp+(n+1)*4]でアクセスできることになります。
つまり、Cの関数では、渡された引き数及び、内部のローカル変数は、すべて[ebp+index]
でアクセスできることになります。コンパイラは、楽にコード生成できますね。
C言語では、関数の呼び出しがネストしますが、その度に下の新しいスタックフレームが作られます。視覚的には、上方向(アドレスが低くなる方向)に伸びます。関数から戻るときには、下に巻き戻る訳です。デバッガは、このスタックフレームを見て現在の関数がどのようにして呼ばれたか(呼び出し履歴)を表示できます。
上記のコードは、少なくともGCC・VC++共デバッグモードでは、上記のコードを生成するはずです。DPIでのクラッシュ解析は、まず呼ばれたところで、引き数が正しく受け渡しされていることを確認することから始まります。うまく動いてくれれば、上記のような知識は不要です。
| ..... | |
| ローカル変数2 | ebp-4*2 |
| ローカル変数1 | ebp-4*1 |
| 関数の戻り番地 | ebp+0 |
| ebpの前の値 | ebp+4 |
| 1番目の引き数 | ebp+4+4 |
| 2番目の引き数 | ebp+4+4*2 |
| 3番目の引き数 | ebp+4+4*3 |
16.2.6.2 VC++の呼び出し規約
まず、C の呼び出し規約は3種類あります。VC++ の名前で言うと __cdecl, __stdcall,
__fastcall がありますが、DPIでは、__cdeclのみが使われています。(ちなみにC++のメンバ関数呼び出しでは、
thiscall というのもあります。これは、インスタンスアドレスをECXに入れておいてアクセスします。DPIでは使いません。)
引数はスタックに push して渡します。32bit 以下の引数は全て 32bit に拡張されてスタックに push されます。push する順番は引数リストの右から左の順番です。戻り値は 32bit に拡張され、eax で返されます。64ビットの場合は、 edx:eax のペアで返されます。浮動小数点変数は 浮動小数レジスタst(0) で返されます。
たとえば、svlogic は、charで1バイトです。これを入力とする関数は、C表記でも charになりますが、実際にスタックにpushされるのは、32ビット=4バイトでpushされます。64bitの場合は、4バイトx2分pushされます。つまり、全ての変数引渡しは、32ビット単位にアラインされているということが言えます。
レジスター使用規約
EAX, ECX, EDX は作業用レジスタで、 関数の中で破壊してかまいません
EBX, ESI, EDI, EBP は関数呼び出し前後で保存されなければなりません
EFLAGS は、方向フラグが前方方向でなければならないことを除いて、 関数呼び出しの間で破壊されると仮定します
関数呼び出し時には FPU スタックは空になっていること
FPU の制御ワードは関数呼び出し前後で保存されなければなりません
浮動小数点数の返値は FPU スタックで返却される。 使わない場合も呼び出し側でスタックをクリアしなければなりません
16.2.7 引数の渡し方
SVでは、多くの場面で4値が使われるでしょう。一方Cでは2値しかありませんから、インターフェースのためにマッピングが必要になります。DPIでは、このエンコードを次のように規定しています。
| SV | DPI ENCODE |
| 0 | 0 |
| 1 | 1 |
| X | 3 |
| Z | 2 |
ですので、1ビット表すのに2ビットが必要になります。ところで、このままでは、複数のビット幅の変換が面倒です。そこでDPIでは多ビットように次の構造体を定義しています。この定義は、VPIでも同じですが、aが上表のLSB,bがMSBを指しています。
typedef struct vpi_vecval {
uint32_t aval;//LRM2005のa は誤り
uint32_t bval;//LRM2005のb は誤り
} s_vpi_vecval, *p_vpi_vecval;//4値でインターフェースする場合
/* (a chunk of) packed bit array */
typedef uint32_t svBitVecVal;//2値でインターフェースする場合
//これらの定義は、シミュレータベンダが提供する"svdpi.h"に書いてあります。
たとえば、10進数の10は、ZXがないので、b=0;aにそのまま10を入れればよいです。
a=10; b=0;
32'hzは、
a=0; b=0xffff_ffff;
16'hfffxは、
a=0xffff; b=0x000f;
と表現できます。 ビット単位の表現形式をスカラ、複数ビット単位の表現形式をベクタ形式と呼びます。
この構造体で、1ビットから32ビットまでのビット幅を表現できます。33ビット以上のビット幅は、この構造体のアレーになります。使用していないビットは、undetermined になります。ユーザは、マスク・符号拡張を行う必要があります。
16.2.3.1 input 引数
引数の受け渡しは、原則的に"値渡し"か"ポインタ渡し"のいずれかです。([ ]アレーは例外でハンドル渡し) "値渡し"は、小さいサイズの引数でのみ次のように定義されており、これ以外は、すべて"ポインタ渡し"になります。
要するに1ビット(bit/logic) もしくは、2値primitive でのみです。(これらは、C リンケージでは、4バイトもしくは、8バイトで固定サイズです。)
SV タイプとC typeの対応は次の通りです。
| SystemVerilog type | C type |
| byte | char |
| shortint | short int |
| int | int |
| longint | long long |
| real | double |
| shortreal | float |
| chandle | void* |
| string | const char* |
| bit | unsigned char |
| logic | unsigned char |
16.2.8.1 function戻り値
以下の例は、全て値渡しの例です。inputのパラメータは、スタックに積まれ、function戻り値は、全てレジスタ(EAX/EDX/FLOAT
REG)で返ります。これは、DPIの規格という訳ではなく、各Cコンパイラの規約によります。幸いにして、__cstd
に関してはGCC/VC/TCC皆同じです。
下の例に stringをプラスしたものが、function戻り値で、定義できる全てです。これ以外は、レジスタで返せるほど小さいオブジェクトではありません。例えばintegerは、32ビットの4値ですが、これは、次の章のinout
または、ouput ポートで返します。なお、DPIでは、ref ポートは定義できません。
SV側:
C側:
svdpi.h
結果です
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
b0=1
logic0=x
byte0=11111111
shortint0=1111111111111111
int0= -1
longint0=ffffffffffffffff
shortreal0=-1.000000
real0=-1.000000
**** Test Done. Total 4.00[msec] ****
16.2.7.2 input/output/inout 引数
inputは、値渡し、ポインタ渡しと2種類あります。少し面倒ですが、SVコンパイラが出力するヘッダファイルの通りにすればOKです。
inout/outputは、ポインタ渡しのみです。メモリの確保は、呼び出し側が行います。
注意するべきは、bit[7:0]は、charではないことです。これは、先のsvBitVecVal構造体のポインタ渡しになります。b[15:0]
が short int でないのは同じです。
struct やunion も引数として渡せます。これらは、一次元の等価なpacked vectorとして2値の場合は、svBitVecVal構造体,4値の場合は、s_vpi_vecval構造体/アレーのポインタ渡しで行えます。
C++側:
ヘッダファイル(シミュレータが自動生成)
C++ソースファイル
結果です
***** Veritak SV Engine Version 0.25 Build Oct.23.2009 *****
................ SV logic0=z
................ SV logic0=x
svlogic0=1
svlogic0=3
................ SV bit0=0
................ SV bit0=1
svbit0=0
svbit0=1
................ SV byte0=7f
byte0=1
byte0=2
short0=1234
short0=1122
int0=12345678
int0=aabb1234
integer={deadbeaf,0}
integer={aabb1234,0}
longint0=abcd1234
longint0=12345678
real0=-1.000000
real0=-11.000000
shortreal0=-1.000000
shortreal0=-21.000000
bitvec0=af
bitvec0=aa
logicvec0={af,0}
logicvec0={aa,0}
**** Test Done. Total 5.00[msec] ****
Notes:
(LRM 1145p に import "DPI-C" context MyCFunc = function integer MapID(int portID);とありますがintの間違いだと思います。)
16.2.9 配列引渡し
配列引渡しの方法は、2通りあります。一つは、生データのポインタを渡す方法です。この方法は、送り側と受け取り側で、配列の種類、大きさ等のPropertyを知っている必要があります。配列サイズを変更したら、受け取る側でもメンテしなければしなければいけない(再コンパイルが必要)のでメンテナンス性はよくありませんが、高速な受け渡しが可能です。もう一つの方法は、シミュレータの内部オブジェクトハンドルを使う方法です。配列を受け取る側は、配列の大きさや次元数等の情報を問い合わせしてからアクセスすることになるのでメンテナンス性ははよいです。しかしその分遅くなってしまいます。
16.2.9.1packed array
サンプルソースです。
これに対して、自動生成されたCのヘッダファイルが次です。
このように配列の大きさの情報が消えてしまいました。ポインタで渡される為です。
設計者は、配列の大きさは、128ビットであることが分かっているので、例えば、次のように書けます。
SV_PACKED_DATA_NELEMSは、sv_dpi.hで定義されているマクロで、ビット幅からチャンク数に変換します。この場合、128ビット幅のVectorを収納可能な、svLogicVecVal構造体の要素数を返します。
結果です。
***** Veritak SV Engine Version 0.27 Build no.27.2009 *****
SVからデータを送ります。 12345678aaaabbbbccccddddeeeeffff
SVから来たデータを表示します。
mem[ 0]={eeeeffff, 0}
mem[ 1]={ccccdddd, 0}
mem[ 2]={aaaabbbb, 0}
mem[ 3]={12345678, 0}
**** Test Done. Total 0.00[msec] ****
<配列を部分的に書き換える>
inout を使って書き換えます。
C++ソースです。inoutなので、constが付きません。svGetPartselLogicは、sv_dpi.hで定義されるAPIで、packed_arrayに対して、32ビット以下のパートセレクトを読み出しを返します。svPutPartselLogicは、同様にパートセレクト(32ビット以下)が書き込みになります。このAPIの制限事項は、
です。
結果です。
***** Veritak SV Engine Version 0.27 Build no.27.2009 *****
SVからデータを送ります。 12345678aaaabbbbccccddddeeeeffff
SVから来たデータを表示します。
packet[ 0]={eeeeffff, 0}
packet[ 1]={ccccdddd, 0}
packet[ 2]={aaaabbbb, 0}
packet[ 3]={12345678, 0}
パートセレクトAPIで packet[64 +:32]={aaaabbbb, 0}
Cで変更されたデータです。 12345678deadbeafccccddddeeeeffff
**** Test Done. Total 4.00[msec] ****
<packed arrayは、1次元のベクタ>
上のサンプルは、4値でしたが、2値にして、packed arrayを2次元にしてみたのが次です。packed
arrayは、SV上で多次元でも、1次元のvector として、渡されることに注意してください。[L:R]
は、[abs(L-R):0]と正規化された解釈でアクセスされます。