From af61060fb0b8728db13f30f9f1e2df78fc11c43e Mon Sep 17 00:00:00 2001 From: Anton Kuzmin Date: Mon, 8 Dec 2025 13:39:07 +0100 Subject: [PATCH 1/2] Switch GateMate from proprietary place-and-route to nextpnr (original Tool API) --- edalize/gatemate.py | 58 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/edalize/gatemate.py b/edalize/gatemate.py index 706f9861..f8d5148c 100644 --- a/edalize/gatemate.py +++ b/edalize/gatemate.py @@ -20,16 +20,16 @@ def get_doc(cls, api_ver): options = { "lists": [ { - "name": "p_r_options", + "name": "nextpnr_options", "type": "string", - "desc": "Additional option for p_r", + "desc": "Additional option for nextpnr", }, ], "members": [ { "name": "device", "type": "String", - "desc": "Required device option for p_r command (e.g. CCGM1A1)", + "desc": "Required device option for nextpnr command (e.g. CCGM1A1)", }, ], } @@ -44,11 +44,11 @@ def get_doc(cls, api_ver): def configure_main(self): (src_files, incdirs) = self._get_fileset_files() - synth_out = self.name + "_synth.v" + synth_out = self.name + ".json" device = self.tool_options.get("device") if not device: - raise RuntimeError("Missing required option 'device' for p_r") + raise RuntimeError("Missing required option 'device' for nextpnr") match = re.search("^CCGM1A([1-9]{1,2})$", device) if not match: @@ -64,21 +64,21 @@ def configure_main(self): if f.file_type == "CCF": if ccf_file: raise RuntimeError( - "p_r only supports one ccf file. Found {} and {}".format( + "nextpnr only supports one ccf file. Found {} and {}".format( ccf_file, f.name ) ) else: ccf_file = f.name - # p_r_log_file = None - p_r_log_file = "p_r.log" + # nextpnr_log_file = None + nextpnr_log_file = "nextpnr.log" # Pass GateMate tool options to yosys self.edam["tool_options"] = { "yosys": { "arch": "gatemate", - "output_format": "verilog", + "output_format": "json", "output_name": synth_out, "yosys_synth_options": self.tool_options.get("yosys_synth_options", []), "yosys_as_subtool": True, @@ -103,28 +103,32 @@ def configure_main(self): commands = EdaCommands() commands.commands = yosys.commands - # PnR & image generation - commands.add_var("P_R := $(shell which p_r)") - targets = self.name + "_00.cfg.bit" + # nextpnr & image generation + commands.add_var("NEXTPNR := $(shell which nextpnr-himbaechel)") + cfg_target = self.name + ".cfg" command = [ - "$(P_R)", - "-A", - device_number, - "-i", - synth_out, - "-o", - self.name, - "-lib", - "ccag", - " ".join(self.tool_options.get("p_r_options", "")), + "$(NEXTPNR)", + "--device", + device, + "--json", + "$<", + "-o out=" + cfg_target, + " ".join(self.tool_options.get("nextpnr_options", "")), ] if ccf_file is not None: - command += ["-ccf", ccf_file] + command += ["-o ccf=" + ccf_file] - if p_r_log_file is not None: - command += [">", p_r_log_file] + if nextpnr_log_file is not None: + command += ["--log", nextpnr_log_file] - commands.add(command, [targets], [synth_out]) + commands.add(command, [cfg_target], [synth_out]) - commands.set_default_target(targets) + commands.add_var("GMPACK := $(shell which gmpack)") + bit_target = self.name + ".bit" + command = [ + "$(GMPACK) $< $@", + ] + commands.add(command, [bit_target], [cfg_target]) + + commands.set_default_target(bit_target) commands.write(os.path.join(self.work_root, "Makefile")) From b49f203f18e81b2b1cb07bb3535d6c9684eb5794 Mon Sep 17 00:00:00 2001 From: Anton Kuzmin Date: Mon, 15 Dec 2025 22:11:21 +0100 Subject: [PATCH 2/2] Implement Flow API for Cologne Chip GateMate FPGAs --- edalize/flows/gatemate.py | 51 +++++++++++++++++++++++++++++++++++ edalize/tools/gmpack.py | 57 +++++++++++++++++++++++++++++++++++++++ edalize/tools/nextpnr.py | 26 +++++++++++++++++- 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 edalize/flows/gatemate.py create mode 100644 edalize/tools/gmpack.py diff --git a/edalize/flows/gatemate.py b/edalize/flows/gatemate.py new file mode 100644 index 00000000..86bef389 --- /dev/null +++ b/edalize/flows/gatemate.py @@ -0,0 +1,51 @@ +# Copyright edalize contributors +# Licensed under the 2-Clause BSD License, see LICENSE for details. +# SPDX-License-Identifier: BSD-2-Clause + +import os.path + +from edalize.flows.edaflow import Edaflow, FlowGraph + +class Gatemate(Edaflow): + """Open source toolchain for Cologne Chip GateMate FPGAs. Uses yosys for synthesis and nextpnr for Place & Route""" + + argtypes = ["vlogdefine", "vlogparam"] + + _flow = { + "yosys": {"fdto": {"arch": "gatemate", "output_format": "json", + "yosys_synth_options": ["-luttree", "-nomx8"], + }}, + "nextpnr": {"deps": ["yosys"], + "fdto": {"arch": "gatemate", + "nextpnr_options": ["--router router2"], + }}, + "gmpack": {"deps": ["nextpnr"]}, + } + + @classmethod + def get_tool_options(cls, flow_options): + tools = flow_options.get("frontends", []) + list(cls._flow) + + flow_defined_tool_options = {} + for k, v in cls._flow.items(): + flow_defined_tool_options[k] = v.get("fdto", {}) + return cls.get_filtered_tool_options(tools, flow_defined_tool_options) + + def configure_flow(self, flow_options): + + flow = self._flow.copy() + + # Add any user-specified frontends to the flow + deps = [] + for frontend in flow_options.get("frontends", []): + flow[frontend] = {"deps": deps} + deps = [frontend] + + flow["yosys"]["deps"] = deps + + name = self.edam["name"] + self.commands.add([], ["synth"], [name + ".json"]) + self.commands.add([], ["bitstream"], [name + ".bit"]) + self.commands.set_default_target("bitstream") + + return FlowGraph.fromdict(flow) diff --git a/edalize/tools/gmpack.py b/edalize/tools/gmpack.py new file mode 100644 index 00000000..25d161d5 --- /dev/null +++ b/edalize/tools/gmpack.py @@ -0,0 +1,57 @@ +# Copyright edalize contributors +# Licensed under the 2-Clause BSD License, see LICENSE for details. +# SPDX-License-Identifier: BSD-2-Clause + +from edalize.tools.edatool import Edatool +from edalize.utils import EdaCommands + +class Gmpack(Edatool): + + description = "Generate binary image for GateMate FPGAs" + + TOOL_OPTIONS = { + "gmpack_options": { + "type": "str", + "desc": "Additional options for gmpack", + "list": True, + }, + } + + def setup(self, edam): + super().setup(edam) + + unused_files = [] + bit_file = self.edam["name"] + ".bit" + gmcfg_file = "" + for f in self.files: + if f.get("file_type") == "gatemateConfig": + if gmcfg_file: + raise RuntimeError( + "gmpack only supports one input file. Found {} and {}".format( + gmcfg_file, f["name"] + ) + ) + gmcfg_file = f["name"] + else: + unused_files.append(f) + + if not gmcfg_file: + raise RuntimeError("No input file specified for gmpack") + + self.edam = edam.copy() + self.edam["files"] = unused_files + self.edam["files"].append({"name": bit_file, "file_type": "gatemateBitFile"}) + + # Image generation + depends = gmcfg_file + targets = bit_file + command = ( + ["gmpack"] + + self.tool_options.get("gmpack_options", []) + + [depends, targets] + ) + + commands = EdaCommands() + commands.add(command, [targets], [depends]) + commands.set_default_target(targets) + self.commands = commands diff --git a/edalize/tools/nextpnr.py b/edalize/tools/nextpnr.py index 65816a9a..84915b6f 100644 --- a/edalize/tools/nextpnr.py +++ b/edalize/tools/nextpnr.py @@ -29,6 +29,7 @@ def setup(self, edam): lpf_file = "" pcf_file = "" sdc_file = "" + ccf_file = "" netlist = "" chipdb_file = "" placement_constraints = [] @@ -59,6 +60,14 @@ def setup(self, edam): ) ) pcf_file = f["name"] + if file_type == "CCF": + if ccf_file: + raise RuntimeError( + "Nextpnr only supports one CCF file. Found {} and {}".format( + ccf_file, f["name"] + ) + ) + ccf_file = f["name"] if file_type == "chipdb": if chipdb_file: raise RuntimeError( @@ -89,7 +98,7 @@ def setup(self, edam): unused_files.append(f) arch = self._require_tool_option("arch") - arches = ["xilinx", "ecp5", "gowin", "ice40"] + arches = ["xilinx", "ecp5", "gowin", "ice40", "gatemate"] if not arch in arches: raise RuntimeError("Invalid arch. Allowed options are " + ", ".join(arches)) @@ -149,6 +158,21 @@ def setup(self, edam): {"name": targets, "file_type": "nextpnrRoutedJson"}, ] nextpnr_postfix = "himbaechel" + elif arch == "gatemate": + device = self.tool_options.get("device") + if not device: + raise RuntimeError( + "Missing required option 'device' for nextpnr-himbaechel" + ) + arch_options += ["--device", device] + + targets = self.name + ".cfg" + constraints = ["-o ccf={}".format(ccf_file)] if ccf_file else [] + output = ["-o out={}".format(targets)] + output_files += [ + {"name": targets, "file_type": "gatemateConfig"}, + ] + nextpnr_postfix = "himbaechel" else: targets = self.name + ".asc" constraints = ["--pcf", pcf_file] if pcf_file else []