Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

Dynamic Scheduler版のVerilatorの中を調べる(その1)

@Vengineerの戯言 : Twitter SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

Verilatorは、オープンソースVerilog HDL/SystemVerilog対応のシミュレータです。 しかしながら、msyksphinzさんの下記のブログにあるように、

  • 全ての遅延記述 (#) は無視される。
  • event系のイベント (waitなど) はサポートされない。
  • Unknownステートはサポートされない

です。また、fork/join もサポートされていません。

msyksphinz.hatenablog.com

antmicro の Dynamic Scheduler 版 verilator

antmicro が github.com にて公開している Dynamic Scheduler では、上記の4つの内、3つ

  • 遅延記述 (#)
  • event系のイベント (waitなど)
  • fork/join

をサポートしています。

テストコード

Dynamic Scheduler のテストコードは、ここ にあります。この中には、

  • clock => #遅延
  • events => ->/@ event
  • fork => fork/join_xxx
  • wait => wait
  • pong => events & wait
  • uart => いろいろ使った例

というテストコードがあります。今回は、各テストコードに対して、Dynamic Scheduler版 Verilator がどのようなC++コードを生成するかを見ていきます。

clock

clockは、#遅延 のテストコードです。Verilog HDLコードは、clock.v です。initial 文の中で、#1 を使って遅延を生成しています。

/* verilator lint_off INFINITELOOP */
module t;
   logic clk;
   int   cyc = 0;

   initial begin
       forever begin
           clk = 1'b0;
           #1;
           clk = 1'b1;
           #1;
       end
   end

   always @(negedge clk) begin
      $write("[%2t] negedge; clk == %b\n", $time, clk);
   end

   always @(posedge clk) begin
      cyc <= cyc + 1;
      if (cyc == 10) begin
         $write("*-* All Finished *-*\n");
         $finish;
      end
      $write("[%2t] posedge; clk == %b\n", $time, clk);
   end

endmodule

テストコードのC++コードは、main.cppです。VerilatorオブジェクトのVtopを生成した後は、Verilated::goFinish() の while ループになっているだけです。

#include <verilated.h>
#include <Vtop.h>
#include <unistd.h>

vluint64_t main_time = 0;
double sc_time_stamp() { return main_time; }

int main(int argc, char *argv[]) {
    Verilated::commandArgs(argc, argv);
    Vtop *top = new Vtop;
    while (!Verilated::gotFinish()) {
        top->eval();
        main_time = top->timeSlotsEarliestTime();
    }
    top->final();
    delete top;
    return 0;
}

生成されたC++コード

生成されたC++コード(build後なのでオブジェクトコードもあります)は、以下のようになっていました。

$ ls build/clock
Vtop      Vtop.h   Vtop__ALL.a    Vtop__ALL.d  Vtop__Slow.cpp  Vtop__Syms.h  Vtop__verFiles.dat  main.d  verilated.d
Vtop.cpp  Vtop.mk  Vtop__ALL.cpp  Vtop__ALL.o  Vtop__Syms.cpp  Vtop__ver.d   Vtop_classes.mk     main.o  verilated.o
  • Vtop.h
  • Vtop.cpp
  • Vtop__Slow.h
  • Vtop__Syms.h
  • Vtop__Syms.cpp

が top.v から生成されたC++コードです。#遅延は、initial 文で使っているので、Vtop__Slow.cpp の中のコードに対応するコードが生成されているはずです。 (なぜ?Vtop__Slow.cpp なのかは? Verilatorの中を調べる(その2)の「Verilog HDL の initial 文はどうなっているのか?」をチェックしてみてください)

initial__TOP__5 メソッドの中が initial 文に対応するコードになります。// Body というコメントから下のコードが initial 文の中の while ループに対応しています。

void Vtop::_initial__TOP__5(Vtop__Syms* __restrict vlSymsp, VerilatedThread* self) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_initial__TOP__5\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    while (1U) {
        {
            std::unique_lock<std::mutex> lck(vlTOPp->t__DOT__clk.mtx());
            vlTOPp->t__DOT__clk.assign_no_lock(0U);
        }
        self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
        if (self->should_exit()) return;
        {
            std::unique_lock<std::mutex> lck(vlTOPp->t__DOT__clk.mtx());
            vlTOPp->t__DOT__clk.assign_no_lock(1U);
        }
        self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
        if (self->should_exit()) return;
    }
}

最初のブロック

        {
            std::unique_lock<std::mutex> lck(vlTOPp->t__DOT__clk.mtx());
            vlTOPp->t__DOT__clk.assign_no_lock(0U);
        }

は、

        clock = 1'b0;

の部分になります。clock に 0 を non-blocking で assign しています。

次の

        self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
        if (self->should_exit()) return;

は、

        #1;

の部分になります。wait_for_time メソッドにて、VL_TIME_Q() + 1U を設定しています。VL_TIME_Qマクロは、verilator/include/verilated.h の中で、(static_cast(vl_time_stamp64())) になっています。vl_time_stamp64() は現在のシミュレーション時間です。つまり、現在の時間 + 1U までウエイトすることをになります。ウエイト後に、何らかの理由でシミュレーションを終了する必要かどうかを should_exit メソッドで確認しています。

次のブロック

        {
            std::unique_lock<std::mutex> lck(vlTOPp->t__DOT__clk.mtx());
            vlTOPp->t__DOT__clk.assign_no_lock(1U);
        }

は、

        clock = 1'b1;

の部分になります。clock に 0 を non-blocking で assign しています。

最後の

        self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
        if (self->should_exit()) return;

は、

        #1;

の部分になります。前の #1 と同じです。

_initial__TOP__5 メソッドは、_eval_initial2 メソッド内で次のように呼ばれています。_initial__TOP__5 メソッドを thread で実行しています。

void Vtop::_eval_initial2(Vtop__Syms* __restrict vlSymsp) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_eval_initial2\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    static bool triggered__initial__TOP__5;
    if (!triggered__initial__TOP__5) {
        auto* _initial__TOP__5__thread = thread_pool.run_once([vlTOPp,vlSymsp] (VerilatedThread* self) {
                vlTOPp->_initial__TOP__5(vlSymsp, self);
            }, "_initial__TOP__5");
        triggered__initial__TOP__5 = true;
    }
}

_eval__initial2 メソッドは、_eval__initial メソッドの中で次のように呼ばれています。_eval__initial メソッドは、Vtop.cppの _eval_initial_loop メソッドで呼ばれています。

void Vtop::_eval_initial(Vtop__Syms* __restrict vlSymsp) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_eval_initial\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    vlTOPp->_eval_initial1(vlSymsp);
    if (Verilated::gotFinish()) return;
    vlTOPp->_eval_initial2(vlSymsp);
    if (Verilated::gotFinish()) return;
    vlTOPp->_eval_initial3(vlSymsp);
}

wait_for_time メソッド

wait_for_time メソッドは、verilator/include/verilated.cpp の中で、VerilatedThreadクラスのメソッドとして次のように定義されています。ここでも should_exit メソッドが使われていますね。 Verilated::timedQPush メソッドで時間(time) をキューに入れて、while ループで timedQWait メソッドで時間が経過するのを待つ、という感じですね。

void VerilatedThread::wait_for_time(VerilatedSyms* symsp, vluint64_t time) {
    std::unique_lock<std::mutex> lck(m_mtx);
    Verilated::timedQPush(symsp, time, this);
    set_idle(true);
    while (m_idle && !should_exit()) { Verilated::timedQWait(symsp, lck); }
    set_idle(false);
}

Verilated::timedQXXXメソッドは、verilator/include/verilated.cpp 次のようになっています。VerilatdSymsクラスの__Vm_timedQp クラス変数に対して、push, active, m_cv.wait を実行しています。

void Verilated::timedQPush(VerilatedSyms* symsp, vluint64_t time,
                           VerilatedThread* thread) VL_MT_SAFE {
    VerilatedSyms::__Vm_timedQp.push(time, thread);
}
void Verilated::timedQActivate(VerilatedSyms* symsp, vluint64_t time) VL_MT_SAFE {
    VerilatedSyms::__Vm_timedQp.activate(time);
}

void Verilated::timedQWait(VerilatedSyms* symsp, std::unique_lock<std::mutex>& lck) VL_MT_SAFE {
    VerilatedSyms::__Vm_timedQp.m_cv.wait(lck);
}

verilator/include/verilated.h ファイルの VerilatedSymsクラスの中では、次のように宣言されています。

class VerilatedSyms VL_NOT_FINAL {
public:  // But for internal use only
#ifdef VL_THREADED
    VerilatedEvalMsgQueue* __Vm_evalMsgQp;
#endif
    static VerilatedTimedQueue __Vm_timedQp;
    VerilatedSyms();
    ~VerilatedSyms();
};

__Vm_timedQp クラス変数は、VerilatedTimedQueue クラスのようで、verilator/include/verilated_imp.h 内で宣言されています。push メソッドとactiveメソッドは、次のようになっています。 push メソッドで、m_timeqキューに (time, thread) を push し、activate メソッドではm_timeqキューのトップにあるものの時間(time)と比較し、時間が経過したら、m_timeqキューのトップにあるthread を idle から activate にして、m_timeqキューから pop している感じです。となると、activate メソッド(Verilated::timedQActiveメソッド)がどこかで呼ばれているはずです。

    /// Push to activate given event at given time
    void push(vluint64_t time, VerilatedThread* thread) VL_EXCLUDES(m_mutex) VL_MT_SAFE {
        VL_DEBUG_IF(if (VL_UNLIKELY(time < VL_TIME_Q())) Verilated::timeBackwardsError(););
        VerilatedLockGuard lock{m_mutex};
        m_timeq.push(std::make_pair(time, thread));
    }
    /// Activate and pop all events earlier than given time
    void activate(vluint64_t time) VL_EXCLUDES(m_mutex) VL_MT_SAFE {
        VerilatedLockGuard lock{m_mutex};
        while (VL_LIKELY(!m_timeq.empty() && m_timeq.top().first <= time)) {
            VerilatedThread* thread = m_timeq.top().second;
            thread->idle(false);
            VL_DEBUG_IF(VL_DBG_MSGF("+    activate %p\n", thread););
            m_timeq.pop();
        }
        m_cv.notify_all();
    }

Verilated::timedQActivateメソッドは、生成されたC++コードである Vtop.cpp の eval_step メソッドの中で呼ばれていました。ウエイトしているthreadの内、現在の時間でactiveになるものを起こします。

void Vtop::eval_step() {
    VL_DEBUG_IF(VL_DBG_MSGF("+++++TOP Evaluate Vtop::eval\n"); );
    Vtop__Syms* __restrict vlSymsp = this->__VlSymsp;  // Setup global symbol table
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
#ifdef VL_DEBUG
    // Debug assertions
    _eval_debug_assertions();
#endif  // VL_DEBUG
    // Initialize
    if (VL_UNLIKELY(!vlSymsp->__Vm_didInit)) {
        _eval_initial_loop(vlSymsp);
        thread_registry.wait_for_idle();
    }
    Verilated::timedQActivate(vlSymsp, VL_TIME_Q());
    // Evaluate till stable
    int __VclockLoop = 0;
    QData __Vchange = 1;

eval_step メソッドは、Vtop.hの中の eval メソッドから次のように呼ばれています。なお、eval メソッドは、テストベンチ側のコードで使われます。

    void eval() { eval_step(); }

should_exit メソッド

should_exit メソッドは、verilator/include/verilated.h のVerilatedThreadクラスの中で次のように定義されています。m_should_exitメンバー変数にtrueが設定されているかを確認するだけで、どこかで should_exit(bool e) メソッドが実行されたかをチェックすることになりますね。should_exit(bool e)メソッドは、Verilated::Threadクラスのdeconstructorで呼ばれていました。

    bool should_exit() { return m_should_exit; }

    void should_exit(bool e) {
        std::unique_lock<std::mutex> lck_d(m_mtx);
        m_should_exit = e;
        m_cv.notify_all();
    }

終わりに

今回は、Antmicro の Dynamic Scheduler版のverilatorにおいて、#遅延 がどのように実装されているのかをみてみました。

#遅延 を実装するために内部で thread を作り、この thread 内で 動いたり、止まったりするようにして実装しているようです。

次回は、events (-> や @event ) の実装についてみてみます。