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 <umartinezcorral@antmicro.com>
Signed-off-by: Krzysztof Boronski <kboronski@antmicro.com>
Signed-off-by: Unai Martinez-Corral <umartinezcorral@antmicro.com>
This commit is contained in:
Krzysztof Boronski 2022-07-20 10:59:28 -05:00 committed by Unai Martinez-Corral
parent bb5d0bb24c
commit c406d26b3a
13 changed files with 392 additions and 17 deletions

20
.github/ice40_test.json vendored Normal file
View File

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

View File

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

View File

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

View File

@ -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():

View File

@ -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}!"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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