diff --git a/src/syn/BUILD b/src/syn/BUILD index 8f45a4df387..ceadbb8ade3 100644 --- a/src/syn/BUILD +++ b/src/syn/BUILD @@ -28,6 +28,7 @@ cc_library( "src/flow/export.h", "src/flow/import.cc", "src/flow/liveness.cc", + "src/flow/loop_check.cc", "src/flow/opt_gatefusion.cc", "src/flow/opt_gatefusion.h", "src/flow/sequential_mapper.cc", diff --git a/src/syn/README.md b/src/syn/README.md index 450290bf9ff..a0577bfedfe 100644 --- a/src/syn/README.md +++ b/src/syn/README.md @@ -33,6 +33,8 @@ synthesize The integrated synthesis tool does not support mapping to latches, always flattens the design, and loses the names of instantiated macros. +`synthesize` fails with an error if the design contains a combinational loop. This includes latches inferred from incomplete assignments in `always` blocks (e.g. a latch-based clock gate), since combinational optimization does not preserve the state such a loop holds. A loop is only reported where each step is combinational: it is broken by a flip-flop, and by a cell output that neither has a liberty `function` using the input nor a combinational timing arc from it (such as a RAM read port). + ## Authors The original `syn` was developed and contributed by Martin Povišer of Precision Innovations Inc. Design of the intermediate representation used by syn internally derives from the [prjunnamed project](https://prjunnamed.org/): `Copyright (C) Project Unnamed contributors`; distributed under the terms of the BSD-0 license. diff --git a/src/syn/include/syn/synthesis.h b/src/syn/include/syn/synthesis.h index 68911afaa5e..232ddcda116 100644 --- a/src/syn/include/syn/synthesis.h +++ b/src/syn/include/syn/synthesis.h @@ -110,6 +110,10 @@ class Synthesis : public sta::dbStaState // combinational driver) whose value is provably never A with a constant. void livenessOpt(bool replace_combinational = false); + // Fail if the graph contains a combinational loop (e.g. an inferred + // latch), which the flow cannot implement faithfully. + void checkCombinationalLoops(); + // Export the mapped netlist to ODB. void exportToOdb(); @@ -149,5 +153,6 @@ void abcRoundtrip(Graph& g, void livenessOpt(Graph& g, utl::Logger* logger, bool replace_combinational = false); +void checkCombinationalLoops(Graph& g, utl::Logger* logger); } // namespace syn diff --git a/src/syn/src/flow/CMakeLists.txt b/src/syn/src/flow/CMakeLists.txt index 3910f82944c..d12a61f7a48 100644 --- a/src/syn/src/flow/CMakeLists.txt +++ b/src/syn/src/flow/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(syn_flow export.cc import.cc liveness.cc + loop_check.cc constant_fold.cc opt_gatefusion.cc sequential_mapper.cc diff --git a/src/syn/src/flow/loop_check.cc b/src/syn/src/flow/loop_check.cc new file mode 100644 index 00000000000..cede5e61aa9 --- /dev/null +++ b/src/syn/src/flow/loop_check.cc @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Detect combinational loops in the graph. Latches from the elaborator +// arrive as feedback muxes, and combinational optimization does not preserve +// the state such a loop holds, so loops are rejected rather than mapped. + +#include +#include +#include +#include +#include +#include + +#include "sta/FuncExpr.hh" +#include "sta/Liberty.hh" +#include "sta/PortDirection.hh" +#include "sta/TimingArc.hh" +#include "sta/TimingRole.hh" +#include "syn/ir/Bundle.h" +#include "syn/ir/Graph.h" +#include "syn/ir/Instance.h" +#include "syn/ir/Net.h" +#include "syn/synthesis.h" +#include "utl/Logger.h" + +namespace syn { + +namespace { + +constexpr size_t kMaxReportedLoops = 10; +constexpr size_t kMaxReportedNames = 8; + +// Map each net to a user-visible name, preferring non-tentative names. +std::unordered_map collectNetNames(const Graph& g) +{ + std::unordered_map names; + std::unordered_map tentative; + g.forEachInstance([&](const Instance* inst) { + const Name* name = inst->try_as(); + if (!name) { + return; + } + const Bundle& value = name->value(); + for (uint32_t i = 0; i < value.width(); i++) { + const Net net = value[i]; + if (net.isConst()) { + continue; + } + const uint32_t id = Graph::netId(net); + auto it = tentative.find(id); + if (it != tentative.end() && (name->tentative() || !it->second)) { + continue; + } + std::string str = name->nameStr(); + if (name->isVector()) { + // Same bit mapping as Synthesis::resolveNetRef, so the name can be + // passed back to dump_fanin_cone. + const uint32_t index + = name->from() <= name->to() ? name->from() + i : name->from() - i; + str += "[" + std::to_string(index) + "]"; + } + names[id] = std::move(str); + tentative[id] = name->tentative(); + } + }); + return names; +} + +// For each output bit of a liberty cell, the input bits it depends on +// combinationally: those named in its function or three_state expression, +// plus those with a combinational or tristate timing arc to it. Bits are +// indexed as in a Target (see importTargets): ports in liberty order, power +// pins skipped, bidirect ports counted as both an input and an output. +using CellDeps = std::vector>; + +CellDeps computeCellDeps(const sta::LibertyCell* cell) +{ + std::vector in_bits; + std::vector out_bits; + // Bit and bus ports map to the indices of the bits they cover. + std::unordered_map> in_idx; + std::unordered_map> out_idx; + auto addBits + = [](const sta::LibertyPort* port, + std::vector& bits, + std::unordered_map>& + idx) { + for (int j = 0; j < port->size(); j++) { + const sta::LibertyPort* bit = port; + if (port->isBus()) { + const int index = port->fromIndex() < port->toIndex() + ? port->fromIndex() + j + : port->fromIndex() - j; + bit = port->findLibertyBusBit(index); + } + const auto i = static_cast(bits.size()); + bits.push_back(bit); + idx[bit].push_back(i); + if (bit != port) { + idx[port].push_back(i); + } + } + }; + + sta::LibertyCellPortIterator port_iter(cell); + while (port_iter.hasNext()) { + const sta::LibertyPort* port = port_iter.next(); + if (port->isPwrGnd()) { + continue; + } + const sta::PortDirection* dir = port->direction(); + if (dir->isInput() || dir->isBidirect()) { + addBits(port, in_bits, in_idx); + } + if (dir->isOutput() || dir->isBidirect()) { + addBits(port, out_bits, out_idx); + } + } + + std::vector> depends( + out_bits.size(), std::vector(in_bits.size(), false)); + for (size_t o = 0; o < out_bits.size(); o++) { + const sta::FuncExpr* function = out_bits[o]->function(); + const sta::FuncExpr* enable = out_bits[o]->tristateEnable(); + for (size_t i = 0; i < in_bits.size(); i++) { + if ((function && function->hasPort(in_bits[i])) + || (enable && enable->hasPort(in_bits[i]))) { + depends[o][i] = true; + } + } + } + for (const sta::TimingArcSet* arc_set : cell->timingArcSets()) { + const sta::TimingRole* role = arc_set->role(); + if (role != sta::TimingRole::combinational() + && role != sta::TimingRole::tristateEnable() + && role != sta::TimingRole::tristateDisable()) { + continue; + } + auto from = in_idx.find(arc_set->from()); + auto to = out_idx.find(arc_set->to()); + if (from == in_idx.end() || to == out_idx.end()) { + continue; + } + for (uint32_t o : to->second) { + for (uint32_t i : from->second) { + depends[o][i] = true; + } + } + } + + CellDeps deps(out_bits.size()); + for (size_t o = 0; o < out_bits.size(); o++) { + for (size_t i = 0; i < in_bits.size(); i++) { + if (depends[o][i]) { + deps[o].push_back(i); + } + } + } + return deps; +} + +// Combinational fanins of a net. Dffs, Others and inputs cut the dependency; +// a Target output depends only on the inputs its liberty model connects it +// to, so an output with neither a function nor a combinational arc (e.g. a +// RAM read port) cuts it too. +class CombFanins +{ + public: + explicit CombFanins(const Graph& g) : g_(g) {} + + template + void visit(Net net, F&& fn) + { + if (net.isConst()) { + return; + } + const auto [inst, offset] = g_.resolve(net); + auto visit = [&](Net fanin) { + if (!fanin.isConst()) { + fn(fanin); + } + }; + if (const Target* target = inst->try_as()) { + const CellDeps& deps = cellDeps(target->cell()); + const Bundle& inputs = target->inputs(); + if (offset >= deps.size()) { + // Layout disagrees with the liberty model; assume full dependency. + inputs.visit(visit); + return; + } + for (uint32_t i : deps[offset]) { + if (i < inputs.width()) { + visit(inputs[i]); + } + } + return; + } + if (inst->hasState() || inst->is()) { + return; + } + if (inst->isSliceable()) { + inst->visitSlice(offset, visit); + } else { + inst->visit(visit); + } + } + + private: + const CellDeps& cellDeps(const sta::LibertyCell* cell) + { + auto it = cell_deps_.find(cell); + if (it == cell_deps_.end()) { + it = cell_deps_.emplace(cell, computeCellDeps(cell)).first; + } + return it->second; + } + + const Graph& g_; + std::unordered_map cell_deps_; +}; + +// Strongly connected components of the combinational dependency graph. +struct Sccs +{ + std::vector id; // per net + std::vector cyclic; // one net from each component that has a cycle +}; + +// Iterative Tarjan over all live nets. +Sccs computeSccs(const Graph& g, CombFanins& comb) +{ + constexpr uint32_t kUnvisited = UINT32_MAX; + const size_t n = g.tableSize(); + Sccs sccs; + sccs.id.assign(n, kUnvisited); + std::vector index(n, kUnvisited); + std::vector lowlink(n, 0); + std::vector on_stack(n, false); + std::vector stack; + uint32_t next_index = 0; + uint32_t next_scc = 0; + + struct Frame + { + uint32_t id; + std::vector fanins; + bool self_loop = false; + size_t next = 0; + }; + + auto makeFrame = [&](uint32_t id) { + Frame frame{id, {}}; + comb.visit(Graph::netFromId(id), [&](Net fanin) { + frame.fanins.push_back(Graph::netId(fanin)); + frame.self_loop |= Graph::netId(fanin) == id; + }); + index[id] = lowlink[id] = next_index++; + stack.push_back(id); + on_stack[id] = true; + return frame; + }; + + g.forEachNet([&](Net root, const Instance*, uint32_t) { + if (root.isConst() || index[Graph::netId(root)] != kUnvisited) { + return; + } + std::vector call_stack; + call_stack.push_back(makeFrame(Graph::netId(root))); + while (!call_stack.empty()) { + Frame& frame = call_stack.back(); + if (frame.next < frame.fanins.size()) { + const uint32_t fanin = frame.fanins[frame.next++]; + if (index[fanin] == kUnvisited) { + call_stack.push_back(makeFrame(fanin)); + } else if (on_stack[fanin]) { + lowlink[frame.id] = std::min(lowlink[frame.id], index[fanin]); + } + continue; + } + const uint32_t id = frame.id; + if (lowlink[id] == index[id]) { + size_t size = 0; + uint32_t member; + do { + member = stack.back(); + stack.pop_back(); + on_stack[member] = false; + sccs.id[member] = next_scc; + size++; + } while (member != id); + if (size > 1 || frame.self_loop) { + sccs.cyclic.push_back(Graph::netFromId(id)); + } + next_scc++; + } + call_stack.pop_back(); + if (!call_stack.empty()) { + Frame& parent = call_stack.back(); + lowlink[parent.id] = std::min(lowlink[parent.id], lowlink[id]); + } + } + }); + return sccs; +} + +// Find a combinational cycle through `net` within its strongly connected +// component. Returns the nets on the cycle, starting and ending at `net`. +std::vector findCycle(CombFanins& comb, const Sccs& sccs, Net net) +{ + const uint32_t scc = sccs.id[Graph::netId(net)]; + std::unordered_map parent; // fanin -> the net it feeds + std::vector stack; + auto expand = [&](Net from) { + comb.visit(from, [&](Net fanin) { + if (sccs.id[Graph::netId(fanin)] == scc + && parent.emplace(Graph::netId(fanin), from).second) { + stack.push_back(fanin); + } + }); + }; + + expand(net); + while (!stack.empty()) { + const Net cur = stack.back(); + stack.pop_back(); + if (cur == net) { + std::vector cycle{net}; + for (Net n = parent.at(Graph::netId(net)); n != net; + n = parent.at(Graph::netId(n))) { + cycle.push_back(n); + } + cycle.push_back(net); + return cycle; + } + expand(cur); + } + return {}; +} + +} // namespace + +void checkCombinationalLoops(Graph& g, utl::Logger* logger) +{ + // Normalize so only live logic is checked. Its loop breakers are not + // relied on: normalization assumes every Target output depends on every + // input and treats sequential Targets as boundaries, so cycles are found + // here from per-pin liberty dependencies instead. + g.normalize(); + + CombFanins comb(g); + const Sccs sccs = computeSccs(g, comb); + if (sccs.cyclic.empty()) { + return; + } + + const auto names = collectNetNames(g); + for (size_t i = 0; i < sccs.cyclic.size() && i < kMaxReportedLoops; i++) { + std::string nets; + size_t reported = 0; + std::vector cycle = findCycle(comb, sccs, sccs.cyclic[i]); + if (!cycle.empty()) { + cycle.pop_back(); // the start net is repeated at the end + } + for (Net net : cycle) { + const uint32_t id = Graph::netId(net); + auto it = names.find(id); + if (it == names.end() && g.resolve(net).first->is()) { + continue; // an alias of its input, which is also on the cycle + } + if (reported == kMaxReportedNames) { + nets += ", ..."; + break; + } + // Unnamed nets print as %, which dump_fanin_cone also accepts. + nets += (reported ? ", " : "") + + (it != names.end() ? it->second : "%" + std::to_string(id)); + reported++; + } + logger->warn(utl::SYN, 79, "Combinational loop through: {}", nets); + } + + logger->error(utl::SYN, + 80, + "Design contains {} combinational loop(s). Combinational " + "loops, including latches inferred from incomplete " + "assignments in always blocks, are not supported.", + sccs.cyclic.size()); +} + +} // namespace syn diff --git a/src/syn/src/synthesis-py.i b/src/syn/src/synthesis-py.i index b68c48a9d20..42abb22778b 100644 --- a/src/syn/src/synthesis-py.i +++ b/src/syn/src/synthesis-py.i @@ -35,6 +35,7 @@ namespace utl { class Logger; } // graph() returns a Graph* whose header isn't wrapped; not needed from Python. %ignore syn::Synthesis::graph; %ignore syn::livenessOpt; +%ignore syn::checkCombinationalLoops; %ignore syn::abcRoundtrip; // Free-function flow entry points reference Graph&, which is forward-declared // only -- not needed from Python anyway, so ignore them. diff --git a/src/syn/src/synthesis.cpp b/src/syn/src/synthesis.cpp index 472cdc29644..26f617cc9e1 100644 --- a/src/syn/src/synthesis.cpp +++ b/src/syn/src/synthesis.cpp @@ -400,6 +400,15 @@ void Synthesis::livenessOpt(bool replace_combinational) syn::livenessOpt(*graph_, logger_, replace_combinational); } +void Synthesis::checkCombinationalLoops() +{ + if (!graph_) { + logger_->error(utl::SYN, 81, "No graph. Run syn::elaborate first."); + return; + } + syn::checkCombinationalLoops(*graph_, logger_); +} + void Synthesis::exportToOdb() { if (!graph_) { diff --git a/src/syn/src/synthesis.i b/src/syn/src/synthesis.i index c2aa2dd731f..e7b080e75ad 100644 --- a/src/syn/src/synthesis.i +++ b/src/syn/src/synthesis.i @@ -161,6 +161,13 @@ liveness_opt_cmd(bool replace_combinational = false) synthesis->livenessOpt(replace_combinational); } +void +check_combinational_loops_cmd() +{ + syn::Synthesis* synthesis = ord::OpenRoad::openRoad()->getSynthesis(); + synthesis->checkCombinationalLoops(); +} + void export_to_odb_cmd() { diff --git a/src/syn/src/synthesis.tcl b/src/syn/src/synthesis.tcl index 73053e2d68c..2e774b1ef3d 100644 --- a/src/syn/src/synthesis.tcl +++ b/src/syn/src/synthesis.tcl @@ -29,6 +29,7 @@ proc synthesize { args } { # now bitblast incl. arithmetic syn::bitblast_cmd true syn::opt_cmd + syn::check_combinational_loops_cmd if { ![info exists flags(-reduce_name_loss)] } { set script {&ps; &st; &dc2 -v; &dc2 -v; &if -g -K 6; &dc2 -v; &dc2 -v; &dc2 -v; &ps} diff --git a/src/syn/test/BUILD b/src/syn/test/BUILD index e32a8c1b273..d2e6663741f 100644 --- a/src/syn/test/BUILD +++ b/src/syn/test/BUILD @@ -16,6 +16,20 @@ regression_test( ], ) +regression_test( + name = "comb_loop", + data = [ + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_typ.lib", + "Nangate45/fakeram45_64x7.lef", + "Nangate45/fakeram45_64x7.lib", + "comb_loop.ok", + "comb_loop.v", + "comb_loop_cells.lef", + "comb_loop_cells.lib", + ], +) + # No gtest_main on ir/graph/opt tests: ABC (via //src/syn/src/ir → # TritModel) ships its own main() that wins over gtest_main; each TU # defines its own. diff --git a/src/syn/test/CMakeLists.txt b/src/syn/test/CMakeLists.txt index 8a15bc7767f..49892073056 100644 --- a/src/syn/test/CMakeLists.txt +++ b/src/syn/test/CMakeLists.txt @@ -7,6 +7,7 @@ or_integration_tests( "syn" TESTS acd_resynth + comb_loop ) set(_syn_test_libs diff --git a/src/syn/test/comb_loop.ok b/src/syn/test/comb_loop.ok new file mode 100644 index 00000000000..975e29703ce --- /dev/null +++ b/src/syn/test/comb_loop.ok @@ -0,0 +1,68 @@ +[INFO ODB-0227] LEF file: Nangate45/Nangate45.lef, created 22 layers, 27 vias, 135 library cells +[INFO ODB-0227] LEF file: Nangate45/fakeram45_64x7.lef, created 1 library cells +[INFO ODB-0227] LEF file: comb_loop_cells.lef, created 2 library cells +Top level design units: + false_loop + +Build succeeded: 0 errors, 0 warnings +[INFO SYN-0056] liveness: replaced 0 register bits, 0 combinational bits +[INFO SYN-0017] mapCombinationals: done +[INFO SYN-0040] gatefusion: 0 fusions applied, area delta 0.0000 + input: 1 cells, 4 bits + name (tentative): 2 cells, 0 bits + output: 1 cells, 0 bits + target: NAND2_X1: 1 cells, 1 bits, area 0.798 + target: OAI21_X1: 1 cells, 1 bits, area 1.064 + target: XNOR2_X1: 2 cells, 2 bits, area 3.192 + target: XOR2_X1: 3 cells, 3 bits, area 4.788 + total: 11 cells, 11 bits, area 9.842 +[INFO SYN-0031] Export to ODB: created 7 instances, 4 nets, 8 ports +false_loop synthesized +Top level design units: + ram_loop + +Build succeeded: 0 errors, 0 warnings +[INFO SYN-0056] liveness: replaced 0 register bits, 0 combinational bits +[INFO SYN-0017] mapCombinationals: done +[INFO SYN-0040] gatefusion: 0 fusions applied, area delta 0.0000 + input: 3 cells, 14 bits + loop_breaker: 7 cells, 7 bits + name (tentative): 4 cells, 0 bits + output: 1 cells, 0 bits + target: LOGIC1_X1: 1 cells, 1 bits, area 0.532 + target: XOR2_X1: 7 cells, 7 bits, area 11.172 + target: fakeram45_64x7: 1 cells, 7 bits, area 3011.12 + total: 24 cells, 36 bits, area 3022.82 +[INFO SYN-0031] Export to ODB: created 9 instances, 15 nets, 21 ports +ram_loop synthesized +Top level design units: + dual_no_loop + +Build succeeded: 0 errors, 0 warnings +[INFO SYN-0056] liveness: replaced 0 register bits, 0 combinational bits +[INFO SYN-0017] mapCombinationals: done +[INFO SYN-0040] gatefusion: 0 fusions applied, area delta 0.0000 + input: 1 cells, 1 bits + loop_breaker: 1 cells, 1 bits + name (tentative): 3 cells, 0 bits + output: 1 cells, 0 bits + target: DUAL: 1 cells, 2 bits, area 2 + total: 7 cells, 4 bits, area 2 +[INFO SYN-0031] Export to ODB: created 1 instances, 1 nets, 2 ports +dual_no_loop synthesized +Top level design units: + half_loop + +Build succeeded: 0 errors, 0 warnings +[INFO SYN-0056] liveness: replaced 0 register bits, 0 combinational bits +[WARNING SYN-0079] Combinational loop through: y, %5 +[ERROR SYN-0080] Design contains 1 combinational loop(s). Combinational loops, including latches inferred from incomplete assignments in always blocks, are not supported. +SYN-0080 +Top level design units: + latch_clock_gate + +Build succeeded: 0 errors, 0 warnings +[INFO SYN-0056] liveness: replaced 0 register bits, 0 combinational bits +[WARNING SYN-0079] Combinational loop through: %9, en_latch, %8 +[ERROR SYN-0080] Design contains 1 combinational loop(s). Combinational loops, including latches inferred from incomplete assignments in always blocks, are not supported. +SYN-0080 diff --git a/src/syn/test/comb_loop.tcl b/src/syn/test/comb_loop.tcl new file mode 100644 index 00000000000..09fb08cb7c5 --- /dev/null +++ b/src/syn/test/comb_loop.tcl @@ -0,0 +1,27 @@ +# Combinational loops (e.g. inferred latches) are rejected by synthesize +read_liberty Nangate45/Nangate45_typ.lib +read_liberty Nangate45/fakeram45_64x7.lib +read_liberty comb_loop_cells.lib +read_lef Nangate45/Nangate45.lef +read_lef Nangate45/fakeram45_64x7.lef +read_lef comb_loop_cells.lef + +sv_elaborate --std=1364-2005 --top false_loop comb_loop.v +synthesize -reduce_name_loss +puts "false_loop synthesized" + +sv_elaborate --std=1364-2005 --top ram_loop comb_loop.v +synthesize -reduce_name_loss +puts "ram_loop synthesized" + +sv_elaborate --std=1364-2005 --top dual_no_loop comb_loop.v +synthesize -reduce_name_loss +puts "dual_no_loop synthesized" + +sv_elaborate --std=1364-2005 --top half_loop comb_loop.v +catch { synthesize -reduce_name_loss } error +puts $error + +sv_elaborate --std=1364-2005 --top latch_clock_gate comb_loop.v +catch { synthesize -reduce_name_loss } error +puts $error diff --git a/src/syn/test/comb_loop.v b/src/syn/test/comb_loop.v new file mode 100644 index 00000000000..de785485a37 --- /dev/null +++ b/src/syn/test/comb_loop.v @@ -0,0 +1,69 @@ +// Each bit of s depends only on lower bits of s, so the word-level +// feedback through the adder is not a loop once bitblasted. +module false_loop ( + input [3:0] a, + output [3:0] s +); + assign s = a + {s[2:0], 1'b0}; +endmodule + +// Latch-based clock gate: the incomplete assignment infers a latch. +module latch_clock_gate ( + input clk_i, + input en_i, + output clk_o +); + reg en_latch; + always @* begin + if (!clk_i) begin + en_latch = en_i; + end + end + assign clk_o = en_latch & clk_i; +endmodule + +// Feedback through a RAM macro: its outputs have no liberty function, so +// it is opaque and breaks the loop. +module ram_loop ( + input clk, + input [5:0] addr, + input [6:0] d, + output [6:0] q +); + fakeram45_64x7 ram ( + .clk(clk), + .ce_in(1'b1), + .we_in(1'b1), + .addr_in(addr), + .wd_in(q ^ d), + .w_mask_in(7'h7f), + .rd_out(q) + ); +endmodule + +// DUAL has Y = A and Z = B, so feeding Y back into B is not a loop. +module dual_no_loop ( + input a, + output z +); + wire y; + DUAL u ( + .A(a), + .B(y), + .Y(y), + .Z(z) + ); +endmodule + +// HALF has Y = A and an output Z without a function; feeding Y back +// into A is a loop even though Z is opaque. +module half_loop ( + output z +); + wire y; + HALF u ( + .A(y), + .Y(y), + .Z(z) + ); +endmodule diff --git a/src/syn/test/comb_loop_cells.lef b/src/syn/test/comb_loop_cells.lef new file mode 100644 index 00000000000..43cad27063f --- /dev/null +++ b/src/syn/test/comb_loop_cells.lef @@ -0,0 +1,79 @@ +VERSION 5.8 ; +BUSBITCHARS "[]" ; +DIVIDERCHAR "/" ; + +MACRO DUAL + CLASS CORE ; + ORIGIN 0 0 ; + FOREIGN DUAL 0 0 ; + SIZE 0.95 BY 1.4 ; + SYMMETRY X Y ; + SITE FreePDK45_38x28_10R_NP_162NW_34O ; + PIN A + DIRECTION INPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.1 0.5 0.17 0.9 ; + END + END A + PIN B + DIRECTION INPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.30000000000000004 0.5 0.37 0.9 ; + END + END B + PIN Y + DIRECTION OUTPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.5 0.5 0.57 0.9 ; + END + END Y + PIN Z + DIRECTION OUTPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.7000000000000001 0.5 0.77 0.9 ; + END + END Z +END DUAL + +MACRO HALF + CLASS CORE ; + ORIGIN 0 0 ; + FOREIGN HALF 0 0 ; + SIZE 0.95 BY 1.4 ; + SYMMETRY X Y ; + SITE FreePDK45_38x28_10R_NP_162NW_34O ; + PIN A + DIRECTION INPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.1 0.5 0.17 0.9 ; + END + END A + PIN Y + DIRECTION OUTPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.30000000000000004 0.5 0.37 0.9 ; + END + END Y + PIN Z + DIRECTION OUTPUT ; + USE SIGNAL ; + PORT + LAYER metal1 ; + RECT 0.5 0.5 0.57 0.9 ; + END + END Z +END HALF + +END LIBRARY diff --git a/src/syn/test/comb_loop_cells.lib b/src/syn/test/comb_loop_cells.lib new file mode 100644 index 00000000000..3ab33675b6c --- /dev/null +++ b/src/syn/test/comb_loop_cells.lib @@ -0,0 +1,44 @@ +library(comb_loop_cells) { + delay_model : table_lookup; + time_unit : "1ns"; + voltage_unit : "1V"; + current_unit : "1mA"; + capacitive_load_unit(1, pf); + + /* Two independent paths: Y depends only on A, Z only on B. */ + cell(DUAL) { + area : 2; + pin(A) { + direction : input; + capacitance : 0.001; + } + pin(B) { + direction : input; + capacitance : 0.001; + } + pin(Y) { + direction : output; + function : "A"; + } + pin(Z) { + direction : output; + function : "B"; + } + } + + /* Y is combinational; Z has no function (opaque). */ + cell(HALF) { + area : 2; + pin(A) { + direction : input; + capacitance : 0.001; + } + pin(Y) { + direction : output; + function : "A"; + } + pin(Z) { + direction : output; + } + } +}