@Vengineerの戯言 : Twitter SystemVerilogの世界へようこそ、すべては、SystemC v0.9公開から始まった
はじめに
Verilatorは、オープンソースのVerilog HDL/SystemVerilog対応のシミュレータです。 しかしながら、msyksphinzさんの下記のブログにあるように、
- 全ての遅延記述 (#) は無視される。
- event系のイベント (waitなど) はサポートされない。
- Unknownステートはサポートされない
です。また、fork/join もサポートされていません。
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
次のブロック
{
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 ) の実装についてみてみます。