Vengineerの妄想

人生を妄想しています。

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

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

はじめに

Dynamic Scheduler版 Verilatorの中を調べる、その2。今回は、events (-> や @event) がどのように実装されているかを調べていきます。

Verilog HDLコード

Verilog HDLコード (examples/events.v) を以下に示します。event がたくさんあります。

  • always @(posedge clk) ブロックの中
  • initial 文
  • @(event)

の3種類でのテストコードのようです。

module t(clk);
   input clk;
   int   cyc = 0;
   event eventA;
   event eventB;
   event eventC;
   event eventD;
   event E1;
   event E2;

   logic[2:0] event_mask;

   always @(posedge clk) begin
      cyc <= cyc + 1;
      if (cyc > 2) $write("[%2t] event_mask == %5b\n", $time, event_mask);
      if (cyc == 0) begin
         for (int i = 0; i < 3; i++) begin
            $write("[%2t] waiting for event A, B, or C...\n", $time);
            @(eventA, eventB, eventC);
            $write("[%2t] got the event!\n", $time);
         end
      end
      else if (cyc == 3) begin
         ->E1;
         $write("[%2t] ->E1; E1.triggered == %b\n", $time, E1.triggered);
      end
      else if (cyc == 4) begin
         $write("[%2t] ->E2\n", $time);
         ->E2;
      end
      else if (cyc == 5) begin
         $write("*-* All Finished *-*\n");
         $finish;
      end
   end

   initial begin
     #2;
     $write("[%2t] triggering event D\n", $time);
     ->eventD;
     #1;
     $write("[%2t] triggering event A\n", $time);
     ->eventA;
     #1;
     $write("[%2t] triggering event B\n", $time);
     ->eventB;
     #1;
     $write("[%2t] triggering event C\n", $time);
     ->eventC;
   end

   always @(E1) begin
      $write("[%2t] @E1; E1.triggered == %b\n", $time, E1.triggered);
      event_mask[1] = 1;
   end

   always @(E2) begin
      $write("[%2t] @E2\n", $time);
      event_mask[2] = 1;
   end

endmodule

生成されたC++コード

まずは、initial 文の部分です。Vtop__Slow.cpp の _initial__TOP__1メソッドの中で次のように定義されていました。// Body コメント以降が対応するコードになります。

void Vtop::_initial__TOP__1(Vtop__Syms* __restrict vlSymsp, VerilatedThread* self) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_initial__TOP__1\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    self->wait_for_time(vlSymsp, VL_TIME_Q() + 2U);
    if (self->should_exit()) return;
    VL_WRITEF("[%2t] triggering event D\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    /* [ -> statement ] */
    vlTOPp->t__DOT__eventD = 1;
    self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
    if (self->should_exit()) return;
    VL_WRITEF("[%2t] triggering event A\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    /* [ -> statement ] */
    vlTOPp->t__DOT__eventA = 1;
    self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
    if (self->should_exit()) return;
    VL_WRITEF("[%2t] triggering event B\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    /* [ -> statement ] */
    vlTOPp->t__DOT__eventB = 1;
    self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
    if (self->should_exit()) return;
    VL_WRITEF("[%2t] triggering event C\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    /* [ -> statement ] */
    vlTOPp->t__DOT__eventC = 1;
}

最初の部分、

    self->wait_for_time(vlSymsp, VL_TIME_Q() + 2U);
    if (self->should_exit()) return;
    VL_WRITEF("[%2t] triggering event D\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    /* [ -> statement ] */
    vlTOPp->t__DOT__eventD = 1;

が 以下の Verilog HDLコードに対応します。#遅延の部分は前回のブログで説明した通り、wait_for_timeメソッドとshould_exitメソッドから構成されます。vlTOPp->t__DOT__eventD = 1 が ->eventD に対応します。

     #2;
     $write("[%2t] triggering event D\n", $time);
     ->eventD;

initial 文内では、これ以降、eventA, eventB, eventC に対しても同じようなことをやっています。なお、eventDに対しては、->eventD はありますが、eventDを待っているコードはどこにもありません。

t__DOT__eventX メンバー変数は、Vtop.h によると、テンプレートの型がCDataのMonitordValue クラスのインスタンスのようです。MonitoredValueクラスはテンプレートの型の変数をメンバーに持つクラスですが、event として使用する場合は、1 と 0 のみを使用するようです。

eventA, eventB, eventC を待っている部分は、always @(posedge clk) ブロックの下記の部分です。

      if (cyc == 0) begin
         for (int i = 0; i < 3; i++) begin
            $write("[%2t] waiting for event A, B, or C...\n", $time);
            @(eventA, eventB, eventC);
            $write("[%2t] got the event!\n", $time);
         end
      end

always ブロックに対応するC++コードは、Vtop.cpp の_sequent__TOP__8 メソッドです。@(eventA, eventB, eventC) の部分は、if (VL_UNLIKELY((0U == vlTOPp->t__DOT__cyc))) の if文のブロック内です。同じようなコードが3回繰り返されますが、for (int i = 0; i < 3; i++) の forループで3回待っているのためです。

L_INLINE_OPT void Vtop::_sequent__TOP__8(Vtop__Syms* __restrict vlSymsp, VerilatedThread* self) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_sequent__TOP__8\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    vlTOPp->verilated_nba_ctrl.schedule(vlTOPp->t__DOT__cyc,
                                        ((IData)(1U)
                                         + vlTOPp->t__DOT__cyc));
    if (VL_UNLIKELY(VL_LTS_III(1,32,32, 2U, vlTOPp->t__DOT__cyc))) {
        VL_WRITEF("[%2t] event_mask == %5b\n",64,(QData) (VL_TIME_UNITED_Q(1)),
                  3,(IData) ((IData)(vlTOPp->t__DOT__event_mask)));
    }
    if (VL_UNLIKELY((0U == vlTOPp->t__DOT__cyc))) {
        VL_WRITEF("[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */
        {
            CData __Vtc__tmp0 = vlTOPp->t__DOT__eventA;
            CData __Vtc__tmp1 = vlTOPp->t__DOT__eventB;
            CData __Vtc__tmp2 = vlTOPp->t__DOT__eventC;
            vlTOPp->t__DOT__eventA.assign_no_notify(0);
            vlTOPp->t__DOT__eventB.assign_no_notify(0);
            vlTOPp->t__DOT__eventC.assign_no_notify(0);
            self->wait_until([&__Vtc__tmp2, &__Vtc__tmp1, &__Vtc__tmp0](auto&& v) -> bool {
                    bool __Vtc__res = std::get<0>(v)
                    || std::get<1>(v)
                    || std::get<2>(v);
                    if (!__Vtc__res) {
                        __Vtc__tmp2 = std::get<2>(v);
                        __Vtc__tmp1 = std::get<1>(v);
                        __Vtc__tmp0 = std::get<0>(v);
                    }
                    return __Vtc__res;
                }, vlTOPp->t__DOT__eventA, vlTOPp->t__DOT__eventB, vlTOPp->t__DOT__eventC);
        }
        if (self->should_exit()) return;
        VL_WRITEF("[%2t] got the event!\n[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)),
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */
        {
            CData __Vtc__tmp0 = vlTOPp->t__DOT__eventA;
            CData __Vtc__tmp1 = vlTOPp->t__DOT__eventB;
            CData __Vtc__tmp2 = vlTOPp->t__DOT__eventC;
            vlTOPp->t__DOT__eventA.assign_no_notify(0);
            vlTOPp->t__DOT__eventB.assign_no_notify(0);
            vlTOPp->t__DOT__eventC.assign_no_notify(0);
            self->wait_until([&__Vtc__tmp2, &__Vtc__tmp1, &__Vtc__tmp0](auto&& v) -> bool {
                    bool __Vtc__res = std::get<0>(v)
                    || std::get<1>(v)
                    || std::get<2>(v);
                    if (!__Vtc__res) {
                        __Vtc__tmp2 = std::get<2>(v);
                        __Vtc__tmp1 = std::get<1>(v);
                        __Vtc__tmp0 = std::get<0>(v);
                    }
                    return __Vtc__res;
                }, vlTOPp->t__DOT__eventA, vlTOPp->t__DOT__eventB, vlTOPp->t__DOT__eventC);
        }
        if (self->should_exit()) return;
        VL_WRITEF("[%2t] got the event!\n[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)),
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */
        {
            CData __Vtc__tmp0 = vlTOPp->t__DOT__eventA;
            CData __Vtc__tmp1 = vlTOPp->t__DOT__eventB;
            CData __Vtc__tmp2 = vlTOPp->t__DOT__eventC;
            vlTOPp->t__DOT__eventA.assign_no_notify(0);
            vlTOPp->t__DOT__eventB.assign_no_notify(0);
            vlTOPp->t__DOT__eventC.assign_no_notify(0);
            self->wait_until([&__Vtc__tmp2, &__Vtc__tmp1, &__Vtc__tmp0](auto&& v) -> bool {
                    bool __Vtc__res = std::get<0>(v)
                    || std::get<1>(v)
                    || std::get<2>(v);
                    if (!__Vtc__res) {
                        __Vtc__tmp2 = std::get<2>(v);
                        __Vtc__tmp1 = std::get<1>(v);
                        __Vtc__tmp0 = std::get<0>(v);
                    }
                    return __Vtc__res;
                }, vlTOPp->t__DOT__eventA, vlTOPp->t__DOT__eventB, vlTOPp->t__DOT__eventC);
        }
        if (self->should_exit()) return;
        VL_WRITEF("[%2t] got the event!\n[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)),
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */
        {
            CData __Vtc__tmp0 = vlTOPp->t__DOT__eventA;
            CData __Vtc__tmp1 = vlTOPp->t__DOT__eventB;
            CData __Vtc__tmp2 = vlTOPp->t__DOT__eventC;
            vlTOPp->t__DOT__eventA.assign_no_notify(0);
            vlTOPp->t__DOT__eventB.assign_no_notify(0);
            vlTOPp->t__DOT__eventC.assign_no_notify(0);
            self->wait_until([&__Vtc__tmp2, &__Vtc__tmp1, &__Vtc__tmp0](auto&& v) -> bool {
                    bool __Vtc__res = std::get<0>(v)
                    || std::get<1>(v)
                    || std::get<2>(v);
                    if (!__Vtc__res) {
                        __Vtc__tmp2 = std::get<2>(v);
                        __Vtc__tmp1 = std::get<1>(v);
                        __Vtc__tmp0 = std::get<0>(v);
                    }
                    return __Vtc__res;
                }, vlTOPp->t__DOT__eventA, vlTOPp->t__DOT__eventB, vlTOPp->t__DOT__eventC);
        }
        if (self->should_exit()) return;
        VL_WRITEF("[%2t] got the event!\n",64,(QData) (VL_TIME_UNITED_Q(1)));
    } else {
        if (VL_UNLIKELY((3U == vlTOPp->t__DOT__cyc))) {
            /* [ -> statement ] */
            vlTOPp->t__DOT__E1 = 1;
            vlTOPp->t__DOT__E1__Vtriggered = 1;
            VL_WRITEF("[%2t] ->E1; E1.triggered == %b\n",
                      64,(QData) (VL_TIME_UNITED_Q(1)),
                      1,(IData) ((IData)(vlTOPp->t__DOT__E1__Vtriggered)));
        } else {
            if (VL_UNLIKELY((4U == vlTOPp->t__DOT__cyc))) {
                VL_WRITEF("[%2t] ->E2\n",64,(QData) (VL_TIME_UNITED_Q(1)));
                /* [ -> statement ] */
                vlTOPp->t__DOT__E2 = 1;
            } else {
                if (VL_UNLIKELY((5U == vlTOPp->t__DOT__cyc))) {
                    VL_WRITEF("*-* All Finished *-*\n");
                    VL_FINISH_MT("/home/vengineer/home/verilator/verilator-dynamic-scheduler-examples/examples/events/events.sv", 3
3, "");
                    return;
                }
            }
        }
    }
}

一回分を抽出したのが下記のコードです。wait_until メソッドを使って、eventA, eventB, eventC のいづれかの値が 1 になるまで待ちます。

        VL_WRITEF("[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */
        {
            CData __Vtc__tmp0 = vlTOPp->t__DOT__eventA;
            CData __Vtc__tmp1 = vlTOPp->t__DOT__eventB;
            CData __Vtc__tmp2 = vlTOPp->t__DOT__eventC;
            vlTOPp->t__DOT__eventA.assign_no_notify(0);
            vlTOPp->t__DOT__eventB.assign_no_notify(0);
            vlTOPp->t__DOT__eventC.assign_no_notify(0);
            self->wait_until([&__Vtc__tmp2, &__Vtc__tmp1, &__Vtc__tmp0](auto&& v) -> bool {
                    bool __Vtc__res = std::get<0>(v)
                    || std::get<1>(v)
                    || std::get<2>(v);
                    if (!__Vtc__res) {
                        __Vtc__tmp2 = std::get<2>(v);
                        __Vtc__tmp1 = std::get<1>(v);
                        __Vtc__tmp0 = std::get<0>(v);
                    }
                    return __Vtc__res;
                }, vlTOPp->t__DOT__eventA, vlTOPp->t__DOT__eventB, vlTOPp->t__DOT__eventC);
        }
        if (self->should_exit()) return;
        VL_WRITEF("[%2t] got the event!\n[%2t] waiting for event A, B, or C...\n",
                  64,(QData) (VL_TIME_UNITED_Q(1)),
                  64,(QData) (VL_TIME_UNITED_Q(1)));
        /* [@ statement] */

wait_until メソッドは、verilator/include/verilated.h の中で次のように定義されています。pred の戻り値がfalseの場合、wait_internal メソッドで f の中で done が true になるまでウェイトする感じです。

    template <typename P, typename... Ts>
    void wait_until(P pred, MonitoredValue<Ts>&... mon_vals) {
        std::atomic_bool done(false);
        auto f = [this, &done, pred, &mon_vals...]() {
            if (pred(std::forward_as_tuple(mon_vals...))) {
                done = true;
                m_cv.notify_all();
                set_idle(false);
            }
        };
        if (!pred(std::forward_as_tuple(mon_vals...)))
            wait_internal(done, MonitoredValueCallback(&mon_vals, f)...);
    }

wait_internal メソッドは、verilator/include/verilated.h の中で次のように定義されています。done が true になるまで、m_cv.wait(lck) を繰り返します。

    // This function is needed to create MonitoredValueCallbacks in place (as arguments) instead of copying/moving them
    void wait_internal(std::atomic_bool& done, MonitoredValueCallback&&...) {
        std::unique_lock<std::mutex> lck(m_mtx);
        set_idle(true);
        while (!should_exit() && !done)
            m_cv.wait(lck);
    }

->E1 と E1.triggerd の部分は、下記のようになっています。

  • ->E1 は、vlTOPp->t__DOT__E1 = 1
  • E1.triggerd は、vlTOPp->t__DOT__E1__Vtriggered = 1

になっています。

        if (VL_UNLIKELY((3U == vlTOPp->t__DOT__cyc))) {
            /* [ -> statement ] */
            vlTOPp->t__DOT__E1 = 1;
            vlTOPp->t__DOT__E1__Vtriggered = 1;
            VL_WRITEF("[%2t] ->E1; E1.triggered == %b\n",
                      64,(QData) (VL_TIME_UNITED_Q(1)),
                      1,(IData) ((IData)(vlTOPp->t__DOT__E1__Vtriggered)));
        } else {

->E1 を受けるのが、下記の部分です。

   always @(E1) begin
      $write("[%2t] @E1; E1.triggered == %b\n", $time, E1.triggered);
      event_mask[1] = 1;
   end

対応するコードは、eval4メソッドの// Body コメント以降の部分です。->E1 が実行されると、if (vlTOPp->tDOT_E1) 文の条件が成り立ちます。vl_TOPp->tDOT_E1.assign_no_lock(0U) で 直ちに 0 にし、sequentTOP5メソッドとsequentTOP6メソッドをthreadで起動し、wait_idleメソッドにてthread がアイドルになるまで待ちます。sequentTOP5 メソッドは特に何もやっていません。_sequentTOP6 メソッドは、alwaysブロックの中を実行しています。なんで2つに分けているんでしょうか?

void Vtop::_eval4(Vtop__Syms* __restrict vlSymsp) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_eval4\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    if (vlTOPp->t__DOT__E1) {
        {
            std::unique_lock<std::mutex> lck(vlTOPp->__eval_change_counter.mtx());
            vlTOPp->__eval_change_counter.assign_no_lock(
                                                         ((IData)(1U)
                                                          + vlTOPp->__eval_change_counter));
        }
        {
            std::unique_lock<std::mutex> lck(vlTOPp->t__DOT__E1.mtx());
            vlTOPp->t__DOT__E1.assign_no_lock(0U);
        }
        if (!Verilated::gotFinish()) {
            auto* _sequent__TOP__5__thread = thread_pool.run_once([vlTOPp,vlSymsp] (VerilatedThread* self) {
                    vlTOPp->_sequent__TOP__5(vlSymsp, self);
                }, "_sequent__TOP__5");
            _sequent__TOP__5__thread->wait_for_idle();
        }
        if (!Verilated::gotFinish()) {
            auto* _sequent__TOP__6__thread = thread_pool.run_once([vlTOPp,vlSymsp] (VerilatedThread* self) {
                    vlTOPp->_sequent__TOP__6(vlSymsp, self);
                }, "_sequent__TOP__6");
            _sequent__TOP__6__thread->wait_for_idle();
        }
    }
}

->E2 と @(E2) も ->E1 と @(E1) と同様な処理になっています。

おわりに

今回は、events(->, @(event))の実装をみてみました。wait_until スレッドを使って、複数のevent待ちを実装しているところがなかなかでした。 次回は、wait の実装がどうなっているかを調べていきます。