From c406d26b3ad1802bf373eba13493a4ba57088c8f Mon Sep 17 00:00:00 2001 From: Krzysztof Boronski Date: Wed, 20 Jul 2022 10:59:28 -0500 Subject: [PATCH] f4pga/flows: support ice40 * add nextpnr base module * add nextpnr-ice40 variant * update part_db.yml * add ice40 flow * add CI job 'Lattice' Co-Authored-By: Unai Martinez-Corral Signed-off-by: Krzysztof Boronski Signed-off-by: Unai Martinez-Corral --- .github/ice40_test.json | 20 ++++ .github/workflows/Pipeline.yml | 41 +++++++ f4pga/context.py | 2 +- f4pga/flows/commands.py | 3 +- f4pga/flows/common_modules/nextpnr.py | 107 ++++++++++++++++++ f4pga/flows/common_modules/nextpnr_ice40.py | 74 ++++++++++++ f4pga/flows/common_modules/synth.py | 45 ++++++-- f4pga/flows/part_db.yml | 49 ++++++++ f4pga/flows/platforms.yml | 41 +++++++ f4pga/setup.py | 10 +- f4pga/wrappers/tcl/__init__.py | 5 +- f4pga/wrappers/tcl/ice40.nextpnr.f4pga.tcl | 12 ++ .../{ice40.f4pga.tcl => ice40.vpr.f4pga.tcl} | 0 13 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 .github/ice40_test.json create mode 100644 f4pga/flows/common_modules/nextpnr.py create mode 100644 f4pga/flows/common_modules/nextpnr_ice40.py create mode 100644 f4pga/wrappers/tcl/ice40.nextpnr.f4pga.tcl rename f4pga/wrappers/tcl/{ice40.f4pga.tcl => ice40.vpr.f4pga.tcl} (100%) diff --git a/.github/ice40_test.json b/.github/ice40_test.json new file mode 100644 index 0000000..fa2d368 --- /dev/null +++ b/.github/ice40_test.json @@ -0,0 +1,20 @@ +{ + "default_part": "ICE40UP5K-UWG30", + "values": { + "top": "top" + }, + "dependencies": { + "sources": [ + "blink.v" + ], + "synth_log": "synth.log", + "nextpnr_log": "nextpnr.log" + }, + "ICE40UP5K-UWG30": { + "default_target": "bitstream", + "dependencies": { + "build_dir": "build", + "pcf": "../../../pcf/fomu-pvt.pcf" + } + } +} diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index b469962..de182e3 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -189,6 +189,47 @@ jobs: path: f4pga-examples/${{ matrix.fam }}/btn_counter/build/top.bit if-no-files-found: error + Lattice: + runs-on: ubuntu-latest + name: '🚦 Lattice | ice40' + env: + F4PGA_INSTALL_DIR: /usr/local + FPGA_FAM: ice40 + + steps: + + - name: 🧰 Checkout + uses: actions/checkout@v3 + + - name: 🛠️ Clone fomu-workshop + run: git clone https://github.com/im-tomu/fomu-workshop + + - name: 🚧 [F4PGA] Test f4pga build + run: | + cat > ice40-test.sh <<'EOF' + set -e + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends python3-pip git + + pip install ./f4pga + + cd fomu-workshop/hdl/verilog/blink + f4pga -vv build --flow ../../../../.github/ice40_test.json + EOF + + docker run --rm -i \ + -v $(pwd):/wrk -w /wrk \ + -e FPGA_FAM=ice40 \ + gcr.io/hdl-containers/impl/icestorm \ + bash -le /wrk/ice40-test.sh + + - name: '📤 Upload artifact: ice40 bitstream' + uses: actions/upload-artifact@v3 + with: + name: Lattice-ice40-Bitstream + path: fomu-workshop/hdl/verilog/blink/build/top.bit + if-no-files-found: error + Python-Tests: runs-on: ubuntu-latest diff --git a/f4pga/context.py b/f4pga/context.py index bea88fe..e8e51df 100644 --- a/f4pga/context.py +++ b/f4pga/context.py @@ -22,7 +22,7 @@ from os import environ FPGA_FAM = environ.get("FPGA_FAM", "xc7") -if FPGA_FAM not in ["xc7", "eos-s3", "qlf_k4n8"]: +if FPGA_FAM not in ["xc7", "eos-s3", "qlf_k4n8", "ice40"]: raise (Exception(f"Unsupported FPGA_FAM <{FPGA_FAM}>!")) F4PGA_DEBUG = environ.get("F4PGA_DEBUG") diff --git a/f4pga/flows/commands.py b/f4pga/flows/commands.py index 468bd37..697f0ce 100644 --- a/f4pga/flows/commands.py +++ b/f4pga/flows/commands.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: Apache-2.0 +from sys import exit as sys_exit from typing import Iterable from pathlib import Path from os import environ @@ -115,7 +116,7 @@ def f4pga_fail(): def f4pga_done(): sfprint(1, f"f4pga: {f4pga_done_str}" f"{Style.RESET_ALL + Fore.RESET}") - exit(0) + sys_exit(0 if "FAILED" not in f4pga_done_str else 1) def setup_resolution_env(): diff --git a/f4pga/flows/common_modules/nextpnr.py b/f4pga/flows/common_modules/nextpnr.py new file mode 100644 index 0000000..9f8f050 --- /dev/null +++ b/f4pga/flows/common_modules/nextpnr.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 F4PGA Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from f4pga.flows.common import ResolutionEnv, get_verbosity_level, sub as common_sub +from f4pga.flows.module import Module, ModuleContext + + +class NextPnrBaseModule(Module): + nextpnr_variant: str + extra_nextpnr_opts: "list[str]" + nextpnr_log_name: "str | None" + use_interchange: bool + + def map_io(self, ctx: ModuleContext) -> "dict[str, ]": + return {} + + def execute(self, ctx: ModuleContext): + nextpnr_cmd = f"nextpnr-{self.nextpnr_variant}" + + nextpnr_opts = [ + "--top", + ctx.values.top, + "--placer", + ctx.values.placer, + "--router", + ctx.values.router, + ] + + if self.use_interchange: + nextpnr_opts += ["--netlist", ctx.takes.ic_logical_netlist] + else: + nextpnr_opts += ["--json", ctx.takes.json] + + if ctx.values.prepack_script is not None: + nextpnr_opts += ["--pre-pack", ctx.values.prepack_script] + if ctx.values.preplace_script is not None: + nextpnr_opts += ["--pre-place", ctx.values.preplace_script] + if ctx.values.preroute_script is not None: + nextpnr_opts += ["--pre-route", ctx.values.preroute_script] + if ctx.values.postroute_script is not None: + nextpnr_opts += ["--post-poute", ctx.values.postroute_script] + if ctx.values.fail_script is not None: + nextpnr_opts += ["--on-fail", ctx.values.fail_script] + + if ctx.values.thread_count: + nextpnr_opts += ["--threads", ctx.values.thread_count] + if ctx.values.parallel: + nextpnr_opts += ["--parallel-refine"] + + nextpnr_opts += self.extra_nextpnr_opts + + if get_verbosity_level() >= 2: + yield "Place-and-routing with nextpnr...\n " f'{nextpnr_cmd} {" ".join(nextpnr_opts)}' + else: + yield "Place-and-routing with nextpnr..." + + res = common_sub(nextpnr_cmd, *nextpnr_opts) + + yield "Saving log..." + log_path = getattr(ctx.outputs, self.nextpnr_log_name) + if log_path is not None: + with open(log_path, "w") as f: + f.write(res.decode()) + + def __init__(self, params: "dict[str, ]", interchange=False): + super().__init__(params) + self.name = "nextpnr" + self.nextpnr_variant = "unknown" + self.extra_nextpnr_opts = [] + + self.no_of_phases = 2 + self.use_interchange = interchange + + self.takes = ["ic_logical_netlist"] if self.use_interchange else ["json"] + + self.values = [ + "top", + "placer", + "router", + "prepack_script?", + "preplace_script?", + "preroute_script?", + "postroute_script?", + "fail_script?", + "thread_count?", + "parallel?", + ] + + self.nextpnr_log_name = f"nextpnr_log" + + self.produces = [f"{self.nextpnr_log_name}!"] diff --git a/f4pga/flows/common_modules/nextpnr_ice40.py b/f4pga/flows/common_modules/nextpnr_ice40.py new file mode 100644 index 0000000..e0396c7 --- /dev/null +++ b/f4pga/flows/common_modules/nextpnr_ice40.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 F4PGA Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pathlib +from f4pga.flows.common import ResolutionEnv +from f4pga.flows.module import ModuleContext +from f4pga.flows.common_modules.nextpnr import NextPnrBaseModule + +import re +from pathlib import Path + + +class Ice40ChipInfo: + subfamily: str + size: str + package_code: str + + def __init__(self, part_name: str): + m = re.match("ICE40([A-Z]*)([0-9]+[A-Z]?)-([A-Z0-9]*)$", part_name.upper()) + assert m is not None + + self.subfamily = m.group(1) + self.size = m.group(2) + self.package_code = m.group(3) + + +class NextPnrModule(NextPnrBaseModule): + def map_io(self, ctx: ModuleContext) -> "dict[str, ]": + return {"ice_asm": str(Path(ctx.takes.json).with_suffix(".ice"))} + + def execute(self, ctx: ModuleContext): + chip_info = Ice40ChipInfo(ctx.values.part_name) + + self.extra_nextpnr_opts = [ + f"--{(chip_info.subfamily + chip_info.size).lower()}", + f"--package", + chip_info.package_code.lower(), + f"--asc", + ctx.outputs.ice_asm, + ] + + if ctx.takes.pcf is not None: + self.extra_nextpnr_opts += ["--pcf", ctx.takes.pcf] + else: + self.extra_nextpnr_opts += ["--pcf-allow-unconstrained"] + + return super().execute(ctx) + + def __init__(self, params: "dict[str, ]"): + super().__init__(params, interchange=False) + self.name = "nextpnr-ice40" + self.nextpnr_variant = "ice40" + self.takes += ["pcf?"] + self.values += ["part_name"] + self.produces += ["ice_asm"] + + +ModuleClass = NextPnrModule diff --git a/f4pga/flows/common_modules/synth.py b/f4pga/flows/common_modules/synth.py index 6fae1aa..56f8176 100755 --- a/f4pga/flows/common_modules/synth.py +++ b/f4pga/flows/common_modules/synth.py @@ -20,11 +20,15 @@ from os import environ from pathlib import Path +from f4pga.context import FPGA_FAM from f4pga.flows.common import decompose_depname, get_verbosity_level, sub as common_sub from f4pga.flows.module import Module, ModuleContext from f4pga.wrappers.tcl import get_script_path as get_tcl_wrapper_path +isLattice = FPGA_FAM == "ice40" + + class SynthModule(Module): extra_products: "list[str]" @@ -69,35 +73,52 @@ class SynthModule(Module): # Execute YOSYS command args_str = "" if ctx.values.read_verilog_args is None else " ".join(ctx.values.read_verilog_args) + + yosys_extra_args = ["-l", ctx.outputs.synth_log] if ctx.outputs.synth_log else [] + if isLattice: + yosys_extra_args.extend(["-D", "PVT=1"]) + common_sub( *( - [ - "yosys", + ["yosys"] + + yosys_extra_args + + [ "-p", ( " ".join([f"read_verilog {args_str} {vfile};" for vfile in ctx.takes.sources]) - + f" tcl {str(get_tcl_wrapper_path())}" + + f" tcl {str(get_tcl_wrapper_path(pnrtool=self.pnrtool))}" ), ] - + (["-l", ctx.outputs.synth_log] if ctx.outputs.synth_log else []) ), env=env, ) - if not Path(ctx.produces.fasm_extra).is_file(): - with Path(ctx.produces.fasm_extra).open("w") as wfptr: - wfptr.write("") + if self.pnrtool == "vpr": + if not Path(ctx.produces.fasm_extra).is_file(): + with Path(ctx.produces.fasm_extra).open("w") as wfptr: + wfptr.write("") def __init__(self, params): self.name = "synthesize" self.no_of_phases = 3 + + self.pnrtool = "nextpnr" if isLattice else "vpr" + self.takes = ["sources", "build_dir?"] # Extra takes for use with TCL scripts extra_takes = params.get("takes") if extra_takes: self.takes += extra_takes - self.produces = ["eblif", "fasm_extra", "json", "synth_json", "synth_log!"] + self.produces = ["json", "synth_log!"] + if self.pnrtool == "vpr": + self.produces.extend( + [ + "eblif", + "fasm_extra", + "synth_json", + ] + ) # Extra products for use with TCL scripts extra_products = params.get("produces") if extra_products: @@ -106,7 +127,13 @@ class SynthModule(Module): else: self.extra_products = [] - self.values = ["top", "device", "tcl_scripts", "yosys_tcl_env?", "read_verilog_args?"] + self.values = [ + "top", + "device", + "tcl_scripts?", + "yosys_tcl_env?", + "read_verilog_args?", + ] self.prod_meta = { "eblif": "Extended BLIF hierarchical sequential designs file\n" "generated by YOSYS", "json": "JSON file containing a design generated by YOSYS", diff --git a/f4pga/flows/part_db.yml b/f4pga/flows/part_db.yml index b04020a..35165fa 100644 --- a/f4pga/flows/part_db.yml +++ b/f4pga/flows/part_db.yml @@ -45,3 +45,52 @@ ql-k4n8_slow: ql-k4n8_fast: - K4N8_FAST + +ice40: + - ICE40LP1K-CB121 + - ICE40LP1K-CB81 + - ICE40LP4K-CM225 + - ICE40LP8K-CM225 + - ICE40LP1K-CM121 + - ICE40LP4K-CM121 + - ICE40LP8K-CM121 + - ICE40LP384-CM36 + - ICE40LP1K-CM36 + - ICE40LP384-CM49 + - ICE40LP1K-CM49 + - ICE40LP1K-CM81 + - ICE40LP4K-CM81 + - ICE40LP8K-CM81 + - ICE40LP1K-QN84 + - ICE40LP384-SG32 + - ICE40LP640-SWG16 + - ICE40LP1K-SWG16 + - ICE40LP384-VQ100 + - ICE40LP640-VQ100 + - ICE40LP1K-VQ100 + - ICE40LP4K-VQ100 + - ICE40LP8K-VQ100 + - ICE40HX1K-CB132 + - ICE40HX4K-CB132 + - ICE40HX8K-CB132 + - ICE40HX1K-VQ100 + - ICE40HX1K-TQ144 + - ICE40HX4K-TQ144 + - ICE40HX8K-CM225 + - ICE40HX8K-CT256 + - ICE40UP3K-UWG30 + - ICE40UP3K-SG48 + - ICE40UP5K-UWG30 + - ICE40UP5K-SG48 + - ICE40UL640-SWG16 + - ICE40UL640-CM36 + - ICE40UL1K-CM36 + - ICE5LP1K-SWG36 + - ICE5LP2K-SWG36 + - ICE5LP4K-SWG36 + - ICE5LP1K-CM36 + - ICE5LP2K-CM36 + - ICE5LP4K-CM36 + - ICE5LP1K-SG48 + - ICE5LP2K-SG48 + - ICE5LP4K-SG4 diff --git a/f4pga/flows/platforms.yml b/f4pga/flows/platforms.yml index 4313795..84954ea 100644 --- a/f4pga/flows/platforms.yml +++ b/f4pga/flows/platforms.yml @@ -180,6 +180,47 @@ xc7a200t: vpr_options: *xc7-vpr_options +ice40: + + values: + device: ICE40UP5K + nextpnr_options: + hx1k: true + + stages: + mk_build_dir: + module: 'common:mkdirs' + params: + build_dir: build/${device} + synth: + module: 'common:synth' + params: + takes: + produces: + prod_meta: + values: + yosys_tcl_env: + OUT_JSON: '${:json}' + pnr: + module: 'common:nextpnr_ice40' + values: + placer: heap + router: router1 + bitstream: + module: 'common:generic_script_wrapper' + params: + stage_name: bitstream + script: icepack + outputs: + bitstream: + mode: file + file: "${:ice_asm[noext]}.bit" + target: "${:ice_asm[noext]}.bit" + inputs: + "#1": "${:ice_asm}" + "#2": "${:ice_asm[noext]}.bit" + + ql-eos-s3: values: diff --git a/f4pga/setup.py b/f4pga/setup.py index 61a286b..c9e8416 100644 --- a/f4pga/setup.py +++ b/f4pga/setup.py @@ -52,10 +52,12 @@ def get_requirements(file: Path) -> List[str]: semver = "0.0.0" version = None -with (packagePath.parent / ".gitcommit").open("r") as rptr: - sha = rptr.read().strip() - if sha != "$Format:%h$": - version = f"{semver}+{sha}" +gitcommit = packagePath.parent / ".gitcommit" +if gitcommit.exists(): + with gitcommit.open("r") as rptr: + sha = rptr.read().strip() + if sha != "$Format:%h$": + version = f"{semver}+{sha}" git = which("git") if git is not None: diff --git a/f4pga/wrappers/tcl/__init__.py b/f4pga/wrappers/tcl/__init__.py index 7b73c5c..1535e0c 100644 --- a/f4pga/wrappers/tcl/__init__.py +++ b/f4pga/wrappers/tcl/__init__.py @@ -28,7 +28,7 @@ ROOT = Path(__file__).resolve().parent ARCHS = {"xc7": ["artix7", "artix7_100t", "artix7_200t", "zynq7", "zynq7_z020", "spartan7"], "eos-s3": ["ql-s3", "pp3"]} -def get_script_path(arch=None): +def get_script_path(arch=None, pnrtool="vpr"): if arch is None: arch = FPGA_FAM for key, val in ARCHS.items(): @@ -37,4 +37,5 @@ def get_script_path(arch=None): break if arch not in ["xc7", "eos-s3", "qlf_k4n8", "ice40"]: raise (Exception(f"Unsupported arch <{arch}>!")) - return ROOT / f"{arch}.f4pga.tcl" + suffix = f".{pnrtool}" if arch == "ice40" else "" + return ROOT / f"{arch}{suffix}.f4pga.tcl" diff --git a/f4pga/wrappers/tcl/ice40.nextpnr.f4pga.tcl b/f4pga/wrappers/tcl/ice40.nextpnr.f4pga.tcl new file mode 100644 index 0000000..66c35c4 --- /dev/null +++ b/f4pga/wrappers/tcl/ice40.nextpnr.f4pga.tcl @@ -0,0 +1,12 @@ +yosys -import + +synth_ice40 -nocarry + +opt_expr -undriven +opt_clean + +attrmap -remove hdlname +setundef -zero -params + +write_json $::env(OUT_JSON) +#write_verilog $::env(OUT_SYNTH_V) diff --git a/f4pga/wrappers/tcl/ice40.f4pga.tcl b/f4pga/wrappers/tcl/ice40.vpr.f4pga.tcl similarity index 100% rename from f4pga/wrappers/tcl/ice40.f4pga.tcl rename to f4pga/wrappers/tcl/ice40.vpr.f4pga.tcl