@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 の実装がどうなっているかを調べていきます。