add GateMate backend
Some checks failed
CI / test (${{ matrix.python-version }}) (false, 3.10) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, 3.11) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, 3.12) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, 3.8) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, 3.9) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, pypy-3.10) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, pypy-3.8) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (false, pypy-3.9) (push) Has been cancelled
CI / test (${{ matrix.python-version }}) (true, 3.13-dev) (push) Has been cancelled
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-boards) (push) Has been cancelled
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-soc) (push) Has been cancelled
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-stdio) (push) Has been cancelled
CI / document (push) Has been cancelled
CI / check-links (push) Has been cancelled
CI / required (push) Has been cancelled
CI / publish-docs (push) Has been cancelled
CI / publish-docs-dev (push) Has been cancelled
CI / publish-schemas (push) Has been cancelled
CI / publish-package (push) Has been cancelled
CI / publish-release (push) Has been cancelled

This commit is contained in:
tmbv 2025-08-17 19:21:16 +02:00
parent bf4f22d2d5
commit a2989f41a7
2 changed files with 404 additions and 0 deletions

View file

@ -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

View file

@ -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_<family>`` Yosys command.
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
* ``script_after_synth``: inserts commands after ``synth_<family>`` in Yosys script.
* ``yosys_opts``: adds extra options for ``yosys``.
* ``nextpnr_opts``: adds extra options for ``nextpnr-<family>``.
* ``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