From af8e85b7d27558bfc11ee95e11d4d9f141aef5e6 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 21 Apr 2025 11:39:33 -0700 Subject: [PATCH 01/10] techlibs/lattice: add missing clock muxes to ECP5 block ram blackboxes prjtrellis documentation shows that EBR clock inputs have optional inverters. The bram techmap outputs those parameters, and nextpnr consumes them. But for whatever reason, Diamond doesn't include those parameters in its blackbox models. This makes synth_lattice fail when targeting ECP5 with a design that maps block RAMs if you include any pass that needs cells_bb_ecp5.v's definitions. This change fixes up the ECP5 bram blackbox models at generation time, by adding the missing parameters back in. Signed-off-by: David Anderson --- techlibs/lattice/cells_bb_ecp5.v | 4 ++++ techlibs/lattice/cells_xtra.py | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/techlibs/lattice/cells_bb_ecp5.v b/techlibs/lattice/cells_bb_ecp5.v index fc22495e2..20a8134c3 100644 --- a/techlibs/lattice/cells_bb_ecp5.v +++ b/techlibs/lattice/cells_bb_ecp5.v @@ -19,6 +19,8 @@ endmodule (* blackbox *) module DP16KD (...); + parameter CLKAMUX = "CLKA"; + parameter CLKBMUX = "CLKB"; parameter DATA_WIDTH_A = 18; parameter DATA_WIDTH_B = 18; parameter REGMODE_A = "NOREG"; @@ -215,6 +217,8 @@ endmodule (* blackbox *) module PDPW16KD (...); + parameter CLKRMUX = "CLKR"; + parameter CLKWMUX = "CLKW"; parameter DATA_WIDTH_W = 36; parameter DATA_WIDTH_R = 36; parameter GSR = "ENABLED"; diff --git a/techlibs/lattice/cells_xtra.py b/techlibs/lattice/cells_xtra.py index c17281cc7..5531fd2b2 100644 --- a/techlibs/lattice/cells_xtra.py +++ b/techlibs/lattice/cells_xtra.py @@ -11,10 +11,11 @@ import re class Cell: - def __init__(self, name, keep=False, port_attrs={}): + def __init__(self, name, keep=False, port_attrs={}, extra_params={}): self.name = name self.keep = keep self.port_attrs = port_attrs + self.extra_params = extra_params self.found = False class State(Enum): @@ -120,8 +121,18 @@ devices = [ #Cell("XOR3"), #Cell("XOR4"), #Cell("XOR5"), - Cell("DP16KD"), - Cell("PDPW16KD"), + Cell("DP16KD", extra_params={ + # Optional clock inverters, present in prjtrellis data but + # not in Diamond bb models. + "CLKAMUX": "CLKA", + "CLKBMUX": "CLKB", + }), + Cell("PDPW16KD", extra_params={ + # Optional clock inverters, present in prjtrellis data but + # not in Diamond bb models. + "CLKWMUX": "CLKW", + "CLKRMUX": "CLKR", + }), #Cell("DPR16X4C"), #Cell("SPR16X4C"), #Cell("LVDSOB"), @@ -795,6 +806,10 @@ def xtract_cells_decl(device, cells, dirs, outf): rng = None module_ports.append((kind, rng, port)) elif l.startswith('parameter ') and state == State.IN_MODULE: + if cell.extra_params: + for name, default in sorted(cell.extra_params.items()): + outf.write(' parameter {} = "{}";\n'.format(name, default)) + cell.extra_params = None l = l.strip() if l.endswith((';', ',')): l = l[:-1] From 059228dd4e11b151df2ccb11d4e48e07abf33e8e Mon Sep 17 00:00:00 2001 From: Jiahui Xu Date: Thu, 19 Jun 2025 18:47:48 +0200 Subject: [PATCH 02/10] Makefile: remove hardcoded -soname for libyosys.so --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b452105b1..ac999417e 100644 --- a/Makefile +++ b/Makefile @@ -747,7 +747,7 @@ libyosys.so: $(filter-out kernel/driver.o,$(OBJS)) ifeq ($(OS), Darwin) $(P) $(CXX) -o libyosys.so -shared -undefined dynamic_lookup -Wl,-install_name,$(LIBDIR)/libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) else - $(P) $(CXX) -o libyosys.so -shared -Wl,-soname,$(LIBDIR)/libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) + $(P) $(CXX) -o libyosys.so -shared -Wl,-soname,libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) endif %.o: %.cc From 6a2984540b5cc0d70966b10b3e6e7c73d5c910a8 Mon Sep 17 00:00:00 2001 From: "Emil J. Tywoniak" Date: Thu, 18 Jul 2024 15:48:52 +0200 Subject: [PATCH 03/10] bitpattern: comments --- kernel/bitpattern.h | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/kernel/bitpattern.h b/kernel/bitpattern.h index 821490dca..0e12e6dce 100644 --- a/kernel/bitpattern.h +++ b/kernel/bitpattern.h @@ -25,6 +25,18 @@ YOSYS_NAMESPACE_BEGIN +/** + * This file implements BitPatternPool for efficiently storing and querying + * sets of fixed-width 2-valued logic constants compressed as "bit patterns". + * A bit pattern can have don't cares on one or more bit positions (State::Sa). + * + * In terms of logic synthesis: + * A BitPatternPool is a sum of products (SOP). + * BitPatternPool::bits_t is a cube. + * + * BitPatternPool does not permit adding new patterns, only removing. + * Its intended use case is in analysing cases in case/match constructs in HDL. + */ struct BitPatternPool { int width; @@ -67,6 +79,9 @@ struct BitPatternPool } } + /** + * Constructs a pool of all possible patterns (all don't-care bits) + */ BitPatternPool(int width) { this->width = width; @@ -78,6 +93,10 @@ struct BitPatternPool } } + /** + * Convert a constant SigSpec to a pattern. Normalize Yosys many-valued + * to three-valued logic. + */ bits_t sig2bits(RTLIL::SigSpec sig) { bits_t bits; @@ -88,6 +107,9 @@ struct BitPatternPool return bits; } + /** + * Two cubes match if their intersection is non-empty. + */ bool match(bits_t a, bits_t b) { log_assert(int(a.bitdata.size()) == width); @@ -98,6 +120,15 @@ struct BitPatternPool return true; } + /** + * Does cube sig overlap any cube in the pool? + * For example: + * pool({aaa}).has_any(01a) == true + * pool({01a}).has_any(01a) == true + * pool({011}).has_any(01a) == true + * pool({01a}).has_any(011) == true + * pool({111}).has_any(01a) == false + */ bool has_any(RTLIL::SigSpec sig) { bits_t bits = sig2bits(sig); @@ -107,6 +138,15 @@ struct BitPatternPool return false; } + /** + * Is cube sig covered by a cube in the pool? + * For example: + * pool({aaa}).has_all(01a) == true + * pool({01a}).has_any(01a) == true + * pool({01a}).has_any(011) == true + * pool({011}).has_all(01a) == false + * pool({111}).has_all(01a) == false + */ bool has_all(RTLIL::SigSpec sig) { bits_t bits = sig2bits(sig); @@ -121,6 +161,12 @@ struct BitPatternPool return false; } + /** + * Remove cube sig from the pool, splitting the remaining cubes. True if success. + * For example: + * Taking 011 out of pool({01a}) -> pool({010}), returns true. + * Taking 011 out of pool({010}) does nothing, returns false. + */ bool take(RTLIL::SigSpec sig) { bool status = false; @@ -143,6 +189,9 @@ struct BitPatternPool return status; } + /** + * Remove all patterns. Returns false if already empty. + */ bool take_all() { if (database.empty()) From 7ee62c832b19d5b18b822af7603a6fb1898ca748 Mon Sep 17 00:00:00 2001 From: "Emil J. Tywoniak" Date: Mon, 18 Aug 2025 19:50:43 +0200 Subject: [PATCH 04/10] bitpattern: unit test --- tests/unit/kernel/bitpatternTest.cc | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/unit/kernel/bitpatternTest.cc diff --git a/tests/unit/kernel/bitpatternTest.cc b/tests/unit/kernel/bitpatternTest.cc new file mode 100644 index 000000000..001d47060 --- /dev/null +++ b/tests/unit/kernel/bitpatternTest.cc @@ -0,0 +1,32 @@ +#include + +#include "kernel/bitpattern.h" +#include "kernel/rtlil.h" + +YOSYS_NAMESPACE_BEGIN + +TEST(BitpatternTest, has) +{ + SigSpec _aaa = {RTLIL::Sa, RTLIL::Sa, RTLIL::Sa}; + SigSpec _01a = {RTLIL::S0, RTLIL::S1, RTLIL::Sa}; + SigSpec _011 = {RTLIL::S0, RTLIL::S1, RTLIL::S1}; + SigSpec _111 = {RTLIL::S1, RTLIL::S1, RTLIL::S1}; + + EXPECT_TRUE(BitPatternPool(_aaa).has_any(_01a)); + EXPECT_TRUE(BitPatternPool(_01a).has_any(_01a)); + // 011 overlaps with 01a + EXPECT_TRUE(BitPatternPool(_011).has_any(_01a)); + // overlap is symmetric + EXPECT_TRUE(BitPatternPool(_01a).has_any(_011)); + EXPECT_FALSE(BitPatternPool(_111).has_any(_01a)); + + EXPECT_TRUE(BitPatternPool(_aaa).has_all(_01a)); + EXPECT_TRUE(BitPatternPool(_01a).has_all(_01a)); + // 011 is covered by 01a + EXPECT_TRUE(BitPatternPool(_01a).has_all(_011)); + // 01a is not covered by 011 + EXPECT_FALSE(BitPatternPool(_011).has_all(_01a)); + EXPECT_FALSE(BitPatternPool(_111).has_all(_01a)); +} + +YOSYS_NAMESPACE_END From 4dea77417153a90b4e55101386649300733b75ed Mon Sep 17 00:00:00 2001 From: "Emil J. Tywoniak" Date: Mon, 18 Aug 2025 17:22:47 +0200 Subject: [PATCH 05/10] opt_muxtree: refactor --- passes/opt/opt_muxtree.cc | 415 ++++++++++++++++++++++++-------------- 1 file changed, 260 insertions(+), 155 deletions(-) diff --git a/passes/opt/opt_muxtree.cc b/passes/opt/opt_muxtree.cc index e1ed5da08..ae9b1a632 100644 --- a/passes/opt/opt_muxtree.cc +++ b/passes/opt/opt_muxtree.cc @@ -30,7 +30,33 @@ USING_YOSYS_NAMESPACE PRIVATE_NAMESPACE_BEGIN -using RTLIL::id2cstr; +/** + * TERMINOLOGY + * + * A multiplexer tree (mux tree) is a tree composed exclusively of muxes. + * By mux, I mean $mux or $pmux. By port, I usually mean input port. + * The children of a node are all the muxes driving its input ports (A, B). + * It must be rooted in a "root mux", a mux which has multiple mux users + * or any number of non-mux users. Only the root and leaf nodes can be + * root muxes, not the internal nodes. Leaf nodes that are root muxes + * are roots of "input trees". + * + * OPERATING PRINCIPLE + * + * This pass traverses mux trees, learning port activations and making + * assumptions about them as it goes. When valid, ports are replaced + * with constants, or removed if they can never be activated. + * When valid, muxes are replaced with shorts from port to output, + * or removed if their outputs are found to be no longer observable. + * + * Input trees can be recursed into if limits_t::recursions_left allows. + * Otherwise, the input tree is queued for a re-run with a fresh knowledge_t. + * At any point, if glob_evals_left goes to 0, the pass terminates. + * + * Unlike share, this pass doesn't use SAT to learn things about logic + * driving the mux control signals, and traverses mux regions from users + * to drivers. + */ struct OptMuxtreeWorker { @@ -38,9 +64,10 @@ struct OptMuxtreeWorker RTLIL::Module *module; SigMap assign_map; int removed_count; - int glob_abort_cnt = 100000; + int glob_evals_left = 100000; struct bitinfo_t { + // Is bit directly used by non-mux cells or ports? bool seen_non_mux; pool mux_users; pool mux_drivers; @@ -50,12 +77,13 @@ struct OptMuxtreeWorker vector bit2info; struct portinfo_t { - int ctrl_sig; - pool input_sigs; - pool input_muxes; - bool const_activated; - bool const_deactivated; - bool enabled; + int ctrl_sig = -1; // No associated control signal by default + pool input_sigs = {}; + pool input_muxes = {}; + bool const_activated = false; + bool const_deactivated = false; + // Is the port reachable from inputs of a mux tree? + bool observable = false; }; struct muxinfo_t { @@ -68,13 +96,16 @@ struct OptMuxtreeWorker vector root_enable_muxes; pool root_mux_rerun; - OptMuxtreeWorker(RTLIL::Design *design, RTLIL::Module *module) : - design(design), module(module), assign_map(module), removed_count(0) - { - log("Running muxtree optimizer on module %s..\n", module->name.c_str()); - - log(" Creating internal representation of mux trees.\n"); + portinfo_t used_port_bit(RTLIL::SigSpec& sig, int mux_idx) { + portinfo_t portinfo = {}; + for (int bit_idx : sig2bits(sig)) { + bit2info[bit_idx].mux_users.insert(mux_idx); + portinfo.input_sigs.insert(bit_idx); + } + return portinfo; + } + void track_mux(Cell* cell) { // Populate bit2info[]: // .seen_non_mux // .mux_users @@ -84,73 +115,59 @@ struct OptMuxtreeWorker // .input_sigs // .const_activated // .const_deactivated - for (auto cell : module->cells()) - { - if (cell->type.in(ID($mux), ID($pmux))) - { - RTLIL::SigSpec sig_a = cell->getPort(ID::A); - RTLIL::SigSpec sig_b = cell->getPort(ID::B); - RTLIL::SigSpec sig_s = cell->getPort(ID::S); - RTLIL::SigSpec sig_y = cell->getPort(ID::Y); + RTLIL::SigSpec sig_a = cell->getPort(ID::A); + RTLIL::SigSpec sig_b = cell->getPort(ID::B); + RTLIL::SigSpec sig_s = cell->getPort(ID::S); + RTLIL::SigSpec sig_y = cell->getPort(ID::Y); - muxinfo_t muxinfo; - muxinfo.cell = cell; + muxinfo_t muxinfo; + muxinfo.cell = cell; + int this_mux_idx = GetSize(mux2info); - for (int i = 0; i < GetSize(sig_s); i++) { - RTLIL::SigSpec sig = sig_b.extract(i*GetSize(sig_a), GetSize(sig_a)); - RTLIL::SigSpec ctrl_sig = assign_map(sig_s.extract(i, 1)); - portinfo_t portinfo; - portinfo.ctrl_sig = sig2bits(ctrl_sig, false).front(); - for (int idx : sig2bits(sig)) { - bit2info[idx].mux_users.insert(GetSize(mux2info)); - portinfo.input_sigs.insert(idx); - } - portinfo.const_activated = ctrl_sig.is_fully_const() && ctrl_sig.as_bool(); - portinfo.const_deactivated = ctrl_sig.is_fully_const() && !ctrl_sig.as_bool(); - portinfo.enabled = false; - muxinfo.ports.push_back(portinfo); - } - - portinfo_t portinfo; - for (int idx : sig2bits(sig_a)) { - bit2info[idx].mux_users.insert(GetSize(mux2info)); - portinfo.input_sigs.insert(idx); - } - portinfo.ctrl_sig = -1; - portinfo.const_activated = false; - portinfo.const_deactivated = false; - portinfo.enabled = false; - muxinfo.ports.push_back(portinfo); - - for (int idx : sig2bits(sig_y)) - bit2info[idx].mux_drivers.insert(GetSize(mux2info)); - - for (int idx : sig2bits(sig_s)) - bit2info[idx].seen_non_mux = true; - - mux2info.push_back(muxinfo); - } - else - { - for (auto &it : cell->connections()) { - for (int idx : sig2bits(it.second)) - bit2info[idx].seen_non_mux = true; - } - } + // Analyze port B + // In case of $pmux, port B is multiple slices, concatenated, one per bit of port S + for (int i = 0; i < GetSize(sig_s); i++) { + RTLIL::SigSpec sig = sig_b.extract(i*GetSize(sig_a), GetSize(sig_a)); + RTLIL::SigSpec ctrl_sig = assign_map(sig_s.extract(i, 1)); + portinfo_t portinfo = used_port_bit(sig, this_mux_idx); + portinfo.ctrl_sig = sig2bits(ctrl_sig, false).front(); + portinfo.const_activated = ctrl_sig.is_fully_const() && ctrl_sig.as_bool(); + portinfo.const_deactivated = ctrl_sig.is_fully_const() && !ctrl_sig.as_bool(); + muxinfo.ports.push_back(portinfo); } + + // Analyze port A + muxinfo.ports.push_back(used_port_bit(sig_a, this_mux_idx)); + + for (int idx : sig2bits(sig_y)) + bit2info[idx].mux_drivers.insert(this_mux_idx); + + for (int idx : sig2bits(sig_s)) + bit2info[idx].seen_non_mux = true; + + mux2info.push_back(muxinfo); + } + + void see_non_mux_cell(Cell* cell) { + for (auto &it : cell->connections()) { + for (int idx : sig2bits(it.second)) + bit2info[idx].seen_non_mux = true; + } + } + + void see_non_mux_wires() { for (auto wire : module->wires()) { if (wire->port_output || wire->get_bool_attribute(ID::keep)) for (int idx : sig2bits(RTLIL::SigSpec(wire))) bit2info[idx].seen_non_mux = true; } + } - if (mux2info.empty()) { - log(" No muxes found in this module.\n"); - return; - } - - // Populate mux2info[].ports[]: - // .input_muxes + // Populate mux2info[].ports[]: + // .input_muxes + void fixup_input_muxes() { + // bit2info knows the mux users and mux drivers of bits + // use this to tell mux2info ports about what muxes are driven by it for (int i = 0; i < GetSize(bit2info); i++) for (int j : bit2info[i].mux_users) for (auto &p : mux2info[j].ports) { @@ -158,12 +175,15 @@ struct OptMuxtreeWorker for (int k : bit2info[i].mux_drivers) p.input_muxes.insert(k); } + } - log(" Evaluating internal representation of mux trees.\n"); - + void populate_roots() { + // mux_to_users[i] means "set of muxes using output of mux i" dict> mux_to_users; - root_muxes.resize(GetSize(mux2info)); + // Pure root muxes (outputs seen by non-muxes) root_enable_muxes.resize(GetSize(mux2info)); + // All root muxes (outputs seen by non-muxes or multiple muxes) + root_muxes.resize(GetSize(mux2info)); for (auto &bi : bit2info) { for (int i : bi.mux_drivers) @@ -177,16 +197,44 @@ struct OptMuxtreeWorker } } - for (auto &it : mux_to_users) - if (GetSize(it.second) > 1) - root_muxes.at(it.first) = true; + for (auto &[driving_mux, user_muxes] : mux_to_users) + if (GetSize(user_muxes) > 1) + root_muxes.at(driving_mux) = true; + } + + OptMuxtreeWorker(RTLIL::Design *design, RTLIL::Module *module) : + design(design), module(module), assign_map(module), removed_count(0) + { + log("Running muxtree optimizer on module %s..\n", module->name.c_str()); + + log(" Creating internal representation of mux trees.\n"); + + for (auto cell : module->cells()) + { + if (cell->type.in(ID($mux), ID($pmux))) + track_mux(cell); + else + see_non_mux_cell(cell); + } + see_non_mux_wires(); + + if (mux2info.empty()) { + log(" No muxes found in this module.\n"); + return; + } + + fixup_input_muxes(); + + log(" Evaluating internal representation of mux trees.\n"); + + populate_roots(); for (int mux_idx = 0; mux_idx < GetSize(root_muxes); mux_idx++) if (root_muxes.at(mux_idx)) { log_debug(" Root of a mux tree: %s%s\n", log_id(mux2info[mux_idx].cell), root_enable_muxes.at(mux_idx) ? " (pure)" : ""); root_mux_rerun.erase(mux_idx); eval_root_mux(mux_idx); - if (glob_abort_cnt == 0) { + if (glob_evals_left == 0) { log(" Giving up (too many iterations)\n"); return; } @@ -198,21 +246,21 @@ struct OptMuxtreeWorker log_assert(root_enable_muxes.at(mux_idx)); root_mux_rerun.erase(mux_idx); eval_root_mux(mux_idx); - if (glob_abort_cnt == 0) { + if (glob_evals_left == 0) { log(" Giving up (too many iterations)\n"); return; } } log(" Analyzing evaluation results.\n"); - log_assert(glob_abort_cnt > 0); + log_assert(glob_evals_left > 0); for (auto &mi : mux2info) { vector live_ports; for (int port_idx = 0; port_idx < GetSize(mi.ports); port_idx++) { portinfo_t &pi = mi.ports[port_idx]; - if (pi.enabled) { + if (pi.observable) { live_ports.push_back(port_idx); } else { log(" dead port %d/%d on %s %s.\n", port_idx+1, GetSize(mi.ports), @@ -290,9 +338,10 @@ struct OptMuxtreeWorker struct knowledge_t { - // database of known inactive signals - // the payload is a reference counter used to manage the - // list. when it is non-zero the signal in known to be inactive + // Known inactive signals + // The payload is a reference counter used to manage the list + // When it is non-zero, the signal in known to be inactive + // When it reaches zero, the map element is removed std::unordered_map known_inactive; // database of known active signals @@ -303,84 +352,123 @@ struct OptMuxtreeWorker std::unordered_set visited_muxes; }; - void eval_mux_port(knowledge_t &knowledge, int mux_idx, int port_idx, bool do_replace_known, bool do_enable_ports, int abort_count) - { - if (glob_abort_cnt == 0) - return; - - muxinfo_t &muxinfo = mux2info[mux_idx]; - - if (do_enable_ports) - muxinfo.ports[port_idx].enabled = true; - + static void activate_port(knowledge_t &knowledge, int port_idx, const muxinfo_t &muxinfo) { + // First, mark all other ports inactive for (int i = 0; i < GetSize(muxinfo.ports); i++) { if (i == port_idx) continue; if (muxinfo.ports[i].ctrl_sig >= 0) ++knowledge.known_inactive[muxinfo.ports[i].ctrl_sig]; } - + // Mark port active unless it's the last one if (port_idx < GetSize(muxinfo.ports)-1 && !muxinfo.ports[port_idx].const_activated) ++knowledge.known_active[muxinfo.ports[port_idx].ctrl_sig]; + } - vector parent_muxes; - for (int m : muxinfo.ports[port_idx].input_muxes) { - auto it = knowledge.visited_muxes.find(m); - if (it != knowledge.visited_muxes.end()) - continue; - knowledge.visited_muxes.insert(it, m); - parent_muxes.push_back(m); - } - for (int m : parent_muxes) { - if (root_enable_muxes.at(m)) - continue; - else if (root_muxes.at(m)) { - if (abort_count == 0) { - root_mux_rerun.insert(m); - root_enable_muxes.at(m) = true; - log_debug(" Removing pure flag from root mux %s.\n", log_id(mux2info[m].cell)); - } else - eval_mux(knowledge, m, false, do_enable_ports, abort_count - 1); - } else - eval_mux(knowledge, m, do_replace_known, do_enable_ports, abort_count); - if (glob_abort_cnt == 0) - return; - } - for (int m : parent_muxes) - knowledge.visited_muxes.erase(m); - - if (port_idx < GetSize(muxinfo.ports)-1 && !muxinfo.ports[port_idx].const_activated) { - auto it = knowledge.known_active.find(muxinfo.ports[port_idx].ctrl_sig); - if (it != knowledge.known_active.end()) + static void deactivate_port(knowledge_t &knowledge, int port_idx, const muxinfo_t &muxinfo) { + auto unlearn = [](std::unordered_map& knowns, int i) { + auto it = knowns.find(i); + if (it != knowns.end()) if (--it->second == 0) - knowledge.known_active.erase(it); - } + knowns.erase(it); + }; + if (port_idx < GetSize(muxinfo.ports)-1 && !muxinfo.ports[port_idx].const_activated) + unlearn(knowledge.known_active, muxinfo.ports[port_idx].ctrl_sig); + + // Undo inactivity assumptions for other ports for (int i = 0; i < GetSize(muxinfo.ports); i++) { if (i == port_idx) continue; - if (muxinfo.ports[i].ctrl_sig >= 0) { - auto it = knowledge.known_inactive.find(muxinfo.ports[i].ctrl_sig); - if (it != knowledge.known_inactive.end()) - if (--it->second == 0) - knowledge.known_inactive.erase(it); - } + if (muxinfo.ports[i].ctrl_sig >= 0) + unlearn(knowledge.known_inactive, muxinfo.ports[i].ctrl_sig); } } + struct limits_t { + // Are we allowed to replace inputs with constants? + // True if knowledge doesn't contain assumptions + bool do_replace_known = true; + // Are we allowed to mark ports as observable? + // True if we're recursing from a pure root mux + bool do_mark_ports_observable = true; + // How many more subtree recursions into input trees can we take? + // Then shalt thou count to three, no more, no less. Three shall be the number thou shalt count, and the number of the counting shall be three. + int recursions_left = 3; + limits_t subtree() const { + limits_t ret = *this; + log_assert(ret.recursions_left > 0); + ret.recursions_left--; + return ret; + } + }; + void eval_mux_port(knowledge_t &knowledge, int mux_idx, int port_idx, limits_t limits) + { + if (glob_evals_left == 0) + return; + + muxinfo_t &muxinfo = mux2info[mux_idx]; + + if (limits.do_mark_ports_observable) + muxinfo.ports[port_idx].observable = true; + + // For the purposes of recursion, we assume the port is active, + // meaning all other ports are inactive + activate_port(knowledge, port_idx, muxinfo); + + vector input_mux_queue; + for (int m : muxinfo.ports[port_idx].input_muxes) { + if (knowledge.visited_muxes.count(m)) + continue; + knowledge.visited_muxes.insert(m); + input_mux_queue.push_back(m); + } + for (int m : input_mux_queue) { + if (root_enable_muxes.at(m)) + continue; + else if (root_muxes.at(m)) { + if (limits.recursions_left == 0) { + // Ran out of subtree depth, re-eval this subtree in the next re-run + root_mux_rerun.insert(m); + root_enable_muxes.at(m) = true; + log_debug(" Removing pure flag from root mux %s.\n", log_id(mux2info[m].cell)); + } else { + auto new_limits = limits.subtree(); + // Since our knowledge includes assumption, we can't generally allow replacing based on it + new_limits.do_replace_known = false; + eval_mux(knowledge, m, new_limits); + } + } else { + // This non-root input mux has only this mux as a user, + // so here we are allowed to pass along do_replace_known + eval_mux(knowledge, m, limits); + } + if (glob_evals_left == 0) + return; + } + + // Allow revisiting input muxes, since evaluating other ports should + // revisit these input muxes with different activation assumptions + for (int m : input_mux_queue) + knowledge.visited_muxes.erase(m); + + // Undo our assumptions that the port is active + deactivate_port(knowledge, port_idx, muxinfo); + } + void replace_known(knowledge_t &knowledge, muxinfo_t &muxinfo, IdString portname) { SigSpec sig = muxinfo.cell->getPort(portname); bool did_something = false; - int width = 0; + int width_if_b = 0; idict ctrl_bits; if (portname == ID::B) - width = GetSize(muxinfo.cell->getPort(ID::A)); + width_if_b = GetSize(muxinfo.cell->getPort(ID::A)); for (int bit : sig2bits(muxinfo.cell->getPort(ID::S), false)) ctrl_bits(bit); - int port_idx = 0, port_off = 0; + int slice_idx = 0, slice_off = 0; vector bits = sig2bits(sig, false); for (int i = 0; i < GetSize(bits); i++) { if (bits[i] >= 0) { @@ -393,17 +481,31 @@ struct OptMuxtreeWorker did_something = true; } if (ctrl_bits.count(bits[i])) { - if (width) { - sig[i] = ctrl_bits.at(bits[i]) == port_idx ? State::S1 : State::S0; - } else { + if (!width_if_b) { + // Single-bit $mux example + // mux: S ? B : A = Y + // A=S + // 0 ? B : 0 = 0 + // 1 ? B : 1 = B + // rewrite to A=0 + // 0 ? B : 0 = 0 + // 1 ? B : 0 = B + // which is equivalent sig[i] = State::S0; + } else { + // "Sliced" $pmux example + // B[i]=S[j] + // i == j => B[i] activated only when B[i] is high, safe to rewrite to 1 + // i != j => B[i] activated only when B[i] is low, safe to rewrite to 0 + sig[i] = ctrl_bits.at(bits[i]) == slice_idx ? State::S1 : State::S0; } did_something = true; } } - if (width) { - if (++port_off == width) - port_idx++, port_off=0; + if (width_if_b) { + // Roll over into next slice + if (++slice_off == width_if_b) + slice_idx++, slice_off=0; } } @@ -414,16 +516,17 @@ struct OptMuxtreeWorker } } - void eval_mux(knowledge_t &knowledge, int mux_idx, bool do_replace_known, bool do_enable_ports, int abort_count) + void eval_mux(knowledge_t &knowledge, int mux_idx, limits_t limits) { - if (glob_abort_cnt == 0) + if (glob_evals_left == 0) return; - glob_abort_cnt--; + glob_evals_left--; muxinfo_t &muxinfo = mux2info[mux_idx]; + log_debug("\t\teval %s (replace %d enable %d)\n", log_id(muxinfo.cell), limits.do_replace_known, limits.do_mark_ports_observable); // set input ports to constants if we find known active or inactive signals - if (do_replace_known) { + if (limits.do_replace_known) { replace_known(knowledge, muxinfo, ID::A); replace_known(knowledge, muxinfo, ID::B); } @@ -433,21 +536,21 @@ struct OptMuxtreeWorker { portinfo_t &portinfo = muxinfo.ports[port_idx]; if (portinfo.const_activated) { - eval_mux_port(knowledge, mux_idx, port_idx, do_replace_known, do_enable_ports, abort_count); + eval_mux_port(knowledge, mux_idx, port_idx, limits); return; } } - // compare ports with known_active signals. if we find a match, only this - // port can be active. do not include the last port (its the default port - // that has no control signals). + // Compare ports with known active control signals. if we find a match, + // only this port can be active. Do not include the last port, + // it's the default port without an associated control signal for (int port_idx = 0; port_idx < GetSize(muxinfo.ports)-1; port_idx++) { portinfo_t &portinfo = muxinfo.ports[port_idx]; if (portinfo.const_deactivated) continue; if (knowledge.known_active.count(portinfo.ctrl_sig) > 0) { - eval_mux_port(knowledge, mux_idx, port_idx, do_replace_known, do_enable_ports, abort_count); + eval_mux_port(knowledge, mux_idx, port_idx, limits); return; } } @@ -462,19 +565,21 @@ struct OptMuxtreeWorker if (port_idx < GetSize(muxinfo.ports)-1) if (knowledge.known_inactive.count(portinfo.ctrl_sig) > 0) continue; - eval_mux_port(knowledge, mux_idx, port_idx, do_replace_known, do_enable_ports, abort_count); + eval_mux_port(knowledge, mux_idx, port_idx, limits); - if (glob_abort_cnt == 0) + if (glob_evals_left == 0) return; } } void eval_root_mux(int mux_idx) { - log_assert(glob_abort_cnt > 0); + log_assert(glob_evals_left > 0); knowledge_t knowledge; knowledge.visited_muxes.insert(mux_idx); - eval_mux(knowledge, mux_idx, true, root_enable_muxes.at(mux_idx), 3); + limits_t limits = {}; + limits.do_mark_ports_observable = root_enable_muxes.at(mux_idx); + eval_mux(knowledge, mux_idx, limits); } }; From b5aa3ab9f7c395275c0aeca6ffc133fdb563f849 Mon Sep 17 00:00:00 2001 From: Robert O'Callahan Date: Mon, 25 Aug 2025 03:09:04 +0000 Subject: [PATCH 06/10] hash_ops should take all parameters by reference instead of requiring copies of vectors, tuples etc --- kernel/hashlib.h | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/kernel/hashlib.h b/kernel/hashlib.h index b43b7302e..9c53e6687 100644 --- a/kernel/hashlib.h +++ b/kernel/hashlib.h @@ -179,58 +179,58 @@ struct hash_ops { }; template struct hash_ops> { - static inline bool cmp(std::pair a, std::pair b) { + static inline bool cmp(const std::pair &a, const std::pair &b) { return a == b; } - [[nodiscard]] static inline Hasher hash_into(std::pair a, Hasher h) { + [[nodiscard]] static inline Hasher hash_into(const std::pair &a, Hasher h) { h = hash_ops

::hash_into(a.first, h); h = hash_ops::hash_into(a.second, h); return h; } - HASH_TOP_LOOP_FST (std::pair a) HASH_TOP_LOOP_SND + HASH_TOP_LOOP_FST (const std::pair &a) HASH_TOP_LOOP_SND }; template struct hash_ops> { - static inline bool cmp(std::tuple a, std::tuple b) { + static inline bool cmp(const std::tuple &a, const std::tuple &b) { return a == b; } template - static inline typename std::enable_if::type hash_into(std::tuple, Hasher h) { + static inline typename std::enable_if::type hash_into(const std::tuple &, Hasher h) { return h; } template - static inline typename std::enable_if::type hash_into(std::tuple a, Hasher h) { + static inline typename std::enable_if::type hash_into(const std::tuple &a, Hasher h) { typedef hash_ops>::type> element_ops_t; h = hash_into(a, h); h = element_ops_t::hash_into(std::get(a), h); return h; } - HASH_TOP_LOOP_FST (std::tuple a) HASH_TOP_LOOP_SND + HASH_TOP_LOOP_FST (const std::tuple &a) HASH_TOP_LOOP_SND }; template struct hash_ops> { - static inline bool cmp(std::vector a, std::vector b) { + static inline bool cmp(const std::vector &a, const std::vector &b) { return a == b; } - [[nodiscard]] static inline Hasher hash_into(std::vector a, Hasher h) { + [[nodiscard]] static inline Hasher hash_into(const std::vector &a, Hasher h) { h.eat((uint32_t)a.size()); for (auto k : a) h.eat(k); return h; } - HASH_TOP_LOOP_FST (std::vector a) HASH_TOP_LOOP_SND + HASH_TOP_LOOP_FST (const std::vector &a) HASH_TOP_LOOP_SND }; template struct hash_ops> { - static inline bool cmp(std::array a, std::array b) { + static inline bool cmp(const std::array &a, const std::array &b) { return a == b; } - [[nodiscard]] static inline Hasher hash_into(std::array a, Hasher h) { + [[nodiscard]] static inline Hasher hash_into(const std::array &a, Hasher h) { for (const auto& k : a) h = hash_ops::hash_into(k, h); return h; } - HASH_TOP_LOOP_FST (std::array a) HASH_TOP_LOOP_SND + HASH_TOP_LOOP_FST (const std::array &a) HASH_TOP_LOOP_SND }; struct hash_cstr_ops { @@ -302,10 +302,10 @@ template<> struct hash_ops { }; template struct hash_ops> { - static inline bool cmp(std::variant a, std::variant b) { + static inline bool cmp(const std::variant &a, const std::variant &b) { return a == b; } - [[nodiscard]] static inline Hasher hash_into(std::variant a, Hasher h) { + [[nodiscard]] static inline Hasher hash_into(const std::variant &a, Hasher h) { std::visit([& h](const auto &v) { h.eat(v); }, a); h.eat(a.index()); return h; @@ -313,10 +313,10 @@ template struct hash_ops> { }; template struct hash_ops> { - static inline bool cmp(std::optional a, std::optional b) { + static inline bool cmp(const std::optional &a, const std::optional &b) { return a == b; } - [[nodiscard]] static inline Hasher hash_into(std::optional a, Hasher h) { + [[nodiscard]] static inline Hasher hash_into(const std::optional &a, Hasher h) { if(a.has_value()) h.eat(*a); else From 9f0904d048751e3f6518713f642ccc021e7f6b22 Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Mon, 25 Aug 2025 07:46:34 +0200 Subject: [PATCH 07/10] Makefile: fix hardcoded -install_name for libyosys.so --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e182f51cc..b3ea79657 100644 --- a/Makefile +++ b/Makefile @@ -745,7 +745,7 @@ $(PROGRAM_PREFIX)yosys$(EXE): $(OBJS) libyosys.so: $(filter-out kernel/driver.o,$(OBJS)) ifeq ($(OS), Darwin) - $(P) $(CXX) -o libyosys.so -shared -undefined dynamic_lookup -Wl,-install_name,$(LIBDIR)/libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) + $(P) $(CXX) -o libyosys.so -shared -undefined dynamic_lookup -Wl,-install_name,libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) else $(P) $(CXX) -o libyosys.so -shared -Wl,-soname,libyosys.so $(LINKFLAGS) $^ $(LIBS) $(LIBS_VERIFIC) endif From b45e5854bfe19fa3c433e327fce19012e1b395c7 Mon Sep 17 00:00:00 2001 From: "Emil J. Tywoniak" Date: Mon, 25 Aug 2025 16:36:07 +0200 Subject: [PATCH 08/10] opt_muxtree: comment wording --- passes/opt/opt_muxtree.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/passes/opt/opt_muxtree.cc b/passes/opt/opt_muxtree.cc index ae9b1a632..98803b935 100644 --- a/passes/opt/opt_muxtree.cc +++ b/passes/opt/opt_muxtree.cc @@ -427,14 +427,17 @@ struct OptMuxtreeWorker if (root_enable_muxes.at(m)) continue; else if (root_muxes.at(m)) { + // This leaf node of the current tree + // is the root of an input tree of the current tree if (limits.recursions_left == 0) { - // Ran out of subtree depth, re-eval this subtree in the next re-run + // Ran out of subtree depth, re-eval this input tree in the next re-run root_mux_rerun.insert(m); root_enable_muxes.at(m) = true; log_debug(" Removing pure flag from root mux %s.\n", log_id(mux2info[m].cell)); } else { auto new_limits = limits.subtree(); - // Since our knowledge includes assumption, we can't generally allow replacing based on it + // Since our knowledge includes assumption, + // we can't generally allow replacing in an input tree based on it new_limits.do_replace_known = false; eval_mux(knowledge, m, new_limits); } From 15d24bf2e65450602f902574a131a8b97d2e6487 Mon Sep 17 00:00:00 2001 From: "N. Engelhardt" Date: Mon, 11 Nov 2024 11:22:05 +0100 Subject: [PATCH 09/10] synth_quicklogic: add -noflatten option --- techlibs/quicklogic/synth_quicklogic.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/techlibs/quicklogic/synth_quicklogic.cc b/techlibs/quicklogic/synth_quicklogic.cc index 07ec769b5..c9b8eb289 100644 --- a/techlibs/quicklogic/synth_quicklogic.cc +++ b/techlibs/quicklogic/synth_quicklogic.cc @@ -78,7 +78,7 @@ struct SynthQuickLogicPass : public ScriptPass { } string top_opt, blif_file, edif_file, family, currmodule, verilog_file, lib_path; - bool abc9, inferAdder, nobram, bramTypes, dsp, ioff; + bool abc9, inferAdder, nobram, bramTypes, dsp, ioff, flatten; void clear_flags() override { @@ -95,6 +95,7 @@ struct SynthQuickLogicPass : public ScriptPass { lib_path = "+/quicklogic/"; dsp = true; ioff = true; + flatten = true; } void set_scratchpad_defaults(RTLIL::Design *design) { @@ -163,6 +164,10 @@ struct SynthQuickLogicPass : public ScriptPass { ioff = false; continue; } + if (args[argidx] == "-noflatten") { + flatten = false; + continue; + } break; } extra_args(args, argidx, design); @@ -207,7 +212,8 @@ struct SynthQuickLogicPass : public ScriptPass { if (check_label("prepare")) { run("proc"); - run("flatten"); + if (flatten) + run("flatten", "(unless -noflatten)"); if (help_mode || family == "pp3") { run("tribuf -logic", " (for pp3)"); } From 83d953e957e90433ed0d57e8b2bca9850b435c18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 00:23:36 +0000 Subject: [PATCH 10/10] Bump version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b3ea79657..4cb86510a 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ ifeq ($(OS), Haiku) CXXFLAGS += -D_DEFAULT_SOURCE endif -YOSYS_VER := 0.56+171 +YOSYS_VER := 0.56+186 YOSYS_MAJOR := $(shell echo $(YOSYS_VER) | cut -d'.' -f1) YOSYS_MINOR := $(shell echo $(YOSYS_VER) | cut -d'.' -f2 | cut -d'+' -f1) YOSYS_COMMIT := $(shell echo $(YOSYS_VER) | cut -d'+' -f2)