Merge pull request #221 from antmicro/dsp-ff-plugin

A plugin for inference of DSP registers
diff --git a/Makefile b/Makefile
index 801c6a2..00e0263 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@
 #
 # SPDX-License-Identifier:ISC
 
-PLUGIN_LIST := fasm xdc params sdc ql-iob design_introspection integrateinv ql-qlf uhdm
+PLUGIN_LIST := fasm xdc params sdc ql-iob design_introspection integrateinv ql-qlf uhdm dsp-ff
 PLUGINS := $(foreach plugin,$(PLUGIN_LIST),$(plugin).so)
 PLUGINS_INSTALL := $(foreach plugin,$(PLUGIN_LIST),install_$(plugin))
 PLUGINS_CLEAN := $(foreach plugin,$(PLUGIN_LIST),clean_$(plugin))
diff --git a/Makefile_plugin.common b/Makefile_plugin.common
index 0870f21..49d6ab5 100644
--- a/Makefile_plugin.common
+++ b/Makefile_plugin.common
@@ -55,7 +55,7 @@
 DATA_DIR ?= $(shell $(YOSYS_CONFIG) --datdir)
 EXTRA_FLAGS ?=
 
-OBJS := $(SOURCES:cc=o)
+OBJS := $(patsubst %.cc,%.o,$(SOURCES))
 
 all: $(NAME).so
 
diff --git a/Makefile_test.common b/Makefile_test.common
index 61abadb..6497321 100644
--- a/Makefile_test.common
+++ b/Makefile_test.common
@@ -35,11 +35,11 @@
 	@set +e; \
 	$$($(1)_verify); \
 	if [ $$$$? -eq 0 ]; then \
-		printf "Test %-18s \e[32mPASSED\e[0m @ %s\n" $(1) $(CURDIR); \
+		printf "Test %-20s \e[32mPASSED\e[0m @ %s\n" $(1) $(CURDIR); \
 		touch $$<; \
 		true; \
 	else \
-		printf "Test %-18s \e[31;1mFAILED\e[0m @ %s\n" $(1) $(CURDIR); \
+		printf "Test %-20s \e[31;1mFAILED\e[0m @ %s\n" $(1) $(CURDIR); \
 		false; \
 	fi
 
@@ -54,15 +54,15 @@
 	rm -f run-$(1).tcl; \
 	if [ ! -z "$$($(1)_negative)" ] && [ $$($(1)_negative) -eq 1 ]; then \
 		if [ $$$$RETVAL -ne 0 ]; then \
-			printf "Negative test %-18s \e[32mPASSED\e[0m @ %s\n" $(1) $(CURDIR); \
+			printf "Negative test %-20s \e[32mPASSED\e[0m @ %s\n" $(1) $(CURDIR); \
 			true; \
 		else \
-			printf "Negative test %-18s \e[31;1mFAILED\e[0m @ %s\n" $(1) $(CURDIR); \
+			printf "Negative test %-20s \e[31;1mFAILED\e[0m @ %s\n" $(1) $(CURDIR); \
 			false; \
 		fi \
 	else \
 		if [ $$$$RETVAL -ne 0 ]; then \
-			echo "Unexpected runtime error"; \
+		    printf "Test %-20s \e[31;1mFAILED\e[0m @ %s\n" $(1) $(CURDIR); \
 			false; \
 		fi \
 	fi
diff --git a/dsp-ff-plugin/Makefile b/dsp-ff-plugin/Makefile
new file mode 100644
index 0000000..3e59b3d
--- /dev/null
+++ b/dsp-ff-plugin/Makefile
@@ -0,0 +1,16 @@
+# Copyright (C) 2020-2021  The SymbiFlow Authors.
+#
+# Use of this source code is governed by a ISC-style
+# license that can be found in the LICENSE file or at
+# https://opensource.org/licenses/ISC
+#
+# SPDX-License-Identifier:ISC
+
+NAME = dsp-ff
+SOURCES = dsp_ff.cc 
+
+include ../Makefile_plugin.common
+
+install:
+	install -D nexus-dsp_rules.txt $(DATA_DIR)/nexus/dsp_rules.txt
+
diff --git a/dsp-ff-plugin/dsp_ff.cc b/dsp-ff-plugin/dsp_ff.cc
new file mode 100644
index 0000000..26e5acd
--- /dev/null
+++ b/dsp-ff-plugin/dsp_ff.cc
@@ -0,0 +1,1373 @@
+#include "kernel/register.h"
+#include "kernel/rtlil.h"
+#include "kernel/sigtools.h"
+
+USING_YOSYS_NAMESPACE
+PRIVATE_NAMESPACE_BEGIN
+
+// ============================================================================
+
+struct DspFF : public Pass {
+
+    /// A structure identifying specific pin in a cell instance
+    struct CellPin {
+        RTLIL::Cell *cell;    /// Cell pointer (nullptr for top-level ports)
+        RTLIL::IdString port; /// Port name
+        int bit;              /// Bit index
+
+        CellPin(RTLIL::Cell *_cell, const RTLIL::IdString &_port, int _bit = 0) : cell(_cell), port(_port), bit(_bit) {}
+
+        CellPin(const CellPin &ref) = default;
+        CellPin(CellPin &&ref) = default;
+
+        unsigned int hash() const
+        {
+            unsigned int h = 0;
+            if (cell != nullptr) {
+                h = mkhash_add(h, cell->hash());
+            }
+            h = mkhash_add(h, port.hash());
+            h = mkhash_add(h, bit);
+            return h;
+        }
+
+        bool operator==(const CellPin &ref) const { return (cell == ref.cell) && (port == ref.port) && (bit == ref.bit); }
+
+        std::string as_string() const
+        {
+            if (cell != nullptr) {
+                return stringf("%s.%s[%d]", RTLIL::unescape_id(cell->name).c_str(), RTLIL::unescape_id(port).c_str(), bit);
+            } else {
+                return stringf("%s[%d]", RTLIL::unescape_id(port).c_str(), bit);
+            }
+        }
+    };
+
+    // ..........................................
+
+    /// Connection map
+    struct ConnMap {
+
+        /// Maps source SigBit to all sinks it drives CellPin.
+        dict<RTLIL::SigBit, std::vector<CellPin>> sinks;
+        /// Maps source SigBit to its driver CellPin
+        dict<RTLIL::SigBit, CellPin> drivers;
+
+        /// Builds the map
+        void build(RTLIL::Module *module, const SigMap &sigmap)
+        {
+            clear();
+
+            // Scan cell ports
+            for (auto *cell : module->cells()) {
+                for (const auto &it : cell->connections_) {
+                    const auto &port = it.first;
+                    const auto &sigbits = it.second.bits();
+                    for (size_t i = 0; i < sigbits.size(); ++i) {
+                        auto sigbit = sigmap(sigbits[i]);
+
+                        // This is an input port (sink))
+                        if (cell->input(port)) {
+                            auto &vec = sinks[sigbit];
+                            vec.push_back(CellPin(cell, port, i));
+                        }
+                        // This is a source
+                        if (cell->output(port)) {
+                            drivers.insert(std::make_pair(sigbit, CellPin(cell, port, i)));
+                        }
+                    }
+                }
+            }
+
+            // Scan top-level ports
+            for (auto &it : module->wires_) {
+                auto *wire = it.second;
+
+                if (!wire->port_input && !wire->port_output) {
+                    continue;
+                }
+
+                RTLIL::SigSpec sigspec(wire, wire->start_offset, wire->width);
+                const auto &sigbits = sigspec.bits();
+                for (size_t i = 0; i < sigbits.size(); ++i) {
+                    auto sigbit = sigbits[i];
+                    if (!sigbit.wire) {
+                        continue;
+                    }
+
+                    // Output port (sink)
+                    if (sigbit.wire->port_output) {
+                        auto &vec = sinks[sigmap(sigbit)];
+                        vec.push_back(CellPin(nullptr, sigbit.wire->name, i));
+                    }
+                    // Input port (source)
+                    if (sigbit.wire->port_input) {
+                        drivers.insert(std::make_pair(sigbit, CellPin(nullptr, sigbit.wire->name, i)));
+                    }
+                }
+            }
+        }
+
+        /// Clears the map
+        void clear()
+        {
+            sinks.clear();
+            drivers.clear();
+        };
+    };
+
+    // ..........................................
+
+    /// Describes a flip-flop type that can be integrated with a DSP cell
+    struct FlopType {
+        RTLIL::IdString name;
+
+        /// A dict of port names indexed by their functions (like "clk", "rst")
+        dict<RTLIL::IdString, RTLIL::IdString> ports;
+
+        struct {
+            /// A list of parameters that must match for all flip-flops
+            std::vector<RTLIL::IdString> matching;
+            /// A dict of parameter values that must match for a flip-flop
+            dict<RTLIL::IdString, RTLIL::Const> required;
+            /// A dict of parameters to be set in the DSP cell after integration
+            dict<RTLIL::IdString, RTLIL::Const> set;
+            /// A dict of parameters to be mapped to the DSP cell after integration
+            dict<RTLIL::IdString, RTLIL::IdString> map;
+        } params;
+    };
+
+    /// Describes a DSP cell port that has built-in register (flip-flops)
+    struct PortType {
+        RTLIL::IdString name;
+
+        /// Range of port pins that have FFs (low to high, inclusive)
+        std::pair<int, int> bits;
+        /// A dict of associated cell ports indexed by their function (like "clk, "rst")
+        /// along with the default value to connect when unused.
+        dict<RTLIL::IdString, std::pair<RTLIL::IdString, RTLIL::Const>> assoc;
+    };
+
+    /// Describes a DSP register
+    struct RegisterType {
+
+        /// Control parameters
+        struct {
+            /// A dict of parameters to be set in the cell after integration
+            dict<RTLIL::IdString, RTLIL::Const> set;
+            /// A dict of parameters to be mapped to the cell after integration
+            dict<RTLIL::IdString, RTLIL::IdString> map;
+        } params;
+
+        /// A list of ports to be connected to specific constants after flip-flop
+        /// integration.
+        dict<RTLIL::IdString, RTLIL::Const> connect;
+
+        unsigned int hash() const
+        {
+            unsigned int h = 0;
+            h = mkhash_add(h, params.set.hash());
+            h = mkhash_add(h, params.map.hash());
+            h = mkhash_add(h, connect.hash());
+            return h;
+        }
+
+        bool operator==(const RegisterType &ref) const
+        {
+            return (params.set == ref.params.set) && (params.map == ref.params.map) && (connect == ref.connect);
+        }
+    };
+
+    /// Describes a DSP cell type
+    struct DspType {
+        RTLIL::IdString name;
+        dict<RegisterType, std::vector<PortType>> registers;
+    };
+
+    /// Describes a changes made to a DSP cell
+    struct DspChanges {
+        pool<RTLIL::IdString> params; // Modified params
+        pool<RTLIL::IdString> conns;  // Altered connections (ports)
+    };
+
+    // ..........................................
+
+    /// Describes unique flip-flop configuration that is exclusive.
+    struct FlopData {
+        RTLIL::IdString type;
+        dict<RTLIL::IdString, RTLIL::SigBit> conns;
+        struct {
+            dict<RTLIL::IdString, RTLIL::Const> flop;
+            dict<RTLIL::IdString, RTLIL::Const> dsp;
+        } params;
+
+        FlopData(const RTLIL::IdString &_type) : type(_type){};
+
+        FlopData(const FlopData &ref) = default;
+        FlopData(FlopData &&ref) = default;
+
+        unsigned int hash() const
+        {
+            unsigned int h = 0;
+            h = mkhash_add(h, type.hash());
+            h = mkhash_add(h, conns.hash());
+            h = mkhash_add(h, params.flop.hash());
+            h = mkhash_add(h, params.dsp.hash());
+            return h;
+        }
+
+        bool operator==(const FlopData &ref) const
+        {
+            return (type == ref.type) && (conns == ref.conns) && (params.flop == ref.params.flop) && (params.dsp == ref.params.dsp);
+        }
+    };
+
+    // ..........................................
+
+    /// Loads FF and DSP integration rules from a file
+    void load_rules(const std::string &a_FileName)
+    {
+
+        // Parses a string and returns a vector of fields delimited by the
+        // given character.
+        auto getFields = [](const std::string &a_String, const char a_Delim = ' ', bool a_KeepEmpty = false) {
+            std::vector<std::string> fields;
+            std::stringstream ss(a_String);
+
+            while (ss.good()) {
+                std::string field;
+                std::getline(ss, field, a_Delim);
+                if (!field.empty() || a_KeepEmpty) {
+                    fields.push_back(field);
+                }
+            }
+
+            return fields;
+        };
+
+        // Parses a vector of strings like "<name>=<value>" starting from the
+        // second one on the list
+        auto parseNameValue = [&](const std::vector<std::string> &strs) {
+            const std::regex expr("(\\S+)=(\\S+)");
+            std::smatch match;
+
+            std::vector<std::pair<std::string, std::string>> vec;
+
+            for (size_t i = 1; i < strs.size(); ++i) {
+                if (std::regex_match(strs[i], match, expr)) {
+                    vec.push_back(std::make_pair(match[1], match[2]));
+                } else {
+                    log_error(" syntax error: '%s'\n", strs[i].c_str());
+                }
+            }
+
+            return vec;
+        };
+
+        // Parses port name as "<name>[<hi>:<lo>]" or just "<name>"
+        auto parsePortName = [&](const std::string &str) {
+            const std::regex expr("^(.*)\\[([0-9]+):([0-9]+)\\]");
+            std::smatch match;
+
+            std::tuple<std::string, int, int> data;
+            auto res = std::regex_match(str, match, expr);
+            if (res) {
+                data = std::make_tuple(std::string(match[1]), std::stoi(match[2]), std::stoi(match[3]));
+
+                if ((std::get<2>(data) > std::get<1>(data)) || std::get<2>(data) < 0 || std::get<1>(data) < 0) {
+                    log_error(" invalid port spec: '%s'\n", str.c_str());
+                }
+            } else {
+                data = std::make_tuple(str, -1, -1);
+            }
+
+            return data;
+        };
+
+        std::ifstream file(a_FileName);
+        std::string line;
+
+        log("Loading rules from '%s'...\n", a_FileName.c_str());
+        if (!file) {
+            log_error(" Error opening file!\n");
+        }
+
+        // Parse each port as if it was associated with its own DSP register.
+        // Group them each time a port definition is complete.
+        PortType portType;
+        RegisterType registerType;
+
+        std::vector<DspType> dspTypes;
+        std::vector<FlopType> flopTypes;
+
+        std::vector<RTLIL::IdString> dspAliases;
+        std::vector<std::string> portNames;
+
+        std::vector<std::string> tok;
+
+        // Parse the file
+        while (1) {
+
+            // Get line
+            std::getline(file, line);
+            if (!file) {
+                break;
+            }
+
+            // Strip comment if any, skip empty lines
+            size_t pos = line.find("#");
+            if (pos != std::string::npos) {
+                line = line.substr(0, pos);
+            }
+            if (line.find_first_not_of(" \r\n\t") == std::string::npos) {
+                continue;
+            }
+
+            // Split the line
+            const auto fields = getFields(line);
+            log_assert(fields.size() >= 1);
+
+            // DSP section
+            if (fields[0] == "dsp") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (!tok.empty()) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.push_back(fields[0]);
+
+                dspTypes.resize(dspTypes.size() + 1);
+                dspTypes.back().name = RTLIL::escape_id(fields[1]);
+
+                dspAliases.clear();
+                for (size_t i = 2; i < fields.size(); ++i) {
+                    dspAliases.push_back(RTLIL::escape_id(fields[i]));
+                }
+            } else if (fields[0] == "enddsp") {
+                if (fields.size() != 1) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() != 1 || tok.back() != "dsp") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.pop_back();
+
+                const auto dspType = dspTypes.back();
+
+                for (const auto &alias : dspAliases) {
+                    dspTypes.push_back(dspType);
+                    dspTypes.back().name = alias;
+                }
+            }
+
+            // DSP port section
+            else if (fields[0] == "port") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() != 1 || tok.back() != "dsp") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.push_back(fields[0]);
+
+                auto spec = parsePortName(fields[1]);
+
+                portType = PortType();
+                portType.name = RTLIL::escape_id(std::get<0>(spec));
+                portType.bits = std::make_pair(std::get<2>(spec), std::get<1>(spec));
+                portType.assoc.insert(std::make_pair(RTLIL::escape_id("clk"), std::make_pair(RTLIL::IdString(), RTLIL::Sx)));
+                portType.assoc.insert(std::make_pair(RTLIL::escape_id("rst"), std::make_pair(RTLIL::IdString(), RTLIL::Sx)));
+                portType.assoc.insert(std::make_pair(RTLIL::escape_id("ena"), std::make_pair(RTLIL::IdString(), RTLIL::Sx)));
+
+                registerType = RegisterType();
+
+                portNames.clear();
+                for (size_t i = 2; i < fields.size(); ++i) {
+                    portNames.push_back(fields[i]);
+                }
+
+            } else if (fields[0] == "endport") {
+                if (fields.size() != 1) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() != 2 || tok.back() != "port") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.pop_back();
+
+                // Store the DSP port
+                auto &dspType = dspTypes.back();
+                dspType.registers[registerType].push_back(portType);
+
+                // Store any extra DSP ports belonging to the same register
+                for (const auto &name : portNames) {
+                    auto spec = parsePortName(name);
+
+                    PortType portTypeCopy = portType;
+                    portTypeCopy.name = RTLIL::escape_id(std::get<0>(spec));
+                    portTypeCopy.bits = std::make_pair(std::get<2>(spec), std::get<1>(spec));
+
+                    dspType.registers[registerType].push_back(portTypeCopy);
+                }
+            }
+
+            // Flip-flop type section
+            else if (fields[0] == "ff") {
+                if (fields.size() != 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (!tok.empty()) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.push_back(fields[0]);
+
+                flopTypes.resize(flopTypes.size() + 1);
+                flopTypes.back().name = RTLIL::escape_id(fields[1]);
+                flopTypes.back().ports.insert(std::make_pair(RTLIL::escape_id("clk"), RTLIL::IdString()));
+                flopTypes.back().ports.insert(std::make_pair(RTLIL::escape_id("rst"), RTLIL::IdString()));
+                flopTypes.back().ports.insert(std::make_pair(RTLIL::escape_id("ena"), RTLIL::IdString()));
+                flopTypes.back().ports.insert(std::make_pair(RTLIL::escape_id("d"), RTLIL::IdString()));
+                flopTypes.back().ports.insert(std::make_pair(RTLIL::escape_id("q"), RTLIL::IdString()));
+            } else if (fields[0] == "endff") {
+                if (fields.size() != 1) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() != 1 || tok.back() != "ff") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+                tok.pop_back();
+            }
+
+            // Signals
+            else if (fields[0] == "clk") {
+                if (tok.size() == 0 || (tok.back() != "port" && tok.back() != "ff")) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                // Associated clock
+                if (tok.back() == "port") {
+                    if (fields.size() != 3) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    portType.assoc[RTLIL::escape_id("clk")] = std::make_pair(RTLIL::escape_id(fields[1]), RTLIL::Const::from_string(fields[2]));
+                } else if (tok.back() == "ff") {
+                    if (fields.size() != 2) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    flopTypes.back().ports[RTLIL::escape_id("clk")] = RTLIL::escape_id(fields[1]);
+                }
+            } else if (fields[0] == "rst") {
+                if (tok.size() == 0 || (tok.back() != "port" && tok.back() != "ff")) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                // Associated reset
+                if (tok.back() == "port") {
+                    if (fields.size() != 3) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    portType.assoc[RTLIL::escape_id("rst")] = std::make_pair(RTLIL::escape_id(fields[1]), RTLIL::Const::from_string(fields[2]));
+                } else if (tok.back() == "ff") {
+                    if (fields.size() != 2) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    flopTypes.back().ports[RTLIL::escape_id("rst")] = RTLIL::escape_id(fields[1]);
+                }
+            } else if (fields[0] == "ena") {
+                if (tok.size() == 0 || (tok.back() != "port" && tok.back() != "ff")) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                // Associated enable
+                if (tok.back() == "port") {
+                    if (fields.size() != 3) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    portType.assoc[RTLIL::escape_id("ena")] = std::make_pair(RTLIL::escape_id(fields[1]), RTLIL::Const::from_string(fields[2]));
+                } else if (tok.back() == "ff") {
+                    if (fields.size() != 2) {
+                        log_error(" syntax error: '%s'\n", line.c_str());
+                    }
+                    flopTypes.back().ports[RTLIL::escape_id("ena")] = RTLIL::escape_id(fields[1]);
+                }
+            }
+
+            else if (fields[0] == "d") {
+                if (fields.size() != 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || tok.back() != "ff") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                flopTypes.back().ports[RTLIL::escape_id("d")] = RTLIL::escape_id(fields[1]);
+            } else if (fields[0] == "q") {
+                if (fields.size() != 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || tok.back() != "ff") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                flopTypes.back().ports[RTLIL::escape_id("q")] = RTLIL::escape_id(fields[1]);
+            }
+
+            // Parameters that must be set to certain values
+            else if (fields[0] == "require") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || tok.back() != "ff") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                const auto vec = parseNameValue(fields);
+                for (const auto &it : vec) {
+                    flopTypes.back().params.required.insert(std::make_pair(RTLIL::escape_id(it.first), RTLIL::Const(it.second)));
+                }
+            }
+            // Parameters that has to match for a flip-flop
+            else if (fields[0] == "match") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || tok.back() != "ff") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                for (size_t i = 1; i < fields.size(); ++i) {
+                    flopTypes.back().params.matching.push_back(RTLIL::escape_id(fields[i]));
+                }
+            }
+            // Parameters to set
+            else if (fields[0] == "set") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || (tok.back() != "port" && tok.back() != "ff")) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                const auto vec = parseNameValue(fields);
+                dict<RTLIL::IdString, RTLIL::Const> set;
+                for (const auto &it : vec) {
+                    set.insert(std::make_pair(RTLIL::escape_id(it.first), RTLIL::Const(it.second)));
+                }
+
+                if (tok.back() == "port") {
+                    registerType.params.set.swap(set);
+                } else if (tok.back() == "ff") {
+                    flopTypes.back().params.set.swap(set);
+                }
+            }
+            // Parameters to copy / map
+            else if (fields[0] == "map") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || (tok.back() != "port" && tok.back() != "ff")) {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                const auto vec = parseNameValue(fields);
+                dict<RTLIL::IdString, RTLIL::IdString> map;
+                for (const auto &it : vec) {
+                    map.insert(std::make_pair(RTLIL::escape_id(it.first), RTLIL::escape_id(it.second)));
+                }
+
+                if (tok.back() == "port") {
+                    registerType.params.map.swap(map);
+                } else if (tok.back() == "ff") {
+                    flopTypes.back().params.map.swap(map);
+                }
+            }
+            // Connections to make
+            else if (fields[0] == "con") {
+                if (fields.size() < 2) {
+                    log_error(" syntax error: '%s'\n", line.c_str());
+                }
+                if (tok.size() == 0 || tok.back() != "port") {
+                    log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+                }
+
+                const auto vec = parseNameValue(fields);
+                for (const auto &it : vec) {
+                    registerType.connect.insert(std::make_pair(RTLIL::escape_id(it.first), RTLIL::Const(it.second)));
+                }
+            }
+
+            else {
+                log_error(" unexpected keyword '%s'\n", fields[0].c_str());
+            }
+        }
+
+        // Convert lists to maps
+        for (const auto &it : dspTypes) {
+            if (m_DspTypes.count(it.name)) {
+                log_error(" duplicated rule for DSP '%s'\n", it.name.c_str());
+            }
+            m_DspTypes.insert(std::make_pair(it.name, it));
+        }
+        for (const auto &it : flopTypes) {
+            if (m_FlopTypes.count(it.name)) {
+                log_error(" duplicated rule for flip-flop '%s'\n", it.name.c_str());
+            }
+            m_FlopTypes.insert(std::make_pair(it.name, it));
+        }
+    }
+
+    void dump_rules()
+    {
+
+        // Dump DSP types
+        log("DSP types:\n");
+        for (const auto &it1 : m_DspTypes) {
+            const auto &dsp = it1.second;
+            log(" %s\n", dsp.name.c_str());
+
+            for (const auto &it2 : dsp.registers) {
+                const auto &reg = it2.first;
+                const auto &ports = it2.second;
+                log(" ports:\n");
+                for (const auto &port : ports) {
+
+                    std::string range;
+                    if (port.bits.first != -1 && port.bits.second != -1) {
+                        range = stringf("[%d:%d]", port.bits.second, port.bits.first);
+                    }
+
+                    log("  %s.%s%s\n", dsp.name.c_str(), port.name.c_str(), range.c_str());
+
+                    for (const auto &it : port.assoc) {
+                        log("   %.3s: %s\n", it.first.c_str(), !it.second.first.empty() ? it.second.first.c_str() : "<none>");
+                    }
+
+                    if (!reg.params.set.empty()) {
+                        log("   set params:\n");
+                        for (const auto &it : reg.params.set) {
+                            log("    %s=%s\n", it.first.c_str(), it.second.decode_string().c_str());
+                        }
+                    }
+                    if (!reg.params.map.empty()) {
+                        log("   map params:\n");
+                        for (const auto &it : reg.params.map) {
+                            log("    %s=%s\n", it.first.c_str(), it.second.c_str());
+                        }
+                    }
+                    if (!reg.connect.empty()) {
+                        log("   connect ports:\n");
+                        for (const auto &it : reg.connect) {
+                            log("    %s.%s=%s\n", dsp.name.c_str(), it.first.c_str(), it.second.as_string().c_str());
+                        }
+                    }
+                }
+            }
+        }
+
+        // Dump flop types
+        log("Flip-flop types:\n");
+        for (const auto &it : m_FlopTypes) {
+            const auto &ff = it.second;
+            log(" %s\n", ff.name.c_str());
+
+            for (const auto &it : ff.ports) {
+                log("  %.3s: %s\n", it.first.c_str(), !it.second.empty() ? it.second.c_str() : "<none>");
+            }
+
+            if (!ff.params.required.empty()) {
+                log("  required params:\n");
+                for (const auto &it : ff.params.required) {
+                    log("   %s=%s\n", it.first.c_str(), it.second.decode_string().c_str());
+                }
+            }
+            if (!ff.params.matching.empty()) {
+                log("  params that must match:\n");
+                for (const auto &it : ff.params.matching) {
+                    log("   %s\n", it.c_str());
+                }
+            }
+            if (!ff.params.set.empty()) {
+                log("  set params:\n");
+                for (const auto &it : ff.params.set) {
+                    log("   %s=%s\n", it.first.c_str(), it.second.decode_string().c_str());
+                }
+            }
+            if (!ff.params.map.empty()) {
+                log("  map params:\n");
+                for (const auto &it : ff.params.map) {
+                    log("   %s=%s\n", it.first.c_str(), it.second.c_str());
+                }
+            }
+        }
+    }
+
+    // ..........................................
+
+    /// Temporary SigBit to SigBit helper map.
+    SigMap m_SigMap;
+    /// Module connection map
+    ConnMap m_ConnMap;
+
+    /// Cells to be removed (per module!)
+    pool<RTLIL::Cell *> m_CellsToRemove;
+    /// DSP cells that got changed
+    dict<RTLIL::Cell *, DspChanges> m_DspChanges;
+
+    /// DSP types
+    dict<RTLIL::IdString, DspType> m_DspTypes;
+    /// Flip-flop types
+    dict<RTLIL::IdString, FlopType> m_FlopTypes;
+
+    // ..........................................
+
+    DspFF() : Pass("dsp_ff", "Integrates flip-flop into DSP blocks") {}
+
+    void help() override
+    {
+        log("\n");
+        log("    dsp_ff -rules <rules.txt> [selection]\n");
+        log("\n");
+        log("Integrates flip-flops with DSP blocks and enables their internal registers.\n");
+        log("\n");
+        log("The pass loads a set of rules from the file given with the '-rules' parameter.\n");
+        log("The rules define what ports of a DSP module have internal registers and what\n");
+        log("has to be done to enable them. They also define compatible flip-flop cell\n");
+        log("types.\n");
+        log("\n");
+        log("The format of the rules file is the following:\n");
+        log("\n");
+        log("  # This is a comment\n");
+        log("\n");
+        log("  dsp <dsp_type> [<dsp_type> ...]\n");
+        log("    port <dsp_port> [<dsp_port> ...]\n");
+        log("      clk <associated clk> <default>\n");
+        log("     [rst <associated reset>] <default>\n");
+        log("     [ena <associated enable>] <default>\n");
+        log("\n");
+        log("     [set <param>=<value> [<param>=<value> ...]]\n");
+        log("     [map <dsp_param>=<ff_param> [<dsp_param>=<ff_param> ...]]\n");
+        log("     [con <port>=<const> [<port>=<const> ...]]\n");
+        log("    endport\n");
+        log("  enddsp\n");
+        log("\n");
+        log("  ff <ff_type>\n");
+        log("    clk <clock input>\n");
+        log("   [rst <reset input>]\n");
+        log("   [ena <enable input>]\n");
+        log("    d   <data input>\n");
+        log("    q   <data output>\n");
+        log("\n");
+        log("    require <param>=<value> [<param>=<value> ...]\n");
+        log("    match   <param> [<param> ...]\n");
+        log("\n");
+        log("    set <param>=<value> [<param>=<value> ...]\n");
+        log("    map <dsp_param>=<ff_param> [<dsp_param>=<ff_param> ...]\n");
+        log("  endff\n");
+        log("\n");
+        log("Each 'dsp' section defines a DSP cell type (can apply to multiple types).\n");
+        log("Within it each 'port' section defines a data port with internal register.\n");
+        log("There can be multiple port names given if they belong to the same control register.\n");
+        log("The port can be specified as a whole (eg. 'DATA') or as a subset of the whole\n");
+        log("(eg. 'DATA[7:0]').\n");
+        log("\n");
+        log("Statemenst 'clk', 'rst' and 'ena' define names of clock, reset and enable\n");
+        log("ports associated with the data port along with default constant values to\n");
+        log("connect them to when a given port has no counterpart in the flip-flop being\n");
+        log("integrated.\n");
+        log("\n");
+        log("The 'set' statement tells how to set control parameter(s) of the DSP that\n");
+        log("enable the input register on the port. The 'map' statement defines how to\n");
+        log("map parameter(s) of the flip-flip being integrated to the DSP. Finally the\n");
+        log("'con' statement informs how to connected control port(s) of the DSP to enable\n");
+        log("the register.\n");
+        log("\n");
+        log("Each 'ff' section defines a flip-flop type that can be integrated into a DSP\n");
+        log("cell. Inside this section 'clk', 'rst', 'ena', 'd' and 'q' define names of\n");
+        log("clock, reset, enable, data in and data out ports of the flip-flop respectively.\n");
+        log("\n");
+        log("The 'require' statement defines parameter(s) that must have specific value\n");
+        log("for a flip-flop to be considered for integration. The 'match' statement\n");
+        log("lists names of flip-flop parameters that must match on all flip-flops connected\n");
+        log("to a single DSP data port.\n");
+        log("\n");
+        log("The 'set' and 'map' statements serve the same function as in the DSP port\n");
+        log("section but here they may differ depending on the flip-flop type being\n");
+        log("integrated.\n");
+    }
+
+    void execute(std::vector<std::string> a_Args, RTLIL::Design *a_Design) override
+    {
+        log_header(a_Design, "Executing DSP_FF pass.\n");
+
+        std::string rulesFile;
+
+        // Parse args
+        size_t argidx;
+        for (argidx = 1; argidx < a_Args.size(); argidx++) {
+            if (a_Args[argidx] == "-rules" && (argidx + 1) < a_Args.size()) {
+                rulesFile = a_Args[++argidx];
+                continue;
+            }
+
+            break;
+        }
+        extra_args(a_Args, argidx, a_Design);
+
+        // Check args
+        if (rulesFile.empty()) {
+            log_cmd_error("No rules file specified!");
+        }
+
+        // Reset state
+        m_CellsToRemove.clear();
+        m_DspChanges.clear();
+        m_DspTypes.clear();
+        m_FlopTypes.clear();
+
+        // Load rules
+        rewrite_filename(rulesFile);
+        load_rules(rulesFile);
+        if (log_force_debug) {
+            dump_rules();
+        }
+
+        // Process modules
+        for (auto module : a_Design->selected_modules()) {
+
+            // Setup the SigMap
+            m_SigMap.clear();
+            m_SigMap.set(module);
+
+            // Build the connection map
+            m_ConnMap.clear();
+            m_ConnMap.build(module, m_SigMap);
+
+            // Look for DSP cells
+            for (auto cell : module->cells()) {
+
+                // Not a DSP
+                if (!m_DspTypes.count(cell->type)) {
+                    continue;
+                }
+
+                // Process all registers
+                auto &dspType = m_DspTypes.at(cell->type);
+                for (auto &rule : dspType.registers) {
+                    processRegister(cell, rule.first, rule.second);
+                }
+            }
+
+            // Remove cells
+            for (const auto &cell : m_CellsToRemove) {
+                module->remove(cell);
+            }
+            m_CellsToRemove.clear();
+        }
+
+        // Clear maps
+        m_SigMap.clear();
+        m_ConnMap.clear();
+    }
+
+    // ..........................................
+
+    bool checkFlop(RTLIL::Cell *a_Cell)
+    {
+        const auto &flopType = m_FlopTypes.at(a_Cell->type);
+        bool isOk = true;
+
+        log_debug("checking connected flip-flop '%s' of type '%s'... ", a_Cell->name.c_str(), a_Cell->type.c_str());
+
+        // Must not have the "keep" attribute
+        if (a_Cell->has_keep_attr()) {
+            log_debug("\n   the 'keep' attribute is set");
+            isOk = false;
+        }
+
+        // Check if required parameters are set as they should be
+        for (const auto &it : flopType.params.required) {
+            const auto curr = a_Cell->getParam(it.first);
+            if (curr != it.second) {
+                log_debug("\n   param '%s' mismatch ('%s' instead of '%s')", it.first.c_str(), curr.decode_string().c_str(),
+                          it.second.decode_string().c_str());
+                isOk = false;
+            }
+        }
+
+        if (isOk) {
+            log_debug("Ok\n");
+        } else {
+            log_debug("\n");
+        }
+        return isOk;
+    }
+
+    bool checkFlopDataAgainstDspRegister(const FlopData &a_FlopData, RTLIL::Cell *a_Cell, const RegisterType &a_Register,
+                                         const std::vector<PortType> &a_Ports)
+    {
+        const auto &flopType = m_FlopTypes.at(a_FlopData.type);
+        const auto &changes = m_DspChanges[a_Cell];
+        bool isOk = true;
+
+        log_debug("  checking connected flip-flop settings against the DSP register... ");
+
+        // Check control signal connections
+        for (const auto &port : a_Ports) {
+            for (const auto &it : port.assoc) {
+                const auto &key = it.first;
+                const auto &port = it.second.first;
+
+                SigBit conn(RTLIL::Sx);
+                if (!port.empty() && a_Cell->hasPort(port)) {
+                    auto sigspec = a_Cell->getPort(port);
+                    auto sigbits = sigspec.bits();
+                    log_assert(sigbits.size() <= 1);
+                    if (!sigbits.empty()) {
+                        conn = m_SigMap(sigbits[0]);
+                    }
+                }
+
+                if (conn.is_wire() || (!conn.is_wire() && conn.data != RTLIL::Sx)) {
+                    if (conn != a_FlopData.conns.at(key)) {
+                        log_debug("\n   connection to port '%s' mismatch", port.c_str());
+                        isOk = false;
+                    }
+                }
+            }
+        }
+
+        auto checkParam = [&](const RTLIL::IdString &name, const RTLIL::Const &curr, const RTLIL::Const &next) {
+            if (curr != next && changes.params.count(name)) {
+                log_debug("\n   the param '%s' mismatch ('%s' instead of '%s')", name.c_str(), curr.decode_string().c_str(),
+                          next.decode_string().c_str());
+                isOk = false;
+                return false;
+            }
+            return true;
+        };
+
+        // Check parameters to be mapped (by the port rule)
+        for (const auto &it : a_Register.params.map) {
+            if (a_Cell->hasParam(it.first) && a_FlopData.params.dsp.count(it.second)) {
+                const auto curr = a_Cell->getParam(it.first);
+                const auto flop = a_FlopData.params.dsp.at(it.second);
+                checkParam(it.first, curr, flop);
+            }
+        }
+
+        // Check parameters to be set (by the port rule)
+        for (const auto &it : a_Register.params.set) {
+            if (a_Cell->hasParam(it.first)) {
+                const auto curr = a_Cell->getParam(it.first);
+                checkParam(it.first, curr, it.second);
+            }
+        }
+
+        // Check parameters to be mapped (by the flip-flop rule)
+        for (const auto &it : flopType.params.map) {
+            if (a_Cell->hasParam(it.first) && a_FlopData.params.dsp.count(it.second)) {
+                const auto curr = a_Cell->getParam(it.first);
+                const auto flop = a_FlopData.params.dsp.at(it.second);
+                checkParam(it.first, curr, flop);
+            }
+        }
+
+        // Check parameters to be set (by the flip-flop rule)
+        for (const auto &it : flopType.params.set) {
+            if (a_Cell->hasParam(it.first)) {
+                const auto curr = a_Cell->getParam(it.first);
+                checkParam(it.first, curr, it.second);
+            }
+        }
+
+        if (isOk) {
+            log_debug("Ok\n");
+        } else {
+            log_debug("\n");
+        }
+        return isOk;
+    }
+
+    /// Returns a string with either wire name or constant value for a SigBit
+    static std::string sigBitName(const RTLIL::SigBit &a_SigBit)
+    {
+        if (a_SigBit.is_wire()) {
+            RTLIL::Wire *w = a_SigBit.wire;
+            return RTLIL::unescape_id(w->name);
+        } else {
+            switch (a_SigBit.data) {
+            case RTLIL::State::S0:
+                return "1'b0";
+            case RTLIL::State::S1:
+                return "1'b1";
+            case RTLIL::State::Sx:
+                return "1'bx";
+            case RTLIL::State::Sz:
+                return "1'bz";
+            case RTLIL::State::Sa:
+                return "-";
+            case RTLIL::State::Sm:
+                return "m";
+            }
+            return "?";
+        }
+    }
+
+    // ..........................................
+
+    void processRegister(RTLIL::Cell *a_Cell, const RegisterType &a_Register, const std::vector<PortType> &a_Ports)
+    {
+
+        // The cell register control parameter(s) must not be set
+        for (const auto &it : a_Register.params.set) {
+            const auto curr = a_Cell->getParam(it.first);
+            if (curr == it.second) {
+                log_debug(" the param '%s' is already set to '%s'\n", it.first.c_str(), it.second.decode_string().c_str());
+                return;
+            }
+        }
+
+        pool<FlopData> groups;
+        dict<RTLIL::IdString, std::vector<RTLIL::Cell *>> flops;
+
+        // Process ports
+        bool flopsOk = true;
+        for (const auto &port : a_Ports) {
+            log_debug(" attempting flip-flop integration for %s.%s of %s\n", a_Cell->type.c_str(), port.name.c_str(), a_Cell->name.c_str());
+
+            if (!a_Cell->hasPort(port.name)) {
+                log_debug("  port unconnected.\n");
+                continue;
+            }
+            log_assert(a_Cell->output(port.name) || a_Cell->input(port.name));
+
+            // Get port connections
+            auto sigspec = a_Cell->getPort(port.name);
+            auto sigbits = sigspec.bits();
+
+            flops[port.name] = std::vector<RTLIL::Cell *>(sigbits.size(), nullptr);
+            for (size_t i = 0; i < sigbits.size(); ++i) {
+                auto sigbit = m_SigMap(sigbits[i]);
+
+                log_debug("  %2zu. ", i);
+
+                // Port connected to a const.
+                if (!sigbit.wire) {
+                    log_debug("constant\n");
+                    continue;
+                }
+
+                // Skip bits out of the specified range
+                if ((port.bits.first >= 0 && (int)i < port.bits.first) || (port.bits.second >= 0 && (int)i > port.bits.second)) {
+                    log_debug("(excluded)\n");
+                    continue;
+                }
+
+                pool<CellPin> others;
+
+                // Get sinks(s), discard the port completely if more than one sink
+                // is found.
+                if (a_Cell->output(port.name)) {
+                    if (m_ConnMap.sinks.count(sigbit)) {
+                        for (const auto &sink : m_ConnMap.sinks.at(sigbit)) {
+                            if (sink.cell != nullptr && m_CellsToRemove.count(sink.cell)) {
+                                continue;
+                            }
+                            others.insert(sink);
+                        }
+                    }
+
+                }
+                // Get driver. Discard if the driver drives something else too
+                else if (a_Cell->input(port.name)) {
+                    if (m_ConnMap.drivers.count(sigbit)) {
+                        auto driver = m_ConnMap.drivers.at(sigbit);
+
+                        if (m_ConnMap.sinks.count(sigbit)) {
+                            auto sinks = m_ConnMap.sinks.at(sigbit);
+                            if (sinks.size() > 1) {
+                                log_debug("multiple sinks (%zu)\n", others.size());
+                                flopsOk = false;
+                                continue;
+                            }
+                        }
+
+                        others.insert(driver);
+                    }
+                }
+
+                // No others - unconnected
+                if (others.empty()) {
+                    log_debug("unconnected\n");
+                    continue;
+                }
+
+                if (others.size() > 1) {
+                    log_debug("multiple sinks (%zu)\n", others.size());
+                    flopsOk = false;
+                    continue;
+                }
+
+                // Get the sink, check if this is a flip-flop
+                auto &other = *others.begin();
+                auto *flop = other.cell;
+
+                if (flop == nullptr) {
+                    if (!other.port.empty()) {
+                        log_debug("connection reaches module edge\n");
+                        flopsOk = false;
+                    }
+                    log_debug("unconnected\n");
+                    continue;
+                }
+
+                if (!m_FlopTypes.count(flop->type)) {
+                    log_debug("non-flip-flop connected\n");
+                    flopsOk = false;
+                    continue;
+                }
+
+                // Check if the connection goes to the data input/output port
+                const auto &flopType = m_FlopTypes.at(flop->type);
+                RTLIL::IdString flopPort;
+                if (a_Cell->output(port.name)) {
+                    flopPort = flopType.ports.at(RTLIL::escape_id("d"));
+                } else if (a_Cell->input(port.name)) {
+                    flopPort = flopType.ports.at(RTLIL::escape_id("q"));
+                }
+
+                if (flopPort != other.port) {
+                    log_debug("connection to non-data port of a flip-flip");
+                    flopsOk = false;
+                    continue;
+                }
+
+                // Check the flip-flop configuration
+                if (!checkFlop(flop)) {
+                    flopsOk = false;
+                    continue;
+                }
+
+                // Get parameters to be mapped to the DSP according to the port
+                // rule.
+                dict<RTLIL::IdString, RTLIL::Const> mappedParams;
+                for (const auto &it : a_Register.params.map) {
+                    if (flop->hasParam(it.second)) {
+                        const auto &value = flop->getParam(it.second);
+                        mappedParams.insert(std::make_pair(it.first, value));
+                    }
+                }
+
+                // Store the flop and its data
+                groups.insert(getFlopData(flop, mappedParams));
+                flops[port.name][i] = flop;
+            }
+        }
+
+        // Cannot integrate for various reasons
+        if (!flopsOk) {
+            log_debug(" cannot use the DSP register\n");
+            return;
+        }
+
+        // No matching flip-flop groups
+        if (groups.empty()) {
+            log_debug(" no matching flip-flops found\n");
+            return;
+        }
+
+        // Do not allow more than a single group
+        if (groups.size() != 1) {
+            log_debug(" %zu flip-flop groups, only a single one allowed\n", groups.size());
+            return;
+        }
+
+        // Validate the flip flop data agains the DSP cell
+        const auto &flopData = *groups.begin();
+        if (!checkFlopDataAgainstDspRegister(flopData, a_Cell, a_Register, a_Ports)) {
+            log_debug(" flip-flops vs. DSP check failed\n");
+            return;
+        }
+
+        // Log connections
+        for (const auto &port : a_Ports) {
+
+            if (!flops.count(port.name)) {
+                continue;
+            }
+
+            log(" %s %s.%s\n", a_Cell->type.c_str(), a_Cell->name.c_str(), port.name.c_str());
+
+            const auto &conns = flops.at(port.name);
+            for (size_t i = 0; i < conns.size(); ++i) {
+                if (conns[i] != nullptr) {
+                    log_debug("  %2zu. %s %s\n", i, conns[i]->type.c_str(), conns[i]->name.c_str());
+                } else if ((port.bits.first >= 0 && (int)i < port.bits.first) || (port.bits.second >= 0 && (int)i > port.bits.second)) {
+                    log_debug("  %2zu. (excluded)\n", i);
+                } else {
+                    log_debug("  %2zu. None\n", i);
+                }
+            }
+        }
+
+        // Reconnect data signals, mark the flip-flop for removal
+        const auto &flopType = m_FlopTypes.at(flopData.type);
+        for (const auto &port : a_Ports) {
+
+            if (!flops.count(port.name)) {
+                continue;
+            }
+
+            const auto &conns = flops.at(port.name);
+            auto sigspec = a_Cell->getPort(port.name);
+            auto sigbits = sigspec.bits();
+
+            for (size_t i = 0; i < conns.size(); ++i) {
+
+                auto *flop = conns[i];
+                if (flop == nullptr) {
+                    continue;
+                }
+
+                RTLIL::IdString flopPort;
+                if (a_Cell->output(port.name)) {
+                    flopPort = flopType.ports.at(RTLIL::escape_id("q"));
+                } else if (a_Cell->input(port.name)) {
+                    flopPort = flopType.ports.at(RTLIL::escape_id("d"));
+                }
+
+                if (!flop->hasPort(flopPort)) {
+                    log_error("cell '%s' does not have port '%s'!\n", flop->type.c_str(), flopPort.c_str());
+                }
+
+                sigbits[i] = SigBit(RTLIL::Sx);
+                auto sigspec = flop->getPort(flopPort);
+                log_assert(sigspec.bits().size() <= 1);
+                if (sigspec.bits().size() == 1) {
+                    sigbits[i] = sigspec.bits()[0];
+                }
+
+                m_CellsToRemove.insert(flop);
+            }
+
+            a_Cell->setPort(port.name, RTLIL::SigSpec(sigbits));
+        }
+
+        // Reconnect (map) control signals. Connect the default value if
+        // a particular signal is not present in the flip-flop.
+        for (const auto &port : a_Ports) {
+            for (const auto &it : port.assoc) {
+                const auto &key = it.first;
+                const auto &port = it.second.first;
+
+                auto conn = RTLIL::SigBit(RTLIL::SigChunk(it.second.second));
+                if (flopData.conns.count(key)) {
+                    conn = flopData.conns.at(key);
+                }
+
+                log_debug(" connecting %s.%s to %s\n", a_Cell->type.c_str(), port.c_str(), sigBitName(conn).c_str());
+                a_Cell->setPort(port, conn);
+                m_DspChanges[a_Cell].conns.insert(port);
+            }
+        }
+
+        // Connect control signals according to the register rule
+        for (const auto &it : a_Register.connect) {
+            log_debug(" connecting %s.%s to %s\n", a_Cell->type.c_str(), it.first.c_str(), it.second.as_string().c_str());
+            a_Cell->setPort(it.first, it.second);
+            m_DspChanges[a_Cell].conns.insert(it.first);
+        }
+
+        // Map parameters (register rule)
+        for (const auto &it : a_Register.params.map) {
+            if (flopData.params.dsp.count(it.second)) {
+                const auto &param = flopData.params.dsp.at(it.second);
+                log_debug(" setting param '%s' to '%s'\n", it.first.c_str(), param.decode_string().c_str());
+                a_Cell->setParam(it.first, param);
+                m_DspChanges[a_Cell].params.insert(it.first);
+            }
+        }
+
+        // Map parameters (flip-flop rule)
+        for (const auto &it : flopType.params.map) {
+            if (flopData.params.dsp.count(it.second)) {
+                const auto &param = flopData.params.dsp.at(it.second);
+                log_debug(" setting param '%s' to '%s'\n", it.first.c_str(), param.decode_string().c_str());
+                a_Cell->setParam(it.first, param);
+                m_DspChanges[a_Cell].params.insert(it.first);
+            }
+        }
+
+        // Set parameters (port rule)
+        for (const auto &it : a_Register.params.set) {
+            log_debug(" setting param '%s' to '%s'\n", it.first.c_str(), it.second.decode_string().c_str());
+            a_Cell->setParam(it.first, it.second);
+            m_DspChanges[a_Cell].params.insert(it.first);
+        }
+
+        // Set parameters (flip-flop rule)
+        for (const auto &it : flopType.params.set) {
+            log_debug(" setting param '%s' to '%s'\n", it.first.c_str(), it.second.decode_string().c_str());
+            a_Cell->setParam(it.first, it.second);
+            m_DspChanges[a_Cell].params.insert(it.first);
+        }
+    }
+
+    // ..........................................
+
+    /// Collects flip-flop connectivity data and parameters which defines the
+    /// group it belongs to.
+    FlopData getFlopData(RTLIL::Cell *a_Cell, const dict<RTLIL::IdString, RTLIL::Const> &a_ExtraParams)
+    {
+        FlopData data(a_Cell->type);
+
+        log_assert(m_FlopTypes.count(a_Cell->type) != 0);
+        const auto &flopType = m_FlopTypes.at(a_Cell->type);
+
+        // Gather connections to control ports
+        for (const auto &it : flopType.ports) {
+
+            // Skip "D" and "Q" as they connection will always differ.
+            if (it.first == RTLIL::escape_id("d") || it.first == RTLIL::escape_id("q")) {
+                continue;
+            }
+
+            if (!it.second.empty() && a_Cell->hasPort(it.second)) {
+                auto sigspec = a_Cell->getPort(it.second);
+                auto sigbits = sigspec.bits();
+                log_assert(sigbits.size() <= 1);
+                if (!sigbits.empty()) {
+                    data.conns[it.first] = m_SigMap(sigbits[0]);
+                }
+            }
+        }
+
+        // Gather flip-flop parameters that need to match
+        for (const auto &it : flopType.params.matching) {
+            log_assert(a_Cell->hasParam(it));
+            data.params.flop.insert(std::make_pair(it, a_Cell->getParam(it)));
+        }
+
+        // Gather flip-flop parameters to be mapped to the DSP as well
+        for (const auto &it : flopType.params.map) {
+            log_assert(a_Cell->hasParam(it.second));
+            data.params.flop.insert(std::make_pair(it.second, a_Cell->getParam(it.second)));
+        }
+
+        // Gather DSP parameters and their values to be set to too
+        for (const auto &it : flopType.params.set) {
+            data.params.dsp.insert(it);
+        }
+
+        // Append extra DSP parameters
+        for (const auto &it : a_ExtraParams) {
+            data.params.dsp.insert(it);
+        }
+
+        return data;
+    }
+
+} DspFF;
+
+PRIVATE_NAMESPACE_END
diff --git a/dsp-ff-plugin/nexus-dsp_rules.txt b/dsp-ff-plugin/nexus-dsp_rules.txt
new file mode 100644
index 0000000..0ed8947
--- /dev/null
+++ b/dsp-ff-plugin/nexus-dsp_rules.txt
@@ -0,0 +1,175 @@
+# Copyright (C) 2020-2022  The SymbiFlow Authors.
+#
+# Use of this source code is governed by a ISC-style
+# license that can be found in the LICENSE file or at
+# https://opensource.org/licenses/ISC
+#
+# SPDX-License-Identifier:ISC
+
+dsp MULT9X9 MULT18X18 MULT18X36 MULT36X36
+  port A SIGNEDA
+    clk CLK 0
+    rst RSTA 0
+    ena CEA 1
+
+    set REGINPUTA=REGISTER
+    map GSR=GSR
+  endport
+  port B SIGNEDB
+    clk CLK 0
+    rst RSTB 0
+    ena CEB 1
+
+    set REGINPUTB=REGISTER
+    map GSR=GSR
+  endport
+  port Z
+    clk CLK 0
+    rst RSTOUT 0
+    ena CEOUT 1
+
+    set REGOUTPUT=REGISTER
+    map GSR=GSR
+  endport
+enddsp
+
+dsp MULTPREADD9X9 MULTPREADD18X18 MULTADDSUB18X18 MULTADDSUB36X36
+  port A SIGNEDA
+    clk CLK 0
+    rst RSTA 0
+    ena CEA 1
+
+    set REGINPUTA=REGISTER
+    map GSR=GSR
+  endport
+  port B SIGNEDB
+    clk CLK 0
+    rst RSTB 0
+    ena CEB 1
+
+    set REGINPUTB=REGISTER
+    map GSR=GSR
+  endport
+  port C SIGNEDC
+    clk CLK 0
+    rst RSTC 0
+    ena CEC 1
+
+    set REGINPUTC=REGISTER
+    map GSR=GSR
+  endport
+  port Z
+    clk CLK 0
+    rst RSTOUT 0
+    ena CEOUT 1
+
+    set REGOUTPUT=REGISTER
+    map GSR=GSR
+  endport
+enddsp
+
+dsp MULTADDSUB9X9WIDE
+  port A0 SIGNED
+    clk CLK 0
+    rst RSTA0A1 0
+    ena CEA0A1 1
+
+    set REGINPUTAB0=REGISTER
+    map GSR=GSR
+  endport
+  port A1 SIGNED
+    clk CLK 0
+    rst RSTA0A1 0
+    ena CEA0A1 1
+
+    set REGINPUTAB1=REGISTER
+    map GSR=GSR
+  endport
+  port A2 SIGNED
+    clk CLK 0
+    rst RSTA2A3 0
+    ena CEA2A3 1
+
+    set REGINPUTAB2=REGISTER
+    map GSR=GSR
+  endport
+  port A3 SIGNED
+    clk CLK 0
+    rst RSTA2A3 0
+    ena CEA2A3 1
+
+    set REGINPUTAB3=REGISTER
+    map GSR=GSR
+  endport
+  port B0 SIGNED
+    clk CLK 0
+    rst RSTB0B1 0
+    ena CEB0B1 1
+
+    set REGINPUTAB0=REGISTER
+    map GSR=GSR
+  endport
+  port B1 SIGNED
+    clk CLK 0
+    rst RSTB0B1 0
+    ena CEB0B1 1
+
+    set REGINPUTAB1=REGISTER
+    map GSR=GSR
+  endport
+  port B2 SIGNED
+    clk CLK 0
+    rst RSTB2B3 0
+    ena CEB2B3 1
+
+    set REGINPUTAB2=REGISTER
+    map GSR=GSR
+  endport
+  port B3 SIGNED
+    clk CLK 0
+    rst RSTB2B3 0
+    ena CEB2B3 1
+
+    set REGINPUTAB3=REGISTER
+    map GSR=GSR
+  endport
+  port C SIGNED
+    clk CLK 0
+    rst RSTC 0
+    ena CEC 1
+
+    set REGINPUTC=REGISTER
+    map GSR=GSR
+  endport
+  port Z
+    clk CLK 0
+    rst RSTOUT 0
+    ena CEOUT 1
+
+    set REGOUTPUT=REGISTER
+    map GSR=GSR
+  endport
+enddsp
+
+ff FD1P3DX
+  clk CK
+  rst CD
+  ena SP
+  d   D
+  q   Q
+
+  match GSR
+  set RESETMODE=ASYNC
+endff
+
+ff FD1P3IX
+  clk CK
+  rst CD
+  ena SP
+  d   D
+  q   Q
+
+  match GSR
+  set RESETMODE=SYNC
+endff
+
diff --git a/dsp-ff-plugin/tests/Makefile b/dsp-ff-plugin/tests/Makefile
new file mode 100644
index 0000000..305092f
--- /dev/null
+++ b/dsp-ff-plugin/tests/Makefile
@@ -0,0 +1,24 @@
+# Copyright (C) 2020-2022  The SymbiFlow Authors.
+#
+# Use of this source code is governed by a ISC-style
+# license that can be found in the LICENSE file or at
+# https://opensource.org/licenses/ISC
+#
+# SPDX-License-Identifier:ISC
+
+TESTS = \
+    nexus_mult \
+    nexus_mult_wide \
+    nexus_fftypes \
+    nexus_conn_conflict \
+    nexus_conn_share \
+    nexus_param_conflict
+
+include $(shell pwd)/../../Makefile_test.common
+
+nexus_mult_verify = true
+nexus_mult_wide_verify = true
+nexus_fftypes_verify = true
+nexus_conn_conflict_verify = true
+nexus_conn_share_verify = true
+nexus_param_conflict_verify = true
diff --git a/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.tcl b/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.tcl
new file mode 100644
index 0000000..87486f9
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.tcl
@@ -0,0 +1,58 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "conflict_dsp_clk"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 9 t:FD1P3IX
+
+set TOP "conflict_ff_clk"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3IX
+
+set TOP "conflict_ff_rst"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3DX
+
+set TOP "conflict_ff_ena"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3IX
+
+set TOP "conflict_dsp_port"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 9 t:FD1P3IX
diff --git a/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.v b/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.v
new file mode 100644
index 0000000..35856d9
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_conn_conflict/nexus_conn_conflict.v
@@ -0,0 +1,151 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module conflict_dsp_clk (
+    input  wire        CLK_A,
+    input  wire        CLK_B,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK_A)
+        ra <= A;
+
+    reg [8:0] rb;
+    always @(posedge CLK_B)
+        rb <= B;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (rb),
+        .Z (Z)
+    );
+
+endmodule
+
+module conflict_ff_clk (
+    input  wire        CLK1,
+    input  wire        CLK2,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [17:0] z;
+
+    always @(posedge CLK1)
+        Z[17:9] <= z[17:9];
+    always @(posedge CLK2)
+        Z[ 8:0] <= z[ 8:0];
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+endmodule
+
+module conflict_ff_rst (
+    input  wire        CLK,
+    input  wire        RST1,
+    input  wire        RST2,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [17:0] z;
+
+    always @(posedge CLK or posedge RST1)
+        if (RST1)
+            Z[17:9] <= 0;
+        else
+            Z[17:9] <= z[17:9];
+    always @(posedge CLK or posedge RST2)
+        if (RST2)
+            Z[ 8:0] <= 0;
+        else
+            Z[ 8:0] <= z[ 8:0];
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+endmodule
+
+module conflict_ff_ena (
+    input  wire        CLK,
+    input  wire        ENA1,
+    input  wire        ENA2,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [17:0] z;
+
+    always @(posedge CLK)
+        if (ENA1)
+            Z[17:9] <= z[17:9];
+    always @(posedge CLK)
+        if (ENA2)
+            Z[ 8:0] <= z[ 8:0];
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+endmodule
+
+module conflict_dsp_port (
+    input  wire        CLK_A,
+    input  wire [ 8:0] A,
+    input  wire        SA,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK_A)
+        ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A       (ra),
+        .SIGNEDA (SA),
+        .B       (B),
+        .Z       (Z)
+    );
+
+endmodule
diff --git a/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.tcl b/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.tcl
new file mode 100644
index 0000000..2f13e67
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.tcl
@@ -0,0 +1,48 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "conflict_out_fanout"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3IX
+
+set TOP "conflict_out_fanout_to_top"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3IX
+
+set TOP "conflict_inp_fanout"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 9 t:FD1P3IX
+
+set TOP "conflict_inp_fanout_to_top"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 9 t:FD1P3IX
+
+
diff --git a/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.v b/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.v
new file mode 100644
index 0000000..88be6b0
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_conn_share/nexus_conn_share.v
@@ -0,0 +1,112 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module conflict_out_fanout (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z,
+    output wire [ 8:0] X,
+);
+
+    wire [17:0] z;
+    always @(posedge CLK)
+        Z <= z;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+    assign X = ~z[8:0];
+
+endmodule
+
+module conflict_out_fanout_to_top (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z,
+    output wire [ 8:0] X,
+);
+
+    wire [17:0] z;
+    always @(posedge CLK)
+        Z <= z;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+    assign X = z[8:0];
+
+endmodule
+
+module conflict_inp_fanout (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z,
+    output wire [ 3:0] X,
+);
+
+    wire [8:0] ra;
+    always @(posedge CLK)
+        ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+    assign X = ~ra[3:0];
+
+endmodule
+
+module conflict_inp_fanout_to_top (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z,
+    output wire [ 3:0] X,
+);
+
+    wire [8:0] ra;
+    always @(posedge CLK)
+        ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+    assign X = ra[3:0];
+
+endmodule
+
diff --git a/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.tcl b/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.tcl
new file mode 100644
index 0000000..fa006c8
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.tcl
@@ -0,0 +1,71 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "mult_ena"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+select -assert-count 0 t:FD1P3DX
+
+set TOP "mult_arst"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+select -assert-count 0 t:FD1P3DX
+
+set TOP "mult_arst_ena"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+select -assert-count 0 t:FD1P3DX
+
+set TOP "mult_srst"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+select -assert-count 0 t:FD1P3DX
+
+set TOP "mult_srst_ena"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+select -assert-count 0 t:FD1P3DX
diff --git a/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.v b/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.v
new file mode 100644
index 0000000..2c5b8ec
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_fftypes/nexus_fftypes.v
@@ -0,0 +1,134 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module mult_ena (
+    input  wire        CLK,
+    input  wire        ENA,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK)
+        if (ENA) ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
+module mult_arst (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK or posedge RST)
+        if (RST) ra <= 0;
+        else     ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
+module mult_arst_ena (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire        ENA,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK or posedge RST)
+        if (RST)      ra <= 0;
+        else if (ENA) ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
+module mult_srst (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK)
+        if (RST) ra <= 0;
+        else     ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
+module mult_srst_ena (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire        ENA,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK)
+        if (RST)      ra <= 0;
+        else if (ENA) ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
diff --git a/dsp-ff-plugin/tests/nexus_mult/README.md b/dsp-ff-plugin/tests/nexus_mult/README.md
new file mode 100644
index 0000000..f72e27c
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_mult/README.md
@@ -0,0 +1 @@
+Simple DSP register inference
diff --git a/dsp-ff-plugin/tests/nexus_mult/nexus_mult.tcl b/dsp-ff-plugin/tests/nexus_mult/nexus_mult.tcl
new file mode 100644
index 0000000..1eef04a
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_mult/nexus_mult.tcl
@@ -0,0 +1,58 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "mult_ireg"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+
+set TOP "mult_oreg"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+
+set TOP "mult_all"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 0 t:FD1P3IX
+
+# The test cannot be run because the equivalence check fails at some internal
+# wires of the DSP simulation model which ends up dangling. So that's not a
+# real issue but makes the test fail.
+
+#set TOP "mult_ctrl"
+#design -load read
+#hierarchy -top ${TOP}
+#synth_nexus -flatten
+#techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+#equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+#design -load postopt
+#yosys cd ${TOP}
+#stat
+#select -assert-count 1 t:MULT9X9
+#select -assert-count 0 t:FD1P3IX
diff --git a/dsp-ff-plugin/tests/nexus_mult/nexus_mult.v b/dsp-ff-plugin/tests/nexus_mult/nexus_mult.v
new file mode 100644
index 0000000..9836f7f
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_mult/nexus_mult.v
@@ -0,0 +1,114 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module mult_ireg (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK)
+        ra <= A;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+
+module mult_oreg (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z
+);
+
+    reg [17:0] z;
+    always @(posedge CLK)
+        Z <= z;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+endmodule
+
+module mult_all (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z
+);
+
+    reg [8:0] ra;
+    always @(posedge CLK)
+        ra <= A;
+
+    reg [8:0] rb;
+    always @(posedge CLK)
+        rb <= B;
+
+    reg [17:0] z;
+    always @(posedge CLK)
+        Z <= z;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (rb),
+        .Z (z)
+    );
+
+endmodule
+
+module mult_ctrl (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire        SA,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z
+);
+
+    reg [8:0] ra;
+    reg       rsa;
+
+    always @(posedge CLK) begin
+        ra  <= A;
+        rsa <= SA;
+    end
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A          (ra),
+        .SIGNEDA    (rsa),
+        .B          (B),
+        .Z          (Z)
+    );
+
+endmodule
+
diff --git a/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.tcl b/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.tcl
new file mode 100644
index 0000000..70f5a61
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.tcl
@@ -0,0 +1,18 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "mult_wide"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULTADDSUB9X9WIDE
+select -assert-count 9 t:FD1P3IX
diff --git a/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.v b/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.v
new file mode 100644
index 0000000..cb539e5
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_mult_wide/nexus_mult_wide.v
@@ -0,0 +1,63 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module mult_wide (
+    input  wire        CLK,
+    input  wire [ 8:0] A0,
+    input  wire [ 8:0] A1,
+    input  wire [ 8:0] A2,
+    input  wire [ 8:0] A3,
+    input  wire [ 8:0] B0,
+    input  wire [ 8:0] B1,
+    input  wire [ 8:0] B2,
+    input  wire [ 8:0] B3,
+    input  wire [53:0] C,
+    output wire [53:0] Z
+);
+
+    reg [8:0] ra0;
+    always @(posedge CLK)
+        ra0 <= A0;
+
+    reg [8:0] rb0;
+    always @(posedge CLK)
+        rb0 <= B0;
+
+    reg [8:0] rb2;
+    always @(posedge CLK)
+        rb2 <= B2;
+
+    MULTADDSUB9X9WIDE # (
+        .REGINPUTAB0("BYPASS"),
+        .REGINPUTAB1("BYPASS"),
+        .REGINPUTAB2("BYPASS"),
+        .REGINPUTAB3("BYPASS"),
+        .REGINPUTC("BYPASS"),
+        .REGADDSUB("BYPASS"),
+        .REGLOADC("BYPASS"),
+        .REGLOADC2("BYPASS"),
+        .REGPIPELINE("BYPASS"),
+        .REGOUTPUT("REGISTER")
+    ) mult (
+        .A0 (ra0),
+        .A1 (A1),
+        .A2 (A2),
+        .A3 (A3),
+        .B0 (rb0),
+        .B1 (B1),
+        .B2 (rb2),
+        .B3 (B3),
+        .C  (C),
+        .Z  (Z),
+
+        .LOADC      (1'b0),
+        .ADDSUB     (4'hF),
+        .SIGNED     (1'b1),
+    );
+
+endmodule
diff --git a/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.tcl b/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.tcl
new file mode 100644
index 0000000..ff46e0b
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.tcl
@@ -0,0 +1,40 @@
+yosys -import
+if { [info procs dsp_ff] == {} } { plugin -i dsp-ff }
+yosys -import  ;# ingest plugin commands
+
+read_verilog $::env(DESIGN_TOP).v
+design -save read
+
+set TOP "conflict_dsp_ctrl_param"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 18 t:FD1P3IX
+
+set TOP "conflict_dsp_common_param"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+equiv_opt -assert -async2sync -map +/nexus/cells_sim.v debug dsp_ff -rules ../../nexus-dsp_rules.txt
+design -load postopt
+yosys cd ${TOP}
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 9 t:FD1P3IX t:FD1P3DX %u
+
+set TOP "conflict_ff_param"
+design -load read
+hierarchy -top ${TOP}
+synth_nexus -flatten
+techmap -map +/nexus/cells_sim.v t:VLO t:VHI %u ;# Unmap VHI and VLO
+debug dsp_ff -rules ../../nexus-dsp_rules.txt
+stat
+select -assert-count 1 t:MULT9X9
+select -assert-count 4 t:FD1P3IX
+select -assert-count 5 t:FD1P3DX
+
diff --git a/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.v b/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.v
new file mode 100644
index 0000000..d9ec0e8
--- /dev/null
+++ b/dsp-ff-plugin/tests/nexus_param_conflict/nexus_param_conflict.v
@@ -0,0 +1,90 @@
+// Copyright (C) 2020-2021  The SymbiFlow Authors.
+//
+// Use of this source code is governed by a ISC-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/ISC
+//
+// SPDX-License-Identifier:ISC
+
+module conflict_dsp_ctrl_param (
+    input  wire        CLK,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output reg  [17:0] Z,
+);
+
+    wire [17:0] z;
+    always @(posedge CLK)
+        Z <= z;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("REGISTER")
+    ) mult (
+        .A (A),
+        .B (B),
+        .Z (z)
+    );
+
+endmodule
+
+module conflict_dsp_common_param (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z,
+);
+
+    wire [8:0] ra;
+    always @(posedge CLK or posedge RST)
+        if (RST) ra <= 0;
+        else     ra <= A;
+
+    wire [8:0] rb;
+    always @(posedge CLK)
+        if (RST) rb <= 0;
+        else     rb <= B;
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (rb),
+        .Z (Z)
+    );
+
+endmodule
+
+module conflict_ff_param (
+    input  wire        CLK,
+    input  wire        RST,
+    input  wire [ 8:0] A,
+    input  wire [ 8:0] B,
+    output wire [17:0] Z,
+);
+
+    wire [8:0] ra;
+    always @(posedge CLK or posedge RST)
+        if (RST) ra[8:4] <= 0;
+        else     ra[8:4] <= A[8:4];
+
+    always @(posedge CLK)
+        if (RST) ra[3:0] <= 0;
+        else     ra[3:0] <= A[3:0];
+
+    MULT9X9 # (
+        .REGINPUTA("BYPASS"),
+        .REGINPUTB("BYPASS"),
+        .REGOUTPUT("BYPASS")
+    ) mult (
+        .A (ra),
+        .B (B),
+        .Z (Z)
+    );
+
+endmodule
+