電気回路/HDL/Verilator で DPI-C の履歴(No.2)
更新DPI-C とは†
SystemVerilog の機能で、SystemVerilog で書かれたテストベンチから C あるいは C++ で書かれたルーチンを呼び出したり、その呼び出された C あるいは C++ で書かれたルーチンから SystemVerilog の task や function を呼び出したりするための規格です。
ModelSim XE を使ってやってみた結果は 電気回路/HDL/ModelSim XE を使った SystemVerilog DPI-C テスト にあります。
今回はこれをフリーのツールである Verilator でやろうという話。
関連記事†
SystemVerilog のソース†
dpic_test.sv
LANG:verilog(linenumber) `timescale 1ns / 1ps // トップモジュール module dpic_test; // 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); $finish; end endmodule
C++ モードでやってみる†
LANG:console $ ls dpic_test.sv $ verilator -cc dpic_test.sv $ ls dpic_test.sv obj_dir/ $ ls obj_dir/ Vdpic_test.cpp Vdpic_test__Dpi.cpp Vdpic_test__Syms.h Vdpic_test_classes.mk Vdpic_test.h Vdpic_test__Dpi.h Vdpic_test__ver.d Vdpic_test.mk Vdpic_test__Syms.cpp Vdpic_test__verFiles.dat $ less Vdpic_test__Dpi.h
としてみた結果は以下の通り。
obj_dir/Vdpic_test__Dpi.h
LANG:cpp(linenumber) // Verilated -*- C++ -*- // DESCRIPTION: Verilator output: Prototypes for DPI import and export functions. // // Verilator includes this file in all generated .cpp files that use DPI functions. // Manually include this file where DPI .c import functions are declared to insure // the C functions match the expectations of the DPI imports. #ifdef __cplusplus extern "C" { #endif // DPI EXPORTS // DPI Export at dpic_test.sv:7 extern void verilog_task (int i, int* o); // DPI IMPORTS // DPI Import at dpic_test.sv:16 extern int c_task (int i, int* o); #ifdef __cplusplus } #endif
うまく行っているようです。
あとは c_task を定義して、その中から verilog_task を呼び出せばいいはず。。。
あれ?
verilog_task がモジュールのメンバー関数ではなく静的関数になっているのはなぜでしょう?
obj_dir/Vdpic_test__Dpi.cpp
LANG:cpp(linenumber) // Verilated -*- C++ -*- // DESCRIPTION: Verilator output: Implementation of DPI export functions. // // Verilator compiles this file in when DPI functions are used. // If you have multiple Verilated designs with the same DPI exported // function names, you will get multiple definition link errors from here. // This is an unfortunate result of the DPI specification. // To solve this, either // 1. Call Vdpic_test::{export_function} instead, // and do not even bother to compile this file // or 2. Compile all __Dpi.cpp files in the same compiler run, // and #ifdefs already inserted here will sort everything out. #include "Vdpic_test__Dpi.h" #include "Vdpic_test.h" #ifndef _VL_DPIDECL_verilog_task #define _VL_DPIDECL_verilog_task void verilog_task (int i, int* o) { // DPI Export at dpic_test.sv:7 return Vdpic_test::verilog_task(i, o); } #endif
あら、Vdpic_test::verilog_task も静的関数なんですね。
モジュール内の信号を参照するような task だったらどうなるのか、 興味のわくところです。
使ってみる†
上記の dpic_test を動かしてみます。
dpic_test_main.cpp
LANG:cpp(linenumber) #include <verilated.h> // Defines common routines #include "Vdpic_test.h" #include "Vdpic_test__Dpi.h" #include "stdio.h" // Vdpic_test からこのルーチンがコールバックされる extern "C" int c_task (int i, int* o) { printf("Hello from c_task(%d)\n", i); verilog_task(i, o); /* さらに verilog の task をコールバックする */ return(0); /* Return success (required by tasks) */ } unsigned int main_time = 0; // Current simulation time double sc_time_stamp () { // Called by $time in Verilog return main_time; } int main(int argc, const char *argv[]) { Verilated::commandArgs(argc, argv); // Remember args Vdpic_test *top = new Vdpic_test(); // Create instance while (!Verilated::gotFinish()) { top->eval(); // Evaluate model main_time++; // Time passes... } top->final(); // Done simulating // // (Though this example doesn't get here) }
を作っておいて、
LANG:console $ verilator -cc dpic_test.sv --exe dpic_test_main.cpp $ cd obj_dir $ make -f Vdpic_test.mk $ ./Vdpic_test.exe Hello from c_task(1) Hello from verilog_task( 1) ret = 1 - dpic_test.sv:23: Verilog $finish
Verilator から System Verilog の dpi-c を利用できそうなことが確認できました。
task の結果が module 内の信号に依存する場合†
上での疑問を解消しておきます。疑問に思っているのは次の点です:
- System Verilog 内の verilog_task はモジュール dpic_test に属しているため、 内部の信号に自由にアクセスできます
- ところが、これが dpi-c でエクスポートされると C++ 側からは 普通のグローバルな関数になっています。
- すなわち、C++ で定義された verilog_task はモジュールに属していない
- 複数の dpic_test モジュールが実装されているときに、どの信号を 参照したら良いのかを正しく判別できるんだろうか?
この点を確認するため、上記のコードをちょっと変えて試してみました。
dpic_test.sv
LANG:verilog(linenumber) `timescale 1ns / 1ps // トップモジュール module dpic_test(input int i); int calculated = 2 * i; // verilog で書いた task task verilog_task(input int i, output int o); $display("Hello from verilog_task(%d)", i); $display("Value of calculated (%d)", calculated); o = 2 * i + calculated; 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); $finish; end endmodule
dpic_test_main.cpp
LANG:cpp(linenumber) #include <verilated.h> // Defines common routines #include "Vdpic_test.h" #include "Vdpic_test__Dpi.h" #include "stdio.h" extern "C" int c_task (int i, int* o) { printf("Hello from c_task(%d)\n", i); verilog_task(i, o); /* Call back into Verilog */ return(0); /* Return success (required by tasks) */ } unsigned int main_time = 0; // Current simulation time double sc_time_stamp () { // Called by $time in Verilog return main_time; } int main(int argc, const char *argv[]) { Verilated::commandArgs(argc, argv); // Remember args Vdpic_test *obj1 = new Vdpic_test(); // Create instance Vdpic_test *obj2 = new Vdpic_test(); // Create instance obj1->i = 1; obj2->i = 2; while (!Verilated::gotFinish()) { obj1->eval(); // Evaluate model obj2->eval(); // Evaluate model main_time++; // Time passes... } obj1->final(); // Done simulating obj2->final(); // Done simulating // // (Though this example doesn't get here) }
としてみました。
LANG:console $ verilator -cc dpic_test.sv -exe dpic_test_main.cpp $ obj_dir/ $ make -f Vdpic_test.mk $ ./Vdpic_test.exe Hello from c_task(1) Hello from verilog_task( 1) Value of calculated ( 2) ret = 4 - dpic_test.sv:26: Verilog $finish Hello from c_task(1) Hello from verilog_task( 1) Value of calculated ( 4) ret = 6 - dpic_test.sv:26: Verilog $finish - dpic_test.sv:26: Second verilog $finish, exiting
驚くべき事に、正しく動いています。
これは、DPI-C 経由で System Verilog から c_task を呼び出した際に、 そのスコープ情報をちゃんと保存してあって、 verilog_task からそのスコープ情報を参照することにより、 どのモジュールの信号を読むか判断しているおかげのようです。
Vdpic_test.cpp
LANG:cpp void Vdpic_test::verilog_task(int i, int* o) { VL_DEBUG_IF(VL_PRINTF(" Vdpic_test::verilog_task\n"); ); // Variables VL_SIG(i__Vcvt,31,0); VL_SIG(o__Vcvt,31,0); // Body static int __Vfuncnum = -1; if (VL_UNLIKELY(__Vfuncnum==-1)) { __Vfuncnum = Verilated::exportFuncNum("verilog_task"); } const VerilatedScope* __Vscopep = Verilated::dpiScope(); Vdpic_test__Vcb_verilog_task_t __Vcb = (Vdpic_test__Vcb_verilog_task_t)__Vscopep->exportFind(__Vfuncnum); i__Vcvt = i; (*__Vcb)((Vdpic_test__Syms*)(__Vscopep->symsp()), i__Vcvt, o__Vcvt); *o = o__Vcvt; }
Verilated::dpiScope というのがそれですね。
ややこしいことをしているものです・・・
とはいえ、これは Verilator に限った事じゃないですね。 DPI-C の規格からすると当然の実装方法と思えてきました。
あと、当然ですが ある Verilog モジュールから export された関数を 別のモジュールに inport された C の関数から呼び出すとおかしなことになりますね。
DPI-C を使って C から verilog 関数を呼び出す使い方は、 モジュール内に閉じた利用に限られるという制約があることを理解できます。
問題は・・・†
Verilator を使う限り、System Verilog 内で #123 や @(posedge signal) などの記法で自由に時刻を進めることができないので、 System Verilog をテストベンチのトップモジュールとするわけに 行かないのがつらいですね。