FPGA開発日記

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

Verilatorのコンパイルフローを観察する (13. C++ファイルの出力)

C++ファイルの出力はmainImp()がその役割を担っているようだ。

  • verilator/src/V3Emit.cpp
void EmitCImp::mainImp(AstNodeModule* modp, bool slow) {
    // Output a module
    AstNodeModule* fileModp = modp;  // Filename constructed using this module
    m_modp = modp;
    m_slow = slow;
    m_fast = !slow;

    UINFO(5, "  Emitting " << prefixNameProtect(modp) << endl);

    m_ofp = newOutCFile(fileModp, !m_fast, true /*source*/);
    emitImpTop(fileModp);
    emitImp(modp);

/* ... 中略 ... */

emitImpTop()がソースファイルのヘッダ出力を担っているようだ。

void EmitCImp::emitImpTop(AstNodeModule* fileModp) {
    puts("\n");
    puts("#include \"" + prefixNameProtect(fileModp) + ".h\"\n");
    puts("#include \"" + symClassName() + ".h\"\n");

    if (v3Global.dpi()) {
        puts("\n");
        puts("#include \"verilated_dpi.h\"\n");
    }

    emitModCUse(fileModp, VUseType::IMP_INCLUDE);
    emitModCUse(fileModp, VUseType::IMP_FWD_CLASS);

    emitTextSection(AstType::atScImpHdr);
}

実装部分の出力はemitImp()がその役割を担っているように思われる。

void EmitCImp::emitImp(AstNodeModule* modp) {
    puts("\n//==========\n");
    if (m_slow) {
        string section;
        emitVarList(modp->stmtsp(), EVL_CLASS_ALL, prefixNameProtect(modp), section /*ref*/);
        if (!VN_IS(modp, Class)) emitCtorImp(modp);
        if (!VN_IS(modp, Class)) emitConfigureImp(modp);
        if (!VN_IS(modp, Class)) emitDestructorImp(modp);
        emitSavableImp(modp);
        emitCoverageImp(modp);
    }

    if (m_fast) {
        emitTextSection(AstType::atScImp);
        if (modp->isTop()) {
            emitWrapFast(modp);
            emitWrapEval(modp);
        }
    }

    // Blocks
    for (AstNode* nodep = modp->stmtsp(); nodep; nodep = nodep->nextp()) {
        if (AstCFunc* funcp = VN_CAST(nodep, CFunc)) {
            maybeSplit(modp);
            mainDoFunc(funcp);
        }
    }
}

まずemitWrapFast()だ。これはどういう意味だろう?

void EmitCImp::emitWrapFast(AstNodeModule* modp) {
    puts("\nVerilatedContext* " + prefixNameProtect(modp) + "::contextp() {\n");
    puts(/**/ "return __VlSymsp->_vm_contextp__;\n");
    puts("}\n");
}

VerilatedContextクラスはどういう意味だろう?以下を調べてみると、これはシミュレーションのための共通ライブラリととらえれば良いようだ。

  • verilator/include/verilated.h
//===========================================================================
/// Verilator simulation context
///
/// The VerilatedContext contains the information common across all models
/// that are interconnected, for example this contains the simulation time
/// and if $finish was executed.
///
/// VerilatedContexts maybe created by the user wrapper code and passed
/// when a model is created.  If this is not done, then Verilator will use
/// the Verilated::defaultContextp()'s global context.

/// VerilatedContext はたがいに接続されるすべてのモデルの共通の情報を保持している。
/// 例えばシミュレーション時間や$finishが実行されたかどうかに関する情報である。

/// VerilatedContextsはユーザラッパーコードによって作成されモデルが作成されたときに渡される。
/// これが完了していない場合にはVerilatorはVerilated::defaultContext()というグローバルなコンテキストを利用する。

次はemitWrapEval()だ。これは非常に長いputs()で構成されている。

void EmitCImp::emitWrapEval(AstNodeModule* modp) {
    puts("\nvoid " + prefixNameProtect(modp) + "::eval_step() {\n");
    puts("VL_DEBUG_IF(VL_DBG_MSGF(\"+++++TOP Evaluate " + prefixNameProtect(modp)
         + "::eval\\n\"); );\n");
    puts(EmitCBaseVisitor::symClassVar() + " = this->__VlSymsp;  // Setup global symbol table\n");
/* ... 中略 ... */

実際に生成されたC++コードを見てみると、組み合わせ回路を評価するのは_eval()に飛ばしているようだ。

/* ... 注意:以下は生成されたコード側 ... */
void Vsimple_assign::eval_step() {
    VL_DEBUG_IF(VL_DBG_MSGF("+++++TOP Evaluate Vsimple_assign::eval\n"); );
    Vsimple_assign__Syms* __restrict vlSymsp = this->__VlSymsp;  // Setup global symbol table
    Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
/* ... 中略 ... */
    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.
/* ... 中略 ... */

_eval()の実装は以下のようになっている。これを生成しているのはどこだ?

/* ... 注意:以下は生成されたコード側 ... */
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);
}

たぶんこれだ。これはvisit()の中だ。これはどうやって生成しているんだろう?以下のコードを見てみると、iterateAndNextNull()がキーポイントになっているように思われる。このvisit()にどうやってたどり着くのかという話と、iterateAndNextNull()の動作はもう少しGDBか何かで挙動を観察する必要がありそう。

    using EmitCStmts::visit;  // Suppress hidden overloaded virtual function warning
    virtual void visit(AstCFunc* nodep) override {
        // TRACE_* and DPI handled elsewhere
        if (nodep->funcType().isTrace()) return;
        if (nodep->dpiImport()) return;
/* ... 中略 ... */
        // "+" in the debug indicates a print from the model
        puts("VL_DEBUG_IF(VL_DBG_MSGF(\"+  ");
        for (int i = 0; i < m_modp->level(); ++i) { puts("  "); }
        puts(prefixNameProtect(m_modp) + "::" + nodep->nameProtect() + "\\n\"); );\n");

        // Declare and set vlTOPp
        if (nodep->symProlog()) puts(EmitCBaseVisitor::symTopAssign() + "\n");

        if (nodep->initsp()) putsDecoration("// Variables\n");
        for (AstNode* subnodep = nodep->argsp(); subnodep; subnodep = subnodep->nextp()) {
            if (AstVar* varp = VN_CAST(subnodep, Var)) {
                if (varp->isFuncReturn()) emitVarDecl(varp, "");
            }
        }
        string section;
        emitVarList(nodep->initsp(), EVL_FUNC_ALL, "", section /*ref*/);
        emitVarList(nodep->stmtsp(), EVL_FUNC_ALL, "", section /*ref*/);

        iterateAndNextNull(nodep->initsp());

        if (nodep->stmtsp()) putsDecoration("// Body\n");
        iterateAndNextNull(nodep->stmtsp());
        if (!m_blkChangeDetVec.empty()) emitChangeDet();