From de9dd39e32e6378c4c7786ba983a324d9ae98f47 Mon Sep 17 00:00:00 2001 From: Chaitanya Sharma <1010chaitanya@gmail.com> Date: Sun, 9 Nov 2025 05:15:30 -0500 Subject: [PATCH 1/3] dualedge support (incl async reset) --- src/async_pattern.cc | 2 +- src/slang_frontend.cc | 108 ++++++++++++++++++++++++++++++++++---- tests/CMakeLists.txt | 1 + tests/various/dualedge.ys | 87 ++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 tests/various/dualedge.ys diff --git a/src/async_pattern.cc b/src/async_pattern.cc index 1826cd2..404f4a2 100644 --- a/src/async_pattern.cc +++ b/src/async_pattern.cc @@ -79,7 +79,7 @@ void TimingPatternInterpretor::handle_always(const ast::ProceduralBlockSymbol &s break; case ast::EdgeKind::BothEdges: - issuer.add_diag(diag::BothEdgesUnsupported, sigev.sourceRange); + triggers.push_back(&sigev); break; } } diff --git a/src/slang_frontend.cc b/src/slang_frontend.cc index 024f6fb..712b381 100644 --- a/src/slang_frontend.cc +++ b/src/slang_frontend.cc @@ -2088,6 +2088,44 @@ struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor RTLIL::Cell *cell; if (aloads.empty()) { + if (clock.edge == ast::EdgeKind::BothEdges) { + // dual-edge: synthesize via posedge+negedge FFs and mux (ref: https://vlsi-soc.blogspot.com/2013/06/dual-edge-triggered-flip-flop.html) + for (auto [named_chunk, name] : generate_subfield_names(driven_chunk, type)) { + std::string var_name = RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())); + + // intermediaries that dont connect directly to output + std::string pos_wire_name = "$driver$pos$q$" + var_name + name; + RTLIL::Wire *pos_q = netlist.canvas->addWire(netlist.canvas->uniquify(pos_wire_name), named_chunk.bitwidth()); + + std::string neg_wire_name = "$driver$neg$q$" + var_name + name; + RTLIL::Wire *neg_q = netlist.canvas->addWire(netlist.canvas->uniquify(neg_wire_name), named_chunk.bitwidth()); + + std::string pos_name = "$driver$pos$" + var_name + name; + RTLIL::Cell *pos_ff = netlist.canvas->addDff(netlist.canvas->uniquify(pos_name), + timing.triggers[0].signal, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + pos_q, + /*edge_polarity=*/true); + transfer_attrs(symbol, pos_ff); + + std::string neg_name = "$driver$neg$" + var_name + name; + RTLIL::Cell *neg_ff = netlist.canvas->addDff(netlist.canvas->uniquify(neg_name), + timing.triggers[0].signal, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + neg_q, + /*edge_polarity=*/false); + transfer_attrs(symbol, neg_ff); + + // mux behavior: A=neg_q (output when clk=0), B=pos_q (output when clk=1), S=clk + std::string mux_name = "$driver$mux$" + var_name + name; + RTLIL::Cell *mux = netlist.canvas->addMux(netlist.canvas->uniquify(mux_name), + /*A=*/neg_q, + /*B=*/pos_q, + /*S=*/timing.triggers[0].signal, + /*Y=*/netlist.convert_static(named_chunk)); + transfer_attrs(symbol, mux); + } + } else { for (auto [named_chunk, name] : generate_subfield_names(driven_chunk, type)) { cell = netlist.canvas->addDff(netlist.canvas->uniquify("$driver$" + RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())) + name), timing.triggers[0].signal, @@ -2095,6 +2133,7 @@ struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor netlist.convert_static(named_chunk), timing.triggers[0].edge_polarity); transfer_attrs(symbol, cell); + } } } else if (aloads.size() == 1) { VariableBits aldff_q; @@ -2110,17 +2149,64 @@ struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor } if (!aldff_q.empty()) { - for (auto driven_chunk2 : aldff_q.chunks()) - for (auto [named_chunk, name] : generate_subfield_names(driven_chunk2, type)) { - cell = netlist.canvas->addAldff(netlist.canvas->uniquify("$driver$" + RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())) + name), - timing.triggers[0].signal, - aloads[0].trigger, - assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), - netlist.convert_static(named_chunk), - aloads[0].values.evaluate(netlist, named_chunk), - timing.triggers[0].edge_polarity, - aloads[0].trigger_polarity); - transfer_attrs(symbol, cell); + if (clock.edge == ast::EdgeKind::BothEdges) { + for (auto driven_chunk2 : aldff_q.chunks()) + for (auto [named_chunk, name] : generate_subfield_names(driven_chunk2, type)) { + std::string var_name = RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())); + + // intermediaries + std::string pos_wire_name = "$driver$pos$q$" + var_name + name; + RTLIL::Wire *pos_q = netlist.canvas->addWire(netlist.canvas->uniquify(pos_wire_name), named_chunk.bitwidth()); + + std::string neg_wire_name = "$driver$neg$q$" + var_name + name; + RTLIL::Wire *neg_q = netlist.canvas->addWire(netlist.canvas->uniquify(neg_wire_name), named_chunk.bitwidth()); + + // posedge aldff + std::string pos_name = "$driver$pos$" + var_name + name; + RTLIL::Cell *pos_ff = netlist.canvas->addAldff(netlist.canvas->uniquify(pos_name), + timing.triggers[0].signal, + aloads[0].trigger, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + pos_q, + aloads[0].values.evaluate(netlist, named_chunk), + /*clk_polarity=*/true, + aloads[0].trigger_polarity); + transfer_attrs(symbol, pos_ff); + + // negedge aldff + std::string neg_name = "$driver$neg$" + var_name + name; + RTLIL::Cell *neg_ff = netlist.canvas->addAldff(netlist.canvas->uniquify(neg_name), + timing.triggers[0].signal, + aloads[0].trigger, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + neg_q, + aloads[0].values.evaluate(netlist, named_chunk), + /*clk_polarity=*/false, + aloads[0].trigger_polarity); + transfer_attrs(symbol, neg_ff); + + // mux behavior: A=neg_q (output when clk=0), B=pos_q (output when clk=1), S=clk + std::string mux_name = "$driver$mux$" + var_name + name; + RTLIL::Cell *mux = netlist.canvas->addMux(netlist.canvas->uniquify(mux_name), + /*A=*/neg_q, + /*B=*/pos_q, + /*S=*/timing.triggers[0].signal, + /*Y=*/netlist.convert_static(named_chunk)); + transfer_attrs(symbol, mux); + } + } else { + for (auto driven_chunk2 : aldff_q.chunks()) + for (auto [named_chunk, name] : generate_subfield_names(driven_chunk2, type)) { + cell = netlist.canvas->addAldff(netlist.canvas->uniquify("$driver$" + RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())) + name), + timing.triggers[0].signal, + aloads[0].trigger, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + netlist.convert_static(named_chunk), + aloads[0].values.evaluate(netlist, named_chunk), + timing.triggers[0].edge_polarity, + aloads[0].trigger_polarity); + transfer_attrs(symbol, cell); + } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1947b5d..c264691 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,6 +57,7 @@ set(ALL_TESTS various/wait_test.ys various/expect_test.ys various/program_test.ys + various/dualedge.ys ) foreach(test_script ${ALL_TESTS}) diff --git a/tests/various/dualedge.ys b/tests/various/dualedge.ys new file mode 100644 index 0000000..89c23bf --- /dev/null +++ b/tests/various/dualedge.ys @@ -0,0 +1,87 @@ +read_slang < Date: Sun, 9 Nov 2025 20:50:28 -0500 Subject: [PATCH 2/3] only allow dualedge when flag passed and follow helper based approach for dualedge based on canvas->addDff/addAldff --- src/async_pattern.cc | 4 + src/diag.cc | 2 +- src/slang_frontend.cc | 186 ++++++++++++++++++++++---------------- src/slang_frontend.h | 1 + tests/various/dualedge.ys | 61 +++++++++++-- 5 files changed, 169 insertions(+), 85 deletions(-) diff --git a/src/async_pattern.cc b/src/async_pattern.cc index 404f4a2..9338323 100644 --- a/src/async_pattern.cc +++ b/src/async_pattern.cc @@ -79,6 +79,10 @@ void TimingPatternInterpretor::handle_always(const ast::ProceduralBlockSymbol &s break; case ast::EdgeKind::BothEdges: + if (!settings.allow_dual_edge_ff.value_or(false)) { + issuer.add_diag(diag::BothEdgesUnsupported, sigev.sourceRange); + break; + } triggers.push_back(&sigev); break; } diff --git a/src/diag.cc b/src/diag.cc index d872c96..5d23971 100644 --- a/src/diag.cc +++ b/src/diag.cc @@ -123,7 +123,7 @@ namespace diag { engine.setSeverity(SignalSensitivityAmbiguous, DiagnosticSeverity::Warning); engine.setMessage(EdgeImplicitMixing, "mixing of implicit and edge sensitivity"); engine.setMessage(GenericTimingUnsyn, "unsynthesizable timing control (ignore with '--ignore-timing')"); - engine.setMessage(BothEdgesUnsupported, "'edge' sensitivity will not be synthesized"); + engine.setMessage(BothEdgesUnsupported, "@(edge) will not be synthesized (override with '--allow-dual-edge-ff')"); engine.setMessage(WaitStatementUnsupported, "wait statement will not be synthesized"); engine.setSeverity(WaitStatementUnsupported, DiagnosticSeverity::Warning); engine.setMessage(NoteSignalEvent, "signal event specified here"); diff --git a/src/slang_frontend.cc b/src/slang_frontend.cc index 712b381..d13e6a2 100644 --- a/src/slang_frontend.cc +++ b/src/slang_frontend.cc @@ -66,6 +66,8 @@ void SynthesisSettings::addOptions(slang::CommandLine &cmdLine) { "For developers: stop after the AST is fully compiled"); cmdLine.add("--no-default-translate-off-format", no_default_translate_off, "Do not interpret any comment directives marking disabled input unless specified with '--translate-off-format'"); + cmdLine.add("--allow-dual-edge-ff", allow_dual_edge_ff, + "Allow synthesis of dual-edge flip-flops (@(edge))"); } namespace ast = slang::ast; @@ -1863,6 +1865,99 @@ struct HierarchyQueue { std::vector queue; }; +// Synthesizes two single-edge FFs (one posedge, one negedge) with the same D input, +// then uses a mux controlled by the clock to select the appropriate FF output. +static void add_dual_edge_dff(NetlistContext &netlist, const ast::ProceduralBlockSymbol &symbol, + const NamedChunk &named, + RTLIL::SigSpec clk, RTLIL::SigSpec d, RTLIL::SigSpec q) +{ + auto [chunk, name] = named; + + log_assert(chunk.variable.get_symbol() != nullptr); + log_assert(netlist.canvas != nullptr); + + std::string base_name = Yosys::stringf("$driver$%s%s", + RTLIL::unescape_id(netlist.id(*chunk.variable.get_symbol())).c_str(), + name.c_str()); + + RTLIL::Wire *pos_q = netlist.canvas->addWire( + netlist.canvas->uniquify(Yosys::stringf("%s$pos$q", base_name.c_str())), + chunk.bitwidth()); // intermediaries + log_assert(pos_q != nullptr); + + RTLIL::Wire *neg_q = netlist.canvas->addWire( + netlist.canvas->uniquify(Yosys::stringf("%s$neg$q", base_name.c_str())), + chunk.bitwidth()); // intermediaries + log_assert(neg_q != nullptr); + + RTLIL::Cell *pos_ff = netlist.canvas->addDff( + netlist.canvas->uniquify(Yosys::stringf("%s$pos", base_name.c_str())), + clk, d, pos_q, /*edge_polarity=*/true); + log_assert(pos_ff != nullptr); + transfer_attrs(symbol, pos_ff); + + // Create negedge FF + RTLIL::Cell *neg_ff = netlist.canvas->addDff( + netlist.canvas->uniquify(Yosys::stringf("%s$neg", base_name.c_str())), + clk, d, neg_q, /*edge_polarity=*/false); + log_assert(neg_ff != nullptr); + transfer_attrs(symbol, neg_ff); + + // behaviour: when clk=0: select neg_q (captures on negedge), when clk=1: select pos_q (captures on posedge) + RTLIL::Cell *mux = netlist.canvas->addMux( + netlist.canvas->uniquify(Yosys::stringf("%s$mux", base_name.c_str())), + /*A=*/neg_q, /*B=*/pos_q, /*S=*/clk, /*Y=*/q); + log_assert(mux != nullptr); + transfer_attrs(symbol, mux); +} + +// async load/reset version of add_dual_edge_dff() +static void add_dual_edge_aldff(NetlistContext &netlist, const ast::ProceduralBlockSymbol &symbol, + const NamedChunk &named, + RTLIL::SigSpec clk, RTLIL::SigSpec aload, RTLIL::SigSpec d, + RTLIL::SigSpec q, RTLIL::SigSpec ad, bool aload_polarity) +{ + auto [chunk, name] = named; + + log_assert(chunk.variable.get_symbol() != nullptr); + log_assert(netlist.canvas != nullptr); + + std::string base_name = Yosys::stringf("$driver$%s%s", + RTLIL::unescape_id(netlist.id(*chunk.variable.get_symbol())).c_str(), + name.c_str()); + + RTLIL::Wire *pos_q = netlist.canvas->addWire( + netlist.canvas->uniquify(Yosys::stringf("%s$pos$q", base_name.c_str())), + chunk.bitwidth()); // intermediaries + log_assert(pos_q != nullptr); + + RTLIL::Wire *neg_q = netlist.canvas->addWire( + netlist.canvas->uniquify(Yosys::stringf("%s$neg$q", base_name.c_str())), + chunk.bitwidth()); // intermediaries + log_assert(neg_q != nullptr); + + RTLIL::Cell *pos_ff = netlist.canvas->addAldff( + netlist.canvas->uniquify(Yosys::stringf("%s$pos", base_name.c_str())), + clk, aload, d, pos_q, ad, + /*clk_polarity=*/true, aload_polarity); + log_assert(pos_ff != nullptr); + transfer_attrs(symbol, pos_ff); + + RTLIL::Cell *neg_ff = netlist.canvas->addAldff( + netlist.canvas->uniquify(Yosys::stringf("%s$neg", base_name.c_str())), + clk, aload, d, neg_q, ad, + /*clk_polarity=*/false, aload_polarity); + log_assert(neg_ff != nullptr); + transfer_attrs(symbol, neg_ff); + + // behaviour: when clk=0: select neg_q (captures on negedge), when clk=1: select pos_q (captures on posedge) + RTLIL::Cell *mux = netlist.canvas->addMux( + netlist.canvas->uniquify(Yosys::stringf("%s$mux", base_name.c_str())), + /*A=*/neg_q, /*B=*/pos_q, /*S=*/clk, /*Y=*/q); + log_assert(mux != nullptr); + transfer_attrs(symbol, mux); +} + struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor { public: HierarchyQueue &queue; @@ -2090,40 +2185,12 @@ struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor if (aloads.empty()) { if (clock.edge == ast::EdgeKind::BothEdges) { // dual-edge: synthesize via posedge+negedge FFs and mux (ref: https://vlsi-soc.blogspot.com/2013/06/dual-edge-triggered-flip-flop.html) - for (auto [named_chunk, name] : generate_subfield_names(driven_chunk, type)) { - std::string var_name = RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())); - - // intermediaries that dont connect directly to output - std::string pos_wire_name = "$driver$pos$q$" + var_name + name; - RTLIL::Wire *pos_q = netlist.canvas->addWire(netlist.canvas->uniquify(pos_wire_name), named_chunk.bitwidth()); - - std::string neg_wire_name = "$driver$neg$q$" + var_name + name; - RTLIL::Wire *neg_q = netlist.canvas->addWire(netlist.canvas->uniquify(neg_wire_name), named_chunk.bitwidth()); - - std::string pos_name = "$driver$pos$" + var_name + name; - RTLIL::Cell *pos_ff = netlist.canvas->addDff(netlist.canvas->uniquify(pos_name), - timing.triggers[0].signal, - assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), - pos_q, - /*edge_polarity=*/true); - transfer_attrs(symbol, pos_ff); - - std::string neg_name = "$driver$neg$" + var_name + name; - RTLIL::Cell *neg_ff = netlist.canvas->addDff(netlist.canvas->uniquify(neg_name), - timing.triggers[0].signal, - assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), - neg_q, - /*edge_polarity=*/false); - transfer_attrs(symbol, neg_ff); - - // mux behavior: A=neg_q (output when clk=0), B=pos_q (output when clk=1), S=clk - std::string mux_name = "$driver$mux$" + var_name + name; - RTLIL::Cell *mux = netlist.canvas->addMux(netlist.canvas->uniquify(mux_name), - /*A=*/neg_q, - /*B=*/pos_q, - /*S=*/timing.triggers[0].signal, - /*Y=*/netlist.convert_static(named_chunk)); - transfer_attrs(symbol, mux); + for (auto named : generate_subfield_names(driven_chunk, type)) { + auto [named_chunk, name] = named; + add_dual_edge_dff(netlist, symbol, named, + timing.triggers[0].signal, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + netlist.convert_static(named_chunk)); } } else { for (auto [named_chunk, name] : generate_subfield_names(driven_chunk, type)) { @@ -2151,48 +2218,15 @@ struct PopulateNetlist : public TimingPatternInterpretor, public ast::ASTVisitor if (!aldff_q.empty()) { if (clock.edge == ast::EdgeKind::BothEdges) { for (auto driven_chunk2 : aldff_q.chunks()) - for (auto [named_chunk, name] : generate_subfield_names(driven_chunk2, type)) { - std::string var_name = RTLIL::unescape_id(netlist.id(*named_chunk.variable.get_symbol())); - - // intermediaries - std::string pos_wire_name = "$driver$pos$q$" + var_name + name; - RTLIL::Wire *pos_q = netlist.canvas->addWire(netlist.canvas->uniquify(pos_wire_name), named_chunk.bitwidth()); - - std::string neg_wire_name = "$driver$neg$q$" + var_name + name; - RTLIL::Wire *neg_q = netlist.canvas->addWire(netlist.canvas->uniquify(neg_wire_name), named_chunk.bitwidth()); - - // posedge aldff - std::string pos_name = "$driver$pos$" + var_name + name; - RTLIL::Cell *pos_ff = netlist.canvas->addAldff(netlist.canvas->uniquify(pos_name), - timing.triggers[0].signal, - aloads[0].trigger, - assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), - pos_q, - aloads[0].values.evaluate(netlist, named_chunk), - /*clk_polarity=*/true, - aloads[0].trigger_polarity); - transfer_attrs(symbol, pos_ff); - - // negedge aldff - std::string neg_name = "$driver$neg$" + var_name + name; - RTLIL::Cell *neg_ff = netlist.canvas->addAldff(netlist.canvas->uniquify(neg_name), - timing.triggers[0].signal, - aloads[0].trigger, - assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), - neg_q, - aloads[0].values.evaluate(netlist, named_chunk), - /*clk_polarity=*/false, - aloads[0].trigger_polarity); - transfer_attrs(symbol, neg_ff); - - // mux behavior: A=neg_q (output when clk=0), B=pos_q (output when clk=1), S=clk - std::string mux_name = "$driver$mux$" + var_name + name; - RTLIL::Cell *mux = netlist.canvas->addMux(netlist.canvas->uniquify(mux_name), - /*A=*/neg_q, - /*B=*/pos_q, - /*S=*/timing.triggers[0].signal, - /*Y=*/netlist.convert_static(named_chunk)); - transfer_attrs(symbol, mux); + for (auto named : generate_subfield_names(driven_chunk2, type)) { + auto [named_chunk, name] = named; + add_dual_edge_aldff(netlist, symbol, named, + timing.triggers[0].signal, + aloads[0].trigger, + assigned.extract(named_chunk.base - driven_chunk.base, named_chunk.bitwidth()), + netlist.convert_static(named_chunk), + aloads[0].values.evaluate(netlist, named_chunk), + aloads[0].trigger_polarity); } } else { for (auto driven_chunk2 : aldff_q.chunks()) diff --git a/src/slang_frontend.h b/src/slang_frontend.h index 245b286..6b6ff3a 100644 --- a/src/slang_frontend.h +++ b/src/slang_frontend.h @@ -410,6 +410,7 @@ struct SynthesisSettings { std::optional empty_blackboxes; std::optional ast_compilation_only; std::optional no_default_translate_off; + std::optional allow_dual_edge_ff; bool disable_instance_caching = false; enum HierMode { diff --git a/tests/various/dualedge.ys b/tests/various/dualedge.ys index 89c23bf..70ffdcc 100644 --- a/tests/various/dualedge.ys +++ b/tests/various/dualedge.ys @@ -1,6 +1,51 @@ +test_slangdiag -expect "@(edge) will not be synthesized (override with '--allow-dual-edge-ff')" read_slang < Date: Mon, 10 Nov 2025 10:49:44 +0100 Subject: [PATCH 3/3] Add dual-edge functional tests --- tests/CMakeLists.txt | 1 + tests/unit/dualedge.ys | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/unit/dualedge.ys diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c264691..750ef1c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,6 +25,7 @@ endif() set(ALL_TESTS unit/async.ys + unit/dualedge.ys unit/function_call.ys unit/latch.ys unit/selftests.tcl diff --git a/tests/unit/dualedge.ys b/tests/unit/dualedge.ys new file mode 100644 index 0000000..3e73d9a --- /dev/null +++ b/tests/unit/dualedge.ys @@ -0,0 +1,70 @@ + +read_slang --allow-dual-edge-ff <