From a2989f41a723eaf60b788a622b23a32be5e3e156 Mon Sep 17 00:00:00 2001 From: tmbv Date: Sun, 17 Aug 2025 19:21:16 +0200 Subject: [PATCH] add GateMate backend --- amaranth/vendor/__init__.py | 4 + amaranth/vendor/_colognechip.py | 400 ++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 amaranth/vendor/_colognechip.py diff --git a/amaranth/vendor/__init__.py b/amaranth/vendor/__init__.py index c60bb4d..2abe079 100644 --- a/amaranth/vendor/__init__.py +++ b/amaranth/vendor/__init__.py @@ -6,6 +6,7 @@ __all__ = [ "AlteraPlatform", "AMDPlatform", + "GateMatePlatform", "GowinPlatform", "IntelPlatform", "LatticeECP5Platform", @@ -30,6 +31,9 @@ def __getattr__(name): if name == "GowinPlatform": from ._gowin import GowinPlatform return GowinPlatform + if name == "GateMatePlatform": + from ._colognechip import GateMatePlatform + return GateMatePlatform if name in ("LatticePlatform", "LatticeECP5Platform", "LatticeMachXO2Platform", "LatticeMachXO3LPlatform"): from ._lattice import LatticePlatform diff --git a/amaranth/vendor/_colognechip.py b/amaranth/vendor/_colognechip.py new file mode 100644 index 0000000..31cf771 --- /dev/null +++ b/amaranth/vendor/_colognechip.py @@ -0,0 +1,400 @@ +from abc import abstractmethod + +from ..hdl import * +from ..hdl._ir import RequirePosedge +from ..lib import io, wiring +from ..build import * + +# FIXME: be sure attribtues are handled correctly + + +class InnerBuffer(wiring.Component): + """A private component used to implement ``lib.io`` buffers. + + Works like ``lib.io.Buffer``, with the following differences: + + - ``port.invert`` is ignored (handling the inversion is the outer buffer's responsibility) + - ``t`` is per-pin inverted output enable + - ``merge_ff`` specifies this is to be used with a simple FF, and P&R should merge these + """ + def __init__(self, direction, port, merge_ff=False): + self.direction = direction + self.port = port + self.merge_ff = merge_ff + + members = {} + + if direction is not io.Direction.Output: + members["i"] = wiring.In(len(port)) + + if direction is not io.Direction.Input: + members["o"] = wiring.Out(len(port)) + members["t"] = wiring.Out(len(port)) + + super().__init__(wiring.Signature(members).flip()) + + def elaborate(self, platform): + m = Module() + + if isinstance(self.port, io.SingleEndedPort): + io_port = self.port.io + + for bit in range(len(self.port)): + name = f"buf{bit}" + if self.direction is io.Direction.Input: + m.submodules[name] = Instance("CC_IBUF", + i_I=io_port[bit], + o_Y=self.i[bit], + p_FF_IBF=self.merge_ff, + ) + elif self.direction is io.Direction.Output: + m.submodules[name] = Instance("CC_TOBUF", + i_T=self.t[bit], + i_A=self.o[bit], + o_O=io_port[bit], + p_FF_OBF=self.merge_ff, + ) + elif self.direction is io.Direction.Bidir: + m.submodules[name] = Instance("CC_IOBUF", + i_T=self.t[bit], + i_Y=self.o[bit], + o_A=self.i[bit], + io_IO=io_port[bit], + p_FF_IBF=self.merge_ff, + p_FF_OBF=self.merge_ff, + ) + else: + assert False # :nocov: + + elif isinstance(self.port, io.DifferentialPort): + p_port = self.port.p + n_port = self.port.n + + for bit in range(len(self.port)): + name = f"buf{bit}" + if self.direction is io.Direction.Input: + m.submodules[name] = Instance("CC_LVDS_IBUF", + i_I_P=p_port[bit], + i_I_N=n_port[bit], + o_Y=self.i[bit], + p_FF_IBF=self.merge_ff, + ) + elif self.direction is io.Direction.Output: + m.submodules[name] = Instance("CC_LVDS_TOBUF", + i_T=self.t[bit], + i_A=self.o[bit], + o_O_P=p_port[bit], + o_O_N=n_port[bit], + p_FF_OBF=self.merge_ff, + ) + elif self.direction is io.Direction.Bidir: + m.submodules[name] = Instance("CC_LVDS_IOBUF", + i_T=self.t[bit], + i_Y=self.o[bit], + o_A=self.i[bit], + io_P=p_port[bit], + io_N=n_port[bit], + p_FF_IBF=self.merge_ff, + p_FF_OBF=self.merge_ff, + ) + else: + assert False # :nocov: + + else: + raise TypeError(f"Unknown port type {self.port!r}") + + + return m + + +class IOBuffer(io.Buffer): + 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: + m.d.comb += self.i.eq(buf.i ^ inv_mask) + + if self.direction is not io.Direction.Input: + m.d.comb += buf.o.eq(self.o ^ inv_mask) + m.d.comb += buf.t.eq(~self.oe.replicate(len(self.port))) + + return m + + +def _make_oereg(m, domain, oe, q): + for bit in range(len(q)): + m.submodules[f"oe_ff{bit}"] = Instance("CC_DFF", + i_CLK=ClockSignal(domain), + i_EN=Const(1), + i_SR=Const(0), + i_D=oe, + o_Q=q[bit], + ) + + +class FFBuffer(io.FFBuffer): + def elaborate(self, platform): + m = Module() + + m.submodules.buf = buf = InnerBuffer(self.direction, self.port, true) + inv_mask = sum(inv << bit for bit, inv in enumerate(self.port.invert)) + + if self.direction is not io.Direction.Output: + m.submodules += RequirePosedge(self.i_domain) + i_inv = Signal.like(self.i) + for bit in range(len(self.port)): + m.submodules[f"i_ff{bit}"] = Instance("CC_DFF", + i_CLK=ClockSignal(self.i_domain), + i_EN=Const(1), + i_SR=Const(0), + i_D=buf.i[bit], + o_Q=i_inv[bit], + ) + m.d.comb += self.i.eq(i_inv ^ inv_mask) + + if self.direction is not io.Direction.Input: + m.submodules += RequirePosedge(self.o_domain) + o_inv = Signal.like(self.o) + m.d.comb += o_inv.eq(self.o ^ inv_mask) + for bit in range(len(self.port)): + m.submodules[f"o_ff{bit}"] = Instance("CC_DFF", + i_CLK=ClockSignal(self.o_domain), + i_EN=Const(1), + i_SR=Const(0), + i_D=o_inv[bit], + o_Q=buf.o[bit], + ) + _make_oereg(m, self.o_domain, ~self.oe, buf.t) + + return m + + +class DDRBuffer(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: + m.submodules += RequirePosedge(self.i_domain) + 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("CC_IDDR", + i_CLK=ClockSignal(self.i_domain), + 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: + m.submodules += RequirePosedge(self.o_domain) + 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("CC_ODDR", + i_CLK=ClockSignal(self.o_domain), + i_DDR=ClockSignal(self.i_domain), # FIXME + 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 GateMatePlatform(TemplatedPlatform): + """ + .. rubric:: Peppercorn toolchain + + Required tools: + * ``yosys`` + * ``nextpnr-himbaechel`` built with the gatemate uarch + * ``gmpack`` + + The environment is populated by running the script specified in the environment variable + ``AMARANTH_ENV_PEPPERCORN``, 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_`` Yosys command. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. + * ``script_after_synth``: inserts commands after ``synth_`` in Yosys script. + * ``yosys_opts``: adds extra options for ``yosys``. + * ``nextpnr_opts``: adds extra options for ``nextpnr-``. + * ``gmpack_opts``: adds extra options for ``gmpack``. + * ``add_preferences``: inserts commands at the end of the LPF file. + + Build products: + * ``{{name}}.rpt``: Yosys log. + * ``{{name}}.json``: synthesized RTL. + * ``{{name}}.tim``: nextpnr log. + * ``{{name}}.config``: ASCII bitstream. + * ``{{name}}.bit``: binary bitstream. + + """ + + toolchain = "pepercorn" + device = property(abstractmethod(lambda: None)) + + _required_tools = [ + "yosys", + "nextpnr-himbaechel", + "gmpack" + ] + + _file_templates = { + **TemplatedPlatform.build_script_templates, + "{{name}}.il": r""" + # {{autogenerated}} + {{emit_rtlil()}} + """, + "{{name}}.debug.v": r""" + /* {{autogenerated}} */ + {{emit_debug_verilog()}} + """, + # Note: synth with -luttree is basically required to fit anything significant on the GateMate, + # as it comes with both L2T4 "LUT trees" and L2T5 ones. Without awareness of this structure, the + # our tools seem to struggle to pack CC_LUT4s efficiently enough into L2T4 and L2T5s. + "{{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_rtlil {{file}} + {% endfor %} + read_rtlil {{name}}.il + {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} + synth_gatemate {{get_override("synth_opts")|options}} -top {{name}} -luttree + {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} + write_json {{name}}.json + """, + "{{name}}.ccf": r""" + # {{autogenerated}} + {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%} + NET "{{port_name}}" Loc = "IO_{{pin_name}}" {%- for key, value in attrs.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_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] + {% else -%} + create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}] + {% endif %} + {% endfor %} + {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} + """, + } + _command_templates = [ + r""" + {{invoke_tool("yosys")}} + {{quiet("-q")}} + {{get_override("yosys_opts")|options}} + -l {{name}}.rpt + {{name}}.ys + """, + r""" + {{invoke_tool("nextpnr-himbaechel")}} + {{quiet("--quiet")}} + {{get_override("nextpnr_opts")|options}} + --log {{name}}.tim + --device {{platform.device}} + --json {{name}}.json + --sdc {{name}}.sdc + -o ccf={{name}}.ccf + -o out={{name}}.config + """, + r""" + {{invoke_tool("gmpack")}} + {{verbose("--verbose")}} + {{get_override("gmpack_opts")|options}} + --input {{name}}.config + --bit {{name}}.bit + """ + ] + + def __init__(self): + super().__init__() + + device = self.device.lower() + + if device.startswith("ccgm1a"): + self.family = "gatemate" + else: + raise ValueError(f"Device '{self.device}' is not recognized") + + @property + def required_tools(self): + return self._required_tools + + @property + def file_templates(self): + return self._file_templates + + @property + def command_templates(self): + return self._command_templates + + + def create_missing_domain(self, name): + if name == "sync" and self.default_clk is not None: + m = Module() + + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i + + if self.default_rst is not None: + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i + else: + rst_i = Const(0) + + # The post-configuration reset 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 + + def get_io_buffer(self, buffer): + if isinstance(buffer, io.Buffer): + result = IOBuffer(buffer.direction, buffer.port) + elif isinstance(buffer, io.FFBuffer): + result = FFBuffer(buffer.direction, buffer.port, + i_domain=buffer.i_domain, + o_domain=buffer.o_domain) + elif isinstance(buffer, io.DDRBuffer): + result = DDRBuffer(buffer.direction, buffer.port, + i_domain=buffer.i_domain, + o_domain=buffer.o_domain) + else: + raise TypeError(f"Unsupported buffer type {buffer!r}") # :nocov: + + if buffer.direction is not io.Direction.Output: + result.i = buffer.i + if buffer.direction is not io.Direction.Input: + result.o = buffer.o + result.oe = buffer.oe + + return result