電気回路/HDL/Verilator で DPI-C

(1877d) 更新

公開メモ

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++ モードでやってみる

Verilator を使って上記 System Verilog ソースを 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

Vdpic_test.cpp/Vdpic_test.h が変換結果ですが、 Vdpic_test__Dpi.cpp/Vdpic_test__Dpi.h というのもできていて、 これが DPI-C インターフェースです。

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

このように、エクスポートされた verilog_task と、 インポートされる c_task の宣言が格納されています。

変換はうまく行っているようです。

あとは 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 =           2
- 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 モジュールが実装されているときに、どの信号を 参照したら良いのかを正しく判別できるんだろうか?

この点を確認するため、上記のコードをちょっと変えて試してみました。

verilog_task から dpic_test モジュール内の信号線 calculated を参照しているところが先ほどと異なります。 インスタンス化した2つの dpic_test モジュールで calculated の値は異なるので、C++ 側から呼び出された verilog_task が正しく値を読み取れるかどうかが見所です。

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

驚くべき事に、正しく動いています。

これは、System Verilog から DPI-C 経由で 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 された関数を 別のモジュールに import された C の関数から呼び出すとおかしなことになりますね。

DPI-C を使って C から verilog 関数を呼び出す使い方は、 モジュール内に閉じた利用に限られるという制約があることを理解できます。

問題は・・・

Verilator を使う限り、System Verilog 内で #123 や @(posedge signal) などの記法で自由に時刻を進めることができないので、 System Verilog をテストベンチのトップモジュールとするわけに 行かないのがつらいですね。

コメント




SystemVerilog DPIとの違い

[たっく] (2011-06-05 (日) 08:58:38)

SystemVerilog DPIでの鍵は、Cインポート関数内で、SVエクスポートタスクを呼ぶことにより自由に時間を消費したりイベントを待ったりしながら、あたかもinitialでverilogHDLを書くように、C側でテストベンチが書けることだと思います。

これは、SVカーネルスケジューラとCインポート関数のコンテキスト(スタックやレジスタ)保存機構によります。(C側は、普通のC関数でよいのですが、が、巧妙に隠蔽されていて、Cユーザは特に大きなスタックを使用しない限り特に気を配る必要がありません。)

Verilatorの場合は、スタティックなスケジュール機構しかないので、時間消費やイベント待ちタスクは呼べない、ということになろうかと思います。

  • そうですよね。Verilator では DPI-C に限らず Verilog 内で時間を進められないのが非常に強い制約になっているので、そこに DPI-C を付け加えてもあんまり恩恵がないと思いました。ログを取ったり、Verilog では記述が面倒な計算をしたり、くらいですかね・・・ -- [武内(管理人)]

Counter: 7585 (from 2010/06/03), today: 2, yesterday: 2