From 04be0caa5247f28169e0dd31877397930e139177 Mon Sep 17 00:00:00 2001 From: Wanda Date: Fri, 19 Apr 2024 08:17:00 +0200 Subject: [PATCH] vendor._lattice: add Nexus support. Co-authored-by: Nate Slager --- README.md | 5 +- amaranth/vendor/_lattice.py | 322 ++++++++++++++++++++++++++++++++++-- docs/platform/lattice.rst | 2 +- 3 files changed, 314 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 72a52265b..1c09fce95 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,9 @@ See the [Language guide](https://amaranth-lang.org/docs/amaranth/latest/guide.ht Amaranth can be used to target any FPGA or ASIC process that accepts behavioral Verilog-2001 as input. It also offers extended support for many FPGA families, providing toolchain integration, abstractions for device-specific primitives, and more. Specifically: * Lattice iCE40 (toolchains: **Yosys+nextpnr**, LSE-iCECube2, Synplify-iCECube2); - * Lattice MachXO2 (toolchains: Diamond); - * Lattice MachXO3L (toolchains: Diamond); + * Lattice MachXO2, MachXO3L (toolchains: **Yosys+nextpnr**, Diamond); * Lattice ECP5 (toolchains: **Yosys+nextpnr**, Diamond); - * Lattice Nexus (toolchains: **Yosys+nextpnr**, Diamond); + * Lattice Nexus (toolchains: **Yosys+nextpnr**, Radiant); * AMD Virtex, Virtex E, Spartan 2, Spartan 2E (toolchains: ISE); * AMD Virtex II, Virtex II Pro (toolchains: ISE); * AMD Spartan 3, Spartan 3E, Spartan 3A, Spartan 3AN, Spartan 3A DSP (toolchains: ISE); diff --git a/amaranth/vendor/_lattice.py b/amaranth/vendor/_lattice.py index f8b10409b..b55a0fd92 100644 --- a/amaranth/vendor/_lattice.py +++ b/amaranth/vendor/_lattice.py @@ -205,6 +205,47 @@ def elaborate(self, platform): return m +class DDRBufferNexus(io.DDRBuffer): + def elaborate(self, platform): + m = Module() + + m.submodules.buf = buf = InnerBuffer(self.direction, self.port) + inv_mask = sum(inv << bit for bit, inv in enumerate(self.port.invert)) + + if self.direction is not io.Direction.Output: + i0_inv = Signal(len(self.port)) + i1_inv = Signal(len(self.port)) + for bit in range(len(self.port)): + m.submodules[f"i_ddr{bit}"] = Instance("IDDRX1", + i_SCLK=ClockSignal(self.i_domain), + i_RST=Const(0), + i_D=buf.i[bit], + o_Q0=i0_inv[bit], + o_Q1=i1_inv[bit], + ) + m.d.comb += self.i[0].eq(i0_inv ^ inv_mask) + m.d.comb += self.i[1].eq(i1_inv ^ inv_mask) + + if self.direction is not io.Direction.Input: + o0_inv = Signal(len(self.port)) + o1_inv = Signal(len(self.port)) + m.d.comb += [ + o0_inv.eq(self.o[0] ^ inv_mask), + o1_inv.eq(self.o[1] ^ inv_mask), + ] + for bit in range(len(self.port)): + m.submodules[f"o_ddr{bit}"] = Instance("ODDRX1", + i_SCLK=ClockSignal(self.o_domain), + i_RST=Const(0), + i_D0=o0_inv[bit], + i_D1=o1_inv[bit], + o_Q=buf.o[bit], + ) + _make_oereg(m, self.o_domain, ~self.oe, buf.t) + + return m + + class LatticePlatform(TemplatedPlatform): """ .. rubric:: Trellis toolchain (ECP5, MachXO2, MachXO3) @@ -236,6 +277,35 @@ class LatticePlatform(TemplatedPlatform): * ``{{name}}.bit``: binary bitstream. * ``{{name}}.svf``: JTAG programming vector. + .. rubric:: Oxide toolchain (Nexus) + + Required tools: + * ``yosys`` + * ``nextpnr-nexus`` + * ``prjoxide`` + + The environment is populated by running the script specified in the environment variable + ``AMARANTH_ENV_OXIDE``, 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_nexus`` Yosys command. + * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script. + * ``script_after_synth``: inserts commands after ``synth_nexus`` in Yosys script. + * ``yosys_opts``: adds extra options for ``yosys``. + * ``nextpnr_opts``: adds extra options for ``nextpnr-nexus``. + * ``prjoxide_opts``: adds extra options for ``prjoxide``. + * ``add_preferences``: inserts commands at the end of the PDC file. + + Build products: + * ``{{name}}.rpt``: Yosys log. + * ``{{name}}.json``: synthesized RTL. + * ``{{name}}.tim``: nextpnr log. + * ``{{name}}.config``: ASCII bitstream. + * ``{{name}}.bit``: binary bitstream. + * ``{{name}}.xcf``: JTAG programming vector. + .. rubric:: Diamond toolchain (ECP5, MachXO2, MachXO3) Required tools: @@ -263,6 +333,30 @@ class LatticePlatform(TemplatedPlatform): * ``{{name}}.svf``: JTAG programming vector (ECP5 only). * ``{{name}}_flash.svf``: JTAG programming vector for FLASH programming (MachXO2, MachXO3 only). * ``{{name}}_sram.svf``: JTAG programming vector for SRAM programming (MachXO2, MachXO3 only). + + .. rubric:: Radiant toolchain (Nexus) + + Required tools: + * ``radiantc`` + + The environment is populated by running the script specified in the environment variable + ``AMARANTH_ENV_RADIANT``, if present. On Linux, radiant_env as provided by Radiant + itself is a good candidate. On Windows, the following script (named ``radiant_env.bat``, + for instance) is known to work:: + + @echo off + set PATH=C:\\lscc\\radiant\\%RADIANT_VERSION%\\bin\\nt64;%PATH% + + Available overrides: + * ``script_project``: inserts commands before ``prj_save`` in Tcl script. + * ``script_after_export``: inserts commands after ``prj_run Export`` in Tcl script. + * ``add_constraints``: inserts commands at the end of the SDC file. + * ``add_preferences``: inserts commands at the end of the PDC file. + + Build products: + * ``{{name}}_impl/{{name}}_impl.htm``: consolidated log. + * ``{{name}}.bit``: binary bitstream. + * ``{{name}}.xcf``: JTAG programming vector. (if using ``programmer``) """ toolchain = None # selected when creating platform @@ -390,6 +484,77 @@ class LatticePlatform(TemplatedPlatform): """ ] + # Oxide templates + + _oxide_required_tools = [ + "yosys", + "nextpnr-nexus", + "prjoxide" + ] + _oxide_file_templates = { + **TemplatedPlatform.build_script_templates, + "{{name}}.il": r""" + # {{autogenerated}} + {{emit_rtlil()}} + """, + "{{name}}.debug.v": r""" + /* {{autogenerated}} */ + {{emit_debug_verilog()}} + """, + "{{name}}.ys": r""" + # {{autogenerated}} + {% for file in platform.iter_files(".v") -%} + read_verilog {{get_override("read_verilog_opts")|options}} {{file}} + {% endfor %} + {% for file in platform.iter_files(".sv") -%} + read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} + {% endfor %} + {% for file in platform.iter_files(".il") -%} + read_ilang {{file}} + {% endfor %} + read_ilang {{name}}.il + delete w:$verilog_initial_trigger + {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} + synth_nexus {{get_override("synth_opts")|options}} -top {{name}} + {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} + write_json {{name}}.json + """, + "{{name}}.pdc": r""" + # {{autogenerated}} + {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%} + ldc_set_location -site {{ '{' }}{{pin_name}}{{ '}' }} {{'['}}get_ports {{port_name}}{{']'}} + {% if attrs -%} + ldc_set_port -iobuf {{ '{' }}{%- for key, value in attrs.items() %}{{key}}={{value}} {% endfor %}{{ '}' }} {{'['}}get_ports {{port_name}}{{']'}} + {% endif %} + {% endfor %} + {{get_override("add_preferences")|default("# (add_preferences placeholder)")}} + """ + } + _oxide_command_templates = [ + r""" + {{invoke_tool("yosys")}} + {{get_override("yosys_opts")|options}} + -l {{name}}.rpt + {{name}}.ys + """, + r""" + {{invoke_tool("nextpnr-nexus")}} + {{get_override("nextpnr_opts")|options}} + --log {{name}}.tim + --device {{platform.device}}-{{platform.speed}}{{platform.package}}{{platform.grade}} + --pdc {{name}}.pdc + --json {{name}}.json + --fasm {{name}}.fasm + """, + r""" + {{invoke_tool("prjoxide")}} + {{verbose("--verbose")}} + {{get_override("prjoxide_opts")|options}} + pack {{name}}.fasm + {{name}}.bit + """ + ] + # Diamond templates _diamond_required_tools = [ @@ -511,6 +676,76 @@ class LatticePlatform(TemplatedPlatform): """, ] + # Radiant templates + + _radiant_required_tools = [ + "radiantc", + ] + _radiant_file_templates = { + **TemplatedPlatform.build_script_templates, + "build_{{name}}.sh": r""" + # {{autogenerated}} + set -e{{verbose("x")}} + if [ -z "$BASH" ] ; then exec /bin/bash "$0" "$@"; fi + {{emit_commands("sh")}} + """, + "{{name}}.v": r""" + /* {{autogenerated}} */ + {{emit_verilog()}} + """, + "{{name}}.debug.v": r""" + /* {{autogenerated}} */ + {{emit_debug_verilog()}} + """, + "{{name}}.tcl": r""" + prj_create -name {{name}} -impl impl \ + -dev {{platform.device}}-{{platform.speed}}{{platform.package}}{{platform.grade}} \ + -synthesis synplify + {% for file in platform.iter_files(".v", ".sv", ".vhd", ".vhdl") -%} + prj_add_source {{file|tcl_quote}} + {% endfor %} + prj_add_source {{name}}.v + prj_add_source {{name}}.sdc + prj_add_source {{name}}.pdc + prj_set_impl_opt top \"{{name}}\" + {{get_override("script_project")|default("# (script_project placeholder)")}} + prj_save + prj_run Synthesis -impl impl -forceOne + prj_run Map -impl impl + prj_run PAR -impl impl + prj_run Export -impl impl -task Bitgen + {{get_override("script_after_export")|default("# (script_after_export placeholder)")}} + """, + # Pre-synthesis SDC constraints + "{{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_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name}}] + {% else -%} + create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")}}] + {% endif %} + {% endfor %} + {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} + """, + # Physical PDC contraints + "{{name}}.pdc": r""" + {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%} + ldc_set_location -site "{{pin_name}}" [get_ports {{port_name|tcl_quote}}] + {% if attrs -%} + ldc_set_port -iobuf { {%- for key, value in attrs.items() %} {{key}}={{value}}{% endfor %} } [get_ports {{port_name|tcl_quote}}] + {% endif %} + {% endfor %} + {{get_override("add_preferences")|default("# (add_preferences placeholder)")}} + """, + } + _radiant_command_templates = [ + # These don't have any usable command-line option overrides. + r""" + {{invoke_tool("radiantc")}} + {{name}}.tcl + """, + ] + # Common logic def __init__(self, *, toolchain=None): @@ -521,16 +756,23 @@ def __init__(self, *, toolchain=None): self.family = "ecp5" elif device.startswith(("lcmxo2-", "lcmxo3l", "lcmxo3d", "lamxo2-", "lamxo3l", "lamxo3d", "lfmnx-")): self.family = "machxo2" + elif device.startswith(("lifcl-", "lfcpnx-", "lfd2nx-", "lfmxo5-", "ut24c")): + self.family = "nexus" else: raise ValueError(f"Device '{self.device}' is not recognized") if toolchain is None: - if self.family == "ecp5": + if self.family == "nexus": + toolchain = "Oxide" + elif self.family == "ecp5": toolchain = "Trellis" else: toolchain = "Diamond" - assert toolchain in ("Trellis", "Diamond") + if self.family == "nexus": + assert toolchain in ("Oxide", "Radiant") + else: + assert toolchain in ("Trellis", "Diamond") self.toolchain = toolchain @property @@ -540,27 +782,39 @@ def required_tools(self): return self._trellis_required_tools_ecp5 elif self.family == "machxo2": return self._trellis_required_tools_machxo2 + if self.toolchain == "Oxide": + return self._oxide_required_tools if self.toolchain == "Diamond": return self._diamond_required_tools + if self.toolchain == "Radiant": + return self._radiant_required_tools assert False @property def file_templates(self): if self.toolchain == "Trellis": return self._trellis_file_templates + if self.toolchain == "Oxide": + return self._oxide_file_templates if self.toolchain == "Diamond": return self._diamond_file_templates + if self.toolchain == "Radiant": + return self._radiant_file_templates assert False @property def command_templates(self): if self.toolchain == "Trellis": return self._trellis_command_templates + if self.toolchain == "Oxide": + return self._oxide_command_templates if self.toolchain == "Diamond": if self.family == "ecp5": return self._diamond_command_templates_ecp5 if self.family == "machxo2": return self._diamond_command_templates_machxo2 + if self.toolchain == "Radiant": + return self._radiant_command_templates assert False # These numbers were extracted from @@ -585,6 +839,9 @@ def default_clk_constraint(self): # It can have a range of frequencies. assert self.osch_frequency in self._supported_osch_freqs return Clock(int(self.osch_frequency * 1e6)) + if self.default_clk == "OSCA": + # Internal high-speed oscillator on Nexus devices. + return Clock(450e6 / self.osca_div) # Otherwise, use the defined Clock resource. return super().default_clk_constraint @@ -615,7 +872,22 @@ def create_missing_domain(self, name): .format(osch_freq, self._supported_osch_freqs)) osch_freq_param = f"{float(osch_freq):.2f}" clk_i = Signal() - m.submodules += [ Instance("OSCH", p_NOM_FREQ=osch_freq_param, i_STDBY=Const(0), o_OSC=clk_i, o_SEDSTDBY=Signal()) ] + m.submodules += Instance("OSCH", p_NOM_FREQ=osch_freq_param, i_STDBY=Const(0), o_OSC=clk_i, o_SEDSTDBY=Signal()) + elif self.default_clk == "OSCA": + if not hasattr(self, "osca_div"): + raise ValueError("OSCA divider (osca_div) must be an integer between 2 " + "and 256") + if not isinstance(self.osca_div, int) or self.osca_div < 2 or self.osca_div > 256: + raise ValueError("OSCA divider (osca_div) must be an integer between 2 " + "and 256, not {!r}" + .format(self.osca_div)) + clk_i = Signal() + m.submodules += Instance("OSCA", + p_HF_CLK_DIV=str(self.osca_div - 1), + i_HFOUTEN=Const(1), + i_HFSDSCEN=Const(0), # HFSDSCEN used for SED/SEC detector + o_HFCLKOUT=clk_i, + ) else: clk_i = self.request(self.default_clk).i if self.default_rst is not None: @@ -627,14 +899,40 @@ def create_missing_domain(self, name): gsr1 = Signal() # There is no end-of-startup signal on Lattice, 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), - ] + if self.family == "nexus": + # On Nexus all the D-type FFs have either an synchronous or asynchronous preset. + # Here we build a simple reset synchronizer from D-type FFs with a positive-level + # asynchronous preset which we tie low + m.submodules += [ + Instance( + "FD1P3BX", + p_GSR="DISABLED", + i_CK=clk_i, + i_D=~rst_i, + i_SP=Const(1), + i_PD=Const(0), + o_Q=gsr0, + ), + Instance( + "FD1P3BX", + p_GSR="DISABLED", + i_CK=clk_i, + i_D=gsr0, + i_SP=Const(1), + i_PD=Const(0), + o_Q=gsr1, + ), + Instance("GSR", p_SYNCMODE="SYNC", i_CLK=clk_i, i_GSR_N=gsr1), + ] + else: + 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. @@ -652,6 +950,8 @@ def get_io_buffer(self, buffer): result = DDRBufferECP5(buffer.direction, buffer.port) elif self.family == "machxo2": result = DDRBufferMachXO2(buffer.direction, buffer.port) + elif self.family == "nexus": + result = DDRBufferNexus(buffer.direction, buffer.port) else: raise NotImplementedError # :nocov: else: diff --git a/docs/platform/lattice.rst b/docs/platform/lattice.rst index 8adf214aa..beefa32ec 100644 --- a/docs/platform/lattice.rst +++ b/docs/platform/lattice.rst @@ -3,7 +3,7 @@ Lattice .. currentmodule:: amaranth.vendor -The :class:`LatticePlatform` class provides a base platform to support Lattice toolchains (not including iCE40 devices, which are supported by :class:`SiliconBluePlatform`). Currently supported devices include ECP5, MachXO2, and MachXO3L. +The :class:`LatticePlatform` class provides a base platform to support Lattice toolchains (not including iCE40 devices, which are supported by :class:`SiliconBluePlatform`). Currently supported devices include ECP5, MachXO2, MachXO3L, and Nexus. The Trellis and Diamond toolchains are supported.