diff --git a/amaranth/vendor/gowin.py b/amaranth/vendor/gowin.py new file mode 100644 index 000000000..15e2565c5 --- /dev/null +++ b/amaranth/vendor/gowin.py @@ -0,0 +1,594 @@ +from abc import abstractproperty +from fractions import Fraction +import re + +from ..hdl import * +from ..lib.cdc import ResetSynchronizer +from ..build import * + +# Acknowledgments: +# Parts of this file originate from https://github.com/tcjie/Gowin + + +__all__ = ["GowinPlatform"] + + +class GowinPlatform(TemplatedPlatform): + """ + .. rubric:: Apicula toolchain + + Required tools: + * ``yosys`` + * ``nextpnr-gowin`` + * ``gowin_pack`` + + The environment is populated by running the script specified in the environment variable + ``AMARANTH_ENV_APICULA``, if present. + + Build products: + * ``{{name}}.fs``: binary bitstream. + + .. rubric:: Gowin toolchain + + Required tools: + * ``gw_sh`` + + The environment is populated by running the script specified in the environment variable + ``AMARANTH_ENV_GOWIN``, if present. + + Build products: + * ``{{name}}.fs``: binary bitstream. + """ + + toolchain = None # selected when creating platform + + part = abstractproperty() + family = abstractproperty() + + def parse_part(self): + # These regular expressions match all >900 parts of Gowin device_info.csv + reg_series = r"(GW[12]{1}[AN]{1}[EFNRSZ]{0,3})-" + reg_voltage = r"(ZV|EV|LV|LX|UV|UX)" + reg_size = r"(1|2|4|9|18|55)" + reg_subseries = r"(?:(B|C|S|X|P5)?)" + reg_package = r"((?:PG|UG|EQ|LQ|MG|M|QN|CS|FN)(?:\d+)(?:P?)(?:A|E|M|CF|C|D|G|H|F|S|T|U|X)?)" + reg_speed = r"((?:C\d{1}/I\d{1})|ES|A\d{1}|I\d{1})" + + match = re.match(reg_series+reg_voltage+reg_size+reg_subseries+reg_package+reg_speed+"$", + self.part) + if not match: + raise ValueError("Supplied part name is invalid") + + self.series = match.group(1) + self.voltage = match.group(2) + self.size = match.group(3) + self.subseries = match.group(4) or "" + self.package = match.group(5) + self.speed = match.group(6) + + match = re.match(reg_series+reg_size+reg_subseries+"$", self.family) + if not match: + raise ValueError("Supplied device family name is invalid") + + self.series_f = match.group(1) + self.size_f = match.group(2) + self.subseries_f = match.group(3) or "" + + # subseries_f is usually more reliable than subseries. + + if self.series != self.series_f: + raise ValueError("Series extracted from supplied part name does not match " + "supplied family series") + if self.size != self.size_f: + raise ValueError("Size extracted from supplied part name does not match " + "supplied family size") + + # _chipdb_device is tied to available chipdb-*.bin files of nextpnr-gowin + @property + def _chipdb_device(self): + # GW1NR series does not have its own chipdb file, but works with GW1N + if self.series == "GW1NR": + return "GW1N-{}{}".format(self.size, self.subseries_f) + return self.family + + _dev_osc_mapping = { + "GW1N-1" : "OSCH", + "GW1N-1P5" : "OSCO", + "GW1N-1P5B" : "OSCO", + "GW1N-1S" : "OSCH", + "GW1N-2" : "OSCO", + "GW1N-2B" : "OSCO", + "GW1N-4" : "OSC", + "GW1N-4B" : "OSC", + "GW1N-9" : "OSC", + "GW1N-9C" : "OSC", + "GW1NR-1" : "OSCH", + "GW1NR-2" : "OSCO", + "GW1NR-2B" : "OSCO", + "GW1NR-4" : "OSC", + "GW1NR-4B" : "OSC", + "GW1NR-9" : "OSC", + "GW1NR-9C" : "OSC", + "GW1NRF-4B" : "OSC", + "GW1NS-2" : "OSCF", + "GW1NS-2C" : "OSCF", + "GW1NS-4" : "OSCZ", + "GW1NS-4C" : "OSCZ", + "GW1NSE-2C" : "OSCF", + "GW1NSER-4C" : "OSCZ", + "GW1NSR-2" : "OSCF", + "GW1NSR-2C" : "OSCF", + "GW1NSR-4" : "OSCZ", + "GW1NSR-4C" : "OSCZ", + "GW1NZ-1" : "OSCZ", + "GW1NZ-1C" : "OSCZ", + "GW2A-18" : "OSC", + "GW2A-18C" : "OSC", + "GW2A-55" : "OSC", + "GW2A-55C" : "OSC", + "GW2AN-18X" : "OSCW", + "GW2AN-55C" : "OSC", + "GW2AN-9X" : "OSCW", + "GW2ANR-18C" : "OSC", + "GW2AR-18" : "OSC", + "GW2AR-18C" : "OSC" + } + + @property + def _osc_type(self): + if self.family in self._dev_osc_mapping: + return self._dev_osc_mapping[self.family] + raise NotImplementedError("Device family {} does not have an assigned oscillator type" + .format(self.family)) + + @property + def _osc_base_freq(self): + osc = self._osc_type + if osc == "OSC": + if self.speed == 4 and self.subseries_f in ("B", "D"): + return 210_000_000 + else: + return 250_000_000 + elif osc in ("OSCZ", "OSCO"): + if self.series == "GW1NSR" and self.speed == "C7/I6": + return 260_000_000 + else: + return 250_000_000 + elif osc in ("OSCF", "OSCH"): + return 240_000_000 + elif osc == "OSCW": + return 200_000_000 + else: + assert False + + @property + def _osc_div(self): + div_min = 2 + div_max = 128 + div_step = 2 + div_frac = Fraction(self._osc_base_freq, self.osc_frequency) + + if div_frac.denominator != 1 or div_frac not in range(div_min, div_max, div_step): + raise ValueError( + "On-chip oscillator frequency (platform.osc_frequency) must be chosen such that " + "the oscillator divider, calculated as ({}/{}), is an integer between {} and {} in " + "steps of {}" + .format(div_frac.numerator, div_frac.denominator, div_min, div_max, div_step)) + + return div_frac.numerator + + # Common templates + + _common_file_templates = { + "{{name}}.cst": r""" + // {{autogenerated}} + {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%} + IO_LOC "{{port_name}}" {{pin_name}}; + {% for attr_name, attr_value in attrs.items() -%} + IO_PORT "{{port_name}}" {{attr_name}}={{attr_value}}; + {% endfor %} + {% endfor %} + """, + } + + # Apicula templates + + _apicula_required_tools = [ + "yosys", + "nextpnr-gowin", + "gowin_pack" + ] + _apicula_file_templates = { + **TemplatedPlatform.build_script_templates, + **_common_file_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_gowin {{get_override("synth_opts")|options}} -top {{name}} -json {{name}}.syn.json + {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} + """, + } + _apicula_command_templates = [ + r""" + {{invoke_tool("yosys")}} + {{quiet("-q")}} + {{get_override("yosys_opts")|options}} + -l {{name}}.rpt + {{name}}.ys + """, + r""" + {{invoke_tool("nextpnr-gowin")}} + {{quiet("--quiet")}} + {{get_override("nextpnr_opts")|options}} + --log {{name}}.tim + --device {{platform.part}} + --family {{platform._chipdb_device}} + --json {{name}}.syn.json + --cst {{name}}.cst + --write {{name}}.pnr.json + """, + r""" + {{invoke_tool("gowin_pack")}} + -d {{platform._chipdb_device}} + -o {{name}}.fs + {{get_override("gowin_pack_opts")|options}} + {{name}}.pnr.json + """ + ] + + # Vendor toolchain templates + + _gowin_required_tools = ["gw_sh"] + _gowin_file_templates = { + **TemplatedPlatform.build_script_templates, + **_common_file_templates, + "{{name}}.v": r""" + /* {{autogenerated}} */ + {{emit_verilog()}} + """, + "{{name}}.tcl": r""" + # {{autogenerated}} + {% for file in platform.iter_files(".v",".sv",".vhd",".vhdl") -%} + add_file {{file}} + {% endfor %} + add_file -type verilog {{name}}.v + add_file -type cst {{name}}.cst + add_file -type sdc {{name}}.sdc + set_device -name {{platform.family}} {{platform.part}} + set_option -verilog_std v2001 -print_all_synthesis_warning 1 -show_all_warn 1 + {{get_override("add_options")|default("# (add_options placeholder)")}} + run all + file delete -force {{name}}.fs + file copy -force impl/pnr/project.fs {{name}}.fs + """, + "{{name}}.sdc": r""" + // {{autogenerated}} + {% for net_signal,port_signal,frequency in platform.iter_clock_constraints() -%} + create_clock -name {{port_signal.name|tcl_escape}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_escape}}] + {% endfor %} + {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} + """, + } + _gowin_command_templates = [ + r""" + {{invoke_tool("gw_sh")}} + {{name}}.tcl + """ + ] + + def __init__(self, *, toolchain="Apicula"): + super().__init__() + + assert toolchain in ("Apicula", "Gowin") + self.toolchain = toolchain + + self.parse_part() + + @property + def required_tools(self): + if self.toolchain == "Apicula": + return self._apicula_required_tools + elif self.toolchain == "Gowin": + return self._gowin_required_tools + assert False + + @property + def file_templates(self): + if self.toolchain == "Apicula": + return self._apicula_file_templates + elif self.toolchain == "Gowin": + return self._gowin_file_templates + assert False + + @property + def command_templates(self): + if self.toolchain == "Apicula": + return self._apicula_command_templates + elif self.toolchain == "Gowin": + return self._gowin_command_templates + assert False + + def add_clock_constraint(self, clock, frequency): + super().add_clock_constraint(clock, frequency) + clock.attrs["keep"] = "true" + + @property + def default_clk_constraint(self): + if self.default_clk == "OSC": + if not hasattr(self, "osc_frequency"): + raise AttributeError( + "Using the on-chip oscillator as the default clock source requires " + "the platform.osc_frequency attribute to be set") + return Clock(self.osc_frequency) + + # Use the defined Clock resource. + return super().default_clk_constraint + + def create_missing_domain(self, name): + if name == "sync" and self.default_clk is not None: + m = Module() + + if self.default_clk == "OSC": + clk_i = Signal() + if self._osc_type == "OSCZ": + m.submodules += Instance(self._osc_type, + p_FREQ_DIV=self._osc_div, + i_OSCEN=Const(1), + o_OSCOUT=clk_i) + elif self._osc_type == "OSCO": + # TODO: Make use of regulator configurable + m.submodules += Instance(self._osc_type, + p_REGULATOR_EN=Const(1), + p_FREQ_DIV=self._osc_div, + i_OSCEN=Const(1), + o_OSCOUT=clk_i) + elif self._osc_type == "OSCF": + m.submodules += Instance(self._osc_type, + p_FREQ_DIV=self._osc_div, + o_OSCOUT30M=None, + o_OSCOUT=clk_i) + else: + m.submodules += Instance(self._osc_type, + p_FREQ_DIV=self._osc_div, + o_OSCOUT=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) + + m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync") + m.domains += ClockDomain("sync") + m.d.comb += ClockSignal("sync").eq(clk_i) + + return m + + 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("DFF", + i_CLK=clk, + i_D=d[bit], + o_Q=q[bit], + ) + + def get_oreg(clk,d,q): + for bit in range(len(q)): + m.submodules += Instance("DFF", + i_CLK=clk, + i_D=d[bit], + o_Q=q[bit] + ) + + def get_iddr(clk,d,q0,q1): + for bit in range(len(d)): + m.submodules += Instance("IDDR", + i_CLK=clk, + i_D=d[bit], + o_Q0=q0[bit], + o_Q1=q1[bit] + ) + + def get_oddr(clk,d0,d1,tx,q0,q1): + for bit in range(len(q0)): + m.submodules += Instance("ODDR", + p_TXCLK_POL=0, # default -> Q1 changes on posedge of CLK + i_CLK=clk, + i_D0=d0[bit], + i_D1=d1[bit], + i_TX=tx[bit], + o_Q0=q0[bit], + o_Q1=q1[bit] + ) + + def get_ineg(y, invert): + if invert: + a = Signal.like(y, name_suffix="_n") + m.d.comb += y.eq(~a) + return a + else: + return y + + def get_oneg(a, invert): + if invert: + y = Signal.like(a, name_suffix="_n") + m.d.comb += y.eq(~a) + return y + else: + return a + + + if "i" in pin.dir: + if pin.xdr < 2: + pin_i = get_ineg(pin.i, i_invert) + elif pin.xdr == 2: + 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_oneg(pin.o, o_invert) + elif pin.xdr == 2: + 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: + i = Signal(pin.width, name="{}_xdr_i".format(pin.name)) + if "o" in pin.dir: + o = Signal(pin.width, name="{}_xdr_o".format(pin.name)) + if pin.dir in ("oe", "io"): + t = Signal(1, name="{}_xdr_t".format(pin.name)) + + if pin.xdr == 0: + if "i" in pin.dir: + i = pin_i + if "o" in pin.dir: + o = pin_o + if pin.dir in ("oe", "io"): + t = ~pin.oe + elif pin.xdr == 1: + if "i" in pin.dir: + get_ireg(pin.i_clk, i, pin_i) + if "o" in pin.dir: + get_oreg(pin.o_clk, pin_o, o) + if pin.dir in ("oe", "io"): + get_oreg(pin.o_clk, ~pin.oe, t) + elif pin.xdr == 2: + if "i" in pin.dir: + get_iddr(pin.i_clk, i, pin_i0, pin_i1) + if pin.dir in ("o", "oe", "io"): + get_oddr(pin.o_clk, pin_o0, pin_o1, ~pin_oe, o, t) + else: + assert False + + return (i, o, t) + + def get_input(self, pin, port, attrs, invert): + self._check_feature("single-ended input", pin, attrs, + valid_xdrs=(0, 1, 2), valid_attrs=True) + m = Module() + i, o, t = self._get_xdr_buffer(m, pin, i_invert=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name, bit)] = Instance("IBUF", + i_I=port.io[bit], + o_O=i[bit] + ) + return m + + def get_output(self, pin, port, attrs, invert): + 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, port.io, o_invert=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name, bit)] = Instance("OBUF", + i_I=o[bit], + o_O=port.io[bit] + ) + return m + + def get_tristate(self, pin, port, attrs, invert): + 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=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name, bit)] = Instance("TBUF", + i_OEN=t, + i_I=o[bit], + o_O=port.io[bit] + ) + return m + + def get_input_output(self, pin, port, attrs, invert): + 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=invert, o_invert=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name, bit)] = Instance("IOBUF", + i_OEN=t, + i_I=o[bit], + o_O=i[bit], + io_IO=port.io[bit] + ) + return m + + def get_diff_input(self, pin, port, attrs, invert): + 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=invert) + for bit in range(pin.wodth): + m.submodules["{}_{}".format(pin.name,bit)] = Instance("TLVDS_IBUF", + i_I=port.p[bit], + i_IB=port.n[bit], + o_O=i[bit] + ) + return m + + def get_diff_output(self, pin, port, attrs, invert): + 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=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name,bit)] = Instance("TLVDS_OBUF", + i_I=o[bit], + o_O=port.p[bit], + o_OB=port.n[bit], + ) + return m + + def get_diff_tristate(self, pin, port, attrs, invert): + 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=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name,bit)] = Instance("TLVDS_TBUF", + i_OEN=t, + i_I=o[bit], + o_O=port.p[bit], + o_OB=port.n[bit] + ) + return m + + def get_diff_input_output(self, pin, port, atttr, invert): + 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=invert, o_invert=invert) + for bit in range(pin.width): + m.submodules["{}_{}".format(pin.name,bit)] = Instance("TLVDS_IOBUF", + i_OEN=t, + i_I=o[bit], + o_O=i[bit], + io_IO=port.p[bit], + io_IOB=port.n[bit] + ) + return m diff --git a/docs/changes.rst b/docs/changes.rst index f9a8eb0de..0b57f04ea 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -110,6 +110,7 @@ Platform integration changes * Added: ``OSCH`` as ``default_clk`` clock source in :class:`vendor.lattice_machxo_2_3l.LatticeMachXO2Or3LPlatform`. * Added: Xray toolchain support in :class:`vendor.xilinx.XilinxPlatform`. +* Added: :class:`vendor.gowin.GowinPlatform`. * Removed: (deprecated in 0.3) :mod:`lattice_machxo2` * Removed: (deprecated in 0.3) :class:`lattice_machxo_2_3l.LatticeMachXO2Or3LPlatform` SVF programming vector ``{{name}}.svf``. * Removed: (deprecated in 0.3) :class:`xilinx_spartan_3_6.XilinxSpartan3APlatform`, :class:`xilinx_spartan_3_6.XilinxSpartan6Platform`, :class:`xilinx_7series.Xilinx7SeriesPlatform`, :class:`xilinx_ultrascale.XilinxUltrascalePlatform`. diff --git a/docs/platform.rst b/docs/platform.rst index 011e48896..c4cf17fa7 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -8,9 +8,10 @@ Platform integration .. toctree:: :maxdepth: 2 + platform/gowin platform/intel platform/lattice-ecp5 platform/lattice-ice40 platform/lattice-machxo-2-3l platform/quicklogic - platform/xilinx \ No newline at end of file + platform/xilinx diff --git a/docs/platform/gowin.rst b/docs/platform/gowin.rst new file mode 100644 index 000000000..7abeee468 --- /dev/null +++ b/docs/platform/gowin.rst @@ -0,0 +1,10 @@ +Gowin +##### + +.. py:module:: amaranth.vendor.gowin + +The :mod:`amaranth.vendor.gowin` module provides a base platform to support Gowin toolchains. + +The Apicula and Gowin toolchains are supported. + +.. autoclass:: GowinPlatform