FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Verilatorのコンパイルフローを観察する (1. 組み合わせ回路を生成するまで)

少し気になってVerilatorの中味を調査している。VerilatorはSystem Verilogのファイルを読み込んでそれをC++に変換しシミュレーションするためのツールだが、VerilogのファイルがどのようなC++ファイルに変換されているのか気になったので見てみることにした。

このモチベーションは、割と巨大なプロジェクトをVerilatorで読み込むと変換すらできなくなってしまうくらいにVerilatorが重たくなってしまうことだ。これの原因が何なのか、原因が分かればラッキーだし、Verilatorのシミュレーションの仕組みについても見てみたい。

例として、以下のような非常にシンプルなデザインを作成した。クロックは存在せず、単純にinからoutへ接続されているだけだ。

module simple_assign
  (
   input logic [ 4: 0]  in,
   output logic [ 4: 0] out
   );

assign out = in;

endmodule // simple_assign

これにWrapperを付けて、Verilatorでコンパイルする。すると、obj_dirに生成C++ファイルとオブジェクトファイルが生成される。

make
verilator --cc --exe  simple_assign.sv -exe tb_simple_assign.cpp
make -C obj_dir -f Vsimple_assign.mk

これの中味を見てみよう。まずはobj_dir/Vsimple_assign.hから。これにはC++クラスが定義されている。

// VL_MODULEはverilated.hに定義されている:
// /// Declare a module, ala SC_MODULE
// #define VL_MODULE(modname) class modname : public VerilatedModule
  VL_MODULE(Vsimple_assign) {
   public:

// VL_IN8とVL_OUT8はverilated.hに定義されている:
// typedef vluint8_t    CData;     ///< Verilated pack data, 1-8 bits
// #define VL_IN8(name, msb, lsb) CData name  ///< Declare input signal, 1-8 bits
// #define VL_OUT8(name, msb, lsb) CData name  ///< Declare output signal, 1-8 bits
     VL_IN8(in,4,0);
     VL_OUT8(out,4,0);

     // INTERNAL VARIABLES
     // Internals; generally not touched by application code
     Vsimple_assign__Syms* __VlSymsp;  // Symbol table

     // CONSTRUCTORS
   private:
     VL_UNCOPYABLE(Vsimple_assign);  ///< Copying not allowed

eval()メソッドはpublicとして公開されており、実体はeval_step()を呼び出している。eval_step()は論理が落ち着くまで(100回程度)回路の評価を繰り返している。

評価の中味はおそらく_eval()の中なので、これを覗いてみよう。

 void Vsimple_assign::eval_step() {
 /* ... 中略 ... */
     do {
         VL_DEBUG_IF(VL_DBG_MSGF("+ Clock loop\n"););
         _eval(vlSymsp);
         if (VL_UNLIKELY(++__VclockLoop > 100)) {
             // About to fail, so enable debug to see what's not settling.
             // Note you must run make with OPT=-DVL_DEBUG for debug prints.
             int __Vsaved_debug = Verilated::debug();
             Verilated::debug(1);
             __Vchange = _change_request(vlSymsp);
             Verilated::debug(__Vsaved_debug);
             VL_FATAL_MT("simple_assign.sv", 1, "",
                 "Verilated model didn't converge\n"
                 "- See DIDNOTCONVERGE in the Verilator manual");
         } else {
             __Vchange = _change_request(vlSymsp);
         }
     } while (VL_UNLIKELY(__Vchange));

_eval()の中では、組み合わせ回路を動かすための記述_combo__TOP__1()が呼び出されている。

 void Vsimple_assign::_eval(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_eval\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->_combo__TOP__1(vlSymsp);
 }

内部では、out変数のポインタに対してin変数のポインタの実体をコピーしている論理が見えた。これがassign)の記述となっている。

 VL_INLINE_OPT void Vsimple_assign::_combo__TOP__1(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_combo__TOP__1\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->out = vlTOPp->in;
 }

ではこれをout = in1 & in2のように変更するとどうなるか。生成されるC++ファイルは以下のように変化する。

 VL_INLINE_OPT void Vsimple_assign::_combo__TOP__1(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_combo__TOP__1\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->out = ((IData)(vlTOPp->in1) & (IData)(vlTOPp->in2));
 }