// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Scheduling - replicate combinational logic // // Code available from: https://verilator.org // //************************************************************************* // // Copyright 2003-2024 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 // //************************************************************************* // // Combinational (including hybrid) logic driven from both the 'act' and 'nba' // region needs to be re-evaluated even if only one of those regions updates // an input variable. We achieve this by replicating such combinational logic // in both the 'act' and 'nba' regions. // // Furthermore we also replicate all combinational logic driven from a top // level input into a separate 'ico' (Input Combinational) region which is // executed at the beginning of the time step. This allows us to change both // data and clock signals during the same 'eval' call while maintaining the // combinational invariant required by V3Order. // // The implementation is a simple graph algorithm, where we build a dependency // graph of all logic in the design, and then propagate the driving region // information through it. We then replicate any logic into its additional // driving regions. // // For more details, please see the internals documentation. // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3Graph.h" #include "V3Sched.h" VL_DEFINE_DEBUG_FUNCTIONS; namespace V3Sched { namespace { // Driving region flags enum RegionFlags : uint8_t { NONE = 0x0, // INPUT = 0x1, // Variable/logic is driven from top level input ACTIVE = 0x2, // Variable/logic is driven from 'act' region logic NBA = 0x4, // Variable/logic is driven from 'nba' region logic OBSERVED = 0x8, // Variable/logic is driven from 'obs' region logic REACTIVE = 0x10, // Variable/logic is driven from 're' region logic }; //############################################################################## // Data structures (graph types) class SchedReplicateVertex VL_NOT_FINAL : public V3GraphVertex { VL_RTTI_IMPL(SchedReplicateVertex, V3GraphVertex) RegionFlags m_drivingRegions{RegionFlags::NONE}; // The regions driving this vertex public: explicit SchedReplicateVertex(V3Graph* graphp) : V3GraphVertex{graphp} {} uint8_t drivingRegions() const { return m_drivingRegions; } void addDrivingRegions(uint8_t regions) { m_drivingRegions = static_cast(m_drivingRegions | regions); } // LCOV_EXCL_START // Debug code string dotColor() const override { switch (static_cast(m_drivingRegions)) { case NONE: return "black"; case INPUT: return "red"; case ACTIVE: return "green"; case NBA: return "blue"; case INPUT | ACTIVE: return "yellow"; case INPUT | NBA: return "magenta"; case ACTIVE | NBA: return "cyan"; case INPUT | ACTIVE | NBA: return "gray80"; // don't want white on white background case REACTIVE: return "gray60"; case REACTIVE | INPUT: return "lightcoral"; case REACTIVE | ACTIVE: return "lightgreen"; case REACTIVE | NBA: return "lightblue"; case REACTIVE | INPUT | ACTIVE: return "lightyellow"; case REACTIVE | INPUT | NBA: return "lightpink"; case REACTIVE | ACTIVE | NBA: return "lightcyan"; case REACTIVE | INPUT | ACTIVE | NBA: return "gray90"; case OBSERVED: return "gray20"; case OBSERVED | INPUT: return "darkred"; case OBSERVED | ACTIVE: return "darkgreen"; case OBSERVED | NBA: return "darkblue"; case OBSERVED | INPUT | ACTIVE: return "gold"; case OBSERVED | INPUT | NBA: return "purple"; case OBSERVED | ACTIVE | NBA: return "darkcyan"; case OBSERVED | INPUT | ACTIVE | NBA: return "gray30"; case REACTIVE | OBSERVED: return "gray40"; case REACTIVE | OBSERVED | INPUT: return "indianred"; case REACTIVE | OBSERVED | ACTIVE: return "olive"; case REACTIVE | OBSERVED | NBA: return "dodgerBlue"; case REACTIVE | OBSERVED | INPUT | ACTIVE: return "khaki"; case REACTIVE | OBSERVED | INPUT | NBA: return "plum"; case REACTIVE | OBSERVED | ACTIVE | NBA: return "lightSeaGreen"; case REACTIVE | OBSERVED | INPUT | ACTIVE | NBA: return "gray50"; default: v3fatal("There are only 5 region bits"); return ""; } } // LCOV_EXCL_STOP }; class SchedReplicateLogicVertex final : public SchedReplicateVertex { VL_RTTI_IMPL(SchedReplicateLogicVertex, SchedReplicateVertex) AstScope* const m_scopep; // The enclosing AstScope of the logic node AstSenTree* const m_senTreep; // The sensitivity of the logic node AstNode* const m_logicp; // The logic node this vertex represents RegionFlags const m_assignedRegion; // The region this logic is originally assigned to public: SchedReplicateLogicVertex(V3Graph* graphp, AstScope* scopep, AstSenTree* senTreep, AstNode* logicp, RegionFlags assignedRegion) : SchedReplicateVertex{graphp} , m_scopep{scopep} , m_senTreep{senTreep} , m_logicp{logicp} , m_assignedRegion{assignedRegion} { addDrivingRegions(assignedRegion); } AstScope* scopep() const { return m_scopep; } AstSenTree* senTreep() const { return m_senTreep; } AstNode* logicp() const { return m_logicp; } RegionFlags assignedRegion() const { return m_assignedRegion; } // For graph dumping string name() const override VL_MT_STABLE { return m_logicp->fileline()->ascii(); }; string dotShape() const override { return "rectangle"; } }; class SchedReplicateVarVertex final : public SchedReplicateVertex { VL_RTTI_IMPL(SchedReplicateVarVertex, SchedReplicateVertex) AstVarScope* const m_vscp; // The AstVarScope this vertex represents public: SchedReplicateVarVertex(V3Graph* graphp, AstVarScope* vscp) : SchedReplicateVertex{graphp} , m_vscp{vscp} { // Top level inputs are if (varp()->isPrimaryInish() || varp()->isSigUserRWPublic() || varp()->isWrittenByDpi() || varp()->sensIfacep()) { addDrivingRegions(INPUT); } // Currently we always execute suspendable processes at the beginning of // the act region, which means combinational logic driven from a suspendable // processes must be present in the 'act' region if (varp()->isWrittenBySuspendable()) addDrivingRegions(ACTIVE); } AstVarScope* vscp() const { return m_vscp; } AstVar* varp() const { return m_vscp->varp(); } AstScope* scopep() const { return m_vscp->scopep(); } // For graph dumping string name() const override VL_MT_STABLE { return m_vscp->name(); } string dotShape() const override { return varp()->isPrimaryInish() ? "invhouse" : "ellipse"; } }; class Graph final : public V3Graph {}; //############################################################################## // Algorithm implementation std::unique_ptr buildGraph(const LogicRegions& logicRegions) { std::unique_ptr graphp{new Graph}; // AstVarScope::user1() -> VarVertx const VNUser1InUse user1InUse; const auto getVarVertex = [&](AstVarScope* vscp) { if (!vscp->user1p()) vscp->user1p(new SchedReplicateVarVertex{graphp.get(), vscp}); return vscp->user1u().to(); }; const auto addEdge = [&](SchedReplicateVertex* fromp, SchedReplicateVertex* top) { new V3GraphEdge{graphp.get(), fromp, top, 1}; }; const auto addLogic = [&](RegionFlags region, AstScope* scopep, AstActive* activep) { AstSenTree* const senTreep = activep->sensesp(); // Predicate for whether a read of the given variable triggers this block std::function readTriggersThisLogic; const VNUser4InUse user4InUse; // bool: Explicit sensitivity of hybrid logic just below if (senTreep->hasClocked()) { // Clocked logic is never triggered by reads readTriggersThisLogic = [](AstVarScope*) { return false; }; } else if (senTreep->hasCombo()) { // Combinational logic is always triggered by reads readTriggersThisLogic = [](AstVarScope*) { return true; }; } else { UASSERT_OBJ(senTreep->hasHybrid(), activep, "unexpected"); // Hybrid logic is triggered by all reads, except for reads of the explicit // sensitivities readTriggersThisLogic = [](AstVarScope* vscp) { return !vscp->user4(); }; senTreep->foreach([](const AstVarRef* refp) { // refp->varScopep()->user4(true); }); } for (AstNode* nodep = activep->stmtsp(); nodep; nodep = nodep->nextp()) { SchedReplicateLogicVertex* const lvtxp = new SchedReplicateLogicVertex{graphp.get(), scopep, senTreep, nodep, region}; const VNUser2InUse user2InUse; const VNUser3InUse user3InUse; nodep->foreach([&](AstVarRef* refp) { AstVarScope* const vscp = refp->varScopep(); SchedReplicateVarVertex* const vvtxp = getVarVertex(vscp); // If read, add var -> logic edge // Note: Use same heuristic as ordering does to ignore written variables // TODO: Use live variable analysis. if (refp->access().isReadOrRW() && !vscp->user3SetOnce() && readTriggersThisLogic(vscp) && !vscp->user2()) { // addEdge(vvtxp, lvtxp); } // If written, add logic -> var edge // Note: See V3Order for why AlwaysPostponed is safe to be ignored. We ignore it // as otherwise we would end up with a false cycle. if (refp->access().isWriteOrRW() && !vscp->user2SetOnce() && !VN_IS(nodep, AlwaysPostponed)) { // addEdge(lvtxp, vvtxp); } }); } }; for (const auto& pair : logicRegions.m_pre) addLogic(ACTIVE, pair.first, pair.second); for (const auto& pair : logicRegions.m_act) addLogic(ACTIVE, pair.first, pair.second); for (const auto& pair : logicRegions.m_nba) addLogic(NBA, pair.first, pair.second); for (const auto& pair : logicRegions.m_obs) addLogic(OBSERVED, pair.first, pair.second); for (const auto& pair : logicRegions.m_react) addLogic(REACTIVE, pair.first, pair.second); return graphp; } void propagateDrivingRegions(SchedReplicateVertex* vtxp) { // Note: The graph is always acyclic, so the recursion will terminate // Nothing to do if already visited if (vtxp->user()) return; // Compute union of driving regions of all inputs uint8_t drivingRegions = 0; for (V3GraphEdge& edge : vtxp->inEdges()) { SchedReplicateVertex* const srcp = edge.fromp()->as(); propagateDrivingRegions(srcp); drivingRegions |= srcp->drivingRegions(); } // Add any new driving regions vtxp->addDrivingRegions(drivingRegions); // Mark as visited vtxp->user(true); } LogicReplicas replicate(Graph* graphp) { LogicReplicas result; for (V3GraphVertex& vtx : graphp->vertices()) { if (SchedReplicateLogicVertex* const lvtxp = vtx.cast()) { const auto replicateTo = [&](LogicByScope& lbs) { lbs.add(lvtxp->scopep(), lvtxp->senTreep(), lvtxp->logicp()->cloneTree(false)); }; const uint8_t targetRegions = lvtxp->drivingRegions() & ~lvtxp->assignedRegion(); UASSERT(!lvtxp->senTreep()->hasClocked() || targetRegions == 0, "replicating clocked logic"); if (targetRegions & INPUT) replicateTo(result.m_ico); if (targetRegions & ACTIVE) replicateTo(result.m_act); if (targetRegions & NBA) replicateTo(result.m_nba); if (targetRegions & OBSERVED) replicateTo(result.m_obs); if (targetRegions & REACTIVE) replicateTo(result.m_react); } } return result; } } // namespace LogicReplicas replicateLogic(LogicRegions& logicRegionsRegions) { // Build the dataflow (dependency) graph const std::unique_ptr graphp = buildGraph(logicRegionsRegions); // Dump for debug if (dumpGraphLevel() >= 6) graphp->dumpDotFilePrefixed("sched-replicate"); // Propagate driving region flags for (V3GraphVertex& vtx : graphp->vertices()) { propagateDrivingRegions(vtx.as()); } // Dump for debug if (dumpGraphLevel() >= 6) graphp->dumpDotFilePrefixed("sched-replicate-propagated"); // Replicate the necessary logic return replicate(graphp.get()); } } // namespace V3Sched