plat, vendor: systematically escape net and file names in Tcl.
[nmigen.git] / nmigen / vendor / lattice_ecp5.py
index 62a6ed57b154039c0b8228a63d85069a1ed57934..cfa20eea22b88a0e969ad6f168c0b501592402be 100644 (file)
@@ -9,20 +9,27 @@ __all__ = ["LatticeECP5Platform"]
 
 class LatticeECP5Platform(TemplatedPlatform):
     """
+    Trellis toolchain
+    -----------------
+
     Required tools:
         * ``yosys``
         * ``nextpnr-ecp5``
         * ``ecppack``
 
+    The environment is populated by running the script specified in the environment variable
+    ``NMIGEN_ENV_Trellis``, if present.
+
     Available overrides:
         * ``verbose``: enables logging of informational messages to standard error.
         * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
         * ``synth_opts``: adds options for ``synth_ecp5`` Yosys command.
         * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
         * ``script_after_synth``: inserts commands after ``synth_ecp5`` in Yosys script.
-        * ``yosys_opts``: adds extra options for Yosys.
-        * ``nextpnr_opts``: adds extra options for nextpnr.
-        * ``ecppack_opts``: adds extra options for ecppack.
+        * ``yosys_opts``: adds extra options for ``yosys``.
+        * ``nextpnr_opts``: adds extra options for ``nextpnr-ecp5``.
+        * ``ecppack_opts``: adds extra options for ``ecppack``.
+        * ``add_preferences``: inserts commands at the end of the LPF file.
 
     Build products:
         * ``{{name}}.rpt``: Yosys log.
@@ -31,22 +38,46 @@ class LatticeECP5Platform(TemplatedPlatform):
         * ``{{name}}.config``: ASCII bitstream.
         * ``{{name}}.bit``: binary bitstream.
         * ``{{name}}.svf``: JTAG programming vector.
+
+    Diamond toolchain
+    -----------------
+
+    Required tools:
+        * ``pnmainc``
+        * ``ddtcmd``
+
+    The environment is populated by running the script specified in the environment variable
+    ``NMIGEN_ENV_Diamond``, if present.
+
+    Available overrides:
+        * ``script_project``: inserts commands before ``prj_project save`` in Tcl script.
+        * ``script_after_export``: inserts commands after ``prj_run Export`` in Tcl script.
+        * ``add_preferences``: inserts commands at the end of the LPF file.
+        * ``add_constraints``: inserts commands at the end of the XDC file.
+
+    Build products:
+        * ``{{name}}_impl/{{name}}_impl.htm``: consolidated log.
+        * ``{{name}}.bit``: binary bitstream.
+        * ``{{name}}.svf``: JTAG programming vector.
     """
 
+    toolchain = None # selected when creating platform
+
     device  = abstractproperty()
     package = abstractproperty()
     speed   = abstractproperty()
+    grade   = "C" # [C]ommercial, [I]ndustrial
+
+    # Trellis templates
 
     _nextpnr_device_options = {
-        "LFE5U-12F":    "--25k",
+        "LFE5U-12F":    "--12k",
         "LFE5U-25F":    "--25k",
         "LFE5U-45F":    "--45k",
         "LFE5U-85F":    "--85k",
-        "LFE5UM-12F":   "--um-25k",
         "LFE5UM-25F":   "--um-25k",
         "LFE5UM-45F":   "--um-45k",
         "LFE5UM-85F":   "--um-85k",
-        "LFE5UM5G-12F": "--um5g-25k",
         "LFE5UM5G-25F": "--um5g-25k",
         "LFE5UM5G-45F": "--um5g-45k",
         "LFE5UM5G-85F": "--um5g-85k",
@@ -59,23 +90,35 @@ class LatticeECP5Platform(TemplatedPlatform):
         "BG756": "caBGA756",
     }
 
-    file_templates = {
+    _trellis_required_tools = [
+        "yosys",
+        "nextpnr-ecp5",
+        "ecppack"
+    ]
+    _trellis_file_templates = {
         **TemplatedPlatform.build_script_templates,
         "{{name}}.il": r"""
             # {{autogenerated}}
-            {{emit_design("rtlil")}}
+            {{emit_rtlil()}}
+        """,
+        "{{name}}.debug.v": r"""
+            /* {{autogenerated}} */
+            {{emit_debug_verilog()}}
         """,
         "{{name}}.ys": r"""
             # {{autogenerated}}
             {% for file in platform.iter_extra_files(".v") -%}
-                read_verilog {{get_override("read_opts")|join(" ")}} {{file}}
+                read_verilog {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_extra_files(".sv") -%}
-                read_verilog -sv {{get_override("read_opts")|join(" ")}} {{file}}
+                read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
+            {% endfor %}
+            {% for file in platform.iter_extra_files(".il") -%}
+                read_ilang {{file}}
             {% endfor %}
             read_ilang {{name}}.il
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
-            synth_ecp5 {{get_override("synth_opts")|join(" ")}} -top {{name}}
+            synth_ecp5 {{get_override("synth_opts")|options}} -top {{name}}
             {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
             write_json {{name}}.json
         """,
@@ -83,28 +126,35 @@ class LatticeECP5Platform(TemplatedPlatform):
             # {{autogenerated}}
             BLOCK ASYNCPATHS;
             BLOCK RESETPATHS;
-            {% for port_name, pin_name, extras in platform.iter_port_constraints_bits() -%}
+            {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
                 LOCATE COMP "{{port_name}}" SITE "{{pin_name}}";
+                {% if attrs -%}
                 IOBUF PORT "{{port_name}}"
-                    {%- for key, value in extras.items() %} {{key}}={{value}}{% endfor %};
+                    {%- for key, value in attrs.items() %} {{key}}={{value}}{% endfor %};
+                {% endif %}
             {% endfor %}
-            {% for signal, frequency in platform.iter_clock_constraints() -%}
-                FREQUENCY PORT "{{signal.name}}" {{frequency}} HZ;
+            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
+                {% if port_signal is not none -%}
+                    FREQUENCY PORT "{{port_signal.name}}" {{frequency}} HZ;
+                {% else -%}
+                    FREQUENCY NET "{{net_signal|hierarchy(".")}}" {{frequency}} HZ;
+                {% endif %}
             {% endfor %}
+            {{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
         """
     }
-    command_templates = [
+    _trellis_command_templates = [
         r"""
-        {{get_tool("yosys")}}
+        {{invoke_tool("yosys")}}
             {{quiet("-q")}}
-            {{get_override("yosys_opts")|join(" ")}}
+            {{get_override("yosys_opts")|options}}
             -l {{name}}.rpt
             {{name}}.ys
         """,
         r"""
-        {{get_tool("nextpnr-ecp5")}}
+        {{invoke_tool("nextpnr-ecp5")}}
             {{quiet("--quiet")}}
-            {{get_override("nextpnr_opts")|join(" ")}}
+            {{get_override("nextpnr_opts")|options}}
             --log {{name}}.tim
             {{platform._nextpnr_device_options[platform.device]}}
             --package {{platform._nextpnr_package_options[platform.package]|upper}}
@@ -114,14 +164,185 @@ class LatticeECP5Platform(TemplatedPlatform):
             --textcfg {{name}}.config
         """,
         r"""
-        {{get_tool("ecppack")}}
+        {{invoke_tool("ecppack")}}
             {{verbose("--verbose")}}
+            {{get_override("ecppack_opts")|options}}
             --input {{name}}.config
             --bit {{name}}.bit
             --svf {{name}}.svf
         """
     ]
 
+    # Diamond templates
+
+    _diamond_required_tools = [
+        "yosys",
+        "pnmainc",
+        "ddtcmd"
+    ]
+    _diamond_file_templates = {
+        **TemplatedPlatform.build_script_templates,
+        "build_{{name}}.sh": r"""
+            # {{autogenerated}}
+            set -e{{verbose("x")}}
+            if [ -z "$BASH" ] ; then exec /bin/bash "$0" "$@"; fi
+            if [ -n "${{platform._toolchain_env_var}}" ]; then
+                bindir=$(dirname "${{platform._toolchain_env_var}}")
+                . "${{platform._toolchain_env_var}}"
+            fi
+            {{emit_commands("sh")}}
+        """,
+        "{{name}}.v": r"""
+            /* {{autogenerated}} */
+            {{emit_verilog()}}
+        """,
+        "{{name}}.debug.v": r"""
+            /* {{autogenerated}} */
+            {{emit_debug_verilog()}}
+        """,
+        "{{name}}.tcl": r"""
+            prj_project new -name {{name}} -impl impl -impl_dir top_impl \
+                -dev {{platform.device}}-{{platform.speed}}{{platform.package}}{{platform.grade}} \
+                -lpf {{name}}.lpf \
+                -synthesis synplify
+            {% for file in platform.iter_extra_files(".v", ".sv", ".vhd", ".vhdl") -%}
+                prj_src add {{file|tcl_escape}}
+            {% endfor %}
+            prj_src add {{name}}.v
+            prj_impl option top {{name}}
+            prj_src add {{name}}.sdc
+            {{get_override("script_project")|default("# (script_project placeholder)")}}
+            prj_project save
+            prj_run Synthesis -impl impl -forceAll
+            prj_run Translate -impl impl -forceAll
+            prj_run Map -impl impl -forceAll
+            prj_run PAR -impl impl -forceAll
+            prj_run Export -impl impl -forceAll -task Bitgen
+            {{get_override("script_after_export")|default("# (script_after_export placeholder)")}}
+        """,
+        "{{name}}.lpf": r"""
+            # {{autogenerated}}
+            BLOCK ASYNCPATHS;
+            BLOCK RESETPATHS;
+            {% for port_name, pin_name, extras in platform.iter_port_constraints_bits() -%}
+                LOCATE COMP "{{port_name}}" SITE "{{pin_name}}";
+                IOBUF PORT "{{port_name}}"
+                    {%- for key, value in extras.items() %} {{key}}={{value}}{% endfor %};
+            {% endfor %}
+            {{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
+        """,
+        "{{name}}.sdc": r"""
+            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
+                {% if port_signal is not none -%}
+                    create_clock -name {{port_signal.name|tcl_escape}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_escape}}]
+                {% else -%}
+                    create_clock -name {{net_signal.name|tcl_escape}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_escape}}]
+                {% endif %}
+            {% endfor %}
+            {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
+        """,
+    }
+    _diamond_command_templates = [
+        # These don't have any usable command-line option overrides.
+        r"""
+        {{invoke_tool("pnmainc")}}
+            {{name}}.tcl
+        """,
+        r"""
+        {{invoke_tool("ddtcmd")}}
+            -oft -bit
+            -if {{name}}_impl/{{name}}_impl.bit -of {{name}}.bit
+        """,
+        r"""
+        {{invoke_tool("ddtcmd")}}
+            -oft -svfsingle -revd -op "Fast Program"
+            -if {{name}}_impl/{{name}}_impl.bit -of {{name}}.svf
+        """,
+    ]
+
+    # Common logic
+
+    def __init__(self, *, toolchain="Trellis"):
+        super().__init__()
+
+        assert toolchain in ("Trellis", "Diamond")
+        self.toolchain = toolchain
+
+    @property
+    def required_tools(self):
+        if self.toolchain == "Trellis":
+            return self._trellis_required_tools
+        if self.toolchain == "Diamond":
+            return self._diamond_required_tools
+        assert False
+
+    @property
+    def file_templates(self):
+        if self.toolchain == "Trellis":
+            return self._trellis_file_templates
+        if self.toolchain == "Diamond":
+            return self._diamond_file_templates
+        assert False
+
+    @property
+    def command_templates(self):
+        if self.toolchain == "Trellis":
+            return self._trellis_command_templates
+        if self.toolchain == "Diamond":
+            return self._diamond_command_templates
+        assert False
+
+    @property
+    def default_clk_constraint(self):
+        if self.default_clk == "OSCG":
+            return Clock(310e6 / self.oscg_div)
+        return super().default_clk_constraint
+
+    def create_missing_domain(self, name):
+        # Lattice ECP5 devices have two global set/reset signals: PUR, which is driven at startup
+        # by the configuration logic and unconditionally resets every storage element, and GSR,
+        # which is driven by user logic and each storage element may be configured as affected or
+        # unaffected by GSR. PUR is purely asynchronous, so even though it is a low-skew global
+        # network, its deassertion may violate a setup/hold constraint with relation to a user
+        # clock. To avoid this, a GSR/SGSR instance should be driven synchronized to user clock.
+        if name == "sync" and self.default_clk is not None:
+            m = Module()
+            if self.default_clk == "OSCG":
+                if not hasattr(self, "oscg_div"):
+                    raise ValueError("OSCG divider (oscg_div) must be an integer between 2 "
+                                     "and 128")
+                if not isinstance(self.oscg_div, int) or self.oscg_div < 2 or self.oscg_div > 128:
+                    raise ValueError("OSCG divider (oscg_div) must be an integer between 2 "
+                                     "and 128, not {!r}"
+                                     .format(self.oscg_div))
+                clk_i = Signal()
+                m.submodules += Instance("OSCG", p_DIV=self.oscg_div, o_OSC=clk_i)
+            else:
+                clk_i = self.request(self.default_clk).i
+            if self.default_rst is not None:
+                rst_i = self.request(self.default_rst).i
+            else:
+                rst_i = Const(0)
+
+            gsr0 = Signal()
+            gsr1 = Signal()
+            # There is no end-of-startup signal on ECP5, but PUR is released after IOB enable, so
+            # a simple reset synchronizer (with PUR as the asynchronous reset) does the job.
+            m.submodules += [
+                Instance("FD1S3AX", p_GSR="DISABLED", i_CK=clk_i, i_D=~rst_i, o_Q=gsr0),
+                Instance("FD1S3AX", p_GSR="DISABLED", i_CK=clk_i, i_D=gsr0,   o_Q=gsr1),
+                # Although we already synchronize the reset input to user clock, SGSR has dedicated
+                # clock routing to the center of the FPGA; use that just in case it turns out to be
+                # more reliable. (None of this is documented.)
+                Instance("SGSR", i_CLK=clk_i, i_GSR=gsr1),
+            ]
+            # GSR implicitly connects to every appropriate storage element. As such, the sync
+            # domain is reset-less; domains driven by other clocks would need to have dedicated
+            # reset circuitry or otherwise meet setup/hold constraints on their own.
+            m.domains += ClockDomain("sync", reset_less=True)
+            m.d.comb += ClockSignal("sync").eq(clk_i)
+            return m
+
     _single_ended_io_types = [
         "HSUL12", "LVCMOS12", "LVCMOS15", "LVCMOS18", "LVCMOS25", "LVCMOS33", "LVTTL33",
         "SSTL135_I", "SSTL135_II", "SSTL15_I", "SSTL15_II", "SSTL18_I", "SSTL18_II",
@@ -139,7 +360,7 @@ class LatticeECP5Platform(TemplatedPlatform):
             return True
         return False
 
-    def _get_xdr_buffer(self, m, pin, i_invert=None, o_invert=None):
+    def _get_xdr_buffer(self, m, pin, *, i_invert=False, o_invert=False):
         def get_ireg(clk, d, q):
             for bit in range(len(q)):
                 m.submodules += Instance("IFS1P3DX",
@@ -178,44 +399,34 @@ class LatticeECP5Platform(TemplatedPlatform):
                     o_Q=q[bit]
                 )
 
-        def get_ixor(z, invert):
-            if invert is None:
-                return z
-            else:
-                a = Signal.like(z, name_suffix="_x{}".format(1 if invert else 0))
-                for bit in range(len(z)):
-                    m.submodules += Instance("LUT4",
-                        p_INIT=0x5555 if invert else 0xaaaa,
-                        i_A=a[bit],
-                        o_Z=z[bit]
-                    )
-                return a
-
-        def get_oxor(a, invert):
-            if invert is None:
+        def get_ineg(z, invert):
+            if invert:
+                a = Signal.like(z, name_suffix="_n")
+                m.d.comb += z.eq(~a)
                 return a
             else:
-                z = Signal.like(a, name_suffix="_x{}".format(1 if invert else 0))
-                for bit in range(len(a)):
-                    m.submodules += Instance("LUT4",
-                        p_INIT=0x5555 if invert else 0xaaaa,
-                        i_A=a[bit],
-                        o_Z=z[bit]
-                    )
                 return z
 
+        def get_oneg(a, invert):
+            if invert:
+                z = Signal.like(a, name_suffix="_n")
+                m.d.comb += z.eq(~a)
+                return z
+            else:
+                return a
+
         if "i" in pin.dir:
             if pin.xdr < 2:
-                pin_i  = get_ixor(pin.i,  i_invert)
+                pin_i  = get_ineg(pin.i,  i_invert)
             elif pin.xdr == 2:
-                pin_i0 = get_ixor(pin.i0, i_invert)
-                pin_i1 = get_ixor(pin.i1, i_invert)
+                pin_i0 = get_ineg(pin.i0, i_invert)
+                pin_i1 = get_ineg(pin.i1, i_invert)
         if "o" in pin.dir:
             if pin.xdr < 2:
-                pin_o  = get_oxor(pin.o,  o_invert)
+                pin_o  = get_oneg(pin.o,  o_invert)
             elif pin.xdr == 2:
-                pin_o0 = get_oxor(pin.o0, o_invert)
-                pin_o1 = get_oxor(pin.o1, o_invert)
+                pin_o0 = get_oneg(pin.o0, o_invert)
+                pin_o1 = get_oneg(pin.o1, o_invert)
 
         i = o = t = None
         if "i" in pin.dir:
@@ -231,7 +442,7 @@ class LatticeECP5Platform(TemplatedPlatform):
             if "o" in pin.dir:
                 o = pin_o
             if pin.dir in ("oe", "io"):
-                t = ~pin_oe
+                t = ~pin.oe
         elif pin.xdr == 1:
             # Note that currently nextpnr will not pack an FF (*FS1P3DX) into the PIO.
             if "i" in pin.dir:
@@ -259,9 +470,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("single-ended input", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        t, o, t = self._get_xdr_buffer(m, pin, i_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, i_invert=invert)
         for bit in range(len(port)):
-            m.submodules += Instance("IB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("IB",
                 i_I=port[bit],
                 o_O=i[bit]
             )
@@ -271,9 +482,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("single-ended output", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, o_invert=invert)
         for bit in range(len(port)):
-            m.submodules += Instance("OB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("OB",
                 i_I=o[bit],
                 o_O=port[bit]
             )
@@ -283,9 +494,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("single-ended tristate", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, o_invert=invert)
         for bit in range(len(port)):
-            m.submodules += Instance("OBZ",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("OBZ",
                 i_T=t,
                 i_I=o[bit],
                 o_O=port[bit]
@@ -296,10 +507,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("single-ended input/output", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, i_invert=True if invert else None,
-                                               o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, i_invert=invert, o_invert=invert)
         for bit in range(len(port)):
-            m.submodules += Instance("BB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("BB",
                 i_T=t,
                 i_I=o[bit],
                 o_O=i[bit],
@@ -311,9 +521,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("differential input", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, i_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, i_invert=invert)
         for bit in range(len(p_port)):
-            m.submodules += Instance("IB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("IB",
                 i_I=p_port[bit],
                 o_O=i[bit]
             )
@@ -323,9 +533,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("differential output", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, o_invert=invert)
         for bit in range(len(p_port)):
-            m.submodules += Instance("OB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("OB",
                 i_I=o[bit],
                 o_O=p_port[bit],
             )
@@ -335,9 +545,9 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("differential tristate", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, o_invert=invert)
         for bit in range(len(p_port)):
-            m.submodules += Instance("OBZ",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("OBZ",
                 i_T=t,
                 i_I=o[bit],
                 o_O=p_port[bit],
@@ -348,13 +558,15 @@ class LatticeECP5Platform(TemplatedPlatform):
         self._check_feature("differential input/output", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
-        i, o, t = self._get_xdr_buffer(m, pin, i_invert=True if invert else None,
-                                               o_invert=True if invert else None)
+        i, o, t = self._get_xdr_buffer(m, pin, i_invert=invert, o_invert=invert)
         for bit in range(len(p_port)):
-            m.submodules += Instance("BB",
+            m.submodules["{}_{}".format(pin.name, bit)] = Instance("BB",
                 i_T=t,
                 i_I=o[bit],
                 o_O=i[bit],
                 io_B=p_port[bit],
             )
         return m
+
+    # CDC primitives are not currently specialized for ECP5.
+    # While Diamond supports false path constraints; nextpnr-ecp5 does not.