// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Scheduling - partitioning // // Code available from: https://verilator.org // //************************************************************************* // // Copyright 2003-2023 by Wilson Snyder. This program is free software; you // can redistribute it and/or modify it under the terms of either the GNU // Lesser General Public License Version 3 or the Perl Artistic License // Version 2.0. // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 // //************************************************************************* // // V3SchedPartition (and in particular V3Sched::partition) partitions all // logic into two regions, the 'act' region contains all logic that might // compute a clock via an update even that falls into the SystemVerilog Active // scheduling region (that is: blocking and continuous assignments in // particular). All other logic is assigned to the 'nba' region. // // To achieve this, we build a dependency graph of all logic in the design, // and trace back from every AstSenItem through all logic that might (via an // Active region update) feed into triggering that AstSenItem. Any such logic // is then assigned to the 'act' region, and all other logic is assigned to // the 'nba' region. // // For later practical purposes, AstAssignPre logic that would be assigned to // the 'act' region is returned separately. Nevertheless, this logic is part of // the 'act' region. // // For more details, please see the internals documentation. // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3EmitV.h" #include "V3Graph.h" #include "V3Sched.h" #include #include #include VL_DEFINE_DEBUG_FUNCTIONS; namespace V3Sched { namespace { class SchedSenVertex final : public V3GraphVertex { VL_RTTI_IMPL(SchedSenVertex, V3GraphVertex) const AstSenItem* const m_senItemp; public: SchedSenVertex(V3Graph* graphp, const AstSenItem* senItemp) : V3GraphVertex{graphp} , m_senItemp{senItemp} {} // LCOV_EXCL_START // Debug code string name() const override { std::ostringstream os; V3EmitV::verilogForTree(const_cast(m_senItemp), os); return os.str(); } string dotShape() const override { return "doubleoctagon"; } string dotColor() const override { return "red"; } // LCOV_EXCL_STOP }; class SchedLogicVertex final : public V3GraphVertex { VL_RTTI_IMPL(SchedLogicVertex, V3GraphVertex) AstScope* const m_scopep; AstSenTree* const m_senTreep; AstNode* const m_logicp; public: SchedLogicVertex(V3Graph* graphp, AstScope* scopep, AstSenTree* senTreep, AstNode* logicp) : V3GraphVertex{graphp} , m_scopep{scopep} , m_senTreep{senTreep} , m_logicp{logicp} {} AstScope* scopep() const { return m_scopep; } AstSenTree* senTreep() const { return m_senTreep; } AstNode* logicp() const { return m_logicp; } // LCOV_EXCL_START // Debug code string name() const override VL_MT_STABLE { return m_logicp->typeName() + ("\n" + m_logicp->fileline()->ascii()); }; string dotShape() const override { return "rectangle"; } // LCOV_EXCL_STOP }; class SchedVarVertex final : public V3GraphVertex { VL_RTTI_IMPL(SchedVarVertex, V3GraphVertex) const AstVarScope* const m_vscp; public: SchedVarVertex(V3Graph* graphp, AstVarScope* vscp) : V3GraphVertex{graphp} , m_vscp{vscp} {} // LCOV_EXCL_START // Debug code string name() const override VL_MT_STABLE { return m_vscp->name(); } string dotShape() const override { return m_vscp->scopep()->isTop() && m_vscp->varp()->isNonOutput() ? "invhouse" : "ellipse"; } string dotColor() const override { return m_vscp->scopep()->isTop() && m_vscp->varp()->isNonOutput() ? "green" : "black"; } // LCOV_EXCL_STOP }; class SchedGraphBuilder final : public VNVisitor { // NODE STATE // AstVarScope::user1() -> SchedVarVertex // AstSenItem::user1p() -> SchedSenVertex // AstVarScope::user2() -> bool: Read of this AstVarScope triggers this logic. // Used only for hybrid logic. const VNUser1InUse m_user1InUse; const VNUser2InUse m_user2InUse; // STATE V3Graph* const m_graphp = new V3Graph; // The dataflow graph being built // The vertices associated with a unique AstSenItem std::unordered_map, SchedSenVertex*> m_senVertices; AstScope* m_scopep = nullptr; // AstScope of the current AstActive AstSenTree* m_senTreep = nullptr; // AstSenTree of the current AstActive // Predicate for whether a read of the given variable triggers this block std::function m_readTriggersThisLogic; // The DPI export trigger variable, if any AstVarScope* const m_dpiExportTriggerp = v3Global.rootp()->dpiExportTriggerp(); SchedVarVertex* getVarVertex(AstVarScope* vscp) const { if (!vscp->user1p()) { SchedVarVertex* const vtxp = new SchedVarVertex{m_graphp, vscp}; // If this variable can be written via a DPI export, add a source edge from the // DPI export trigger vertex. This ensures calls to DPI exports that might write a // clock end up in the 'act' region. if (vscp->varp()->isWrittenByDpi()) { new V3GraphEdge{m_graphp, getVarVertex(m_dpiExportTriggerp), vtxp, 1}; } vscp->user1p(vtxp); } return vscp->user1u().to(); } SchedSenVertex* getSenVertex(AstSenItem* senItemp) { if (!senItemp->user1p()) { // There is a unique SchedSenVertex for each globally unique AstSenItem. Multiple // AstSenTree might use the same AstSenItem (e.g.: posedge clk1 or rst, posedge clk2 or // rst), so we use a hash map to get the unique SchedSenVertex. (Note: This creates // separate vertices for ET_CHANGED and ET_HYBRID over the same expression, but that is // OK for now). const auto pair = m_senVertices.emplace(*senItemp, nullptr); // If it does not exist, create it if (pair.second) { // Create the vertex SchedSenVertex* const vtxp = new SchedSenVertex{m_graphp, senItemp}; // Connect up the variable references senItemp->sensp()->foreach([&](AstVarRef* refp) { new V3GraphEdge{m_graphp, getVarVertex(refp->varScopep()), vtxp, 1}; }); // Store back to hash map so we can find it next time pair.first->second = vtxp; } // Cache sensitivity vertex senItemp->user1p(pair.first->second); } return senItemp->user1u().to(); } void visitLogic(AstNode* nodep) { UASSERT_OBJ(m_senTreep, nodep, "Should be under AstActive"); SchedLogicVertex* const logicVtxp = new SchedLogicVertex{m_graphp, m_scopep, m_senTreep, nodep}; // Clocked or hybrid logic has explicit sensitivity, so add edge from sensitivity vertex if (!m_senTreep->hasCombo()) { m_senTreep->foreach([this, nodep, logicVtxp](AstSenItem* senItemp) { if (senItemp->isIllegal()) return; UASSERT_OBJ(senItemp->isClocked() || senItemp->isHybrid(), nodep, "Non-clocked SenItem under clocked SenTree"); V3GraphVertex* const eventVtxp = getSenVertex(senItemp); new V3GraphEdge{m_graphp, eventVtxp, logicVtxp, 10}; }); } // Add edges based on references nodep->foreach([this, logicVtxp](const AstVarRef* vrefp) { AstVarScope* const vscp = vrefp->varScopep(); if (vrefp->access().isReadOrRW() && m_readTriggersThisLogic(vscp)) { new V3GraphEdge{m_graphp, getVarVertex(vscp), logicVtxp, 10}; } if (vrefp->access().isWriteOrRW()) { new V3GraphEdge{m_graphp, logicVtxp, getVarVertex(vscp), 10}; } }); // If the logic calls a 'context' DPI import, it might fire the DPI Export trigger if (m_dpiExportTriggerp) { nodep->foreach([this, logicVtxp](const AstCCall* callp) { if (!callp->funcp()->dpiImportWrapper()) return; if (!callp->funcp()->dpiContext()) return; new V3GraphEdge{m_graphp, logicVtxp, getVarVertex(m_dpiExportTriggerp), 10}; }); } } // VISIT methods void visit(AstActive* nodep) override { AstSenTree* const senTreep = nodep->sensesp(); UASSERT_OBJ(senTreep->hasClocked() || senTreep->hasCombo() || senTreep->hasHybrid(), nodep, "Unhandled"); UASSERT_OBJ(!m_senTreep, nodep, "Should not nest"); // Mark explicit sensitivities as not triggering these blocks if (senTreep->hasHybrid()) { AstNode::user2ClearTree(); senTreep->foreach([](const AstVarRef* refp) { // refp->varScopep()->user2(true); }); } m_senTreep = senTreep; iterateChildrenConst(nodep); m_senTreep = nullptr; } void visit(AstNodeProcedure* nodep) override { visitLogic(nodep); } void visit(AstNodeAssign* nodep) override { visitLogic(nodep); } void visit(AstCoverToggle* nodep) override { visitLogic(nodep); } void visit(AstAlwaysPublic* nodep) override { visitLogic(nodep); } // Pre and Post logic are handled separately void visit(AstAssignPre* nodep) override {} void visit(AstAssignPost* nodep) override {} void visit(AstAlwaysPost* nodep) override {} // LCOV_EXCL_START // Ignore void visit(AstInitialStatic* nodep) override { nodep->v3fatalSrc("Should not need ordering"); } void visit(AstInitial* nodep) override { // nodep->v3fatalSrc("Should not need ordering"); } void visit(AstFinal* nodep) override { // nodep->v3fatalSrc("Should not need ordering"); } // Default - Any other AstActive content not handled above will hit this void visit(AstNode* nodep) override { // nodep->v3fatalSrc("Should be handled above"); } // LCOV_EXCL_STOP SchedGraphBuilder(const LogicByScope& clockedLogic, const LogicByScope& combinationalLogic, const LogicByScope& hybridLogic) { // Build the data flow graph const auto iter = [this](const LogicByScope& lbs) { for (const auto& pair : lbs) { m_scopep = pair.first; iterate(pair.second); m_scopep = nullptr; } }; // Clocked logic is never triggered by reads m_readTriggersThisLogic = [](AstVarScope*) { return false; }; iter(clockedLogic); // Combinational logic is always triggered by reads m_readTriggersThisLogic = [](AstVarScope*) { return true; }; iter(combinationalLogic); // Hybrid logic is triggered by all reads, except for reads of the explicit sensitivities m_readTriggersThisLogic = [](AstVarScope* vscp) { return !vscp->user2(); }; iter(hybridLogic); } public: // Build the dataflow graph for partitioning static std::unique_ptr build(const LogicByScope& clockedLogic, const LogicByScope& combinationalLogic, const LogicByScope& hybridLogic) { SchedGraphBuilder visitor{clockedLogic, combinationalLogic, hybridLogic}; return std::unique_ptr{visitor.m_graphp}; } }; void colorActiveRegion(const V3Graph& graph) { // Work queue for depth first traversal std::vector queue{}; // Trace from all SchedSenVertex for (V3GraphVertex* vtxp = graph.verticesBeginp(); vtxp; vtxp = vtxp->verticesNextp()) { if (const auto activeEventVtxp = vtxp->cast()) { queue.push_back(activeEventVtxp); } } // Depth first traversal while (!queue.empty()) { // Pop next work item V3GraphVertex& vtx = *queue.back(); queue.pop_back(); // If not first encounter, move on if (vtx.color() != 0) continue; // Mark vertex as being in active region vtx.color(1); // Enqueue all parent vertices that feed this vertex. for (V3GraphEdge* edgep = vtx.inBeginp(); edgep; edgep = edgep->inNextp()) { queue.push_back(edgep->fromp()); } // If this is a logic vertex, also enqueue all variable vertices that are driven from this // logic. This will ensure that if a variable is set in the active region, then all // settings of that variable will be in the active region. if (vtx.is()) { for (V3GraphEdge* edgep = vtx.outBeginp(); edgep; edgep = edgep->outNextp()) { UASSERT(edgep->top()->is(), "Should be var vertex"); queue.push_back(edgep->top()); } } } } } // namespace LogicRegions partition(LogicByScope& clockedLogic, LogicByScope& combinationalLogic, LogicByScope& hybridLogic) { UINFO(2, __FUNCTION__ << ": " << endl); // Build the graph const std::unique_ptr graphp = SchedGraphBuilder::build(clockedLogic, combinationalLogic, hybridLogic); if (dumpGraphLevel() >= 6) graphp->dumpDotFilePrefixed("sched"); // Partition into Active and NBA regions colorActiveRegion(*(graphp.get())); if (dumpGraphLevel() >= 6) graphp->dumpDotFilePrefixed("sched-partitioned", true); LogicRegions result; for (V3GraphVertex* vtxp = graphp->verticesBeginp(); vtxp; vtxp = vtxp->verticesNextp()) { if (const auto lvtxp = vtxp->cast()) { LogicByScope& lbs = lvtxp->color() ? result.m_act : result.m_nba; AstNode* const logicp = lvtxp->logicp(); logicp->unlinkFrBack(); lbs.add(lvtxp->scopep(), lvtxp->senTreep(), logicp); } } // Partition the Pre logic { const VNUser1InUse user1InUse; // AstVarScope::user1() -> bool: read in Active region const VNUser2InUse user2InUse; // AstVarScope::user2() -> bool: written in Active region const auto markVars = [](AstNode* nodep) { nodep->foreach([](const AstNodeVarRef* vrefp) { AstVarScope* const vscp = vrefp->varScopep(); if (vrefp->access().isReadOrRW()) vscp->user1(true); if (vrefp->access().isWriteOrRW()) vscp->user2(true); }); }; for (const auto& pair : result.m_act) { AstActive* const activep = pair.second; markVars(activep->sensesp()); markVars(activep); } // AstAssignPre, AstAssignPost and AstAlwaysPost should only appear under a clocked // AstActive, and should be the only thing left at this point. for (const auto& pair : clockedLogic) { AstScope* const scopep = pair.first; AstActive* const activep = pair.second; for (AstNode *nodep = activep->stmtsp(), *nextp; nodep; nodep = nextp) { nextp = nodep->nextp(); if (AstAssignPre* const logicp = VN_CAST(nodep, AssignPre)) { bool toActiveRegion = false; logicp->foreach([&](const AstNodeVarRef* vrefp) { AstVarScope* const vscp = vrefp->varScopep(); if (vrefp->access().isReadOnly()) { // Variable only read in Pre, and is written in active region if (vscp->user2()) toActiveRegion = true; } else { // Variable written in Pre, and referenced in active region if (vscp->user1() || vscp->user2()) toActiveRegion = true; } }); LogicByScope& lbs = toActiveRegion ? result.m_pre : result.m_nba; logicp->unlinkFrBack(); lbs.add(scopep, activep->sensesp(), logicp); } else { UASSERT_OBJ(VN_IS(nodep, AssignPost) || VN_IS(nodep, AlwaysPost), nodep, "Unexpected node type " << nodep->typeName()); nodep->unlinkFrBack(); result.m_nba.add(scopep, activep->sensesp(), nodep); } } } } // Clean up remains of inputs clockedLogic.deleteActives(); combinationalLogic.deleteActives(); hybridLogic.deleteActives(); return result; } } // namespace V3Sched