diff --git a/src/async_pattern.cc b/src/async_pattern.cc index 1826cd2..9338323 100644 --- a/src/async_pattern.cc +++ b/src/async_pattern.cc @@ -79,7 +79,11 @@ void TimingPatternInterpretor::handle_always(const ast::ProceduralBlockSymbol &s break; case ast::EdgeKind::BothEdges: - issuer.add_diag(diag::BothEdgesUnsupported, sigev.sourceRange); + 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 024f6fb..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; @@ -2088,6 +2183,16 @@ 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 : 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)) { 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 +2200,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 +2216,31 @@ 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 : 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()) + 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/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/CMakeLists.txt b/tests/CMakeLists.txt index 1947b5d..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 @@ -57,6 +58,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/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 <