diff --git a/docs/guide/control.rst b/docs/guide/control.rst index d42afceff..a01396040 100644 --- a/docs/guide/control.rst +++ b/docs/guide/control.rst @@ -100,6 +100,27 @@ The grammar of control commands is as follows: Same as :option:`/*verilator&32;forceable*/` metacomment. +.. option:: fsm_register_wrapper -module "" -d "" -q "" -clock "" [-reset ""] [-reset_value ""] + + Declares that the specified module is a transparent FSM state-register + wrapper for FSM coverage extraction. One declaration applies to all + instances of that module. + + The ``-d`` option names the wrapper input port carrying next-state data. + The ``-q`` option names the wrapper output port carrying registered state. + The ``-clock`` option names the wrapper clock port. + + Verilator does not infer wrapper semantics from module or port naming + conventions. A wrapper module must be declared with this option before + Verilator will use instances of it to extract FSM state and transition + coverage. + + The ``-reset`` and ``-reset_value`` options are optional. Reset arcs are + emitted only when both the reset polarity can be inferred from the + wrapper's event control and ``-reset_value`` names a static parameter value. + If reset metadata is incomplete, Verilator may still emit FSM state and + transition coverage without reset arcs. + .. option:: full_case -file "" -lines Same as ``//synthesis full_case``. When these synthesis directives diff --git a/docs/guide/simulating.rst b/docs/guide/simulating.rst index 842a21dff..5fb8ab41e 100644 --- a/docs/guide/simulating.rst +++ b/docs/guide/simulating.rst @@ -262,6 +262,35 @@ the extracted coverage model: - ``/*verilator fsm_arc_include_cond*/`` keeps conditional branch arcs that would otherwise be skipped by the conservative extractor. +State registers may also be wrapped by a transparent instance, for +example a project flop wrapper or primitive. Such wrappers must be +described explicitly with a VLT command file action before Verilator will +use their data, state, clock, or reset connections for FSM extraction: + +.. code-block:: sv + + `verilator_config + fsm_register_wrapper -module "my_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" + +The same command may be placed in a separate ``.vlt`` file: + +.. code-block:: sv + + fsm_register_wrapper -module "my_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" + +Optional reset metadata may also be supplied: + +.. code-block:: sv + + fsm_register_wrapper -module "my_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" \ + -reset "rst_ni" -reset_value "ResetValue" + +Reset arcs are emitted only when the configured reset port has an +inferable edge in the wrapper and the configured reset value parameter is +statically resolvable. If reset metadata is incomplete, Verilator warns +and may still emit FSM state and transition coverage, but reset arcs are +omitted. + Reset transitions are included in the collected data either way. By default, :command:`verilator_coverage` summarizes reset-only arcs rather than printing them alongside non-reset arcs. Use diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index 1d68873b8..fc10d67ec 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -1964,6 +1964,7 @@ class AstVar final : public AstNode { bool m_attrSFormat : 1; // User sformat attribute bool m_attrSplitVar : 1; // declared with split_var metacomment bool m_attrFsmState : 1; // declared with fsm_state metacomment + bool m_attrFsmRegisterWrapper : 1; // connected to an fsm_register_wrapper instance bool m_attrFsmResetArc : 1; // declared with fsm_reset_arc metacomment bool m_attrFsmArcInclCond : 1; // declared with fsm_arc_include_cond metacomment bool m_fileDescr : 1; // File descriptor @@ -2025,6 +2026,7 @@ class AstVar final : public AstNode { m_attrSFormat = false; m_attrSplitVar = false; m_attrFsmState = false; + m_attrFsmRegisterWrapper = false; m_attrFsmResetArc = false; m_attrFsmArcInclCond = false; m_fileDescr = false; @@ -2172,6 +2174,7 @@ public: void attrSFormat(bool flag) { m_attrSFormat = flag; } void attrSplitVar(bool flag) { m_attrSplitVar = flag; } void attrFsmState(bool flag) { m_attrFsmState = flag; } + void attrFsmRegisterWrapper(bool flag) { m_attrFsmRegisterWrapper = flag; } void attrFsmResetArc(bool flag) { m_attrFsmResetArc = flag; } void attrFsmArcInclCond(bool flag) { m_attrFsmArcInclCond = flag; } void rand(const VRandAttr flag) { m_rand = flag; } @@ -2338,6 +2341,7 @@ public: bool attrSFormat() const { return m_attrSFormat; } bool attrSplitVar() const { return m_attrSplitVar; } bool attrFsmState() const { return m_attrFsmState; } + bool attrFsmRegisterWrapper() const { return m_attrFsmRegisterWrapper; } bool attrFsmResetArc() const { return m_attrFsmResetArc; } bool attrFsmArcInclCond() const { return m_attrFsmArcInclCond; } bool attrIsolateAssign() const { return m_attrIsolateAssign; } diff --git a/src/V3Control.cpp b/src/V3Control.cpp index 2d281e367..1973d7eb3 100644 --- a/src/V3Control.cpp +++ b/src/V3Control.cpp @@ -227,8 +227,10 @@ class V3ControlModule final { V3ControlVarResolver m_params; // Parameters in module V3ControlVarResolver m_ports; // Ports in module V3ControlVarResolver m_vars; // Variables in module + V3Control::FsmRegisterWrapper m_fsmRegisterWrapper; // FSM wrapper descriptor std::unordered_set m_coverageOffBlocks; // List of block names for coverage_off std::set m_modPragmas; // List of Pragmas for modules + bool m_hasFsmRegisterWrapper = false; // Whether descriptor has been set bool m_inline = false; // Whether to force the inline bool m_inlineValue = false; // The inline value (on/off) @@ -240,6 +242,10 @@ public: m_params.update(m.m_params); m_ports.update(m.m_ports); m_vars.update(m.m_vars); + if (m.m_hasFsmRegisterWrapper) { + m_fsmRegisterWrapper = m.m_fsmRegisterWrapper; + m_hasFsmRegisterWrapper = true; + } for (const string& i : m.m_coverageOffBlocks) m_coverageOffBlocks.insert(i); if (!m_inline) { m_inline = m.m_inline; @@ -261,6 +267,18 @@ public: m_inline = true; m_inlineValue = set; } + void setFsmRegisterWrapper(FileLine* fl, const V3Control::FsmRegisterWrapper& desc) { + if (m_hasFsmRegisterWrapper) { + fl->v3warn(BADVLTPRAGMA, "Duplicate fsm_register_wrapper descriptor for module " + << AstNode::prettyNameQ(desc.moduleName) + << "; replacing previous descriptor"); + } + m_fsmRegisterWrapper = desc; + m_hasFsmRegisterWrapper = true; + } + const V3Control::FsmRegisterWrapper* fsmRegisterWrapperp() const { + return m_hasFsmRegisterWrapper ? &m_fsmRegisterWrapper : nullptr; + } void addModulePragma(VPragmaType pragma) { m_modPragmas.insert(pragma); } void apply(AstNodeModule* modp) { @@ -877,6 +895,23 @@ void V3Control::addHierWorkers(FileLine* fl, const string& model, int workers) { V3ControlResolver::s().addHierWorkers(fl, model, workers); } +void V3Control::addFsmRegisterWrapper(FileLine* fl, const string& module, const string& d, + const string& q, const string& clock, + const string& reset, const string& resetValue) { + string missing; + if (module.empty()) missing += "-module"; + if (d.empty()) missing = VString::dot(missing, ", ", "-d"); + if (q.empty()) missing = VString::dot(missing, ", ", "-q"); + if (clock.empty()) missing = VString::dot(missing, ", ", "-clock"); + if (!missing.empty()) { + fl->v3error("fsm_register_wrapper missing " << missing); + return; + } + + const FsmRegisterWrapper desc{module, d, q, clock, reset, resetValue}; + V3ControlResolver::s().modules().at(module).setFsmRegisterWrapper(fl, desc); +} + void V3Control::addIgnore(V3ErrorCode code, bool on, const string& filename, int min, int max) { UINFO(9, "addIgnore " << code << " " << min << "-" << max << " fn=" << filename); if (filename == "*") { // For "lint_off/lint_on [--rule x]" @@ -1066,6 +1101,10 @@ int V3Control::getHierWorkers(const string& model) { FileLine* V3Control::getHierWorkersFileLine(const string& model) { return V3ControlResolver::s().getHierWorkersFileLine(model); } +const V3Control::FsmRegisterWrapper* V3Control::getFsmRegisterWrapper(const string& module) { + V3ControlModule* const modp = V3ControlResolver::s().modules().resolve(module); + return modp ? modp->fsmRegisterWrapperp() : nullptr; +} uint64_t V3Control::getProfileData(const string& hierDpi) { return V3ControlResolver::s().getProfileData(hierDpi); } diff --git a/src/V3Control.h b/src/V3Control.h index c5424a85b..68173625e 100644 --- a/src/V3Control.h +++ b/src/V3Control.h @@ -29,6 +29,15 @@ class V3Control final { public: + struct FsmRegisterWrapper final { + string moduleName; + string d; + string q; + string clock; + string reset; + string resetValue; + }; + enum class VarSpecKind { PARAM, // Select only matching parameters PORT, // Select only matching ports @@ -40,6 +49,9 @@ public: static void addCoverageBlockOff(const string& file, int lineno); static void addCoverageBlockOff(const string& module, const string& blockname); static void addHierWorkers(FileLine* fl, const string& model, int workers); + static void addFsmRegisterWrapper(FileLine* fl, const string& module, const string& d, + const string& q, const string& clock, + const string& reset, const string& resetValue); static void addIgnore(V3ErrorCode code, bool on, const string& filename, int min, int max); static void addIgnoreMatch(V3ErrorCode code, const string& filename, const string& contents, const string& match); @@ -64,6 +76,7 @@ public: static int getHierWorkers(const string& model); static FileLine* getHierWorkersFileLine(const string& model); + static const FsmRegisterWrapper* getFsmRegisterWrapper(const string& module); static uint64_t getProfileData(const string& hierDpi); static uint64_t getProfileData(const string& model, const string& key); static FileLine* getProfileDataFileLine(); diff --git a/src/V3FsmDetect.cpp b/src/V3FsmDetect.cpp index 64b3231f4..83da5908b 100644 --- a/src/V3FsmDetect.cpp +++ b/src/V3FsmDetect.cpp @@ -28,6 +28,7 @@ #include "V3FsmDetect.h" #include "V3Ast.h" +#include "V3Control.h" #include "V3Graph.h" #include @@ -108,22 +109,73 @@ struct FsmSenDesc final { struct FsmResetCondDesc final { // Reset signal used by the FSM in the saved scoped AST. AstVarScope* varScopep = nullptr; + bool activeLow = false; }; class FsmResetArcDesc final { FsmStateValue m_toValue; // Encoded reset target state. - AstNodeAssign* m_nodep = nullptr; // Source assignment for warnings and emitted metadata + AstNode* m_nodep = nullptr; // Source node for warnings and emitted metadata. + AstNodeExpr* m_valuep = nullptr; // Expression that provided the reset value. public: FsmResetArcDesc() = default; FsmResetArcDesc(FsmStateValue toValue, AstNodeAssign* nodep) : m_toValue{toValue} - , m_nodep{nodep} {} + , m_nodep{nodep} + , m_valuep{nodep->rhsp()} {} + FsmResetArcDesc(FsmStateValue toValue, AstNode* nodep, AstNodeExpr* valuep) + : m_toValue{toValue} + , m_nodep{nodep} + , m_valuep{valuep} {} FsmStateValue toValue() const { return m_toValue; } - AstNodeAssign* nodep() const { return m_nodep; } + AstNode* nodep() const { return m_nodep; } + AstNodeExpr* valuep() const { return m_valuep; } }; +struct FsmWrapperRoles final { + string dPort; + string qPort; + string clkPort; + string rstPort; + string rstValParam; + bool hasRstActiveLow = false; + bool rstActiveLow = false; +}; + +static bool fsmWrapperResetPolarityFromWrapperAst(AstCell* cellp, const string& portName, + bool& activeLow) { + bool matched = false; + cellp->modp()->foreach([&](AstSenItem* itemp) { + AstNodeVarRef* const vrefp = itemp->varrefp(); + if (!vrefp) return; + if (vrefp->varp()->name() != portName) return; + activeLow = itemp->edgeType() == VEdgeType::ET_NEGEDGE; + matched = true; + }); + return matched; +} + +static const V3Control::FsmRegisterWrapper* fsmRegisterWrapperDesc(AstCell* cellp) { + AstNodeModule* const modp = cellp->modp(); + const string origName = modp->origName(); + if (const V3Control::FsmRegisterWrapper* const descp + = V3Control::getFsmRegisterWrapper(origName)) { + return descp; + } + return V3Control::getFsmRegisterWrapper(modp->prettyDehashOrigOrName()); +} + +static FsmWrapperRoles rolesFromDesc(const V3Control::FsmRegisterWrapper& desc) { + FsmWrapperRoles roles; + roles.dPort = desc.d; + roles.qPort = desc.q; + roles.clkPort = desc.clock; + roles.rstPort = desc.reset; + roles.rstValParam = desc.resetValue; + return roles; +} + class FsmRegisterCandidate final { AstScope* m_scopep = nullptr; // Owning scope for the paired FSM. AstAlways* m_alwaysp = nullptr; // Register process that commits the state. @@ -135,6 +187,7 @@ class FsmRegisterCandidate final { bool m_hasResetCond = false; // Whether the FSM had a modeled reset predicate. bool m_resetInclude = false; // Whether reset arcs count toward summary totals. bool m_inclCond = false; // Whether conditional/default arcs are kept explicitly. + FileLine* m_flp = nullptr; // Representative source location. public: AstScope* scopep() const { return m_scopep; } @@ -157,6 +210,8 @@ public: void resetInclude(bool flag) { m_resetInclude = flag; } bool inclCond() const { return m_inclCond; } void inclCond(bool flag) { m_inclCond = flag; } + FileLine* fileline() const { return m_flp; } + void fileline(FileLine* flp) { m_flp = flp; } }; class FsmComboAlways final { @@ -386,6 +441,8 @@ struct FsmIfChainCandidate final { // Aliases are accepted only when they are equivalent to spelling the state // comparison inline; this avoids inferring FSM semantics from arbitrary logic. using FsmAliasMap = std::unordered_map; +using FsmCellPortMap = std::unordered_map; +using FsmCellPortAliasMap = std::unordered_map; struct StateConstLabel final { string text; @@ -439,11 +496,21 @@ class FsmDetectVisitor final : public VNVisitor { std::vector m_oneBlockAlwayss; std::vector m_comboAlwayss; std::vector m_nonComboAlwayss; + // Wrapper FSM detection has a second path for designs compiled without + // inlining. In that shape the state register stays behind an AstCell, so we + // remember candidate cells and resolve them only after the surrounding + // transition logic and post-link port wiring have both been seen. + std::vector> m_wrapperCells; std::unordered_map m_comboPaired; // Continuous aliases are order-independent, while procedural aliases must // remain source-order scoped to avoid using assignments not yet executed. FsmAliasMap m_stateAliases; std::unordered_set m_ambiguousStateAliases; + // A surviving wrapper's semantic d/q relationship is split across the + // parent scope and the child module scope. This table is the narrow bridge + // between those scopes: only transparent port aliases are recorded, so the + // detector does not become a general cross-module dataflow engine. + FsmCellPortAliasMap m_cellPortAliases; // METHODS // Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the @@ -460,6 +527,93 @@ class FsmDetectVisitor final : public VNVisitor { + firstCand.warnNodep->warnContextSecondary(); } + static bool rejectFsmWrapperCell(AstCell* cellp, const string& reason) { + cellp->v3warn(COVERIGN, "Ignoring unsupported: " + reason); + return false; + } + + static bool simpleParamStateValue(AstCell* cellp, const string& name, FsmStateValue& value, + AstNodeExpr*& valuepr) { + // Cell-path reset recovery must behave like the inlined path when the + // instance relies on a parameter default. Looking into the linked module + // default preserves that equivalence while keeping the cell detector's + // contract narrow: only static, known reset encodings become reset arcs. + valuepr = nullptr; + for (AstNode* stmtp = cellp->modp()->stmtsp(); stmtp; stmtp = stmtp->nextp()) { + AstVar* const varp = VN_CAST(stmtp, Var); + if (!varp || !varp->isParam() || varp->name() != name) continue; + valuepr = VN_AS(varp->valuep(), NodeExpr); + return constValueStatus(valuepr, value) == ConstValueStatus::OK; + } + return false; + } + + static bool childPortInScope(AstVarScope* vscp, AstScope* parentScopep, AstCell*& cellpr) { + if (!vscp->varp()->isIO()) return false; + AstScope* const scopep = vscp->scopep(); + UASSERT_OBJ(scopep, vscp, "VarScope without scope"); + if (scopep->aboveScopep() != parentScopep) return false; + UASSERT_OBJ(scopep->aboveCellp(), vscp, + "Child port scope should retain the instance that created it"); + cellpr = scopep->aboveCellp(); + return true; + } + + static AstVarScope* simpleAssignVarScope(AstNodeExpr* exprp) { + AstVarRef* const vrefp = VN_CAST(exprp, VarRef); + return vrefp ? vrefp->varScopep() : nullptr; + } + + void addWrapperCell(AstScope* scopep, AstCell* cellp) { + m_cellPortAliases.emplace(cellp, FsmCellPortMap{}); + const std::pair item{scopep, cellp}; + if (std::find(m_wrapperCells.cbegin(), m_wrapperCells.cend(), item) + != m_wrapperCells.cend()) { + return; + } + m_wrapperCells.emplace_back(item); + } + + void collectCellPortAlias(AstAssignW* nodep) { + UASSERT_OBJ(m_scopep, nodep, "Cell port alias collection requires a scoped assignment"); + AstVarScope* const lhsVscp = simpleAssignVarScope(nodep->lhsp()); + AstVarScope* const rhsVscp = simpleAssignVarScope(nodep->rhsp()); + if (!lhsVscp || !rhsVscp) return; + AstCell* cellp = nullptr; + // The cell path is intentionally a transparent-wrapper recognizer. A + // direct parent<->child variable assignment preserves the register's + // identity across the hierarchy boundary; any expression, slice, or + // transform is outside this phase's contract and therefore not recorded. + if (childPortInScope(lhsVscp, m_scopep, cellp)) { + if (!fsmRegisterWrapperDesc(cellp)) return; + UASSERT_OBJ(lhsVscp->varp()->isInput(), nodep, + "Child-side port alias lhs should be an input"); + UASSERT_OBJ(rhsVscp->scopep() == m_scopep, nodep, + "Child input port alias should connect from the parent scope"); + m_cellPortAliases[cellp][lhsVscp->varp()->name()] = rhsVscp; + addWrapperCell(m_scopep, cellp); + } else if (childPortInScope(rhsVscp, m_scopep, cellp)) { + if (!fsmRegisterWrapperDesc(cellp)) return; + UASSERT_OBJ(rhsVscp->varp()->isWritable(), nodep, + "Child-side port alias rhs should be writable"); + UASSERT_OBJ(lhsVscp->scopep() == m_scopep, nodep, + "Child output port alias should connect into the parent scope"); + m_cellPortAliases[cellp][rhsVscp->varp()->name()] = lhsVscp; + addWrapperCell(m_scopep, cellp); + } + } + + AstVarScope* roleVarScope(AstCell* cellp, const string& portName) const { + // At this point explicit AstPin expressions have been lowered away, so + // role resolution crosses the wrapper boundary only through the + // transparent alias table above. This keeps wrapper support aligned with + // direct-register detection instead of growing into interprocedural FSM + // inference. + const FsmCellPortMap& ports = m_cellPortAliases.at(cellp); + const FsmCellPortMap::const_iterator portIt = ports.find(portName); + return portIt == ports.end() ? nullptr : portIt->second; + } + class RegisterAlwaysAnalyzer final { AstScope* const m_scopep; @@ -546,6 +700,85 @@ class FsmDetectVisitor final : public VNVisitor { } }; + bool matchFsmWrapperCell(AstScope* scopep, AstCell* cellp, FsmRegisterCandidate& cand) const { + FsmWrapperRoles roles = rolesFromDesc(*fsmRegisterWrapperDesc(cellp)); + + AstVarScope* const nextVscp = roleVarScope(cellp, roles.dPort); + AstVarScope* const stateVscp = roleVarScope(cellp, roles.qPort); + if (!nextVscp || !stateVscp) { + return rejectFsmWrapperCell( + cellp, "fsm_register_wrapper d and q connections must be simple variables"); + } + AstVarScope* const clkVscp = roleVarScope(cellp, roles.clkPort); + if (!clkVscp) { + return rejectFsmWrapperCell( + cellp, "fsm_register_wrapper instance requires a simple clock connection"); + } + + FsmSenDesc clkSense; + clkSense.edgeType = VEdgeType::ET_POSEDGE; + clkSense.varScopep = clkVscp; + cand.senses().push_back(clkSense); + + AstVarScope* resetVscp = nullptr; + if (!roles.rstPort.empty()) resetVscp = roleVarScope(cellp, roles.rstPort); + if (resetVscp) { + // The descriptor identifies the reset port but not its polarity. Use + // the wrapper's own event control AST as the contract for sampling + // the connected parent signal. + bool inferredActiveLow = false; + if (fsmWrapperResetPolarityFromWrapperAst(cellp, roles.rstPort, inferredActiveLow)) { + roles.hasRstActiveLow = true; + roles.rstActiveLow = inferredActiveLow; + } + } + + AstNodeExpr* resetValuep = nullptr; + FsmStateValue resetValue; + const bool hasResetValue + = !roles.rstValParam.empty() + && simpleParamStateValue(cellp, roles.rstValParam, resetValue, resetValuep); + if (resetVscp && roles.hasRstActiveLow && hasResetValue) { + FsmSenDesc rstSense; + rstSense.edgeType = roles.rstActiveLow ? VEdgeType::ET_NEGEDGE : VEdgeType::ET_POSEDGE; + rstSense.varScopep = resetVscp; + cand.senses().push_back(rstSense); + cand.resetCond().varScopep = resetVscp; + cand.resetCond().activeLow = roles.rstActiveLow; + cand.hasResetCond(true); + cand.resetArcs().emplace_back(resetValue, cellp, resetValuep); + } else if (!roles.rstPort.empty() || !roles.rstValParam.empty()) { + string reason; + if (roles.rstPort.empty()) { + reason = "reset port is not configured"; + } else if (!resetVscp) { + reason = "reset connection is missing or not a simple variable"; + } else if (!roles.hasRstActiveLow) { + reason = "reset polarity could not be inferred from the wrapper"; + } else if (roles.rstValParam.empty()) { + reason = "reset_value parameter is not configured"; + } else { + reason = "reset_value parameter is missing or not static"; + } + cellp->v3warn(COVERIGN, + "Ignoring unsupported: fsm_register_wrapper reset arcs require both " + "reset polarity and static reset value; " + reason); + } + + // This candidate represents a register proven through an instance + // boundary, so there is no parent always_ff body to annotate. Lowering + // treats null alwaysp as the explicit cell-path contract and builds its + // sampling block from the recovered clock/reset interface instead. + cand.scopep(scopep); + cand.alwaysp(nullptr); + cand.stateVscp(stateVscp); + cand.nextVscp(nextVscp); + cand.resetInclude(stateVscp->varp()->attrFsmResetArc()); + cand.inclCond(stateVscp->varp()->attrFsmArcInclCond()); + cand.fileline(cellp->fileline()); + return true; + } + class ComboAlwaysAnalyzer final { public: struct ComboMatch final { @@ -748,6 +981,7 @@ class FsmDetectVisitor final : public VNVisitor { static AstNodeAssign* directCondStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp, AstVarScope*& fromVscp, AstNodeExpr*& condp, + bool& resetActiveLow, FsmStateValue& resetValue) { AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); if (!assp) return nullptr; @@ -756,12 +990,18 @@ class FsmDetectVisitor final : public VNVisitor { "conditional register commit lhs should be normalized to a VarRef"); AstCond* const rhsp = VN_CAST(assp->rhsp(), Cond); if (!rhsp) return nullptr; - AstVarRef* const elsep = VN_CAST(rhsp->elsep(), VarRef); - if (!elsep || constValueStatus(rhsp->thenp(), resetValue) != ConstValueStatus::OK) { + if (AstVarRef* const elsep = VN_CAST(rhsp->elsep(), VarRef)) { + if (constValueStatus(rhsp->thenp(), resetValue) != ConstValueStatus::OK) return nullptr; + fromVscp = elsep->varScopep(); + resetActiveLow = false; + } else if (AstVarRef* const thenp = VN_CAST(rhsp->thenp(), VarRef)) { + if (constValueStatus(rhsp->elsep(), resetValue) != ConstValueStatus::OK) return nullptr; + fromVscp = thenp->varScopep(); + resetActiveLow = true; + } else { return nullptr; } stateVscp = lhsp->varScopep(); - fromVscp = elsep->varScopep(); condp = rhsp->condp(); return assp; } @@ -1169,11 +1409,20 @@ class FsmDetectVisitor final : public VNVisitor { cand.hasResetCond(cand.resetCond().varScopep != nullptr); } else { AstNodeExpr* resetCondp = nullptr; + bool resetActiveLow = false; FsmStateValue resetValue; if (AstNodeAssign* const assp - = directCondStateVarAssign(nodep, stateVscp, nextVscp, resetCondp, resetValue)) { + = directCondStateVarAssign(nodep, stateVscp, nextVscp, resetCondp, resetActiveLow, + resetValue)) { + // Inlined wrappers can normalize into a compact active-low + // assignment form that earlier direct-register FSM support did + // not accept. The pre-inline marker is the architectural fence: + // it lets wrapper-derived registers use that shape without + // changing the meaning of unrelated legacy RTL. + if (resetActiveLow && !stateVscp->varp()->attrFsmRegisterWrapper()) return false; cand.resetArcs().emplace_back(resetValue, assp); cand.resetCond() = describeResetCond(resetCondp); + cand.resetCond().activeLow = resetActiveLow; cand.hasResetCond(cand.resetCond().varScopep != nullptr); } else if (!nodeStateVarAssign(nodep, stateVscp, nextVscp)) { return false; @@ -1186,6 +1435,7 @@ class FsmDetectVisitor final : public VNVisitor { cand.senses() = describeSenTree(alwaysp->sentreep()); cand.resetInclude(stateVscp->varp()->attrFsmResetArc()); cand.inclCond(stateVscp->varp()->attrFsmArcInclCond()); + cand.fileline(alwaysp->fileline()); return true; } @@ -1253,7 +1503,7 @@ class FsmDetectVisitor final : public VNVisitor { FsmStateSpace& stateSpace) { for (const FsmResetArcDesc& resetArc : resetArcs) { StateConstLabel label{resetArc.toValue().ascii(), false, 0}; - if (AstConst* const constp = VN_CAST(resetArc.nodep()->rhsp(), Const)) { + if (AstConst* const constp = VN_CAST(resetArc.valuep(), Const)) { label = stateLabelForConst(constp); } UASSERT_OBJ( @@ -1768,6 +2018,14 @@ class FsmDetectVisitor final : public VNVisitor { // Continuous aliases are unordered hardware connections, so source // order should not affect whether an if-chain FSM is recognized. collectAliasFromAssign(nodep, m_stateAliases, m_ambiguousStateAliases); + collectCellPortAlias(nodep); + iterateChildren(nodep); + } + + void visit(AstCell* nodep) override { + // Cells are matched after the full traversal because linkdot lowers + // uninlined port connections into sibling continuous assignments. + if (m_scopep && fsmRegisterWrapperDesc(nodep)) addWrapperCell(m_scopep, nodep); iterateChildren(nodep); } @@ -1781,6 +2039,12 @@ public: FsmDetectVisitor(FsmState& state, AstNetlist* rootp) : m_state{state} { iterate(rootp); + for (const std::pair& wrapperCell : m_wrapperCells) { + FsmRegisterCandidate reg; + if (matchFsmWrapperCell(wrapperCell.first, wrapperCell.second, reg)) { + m_registerCandidates.emplace_back(reg); + } + } for (const FsmComboAlways& oneBlock : m_oneBlockAlwayss) processOneBlockAlways(oneBlock); for (const FsmComboAlways& combo : m_comboAlwayss) processComboAlways(combo); for (const FsmComboAlways& combo : m_nonComboAlwayss) warnUnsupportedComboAlways(combo); @@ -1812,8 +2076,9 @@ class FsmLowerVisitor final { } static AstNodeExpr* buildResetCond(FileLine* flp, AstVarScope* resetVscp, - const FsmResetCondDesc&) { - return new AstVarRef{flp, resetVscp, VAccess::READ}; + const FsmResetCondDesc& desc) { + AstNodeExpr* const refp = new AstVarRef{flp, resetVscp, VAccess::READ}; + return desc.activeLow ? static_cast(new AstLogNot{flp, refp}) : refp; } // Rebuild the original event control from the saved sense description so @@ -1862,14 +2127,25 @@ class FsmLowerVisitor final { scopep->addBlocksp(initActivep); AstAlwaysPost* const covPostp = new AstAlwaysPost{flp}; - // Save the previous state as plain sequential logic at the front of - // the original always_ff body, then evaluate coverage in post logic - // after the delayed state update commits. This avoids a scheduler race - // between a separate AstAlwaysPre task and the real state commit. - AstNode* const bodysp = alwaysp->stmtsp()->unlinkFrBackWithNext(); - alwaysp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE}, - new AstVarRef{flp, stateVscp, VAccess::READ}}); - alwaysp->addStmtsp(bodysp); + bool updatePrevAfterPost = false; + if (alwaysp) { + // Save the previous state as plain sequential logic at the front of + // the original always_ff body, then evaluate coverage in post logic + // after the delayed state update commits. This avoids a scheduler + // race between a separate AstAlwaysPre task and the real state + // commit for direct parent-level registers. + AstNode* const bodysp = alwaysp->stmtsp()->unlinkFrBackWithNext(); + alwaysp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE}, + new AstVarRef{flp, stateVscp, VAccess::READ}}); + alwaysp->addStmtsp(bodysp); + } else { + // Wrapper-derived register candidates do not have a parent + // always_ff body to splice into. Sample coverage first, then save + // the current state for the next clock tick; this survives cell + // boundary scheduling where the real flop update lives elsewhere. + updatePrevAfterPost = true; + prevVscp->varp()->setIgnorePostRead(); + } for (const V3GraphVertex& vtx : graph.vertices()) { const FsmVertex* const vertexp = vtx.as(); @@ -1960,6 +2236,10 @@ class FsmLowerVisitor final { covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}}); } } + if (updatePrevAfterPost) { + covPostp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE}, + new AstVarRef{flp, stateVscp, VAccess::READ}}); + } AstSenTree* const sentreep = buildSenTree(flp, graph.senses()); AstActive* const activep = new AstActive{flp, "fsm-coverage", sentreep}; @@ -1979,8 +2259,48 @@ public: } }; +// Wrapper FSM support has two architectural paths. If V3Inline removes the +// wrapper, the main detector will later see an ordinary parent-scope always_ff; +// this pre-inline visitor leaves just enough provenance on the q-side state +// variable for that direct path to accept wrapper-specific normalized shapes. +// If the wrapper survives, this marker is harmless and the cell-path detector +// builds a register candidate from the instance itself. +class FsmWrapperMarkerVisitor final : public VNVisitor { + static AstPin* findPin(AstCell* cellp, const string& name) { + for (AstPin* pinp = cellp->pinsp(); pinp; pinp = VN_AS(pinp->nextp(), Pin)) { + if (pinp->name() == name) return pinp; + } + return nullptr; + } + + void visit(AstCell* cellp) override { + if (const V3Control::FsmRegisterWrapper* const descp = fsmRegisterWrapperDesc(cellp)) { + AstPin* const qp = findPin(cellp, descp->q); + if (qp && VN_IS(qp->exprp(), VarRef)) { + AstVarRef* const qrefp = VN_AS(qp->exprp(), VarRef); + // The q-side parent variable is the point where the wrapper + // abstraction collapses into direct RTL after inlining. + // Marking only that variable keeps the provenance narrow: + // transition detection still has to prove the d/q FSM pair. + qrefp->varp()->attrFsmRegisterWrapper(true); + } + } + iterateChildren(cellp); + } + + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + explicit FsmWrapperMarkerVisitor(AstNetlist* rootp) { iterate(rootp); } +}; + } // namespace +void V3FsmDetect::markWrapperStateVars(AstNetlist* rootp) { + UINFO(2, __FUNCTION__ << ":"); + FsmWrapperMarkerVisitor marker{rootp}; +} + void V3FsmDetect::detect(AstNetlist* rootp) { UINFO(2, __FUNCTION__ << ":"); FsmState state; diff --git a/src/V3FsmDetect.h b/src/V3FsmDetect.h index 878efe5be..88faad8bd 100644 --- a/src/V3FsmDetect.h +++ b/src/V3FsmDetect.h @@ -24,6 +24,10 @@ class AstNetlist; class V3FsmDetect final { public: + // Mark parent-scope state variables connected through configured wrapper + // instances before inlining lowers those instances away. + static void markWrapperStateVars(AstNetlist* rootp) VL_MT_DISABLED; + // Detect FSMs while the original clocked/case structure is still visible, // then immediately lower the recovered graphs into concrete coverage // instrumentation as a second local phase in the same pass. diff --git a/src/V3ParseImp.cpp b/src/V3ParseImp.cpp index 3873d2069..ee6459f5e 100644 --- a/src/V3ParseImp.cpp +++ b/src/V3ParseImp.cpp @@ -39,6 +39,7 @@ #include "V3PreShell.h" #include "V3Stats.h" +#include #include VL_DEFINE_DEBUG_FUNCTIONS; diff --git a/src/Verilator.cpp b/src/Verilator.cpp index f67e830b2..5bce5643f 100644 --- a/src/Verilator.cpp +++ b/src/Verilator.cpp @@ -316,6 +316,7 @@ static void process() { // Module inlining // Cannot remove dead variables after this, as alias information for final // V3Scope's V3LinkDot is in the AstVar. + if (v3Global.opt.coverageFsm()) V3FsmDetect::markWrapperStateVars(v3Global.rootp()); if (v3Global.opt.fInline()) { V3Inline::inlineAll(v3Global.rootp()); V3LinkDot::linkDotArrayed(v3Global.rootp()); // Cleanup as made new modules diff --git a/src/verilog.l b/src/verilog.l index 4dc79f210..36058b0a6 100644 --- a/src/verilog.l +++ b/src/verilog.l @@ -136,6 +136,7 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5} "coverage_off" { FL; return yVLT_COVERAGE_OFF; } "coverage_on" { FL; return yVLT_COVERAGE_ON; } "forceable" { FL; return yVLT_FORCEABLE; } + "fsm_register_wrapper" { FL; return yVLT_FSM_REGISTER_WRAPPER; } "full_case" { FL; return yVLT_FULL_CASE; } "hier_block" { FL; return yVLT_HIER_BLOCK; } "hier_params" { FL; return yVLT_HIER_PARAMS; } @@ -166,6 +167,8 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5} -?"-block" { FL; return yVLT_D_BLOCK; } -?"-contents" { FL; return yVLT_D_CONTENTS; } -?"-cost" { FL; return yVLT_D_COST; } + -?"-clock" { FL; return yVLT_D_CLOCK; } + -?"-d" { FL; return yVLT_D_D; } -?"-file" { FL; return yVLT_D_FILE; } -?"-function" { FL; return yVLT_D_FUNCTION; } -?"-hier-dpi" { FL; return yVLT_D_HIER_DPI; } @@ -178,6 +181,9 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5} -?"-param" { FL; return yVLT_D_PARAM; } -?"-port" { FL; return yVLT_D_PORT; } -?"-rule" { FL; return yVLT_D_RULE; } + -?"-q" { FL; return yVLT_D_Q; } + -?"-reset" { FL; return yVLT_D_RESET; } + -?"-reset_value" { FL; return yVLT_D_RESET_VALUE; } -?"-scope" { FL; return yVLT_D_SCOPE; } -?"-task" { FL; return yVLT_D_TASK; } -?"-var" { FL; return yVLT_D_VAR; } diff --git a/src/verilog.y b/src/verilog.y index ab969b77b..477c6fa80 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -245,6 +245,7 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"}) %token yVLT_COVERAGE_OFF "coverage_off" %token yVLT_COVERAGE_ON "coverage_on" %token yVLT_FORCEABLE "forceable" +%token yVLT_FSM_REGISTER_WRAPPER "fsm_register_wrapper" %token yVLT_FULL_CASE "full_case" %token yVLT_HIER_BLOCK "hier_block" %token yVLT_HIER_PARAMS "hier_params" @@ -275,6 +276,8 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"}) %token yVLT_D_BLOCK "--block" %token yVLT_D_CONTENTS "--contents" %token yVLT_D_COST "--cost" +%token yVLT_D_CLOCK "--clock" +%token yVLT_D_D "--d" %token yVLT_D_FILE "--file" %token yVLT_D_FUNCTION "--function" %token yVLT_D_HIER_DPI "--hier-dpi" @@ -287,6 +290,9 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"}) %token yVLT_D_PARAM "--param" %token yVLT_D_PORT "--port" %token yVLT_D_RULE "--rule" +%token yVLT_D_Q "--q" +%token yVLT_D_RESET "--reset" +%token yVLT_D_RESET_VALUE "--reset_value" %token yVLT_D_SCOPE "--scope" %token yVLT_D_TASK "--task" %token yVLT_D_VAR "--var" @@ -8291,6 +8297,8 @@ vltItem: { V3Control::addProfileData($1, *$2, $3->toUQuad()); } | yVLT_PROFILE_DATA vltDModel vltDMtask vltDCost { V3Control::addProfileData($1, *$2, *$3, $4->toUQuad()); } + | yVLT_FSM_REGISTER_WRAPPER vltDModule vltDFsmD vltDFsmQ vltDFsmClock vltDFsmResetE vltDFsmResetValueE + { V3Control::addFsmRegisterWrapper($1, *$2, *$3, *$4, *$5, *$6, *$7); } | yVLT_VERILATOR_LIB vltDModule { V3Control::addModulePragma(*$2, VPragmaType::VERILATOR_LIB); } ; @@ -8331,6 +8339,28 @@ vltDFile: // --file yVLT_D_FILE str { $$ = $2; } ; +vltDFsmClock: // --clock + yVLT_D_CLOCK str { $$ = $2; } + ; + +vltDFsmD: // --d + yVLT_D_D str { $$ = $2; } + ; + +vltDFsmQ: // --q + yVLT_D_Q str { $$ = $2; } + ; + +vltDFsmResetE: // [--reset ] + /* empty */ { static string empty; $$ = ∅ } + | yVLT_D_RESET str { $$ = $2; } + ; + +vltDFsmResetValueE: // [--reset_value ] + /* empty */ { static string empty; $$ = ∅ } + | yVLT_D_RESET_VALUE str { $$ = $2; } + ; + vltDHierDpi: // --hier-dpi yVLT_D_HIER_DPI str { $$ = $2; } ; diff --git a/test_regress/t/t_cover_fsm_negative_extract.out b/test_regress/t/t_cover_fsm_negative_extract.out index 06c1fd797..4c179d7e0 100644 --- a/test_regress/t/t_cover_fsm_negative_extract.out +++ b/test_regress/t/t_cover_fsm_negative_extract.out @@ -4,7 +4,7 @@ // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 - + module fsm_if_mixed_vars_bad ( input logic clk ); @@ -13,20 +13,20 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t other_q; state_t state_d; - + always_comb begin state_d = state_q; if (state_q == S0) state_d = S1; else if (other_q == S1) state_d = S2; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_one_branch_bad ( input logic clk ); @@ -34,18 +34,18 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if (state_q == S0) state_d = S1; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_duplicate_bad ( input logic clk ); @@ -53,19 +53,19 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if (state_q == S0) state_d = S1; else if (state_q == S0) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_two_comparisons_bad ( input logic clk, input logic start @@ -75,19 +75,19 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if ((state_q == S0) && (state_q == S1)) state_d = S2; else if (state_q == S1) state_d = S0; end - + always_ff @(posedge clk) state_q <= start ? state_d : state_q; endmodule - + module fsm_if_or_bad ( input logic clk ); @@ -96,19 +96,19 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if ((state_q == S0) || (state_q == S1)) state_d = S2; else if (state_q == S2) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_alias_guard_bad ( input logic clk, input logic start @@ -118,21 +118,21 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + logic idle_state; state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; idle_state = (state_q == S0) && start; if (idle_state) state_d = S1; else if (state_q == S1) state_d = S2; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_ambiguous_alias_bad ( input logic clk ); @@ -141,11 +141,11 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + logic idle_state; state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; idle_state = (state_q == S0); @@ -154,10 +154,10 @@ if (idle_state) state_d = S2; else if (state_q == S2) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_missing_default_bad ( input logic clk ); @@ -166,19 +166,19 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = S0; if (state_d == S0) state_d = S1; else if (state_d == S1) state_d = S2; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_no_assign_bad ( input logic clk ); @@ -186,21 +186,21 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + logic flag; state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin flag = 1'b0; state_d = state_q; if (state_q == S0) flag = 1'b1; else if (state_q == S1) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_nonvar_compare_bad ( input logic clk ); @@ -208,10 +208,10 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; %000003 if ((state_q + 2'd0) == S0) state_d = S1; @@ -222,10 +222,10 @@ %000003 // [fsm_state t.nonvar_compare_u.state_q::S1] else if (state_q == S1) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_var_rhs_compare_bad ( input logic clk ); @@ -233,19 +233,19 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if (state_q == state_d) state_d = S1; else if (state_q == S1) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_var_target_bad ( input logic clk, input logic [1:0] dyn @@ -255,19 +255,19 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if (state_q == S0) state_d = state_t'(dyn); else if (state_q == S1) state_d = S2; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_alias_other_state_bad ( input logic clk ); @@ -275,12 +275,12 @@ S0 = 2'd0, S1 = 2'd1 } state_t; - + logic idle_state; state_t state_q /*verilator fsm_state*/; state_t other_q; state_t state_d; - + always_comb begin state_d = state_q; idle_state = (state_q == S0); @@ -288,10 +288,10 @@ if (idle_state) state_d = S1; else if (state_q == S1) state_d = S0; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_bit_or_bad ( input logic clk, input logic start @@ -301,19 +301,19 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if ((state_q == S0) | start) state_d = S1; else if (state_q == S1) state_d = S2; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_if_reduction_bad ( input logic clk ); @@ -322,10 +322,10 @@ S1 = 2'd1, S2 = 2'd2 } state_t; - + state_t state_q /*verilator fsm_state*/; state_t state_d; - + always_comb begin state_d = state_q; if (&state_q) state_d = S1; @@ -335,26 +335,45 @@ if (^state_q) state_d = S0; else if (state_q == S0) state_d = S1; end - + always_ff @(posedge clk) state_q <= state_d; endmodule - + module fsm_direct_active_low_dynamic_reset_bad ( + input logic clk, + input logic rst_n, + input logic [1:0] dyn_reset + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + always_comb begin + state_d = state_q; + if (state_q == S0) state_d = S1; + else state_d = S0; + end + always_ff @(posedge clk or negedge rst_n) begin + state_q <= rst_n ? state_d : state_t'(dyn_reset); + end + endmodule module t ( input logic clk ); - + typedef enum logic [2:0] { S0 = 3'd0, S1 = 3'd1, S2 = 3'd2 } state_t; - + int cyc; logic start; logic side; logic [2:0] dyn_case; state_t state /*verilator fsm_reset_arc*/; - + fsm_if_mixed_vars_bad mixed_vars_u (.clk(clk)); fsm_if_one_branch_bad one_branch_u (.clk(clk)); fsm_if_duplicate_bad duplicate_u (.clk(clk)); @@ -370,14 +389,16 @@ fsm_if_alias_other_state_bad alias_other_state_u (.clk(clk)); fsm_if_bit_or_bad bit_or_u (.clk(clk), .start(start)); fsm_if_reduction_bad reduction_u (.clk(clk)); - + fsm_direct_active_low_dynamic_reset_bad active_low_dynamic_reset_u ( + .clk(clk), .rst_n(cyc != 0), .dyn_reset(dyn_case[1:0])); + initial begin cyc = 0; start = 1'b0; side = 1'b0; dyn_case = 3'd7; end - + always @(posedge clk) begin cyc <= cyc + 1; if (cyc == 1) side <= 1'b1; @@ -387,7 +408,7 @@ $finish; end end - + // The grouped S0/dyn_case arm keeps the supported S0 baseline while the // non-constant case item is skipped. The S1 and default arms are // deliberately unsupported extractor shapes: one has two meaningful @@ -416,6 +437,6 @@ endcase end end - + endmodule - + diff --git a/test_regress/t/t_cover_fsm_negative_extract.py b/test_regress/t/t_cover_fsm_negative_extract.py index 53a99158c..de42b16de 100755 --- a/test_regress/t/t_cover_fsm_negative_extract.py +++ b/test_regress/t/t_cover_fsm_negative_extract.py @@ -32,6 +32,13 @@ test.run(cmd=[ ], verilator_run=True) -test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +annotated_filename = test.obj_dir + "/annotated/" + test.name + ".v" +normalized_filename = test.obj_dir + "/annotated/" + test.name + ".normalized.v" +with open(annotated_filename, encoding="utf-8") as in_file: + with open(normalized_filename, "w", encoding="utf-8") as out_file: + for line in in_file: + out_file.write(line.rstrip() + "\n") + +test.files_identical(normalized_filename, test.golden_filename) test.passes() diff --git a/test_regress/t/t_cover_fsm_negative_extract.v b/test_regress/t/t_cover_fsm_negative_extract.v index 251b2a6c7..a43942139 100644 --- a/test_regress/t/t_cover_fsm_negative_extract.v +++ b/test_regress/t/t_cover_fsm_negative_extract.v @@ -332,7 +332,26 @@ module fsm_if_reduction_bad ( always_ff @(posedge clk) state_q <= state_d; endmodule - +module fsm_direct_active_low_dynamic_reset_bad ( + input logic clk, + input logic rst_n, + input logic [1:0] dyn_reset +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + always_comb begin + state_d = state_q; + if (state_q == S0) state_d = S1; + else state_d = S0; + end + always_ff @(posedge clk or negedge rst_n) begin + state_q <= rst_n ? state_d : state_t'(dyn_reset); + end +endmodule module t ( input logic clk ); @@ -364,6 +383,8 @@ module t ( fsm_if_alias_other_state_bad alias_other_state_u (.clk(clk)); fsm_if_bit_or_bad bit_or_u (.clk(clk), .start(start)); fsm_if_reduction_bad reduction_u (.clk(clk)); + fsm_direct_active_low_dynamic_reset_bad active_low_dynamic_reset_u ( + .clk(clk), .rst_n(cyc != 0), .dyn_reset(dyn_case[1:0])); initial begin cyc = 0; diff --git a/test_regress/t/t_fsm_register_wrapper.out b/test_regress/t/t_fsm_register_wrapper.out new file mode 100644 index 000000000..6490e4957 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper.out @@ -0,0 +1,522 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage for fsm_register_wrapper state register wrappers + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module my_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end + endmodule + + module prim_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] d_i, + output logic [Width-1:0] q_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) q_o <= ResetValue; + else q_o <= d_i; + end + endmodule + + module prim_sparse_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end + endmodule + + module odd_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout + ); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end + endmodule + + module ambiguous_fsm_flop ( + input logic clk_i, + input logic rst_ni, + input logic [1:0] state_i, + input logic [1:0] d_i, + output logic [1:0] state_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= 2'd0; + else state_o <= state_i ^ d_i; + end + endmodule + + module fsm_auto ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.auto_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.auto_u.state_q::S0->S0] +%000001 // [fsm_arc t.auto_u.state_q::S0->S1] +%000001 // [fsm_arc t.auto_u.state_q::S1->S2] +%000001 // [fsm_state t.auto_u.state_q::S0] +%000001 // [fsm_state t.auto_u.state_q::S1] +%000001 // [fsm_state t.auto_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_noargs_hint ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.noargs_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.noargs_u.state_q::S0->S0] +%000001 // [fsm_arc t.noargs_u.state_q::S0->S1] +%000001 // [fsm_arc t.noargs_u.state_q::S1->S2] +%000001 // [fsm_state t.noargs_u.state_q::S0] +%000001 // [fsm_state t.noargs_u.state_q::S1] +%000001 // [fsm_state t.noargs_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_prim ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.prim_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.prim_u.state_q::S0->S0] +%000001 // [fsm_arc t.prim_u.state_q::S0->S1] +%000001 // [fsm_arc t.prim_u.state_q::S1->S2] +%000001 // [fsm_state t.prim_u.state_q::S0] +%000001 // [fsm_state t.prim_u.state_q::S1] +%000001 // [fsm_state t.prim_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); + endmodule + + module fsm_sparse_prim ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000005 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.sparse_prim_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000005 // [fsm_arc t.sparse_prim_u.state_q::S0->S0] +%000001 // [fsm_arc t.sparse_prim_u.state_q::S0->S1] +%000001 // [fsm_arc t.sparse_prim_u.state_q::S1->S2] +%000002 // [fsm_state t.sparse_prim_u.state_q::S0] +%000001 // [fsm_state t.sparse_prim_u.state_q::S1] +%000001 // [fsm_state t.sparse_prim_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_ifchain ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + if (state_q == S0) begin + state_d = start ? S1 : S0; + end + else if (state_q == S1) begin + state_d = S2; + end + else begin + state_d = S0; + end + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_wide_sparse ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [39:0] { + S0 = 40'h0000_0000_01, + S1 = 40'h8000_0000_02, + S2 = 40'hffff_0000_03 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000005 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.wide_sparse_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000005 // [fsm_arc t.wide_sparse_u.state_q::S0->S0] +%000001 // [fsm_arc t.wide_sparse_u.state_q::S0->S1] +%000001 // [fsm_arc t.wide_sparse_u.state_q::S1->S2] +%000002 // [fsm_state t.wide_sparse_u.state_q::S0] +%000001 // [fsm_state t.wide_sparse_u.state_q::S1] +%000001 // [fsm_state t.wide_sparse_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_annotated ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.annotated_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.annotated_u.state_q::S0->S0] +%000001 // [fsm_arc t.annotated_u.state_q::S0->S1] +%000001 // [fsm_arc t.annotated_u.state_q::S1->S2] +%000001 // [fsm_state t.annotated_u.state_q::S0] +%000001 // [fsm_state t.annotated_u.state_q::S1] +%000001 // [fsm_state t.annotated_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + odd_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .din(state_d), + .dout(state_q) + ); + endmodule + + module fsm_non_simple ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.non_simple_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.non_simple_u.state_q::S0->S0] +%000001 // [fsm_arc t.non_simple_u.state_q::S0->S1] +%000001 // [fsm_arc t.non_simple_u.state_q::S1->S2] +%000001 // [fsm_state t.non_simple_u.state_q::S0] +%000001 // [fsm_state t.non_simple_u.state_q::S1] +%000001 // [fsm_state t.non_simple_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d[1:0]), + .state_o(state_q) + ); + endmodule + + module fsm_ambiguous ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + ambiguous_fsm_flop u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .d_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_ignored ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.ignored_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.ignored_u.state_q::S0->S0] +%000001 // [fsm_arc t.ignored_u.state_q::S0->S1] +%000001 // [fsm_arc t.ignored_u.state_q::S1->S2] +%000001 // [fsm_state t.ignored_u.state_q::S0] +%000001 // [fsm_state t.ignored_u.state_q::S1] +%000001 // [fsm_state t.ignored_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + odd_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .din(state_d), + .dout(state_q) + ); + endmodule + + module t ( + input logic clk + ); + logic rst_n; + logic start; + integer cyc; + + initial begin + rst_n = 1'b0; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst_n <= 1'b1; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + fsm_auto auto_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_noargs_hint noargs_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim sparse_prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ifchain ifchain_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_wide_sparse wide_sparse_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_annotated annotated_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_non_simple non_simple_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ambiguous ambiguous_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ignored ignored_u (.clk(clk), .rst_n(rst_n), .start(start)); + endmodule + diff --git a/test_regress/t/t_fsm_register_wrapper.py b/test_regress/t/t_fsm_register_wrapper.py new file mode 100644 index 000000000..e92b74275 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage for fsm_register_wrapper state register wrappers +# +# 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-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm t/t_fsm_register_wrapper.vlt']) + +test.execute() + +test.run( + cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", + ], + verilator_run=True, +) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_fsm_register_wrapper.v b/test_regress/t/t_fsm_register_wrapper.v new file mode 100644 index 000000000..69adfc2f1 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper.v @@ -0,0 +1,456 @@ +// DESCRIPTION: Verilator: FSM coverage for fsm_register_wrapper state register wrappers +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module my_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end +endmodule + +module prim_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] d_i, + output logic [Width-1:0] q_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) q_o <= ResetValue; + else q_o <= d_i; + end +endmodule + +module prim_sparse_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end +endmodule + +module odd_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module ambiguous_fsm_flop ( + input logic clk_i, + input logic rst_ni, + input logic [1:0] state_i, + input logic [1:0] d_i, + output logic [1:0] state_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= 2'd0; + else state_o <= state_i ^ d_i; + end +endmodule + +module fsm_auto ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_noargs_hint ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_prim ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); +endmodule + +module fsm_sparse_prim ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_ifchain ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + if (state_q == S0) begin + state_d = start ? S1 : S0; + end + else if (state_q == S1) begin + state_d = S2; + end + else begin + state_d = S0; + end + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_wide_sparse ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [39:0] { + S0 = 40'h0000_0000_01, + S1 = 40'h8000_0000_02, + S2 = 40'hffff_0000_03 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_annotated ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + odd_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .din(state_d), + .dout(state_q) + ); +endmodule + +module fsm_non_simple ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d[1:0]), + .state_o(state_q) + ); +endmodule + +module fsm_ambiguous ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + ambiguous_fsm_flop u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .d_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_ignored ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + odd_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .din(state_d), + .dout(state_q) + ); +endmodule + +module t ( + input logic clk +); + logic rst_n; + logic start; + integer cyc; + + initial begin + rst_n = 1'b0; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst_n <= 1'b1; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + fsm_auto auto_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_noargs_hint noargs_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim sparse_prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ifchain ifchain_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_wide_sparse wide_sparse_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_annotated annotated_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_non_simple non_simple_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ambiguous ambiguous_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_ignored ignored_u (.clk(clk), .rst_n(rst_n), .start(start)); +endmodule diff --git a/test_regress/t/t_fsm_register_wrapper.vlt b/test_regress/t/t_fsm_register_wrapper.vlt new file mode 100644 index 000000000..06889dc97 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper.vlt @@ -0,0 +1,12 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT descriptors +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +`verilator_config + +fsm_register_wrapper -module "my_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" -reset "rst_ni" -reset_value "ResetValue" +fsm_register_wrapper -module "prim_flop" -d "d_i" -q "q_o" -clock "clk_i" -reset "rst_ni" -reset_value "ResetValue" +fsm_register_wrapper -module "prim_sparse_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" -reset "rst_ni" -reset_value "ResetValue" +fsm_register_wrapper -module "odd_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "rst_n" -reset_value "ResetValue" diff --git a/test_regress/t/t_fsm_register_wrapper_noinline.out b/test_regress/t/t_fsm_register_wrapper_noinline.out new file mode 100644 index 000000000..fd8303202 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_noinline.out @@ -0,0 +1,850 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage for non-inlined fsm_register_wrapper state register wrappers + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module my_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end + endmodule + + module prim_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] d_i, + output logic [Width-1:0] q_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) q_o <= ResetValue; + else q_o <= d_i; + end + endmodule + + module prim_sparse_fsm_flop #( + parameter int Width = 1, + parameter type StateEnumT = logic [Width-1:0], + parameter logic [Width-1:0] ResetValue = '0, + parameter bit EnableAlertTriggerSVA = 1 + ) ( + input logic clk_i, + input logic rst_ni, + input StateEnumT state_i, + output StateEnumT state_o + ); + logic unused_alert_sva; + logic [Width-1:0] state_raw; + + prim_flop #( + .Width(Width), + .ResetValue(ResetValue) + ) u_state_flop ( + .clk_i(clk_i), + .rst_ni(rst_ni), + .d_i(state_i), + .q_o(state_raw) + ); + + assign state_o = StateEnumT'(state_raw); + assign unused_alert_sva = EnableAlertTriggerSVA; + endmodule + + module active_high_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic rst, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk or posedge rst) begin + if (rst) q <= ResetValue; + else q <= d; + end + endmodule + + module active_high_rst_i_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic rst_i, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk or posedge rst_i) begin + if (rst_i) q <= ResetValue; + else q <= d; + end + endmodule + + module custom_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + logic [Width-1:0] d_shadow; + + always_comb begin + d_shadow = d; + end + + always_ff @(posedge clk or negedge my_reset) begin + if (!my_reset) q <= ResetValue; + else q <= d_shadow; + end + endmodule + + module custom_reset_active_high_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk or posedge my_reset) begin + if (my_reset) q <= ResetValue; + else q <= d; + end + endmodule + + module no_reset_fsm_flop #( + parameter int Width = 1 + ) ( + input logic clk, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk) begin + q <= d; + end + endmodule + + module reset_no_param_fsm_flop #( + parameter int Width = 1 + ) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) q <= '0; + else q <= d; + end + endmodule + + module sync_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 + ) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q + ); + always_ff @(posedge clk) begin + if (my_reset) q <= ResetValue; + else q <= d; + end + endmodule + + module ambiguous_fsm_flop ( + input logic clk_i, + input logic rst_ni, + input logic [1:0] state_i, + input logic [1:0] d_i, + output logic [1:0] state_o + ); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= 2'd0; + else state_o <= state_i ^ d_i; + end + endmodule + + module fsm_auto ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.auto_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.auto_u.state_q::S0->S0] +%000001 // [fsm_arc t.auto_u.state_q::S0->S1] +%000001 // [fsm_arc t.auto_u.state_q::S1->S2] +%000001 // [fsm_state t.auto_u.state_q::S0] +%000001 // [fsm_state t.auto_u.state_q::S1] +%000001 // [fsm_state t.auto_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_prim ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.prim_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.prim_u.state_q::S0->S0] +%000001 // [fsm_arc t.prim_u.state_q::S0->S1] +%000001 // [fsm_arc t.prim_u.state_q::S1->S2] +%000001 // [fsm_state t.prim_u.state_q::S0] +%000001 // [fsm_state t.prim_u.state_q::S1] +%000001 // [fsm_state t.prim_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); + endmodule + + module fsm_prim_override ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.prim_override_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.prim_override_u.state_q::S0->S0] +%000001 // [fsm_arc t.prim_override_u.state_q::S0->S1] +%000001 // [fsm_arc t.prim_override_u.state_q::S1->S2] +%000001 // [fsm_state t.prim_override_u.state_q::S0] +%000001 // [fsm_state t.prim_override_u.state_q::S1] +%000001 // [fsm_state t.prim_override_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); + endmodule + + module fsm_sparse_prim ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000005 unique case (state_q) + // [FSM coverage] +%000005 // [fsm_arc t.sparse_prim_u.state_q::S0->S0] +%000001 // [fsm_arc t.sparse_prim_u.state_q::S0->S1] +%000001 // [fsm_arc t.sparse_prim_u.state_q::S1->S2] +%000002 // [fsm_state t.sparse_prim_u.state_q::S0] +%000001 // [fsm_state t.sparse_prim_u.state_q::S1] +%000001 // [fsm_state t.sparse_prim_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .StateEnumT(state_t), + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_sparse_prim_override ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000005 unique case (state_q) + // [FSM coverage] +%000005 // [fsm_arc t.sparse_prim_override_u.state_q::S0->S0] +%000001 // [fsm_arc t.sparse_prim_override_u.state_q::S0->S1] +%000001 // [fsm_arc t.sparse_prim_override_u.state_q::S1->S2] +%000002 // [fsm_state t.sparse_prim_override_u.state_q::S0] +%000001 // [fsm_state t.sparse_prim_override_u.state_q::S1] +%000001 // [fsm_state t.sparse_prim_override_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .StateEnumT(state_t), + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_active_high ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000007 unique case (state_q) + // [FSM coverage] +%000002 // [fsm_arc t.active_high_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000007 // [fsm_arc t.active_high_u.state_q::S0->S0] +%000001 // [fsm_arc t.active_high_u.state_q::S0->S1] +%000001 // [fsm_arc t.active_high_u.state_q::S1->S2] +%000001 // [fsm_state t.active_high_u.state_q::S0] +%000001 // [fsm_state t.active_high_u.state_q::S1] +%000001 // [fsm_state t.active_high_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + active_high_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst(rst), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_active_high_rst_i ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000007 unique case (state_q) + // [FSM coverage] +%000002 // [fsm_arc t.active_high_rst_i_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000007 // [fsm_arc t.active_high_rst_i_u.state_q::S0->S0] +%000001 // [fsm_arc t.active_high_rst_i_u.state_q::S0->S1] +%000001 // [fsm_arc t.active_high_rst_i_u.state_q::S1->S2] +%000001 // [fsm_state t.active_high_rst_i_u.state_q::S0] +%000001 // [fsm_state t.active_high_rst_i_u.state_q::S1] +%000001 // [fsm_state t.active_high_rst_i_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + active_high_rst_i_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_i(rst), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_no_reset ( + input logic clk, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000006 // [fsm_arc t.no_reset_u.state_q::S0->S0] +%000001 // [fsm_arc t.no_reset_u.state_q::S0->S1] +%000001 // [fsm_arc t.no_reset_u.state_q::S1->S2] +%000001 // [fsm_state t.no_reset_u.state_q::S0] +%000001 // [fsm_state t.no_reset_u.state_q::S1] +%000001 // [fsm_state t.no_reset_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + no_reset_fsm_flop #( + .Width($bits(state_t)) + ) u_state_regs ( + .clk(clk), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_reset_no_param ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000006 // [fsm_arc t.reset_no_param_u.state_q::S0->S0] +%000001 // [fsm_arc t.reset_no_param_u.state_q::S0->S1] +%000001 // [fsm_arc t.reset_no_param_u.state_q::S1->S2] +%000001 // [fsm_state t.reset_no_param_u.state_q::S0] +%000001 // [fsm_state t.reset_no_param_u.state_q::S1] +%000001 // [fsm_state t.reset_no_param_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + reset_no_param_fsm_flop #( + .Width($bits(state_t)) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_sync_reset_unknown_polarity ( + input logic clk, + input logic parent_reset, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000006 // [fsm_arc t.sync_reset_u.state_q::S0->S0] +%000001 // [fsm_arc t.sync_reset_u.state_q::S0->S1] +%000001 // [fsm_arc t.sync_reset_u.state_q::S1->S2] +%000001 // [fsm_state t.sync_reset_u.state_q::S0] +%000001 // [fsm_state t.sync_reset_u.state_q::S1] +%000001 // [fsm_state t.sync_reset_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + sync_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_custom_reset_parent_polarity ( + input logic clk, + input logic parent_reset, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.custom_reset_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.custom_reset_u.state_q::S0->S0] +%000001 // [fsm_arc t.custom_reset_u.state_q::S0->S1] +%000001 // [fsm_arc t.custom_reset_u.state_q::S1->S2] +%000001 // [fsm_state t.custom_reset_u.state_q::S0] +%000001 // [fsm_state t.custom_reset_u.state_q::S1] +%000001 // [fsm_state t.custom_reset_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + custom_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_custom_reset_active_high ( + input logic clk, + input logic parent_reset, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000007 unique case (state_q) + // [FSM coverage] +%000002 // [fsm_arc t.custom_reset_active_high_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000007 // [fsm_arc t.custom_reset_active_high_u.state_q::S0->S0] +%000001 // [fsm_arc t.custom_reset_active_high_u.state_q::S0->S1] +%000001 // [fsm_arc t.custom_reset_active_high_u.state_q::S1->S2] +%000001 // [fsm_state t.custom_reset_active_high_u.state_q::S0] +%000001 // [fsm_state t.custom_reset_active_high_u.state_q::S1] +%000001 // [fsm_state t.custom_reset_active_high_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + custom_reset_active_high_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); + endmodule + + module fsm_ambiguous_ignored ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + ambiguous_fsm_flop u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .d_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_missing_reset_param ( + input logic clk, + input logic rst_n, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.missing_reset_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.missing_reset_u.state_q::S0->S0] +%000001 // [fsm_arc t.missing_reset_u.state_q::S0->S1] +%000001 // [fsm_arc t.missing_reset_u.state_q::S1->S2] +%000001 // [fsm_state t.missing_reset_u.state_q::S0] +%000001 // [fsm_state t.missing_reset_u.state_q::S1] +%000001 // [fsm_state t.missing_reset_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + endmodule + + module fsm_competing_direct ( + input logic clk, + input logic rst_n, + input logic start + ); + /* verilator lint_off BLKANDNBLK */ + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; +%000006 unique case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.competing_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000006 // [fsm_arc t.competing_u.state_q::S0->S0] +%000001 // [fsm_arc t.competing_u.state_q::S0->S1] +%000001 // [fsm_arc t.competing_u.state_q::S1->S2] +%000001 // [fsm_state t.competing_u.state_q::S0] +%000001 // [fsm_state t.competing_u.state_q::S1] +%000001 // [fsm_state t.competing_u.state_q::S2] + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) state_q <= S0; + else state_q <= state_d; + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + /* verilator lint_on BLKANDNBLK */ + endmodule + + module t ( + input logic clk + ); + logic rst_n; + logic rst; + logic start; + integer cyc; + + initial begin + rst_n = 1'b0; + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst_n <= 1'b1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + fsm_auto auto_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim_override prim_override_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim sparse_prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim_override sparse_prim_override_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_active_high active_high_u (.clk(clk), .rst(rst), .start(start)); + fsm_active_high_rst_i active_high_rst_i_u (.clk(clk), .rst(rst), .start(start)); + fsm_no_reset no_reset_u (.clk(clk), .start(start)); + fsm_reset_no_param reset_no_param_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sync_reset_unknown_polarity sync_reset_u (.clk(clk), .parent_reset(rst), .start(start)); + fsm_custom_reset_parent_polarity custom_reset_u (.clk(clk), .parent_reset(rst_n), .start(start)); + fsm_custom_reset_active_high custom_reset_active_high_u ( + .clk(clk), .parent_reset(rst), .start(start)); + fsm_ambiguous_ignored ambiguous_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_missing_reset_param missing_reset_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_competing_direct competing_u (.clk(clk), .rst_n(rst_n), .start(start)); + endmodule + diff --git a/test_regress/t/t_fsm_register_wrapper_noinline.py b/test_regress/t/t_fsm_register_wrapper_noinline.py new file mode 100644 index 000000000..a95e3a4be --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_noinline.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage for non-inlined fsm_register_wrapper state register wrappers +# +# 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-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm -fno-inline t/t_fsm_register_wrapper_noinline.vlt']) + +test.execute() + +test.run( + cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", + ], + verilator_run=True, +) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_fsm_register_wrapper_noinline.v b/test_regress/t/t_fsm_register_wrapper_noinline.v new file mode 100644 index 000000000..439142c83 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_noinline.v @@ -0,0 +1,741 @@ +// DESCRIPTION: Verilator: FSM coverage for non-inlined fsm_register_wrapper state register wrappers +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module my_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] state_i, + output logic [Width-1:0] state_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= ResetValue; + else state_o <= state_i; + end +endmodule + +module prim_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk_i, + input logic rst_ni, + input logic [Width-1:0] d_i, + output logic [Width-1:0] q_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) q_o <= ResetValue; + else q_o <= d_i; + end +endmodule + +module prim_sparse_fsm_flop #( + parameter int Width = 1, + parameter type StateEnumT = logic [Width-1:0], + parameter logic [Width-1:0] ResetValue = '0, + parameter bit EnableAlertTriggerSVA = 1 +) ( + input logic clk_i, + input logic rst_ni, + input StateEnumT state_i, + output StateEnumT state_o +); + logic unused_alert_sva; + logic [Width-1:0] state_raw; + + prim_flop #( + .Width(Width), + .ResetValue(ResetValue) + ) u_state_flop ( + .clk_i(clk_i), + .rst_ni(rst_ni), + .d_i(state_i), + .q_o(state_raw) + ); + + assign state_o = StateEnumT'(state_raw); + assign unused_alert_sva = EnableAlertTriggerSVA; +endmodule + +module active_high_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk or posedge rst) begin + if (rst) q <= ResetValue; + else q <= d; + end +endmodule + +module active_high_rst_i_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_i, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk or posedge rst_i) begin + if (rst_i) q <= ResetValue; + else q <= d; + end +endmodule + +module custom_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + logic [Width-1:0] d_shadow; + + always_comb begin + d_shadow = d; + end + + always_ff @(posedge clk or negedge my_reset) begin + if (!my_reset) q <= ResetValue; + else q <= d_shadow; + end +endmodule + +module custom_reset_active_high_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk or posedge my_reset) begin + if (my_reset) q <= ResetValue; + else q <= d; + end +endmodule + +module no_reset_fsm_flop #( + parameter int Width = 1 +) ( + input logic clk, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk) begin + q <= d; + end +endmodule + +module reset_no_param_fsm_flop #( + parameter int Width = 1 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) q <= '0; + else q <= d; + end +endmodule + +module sync_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic my_reset, + input logic [Width-1:0] d, + output logic [Width-1:0] q +); + always_ff @(posedge clk) begin + if (my_reset) q <= ResetValue; + else q <= d; + end +endmodule + +module ambiguous_fsm_flop ( + input logic clk_i, + input logic rst_ni, + input logic [1:0] state_i, + input logic [1:0] d_i, + output logic [1:0] state_o +); + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) state_o <= 2'd0; + else state_o <= state_i ^ d_i; + end +endmodule + +module fsm_auto ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_prim ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); +endmodule + +module fsm_prim_override ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .d_i(state_d), + .q_o(state_q) + ); +endmodule + +module fsm_sparse_prim ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .StateEnumT(state_t), + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_sparse_prim_override ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [5:0] { + S0 = 6'b00_0001, + S1 = 6'b10_0100, + S2 = 6'b11_0010 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + prim_sparse_fsm_flop #( + .StateEnumT(state_t), + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_active_high ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + active_high_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst(rst), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_active_high_rst_i ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + active_high_rst_i_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .rst_i(rst), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_no_reset ( + input logic clk, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + no_reset_fsm_flop #( + .Width($bits(state_t)) + ) u_state_regs ( + .clk(clk), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_reset_no_param ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + reset_no_param_fsm_flop #( + .Width($bits(state_t)) + ) u_state_regs ( + .clk(clk), + .rst_n(rst_n), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_sync_reset_unknown_polarity ( + input logic clk, + input logic parent_reset, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + sync_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_custom_reset_parent_polarity ( + input logic clk, + input logic parent_reset, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + custom_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_custom_reset_active_high ( + input logic clk, + input logic parent_reset, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + custom_reset_active_high_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk(clk), + .my_reset(parent_reset), + .d(state_d), + .q(state_q) + ); +endmodule + +module fsm_ambiguous_ignored ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + ambiguous_fsm_flop u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .d_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_missing_reset_param ( + input logic clk, + input logic rst_n, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); +endmodule + +module fsm_competing_direct ( + input logic clk, + input logic rst_n, + input logic start +); + /* verilator lint_off BLKANDNBLK */ + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + unique case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) state_q <= S0; + else state_q <= state_d; + end + + my_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_state_regs ( + .clk_i(clk), + .rst_ni(rst_n), + .state_i(state_d), + .state_o(state_q) + ); + /* verilator lint_on BLKANDNBLK */ +endmodule + +module t ( + input logic clk +); + logic rst_n; + logic rst; + logic start; + integer cyc; + + initial begin + rst_n = 1'b0; + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst_n <= 1'b1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + fsm_auto auto_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_prim_override prim_override_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim sparse_prim_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sparse_prim_override sparse_prim_override_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_active_high active_high_u (.clk(clk), .rst(rst), .start(start)); + fsm_active_high_rst_i active_high_rst_i_u (.clk(clk), .rst(rst), .start(start)); + fsm_no_reset no_reset_u (.clk(clk), .start(start)); + fsm_reset_no_param reset_no_param_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_sync_reset_unknown_polarity sync_reset_u (.clk(clk), .parent_reset(rst), .start(start)); + fsm_custom_reset_parent_polarity custom_reset_u (.clk(clk), .parent_reset(rst_n), .start(start)); + fsm_custom_reset_active_high custom_reset_active_high_u ( + .clk(clk), .parent_reset(rst), .start(start)); + fsm_ambiguous_ignored ambiguous_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_missing_reset_param missing_reset_u (.clk(clk), .rst_n(rst_n), .start(start)); + fsm_competing_direct competing_u (.clk(clk), .rst_n(rst_n), .start(start)); +endmodule diff --git a/test_regress/t/t_fsm_register_wrapper_noinline.vlt b/test_regress/t/t_fsm_register_wrapper_noinline.vlt new file mode 100644 index 000000000..e5d2b9317 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_noinline.vlt @@ -0,0 +1,18 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT descriptors +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +`verilator_config + +fsm_register_wrapper -module "my_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" -reset "rst_ni" -reset_value "ResetValue" +fsm_register_wrapper -module "prim_flop" -d "d_i" -q "q_o" -clock "clk_i" -reset "rst_ni" -reset_value "ResetValue" +fsm_register_wrapper -module "prim_sparse_fsm_flop" -d "state_i" -q "state_o" -clock "clk_i" +fsm_register_wrapper -module "active_high_fsm_flop" -d "d" -q "q" -clock "clk" -reset "rst" -reset_value "ResetValue" +fsm_register_wrapper -module "active_high_rst_i_fsm_flop" -d "d" -q "q" -clock "clk" -reset "rst_i" -reset_value "ResetValue" +fsm_register_wrapper -module "custom_reset_fsm_flop" -d "d" -q "q" -clock "clk" -reset "my_reset" -reset_value "ResetValue" +fsm_register_wrapper -module "custom_reset_active_high_fsm_flop" -d "d" -q "q" -clock "clk" -reset "my_reset" -reset_value "ResetValue" +fsm_register_wrapper -module "no_reset_fsm_flop" -d "d" -q "q" -clock "clk" +fsm_register_wrapper -module "reset_no_param_fsm_flop" -d "d" -q "q" -clock "clk" +fsm_register_wrapper -module "sync_reset_fsm_flop" -d "d" -q "q" -clock "clk" diff --git a/test_regress/t/t_fsm_register_wrapper_vlt_bad.out b/test_regress/t/t_fsm_register_wrapper_vlt_bad.out new file mode 100644 index 000000000..aa853eb36 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_vlt_bad.out @@ -0,0 +1,21 @@ +%Error: t/t_fsm_register_wrapper_vlt_bad.vlt:9:1: fsm_register_wrapper missing -module + 9 | fsm_register_wrapper -module "" -d "din" -q "dout" -clock "clk" + | ^~~~~~~~~~~~~~~~~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_fsm_register_wrapper_vlt_bad.vlt:10:1: fsm_register_wrapper missing -module, -d, -q, -clock + 10 | fsm_register_wrapper -module "" -d "" -q "" -clock "" + | ^~~~~~~~~~~~~~~~~~~~ +%Error: t/t_fsm_register_wrapper_vlt_bad.vlt:11:1: fsm_register_wrapper missing -d + 11 | fsm_register_wrapper -module "missing_d_fsm_flop" -d "" -q "dout" -clock "clk" + | ^~~~~~~~~~~~~~~~~~~~ +%Error: t/t_fsm_register_wrapper_vlt_bad.vlt:12:1: fsm_register_wrapper missing -q + 12 | fsm_register_wrapper -module "missing_q_fsm_flop" -d "din" -q "" -clock "clk" + | ^~~~~~~~~~~~~~~~~~~~ +%Error: t/t_fsm_register_wrapper_vlt_bad.vlt:13:1: fsm_register_wrapper missing -clock + 13 | fsm_register_wrapper -module "missing_clock_fsm_flop" -d "din" -q "dout" -clock "" + | ^~~~~~~~~~~~~~~~~~~~ +%Error-BADVLTPRAGMA: t/t_fsm_register_wrapper_vlt_bad.vlt:15:1: Duplicate fsm_register_wrapper descriptor for module 'duplicate_fsm_flop'; replacing previous descriptor + 15 | fsm_register_wrapper -module "duplicate_fsm_flop" -d "din" -q "dout" -clock "clk" + | ^~~~~~~~~~~~~~~~~~~~ + ... For error description see https://verilator.org/warn/BADVLTPRAGMA?v=latest +%Error: Exiting due to diff --git a/test_regress/t/t_fsm_register_wrapper_vlt_bad.py b/test_regress/t/t_fsm_register_wrapper_vlt_bad.py new file mode 100644 index 000000000..40d3932a7 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_vlt_bad.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM register wrapper VLT bad descriptor coverage +# +# 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-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint( + verilator_flags2=["t/t_fsm_register_wrapper_vlt_bad.vlt"], + fails=True, + expect_filename=test.golden_filename, +) + +test.passes() diff --git a/test_regress/t/t_fsm_register_wrapper_vlt_bad.v b/test_regress/t/t_fsm_register_wrapper_vlt_bad.v new file mode 100644 index 000000000..1223fd386 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_vlt_bad.v @@ -0,0 +1,8 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT bad descriptor coverage +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t; +endmodule diff --git a/test_regress/t/t_fsm_register_wrapper_vlt_bad.vlt b/test_regress/t/t_fsm_register_wrapper_vlt_bad.vlt new file mode 100644 index 000000000..07a6f3e58 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_vlt_bad.vlt @@ -0,0 +1,15 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT bad descriptor coverage +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +`verilator_config + +fsm_register_wrapper -module "" -d "din" -q "dout" -clock "clk" +fsm_register_wrapper -module "" -d "" -q "" -clock "" +fsm_register_wrapper -module "missing_d_fsm_flop" -d "" -q "dout" -clock "clk" +fsm_register_wrapper -module "missing_q_fsm_flop" -d "din" -q "" -clock "clk" +fsm_register_wrapper -module "missing_clock_fsm_flop" -d "din" -q "dout" -clock "" +fsm_register_wrapper -module "duplicate_fsm_flop" -d "din" -q "dout" -clock "clk" +fsm_register_wrapper -module "duplicate_fsm_flop" -d "din" -q "dout" -clock "clk" diff --git a/test_regress/t/t_fsm_register_wrapper_warn_bad.out b/test_regress/t/t_fsm_register_wrapper_warn_bad.out new file mode 100644 index 000000000..9ee19b6f2 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_warn_bad.out @@ -0,0 +1,35 @@ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:156:5: Ignoring unsupported: fsm_register_wrapper d and q connections must be simple variables + : ... note: In instance 't' + 156 | ) u_bad_simple ( + | ^~~~~~~~~~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:166:5: Ignoring unsupported: fsm_register_wrapper d and q connections must be simple variables + : ... note: In instance 't' + 166 | ) u_missing_q_port ( + | ^~~~~~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:176:5: Ignoring unsupported: fsm_register_wrapper instance requires a simple clock connection + : ... note: In instance 't' + 176 | ) u_missing_clock_port ( + | ^~~~~~~~~~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:186:5: Ignoring unsupported: fsm_register_wrapper reset arcs require both reset polarity and static reset value; reset_value parameter is not configured + : ... note: In instance 't' + 186 | ) u_partial_reset ( + | ^~~~~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:196:5: Ignoring unsupported: fsm_register_wrapper reset arcs require both reset polarity and static reset value; reset_value parameter is missing or not static + : ... note: In instance 't' + 196 | ) u_missing_reset_value ( + | ^~~~~~~~~~~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:206:5: Ignoring unsupported: fsm_register_wrapper reset arcs require both reset polarity and static reset value; reset port is not configured + : ... note: In instance 't' + 206 | ) u_value_no_reset ( + | ^~~~~~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:216:5: Ignoring unsupported: fsm_register_wrapper reset arcs require both reset polarity and static reset value; reset polarity could not be inferred from the wrapper + : ... note: In instance 't' + 216 | ) u_sync_reset ( + | ^~~~~~~~~~~~ +%Warning-COVERIGN: t/t_fsm_register_wrapper_warn_bad.v:226:5: Ignoring unsupported: fsm_register_wrapper reset arcs require both reset polarity and static reset value; reset connection is missing or not a simple variable + : ... note: In instance 't' + 226 | ) u_missing_reset_connection ( + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_fsm_register_wrapper_warn_bad.py b/test_regress/t/t_fsm_register_wrapper_warn_bad.py new file mode 100644 index 000000000..2ff9f32f0 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_warn_bad.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM register wrapper VLT warning coverage +# +# 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-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint( + verilator_flags2=["--coverage-fsm", "-fno-inline", "t/t_fsm_register_wrapper_warn_bad.vlt"], + fails=True, + expect_filename=test.golden_filename, +) + +test.passes() diff --git a/test_regress/t/t_fsm_register_wrapper_warn_bad.v b/test_regress/t/t_fsm_register_wrapper_warn_bad.v new file mode 100644 index 000000000..38dbede1b --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_warn_bad.v @@ -0,0 +1,232 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT warning coverage +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module odd_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module missing_q_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module missing_clock_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module partial_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module missing_reset_value_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module value_no_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module sync_reset_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk) begin + if (rst) dout <= ResetValue; + else dout <= din; + end +endmodule + +module missing_reset_connection_fsm_flop #( + parameter int Width = 1, + parameter logic [Width-1:0] ResetValue = '0 +) ( + input logic clk, + input logic rst_n, + input logic [Width-1:0] din, + output logic [Width-1:0] dout +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) dout <= ResetValue; + else dout <= din; + end +endmodule + +module t ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic rst_n; + logic rst; + state_t state_q; + state_t missing_d; + state_t missing_q; + state_t missing_clock_d; + state_t missing_clock_q; + state_t partial_reset_d; + state_t partial_reset_q; + state_t missing_reset_value_d; + state_t missing_reset_value_q; + state_t value_no_reset_d; + state_t value_no_reset_q; + state_t sync_reset_d; + state_t sync_reset_q; + state_t missing_reset_connection_d; + state_t missing_reset_connection_q; + + odd_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_bad_simple ( + .clk(clk), + .rst_n(rst_n), + .din(S0), + .dout(state_q) + ); + + missing_q_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_missing_q_port ( + .clk(clk), + .rst_n(rst_n), + .din(missing_d), + .dout(missing_q) + ); + + missing_clock_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_missing_clock_port ( + .clk(clk), + .rst_n(rst_n), + .din(missing_clock_d), + .dout(missing_clock_q) + ); + + partial_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_partial_reset ( + .clk(clk), + .rst_n(rst_n), + .din(partial_reset_d), + .dout(partial_reset_q) + ); + + missing_reset_value_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_missing_reset_value ( + .clk(clk), + .rst_n(rst_n), + .din(missing_reset_value_d), + .dout(missing_reset_value_q) + ); + + value_no_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_value_no_reset ( + .clk(clk), + .rst_n(rst_n), + .din(value_no_reset_d), + .dout(value_no_reset_q) + ); + + sync_reset_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_sync_reset ( + .clk(clk), + .rst(rst), + .din(sync_reset_d), + .dout(sync_reset_q) + ); + + missing_reset_connection_fsm_flop #( + .Width($bits(state_t)), + .ResetValue(S0) + ) u_missing_reset_connection ( + .clk(clk), + .rst_n(rst_n), + .din(missing_reset_connection_d), + .dout(missing_reset_connection_q) + ); +endmodule diff --git a/test_regress/t/t_fsm_register_wrapper_warn_bad.vlt b/test_regress/t/t_fsm_register_wrapper_warn_bad.vlt new file mode 100644 index 000000000..d535edb91 --- /dev/null +++ b/test_regress/t/t_fsm_register_wrapper_warn_bad.vlt @@ -0,0 +1,16 @@ +// DESCRIPTION: Verilator: FSM register wrapper VLT warning coverage +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +`verilator_config + +fsm_register_wrapper -module "odd_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "rst_n" -reset_value "ResetValue" +fsm_register_wrapper -module "missing_q_fsm_flop" -d "din" -q "missing" -clock "clk" -reset "rst_n" -reset_value "ResetValue" +fsm_register_wrapper -module "missing_clock_fsm_flop" -d "din" -q "dout" -clock "missing" -reset "rst_n" -reset_value "ResetValue" +fsm_register_wrapper -module "partial_reset_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "rst_n" +fsm_register_wrapper -module "missing_reset_value_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "rst_n" -reset_value "MissingParam" +fsm_register_wrapper -module "value_no_reset_fsm_flop" -d "din" -q "dout" -clock "clk" -reset_value "ResetValue" +fsm_register_wrapper -module "sync_reset_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "rst" -reset_value "ResetValue" +fsm_register_wrapper -module "missing_reset_connection_fsm_flop" -d "din" -q "dout" -clock "clk" -reset "missing_rst" -reset_value "ResetValue"