From e441bd60fa2071f0d13b03b71f50452ac207fc1e Mon Sep 17 00:00:00 2001 From: Piense Date: Mon, 24 Aug 2020 16:15:13 +0200 Subject: [PATCH] build/lattice: add initial Radiant support for NX FPGA family (Crosslink-NX/Certus-NX). --- litex/build/lattice/common.py | 107 ++++++++++++- litex/build/lattice/platform.py | 4 +- litex/build/lattice/radiant.py | 258 ++++++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 litex/build/lattice/radiant.py diff --git a/litex/build/lattice/common.py b/litex/build/lattice/common.py index 14cc68276..f1007f0ef 100644 --- a/litex/build/lattice/common.py +++ b/litex/build/lattice/common.py @@ -4,6 +4,7 @@ # Copyright (c) 2015-2020 Florent Kermarrec # Copyright (c) 2017 William D. Jones # Copyright (c) 2019 David Shah +# Copyright (c) 2020 Piense # SPDX-License-Identifier: BSD-2-Clause from migen.fhdl.module import Module @@ -144,7 +145,111 @@ lattice_ecp5_trellis_special_overrides = { DDROutput: LatticeECP5DDROutput } -# iCE40 AsyncResetSynchronizer ---------------------------------------------------------------------- + +# NX AsyncResetSynchronizer ------------------------------------------------------------------------ + +class LatticeNXsyncResetSynchronizerImpl(Module): + def __init__(self, cd, async_reset): + rst1 = Signal() + self.specials += [ + Instance("FD1P3BX", + i_D = 0, + i_PD = async_reset, + i_CK = cd.clk, + i_SP = 1, + o_Q = rst1), + Instance("FD1P3BX", + i_D = rst1, + i_PD = async_reset, + i_CK = cd.clk, + i_SP = 1, + o_Q = cd.rst) + ] + + +class LatticeNXAsyncResetSynchronizer: + @staticmethod + def lower(dr): + return LatticeNXsyncResetSynchronizerImpl(dr.cd, dr.async_reset) + + +# NX SDR Input ------------------------------------------------------------------------------------- + +class LatticeNXSDRInputImpl(Module): + def __init__(self, i, o, clk): + self.specials += Instance("IFD1P3BX", + i_SCLK = clk, + i_PD = 0, + i_SP = 1, + i_D = i, + o_Q = o, + ) + +class LatticeNXSDRInput: + @staticmethod + def lower(dr): + return LatticeNXSDRInputImpl(dr.i, dr.o, dr.clk) + +# NX SDR Output ------------------------------------------------------------------------------------ + +class LatticeNXSDROutputImpl(Module): + def __init__(self, i, o, clk): + self.specials += Instance("OFD1P3BX", + i_SCLK = clk, + i_PD = 0, + i_SP = 1, + i_D = i, + o_Q = o, + ) + +class LatticeNXSDROutput: + @staticmethod + def lower(dr): + return LatticeNXSDROutputImpl(dr.i, dr.o, dr.clk) + +# NX DDR Input ------------------------------------------------------------------------------------- + +class LatticeNXDDRInputImpl(Module): + def __init__(self, i, o1, o2, clk): + self.specials += Instance("IDDRX1", + i_SCLK = clk, + i_D = i, + o_Q0 = o1, + o_Q1 = o2, + ) + +class LatticeNXDDRInput: + @staticmethod + def lower(dr): + return LatticeNXDDRInputImpl(dr.i, dr.o1, dr.o2, dr.clk) + +# NX DDR Output ------------------------------------------------------------------------------------ + +class LatticeNXDDROutputImpl(Module): + def __init__(self, i1, i2, o, clk): + self.specials += Instance("ODDRX1", + i_SCLK = clk, + i_D0 = i1, + i_D1 = i2, + o_Q = o, + ) + +class LatticeNXDDROutput: + @staticmethod + def lower(dr): + return LatticeNXDDROutputImpl(dr.i1, dr.i2, dr.o, dr.clk) + +# NX Special Overrides ----------------------------------------------------------------------------- + +lattice_NX_special_overrides = { + AsyncResetSynchronizer: LatticeNXAsyncResetSynchronizer, + SDRInput: LatticeNXSDRInput, + SDROutput: LatticeNXSDROutput, + DDRInput: LatticeNXDDRInput, + DDROutput: LatticeNXDDROutput, +} + +# iCE40 AsyncResetSynchronizer --------------------------------------------------------------------- class LatticeiCE40AsyncResetSynchronizerImpl(Module): def __init__(self, cd, async_reset): diff --git a/litex/build/lattice/platform.py b/litex/build/lattice/platform.py index 1dc1a8a2c..aaa0c35d6 100644 --- a/litex/build/lattice/platform.py +++ b/litex/build/lattice/platform.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: BSD-2-Clause from litex.build.generic_platform import GenericPlatform -from litex.build.lattice import common, diamond, icestorm, trellis +from litex.build.lattice import common, diamond, icestorm, trellis, radiant # LatticePlatform ---------------------------------------------------------------------------------- @@ -22,6 +22,8 @@ class LatticePlatform(GenericPlatform): elif toolchain == "icestorm": self.bitstream_ext = ".bin" self.toolchain = icestorm.LatticeIceStormToolchain() + elif toolchain == "radiant": + self.toolchain = radiant.LatticeRadiantToolchain() else: raise ValueError("Unknown toolchain") diff --git a/litex/build/lattice/radiant.py b/litex/build/lattice/radiant.py new file mode 100644 index 000000000..5e8aa8c1a --- /dev/null +++ b/litex/build/lattice/radiant.py @@ -0,0 +1,258 @@ +# +# This file is part of LiteX. +# +# Copyright (c) 2020 Piense +# Copyright (c) 2015-2019 Florent Kermarrec +# Copyright (c) 2017-2018 Sergiusz Bazanski +# Copyright (c) 2017 William D. Jones +# SPDX-License-Identifier: BSD-2-Clause + +import os +import re +import sys +import math +import subprocess +import shutil + +from migen.fhdl.structure import _Fragment + +from litex.gen.fhdl.verilog import DummyAttrTranslate + +from litex.build.generic_platform import * +from litex.build import tools +from litex.build.lattice import common + + +# Constraints (.ldc) ------------------------------------------------------------------------------- + +def _format_constraint(c): + if isinstance(c, Pins): + return ("ldc_set_location -site {" + c.identifiers[0] + "} [get_ports ","]") + elif isinstance(c, IOStandard): + return ("ldc_set_port -iobuf {IO_TYPE="+c.name+"} [get_ports ", "]") + elif isinstance(c, Misc): + return ("ldc_set_port -iobuf {"+c.misc+"} [get_ports ", "]" ) + + +def _format_ldc(signame, pin, others, resname): + fmt_c = [_format_constraint(c) for c in ([Pins(pin)] + others)] + ldc = [] + for pre, suf in fmt_c: + ldc.append(pre + signame + suf) + return "\n".join(ldc) + + +def _build_pdc(named_sc, named_pc, clocks, vns, build_name): + pdc = [] + + for sig, pins, others, resname in named_sc: + if len(pins) > 1: + for i, p in enumerate(pins): + pdc.append(_format_ldc(sig + "[" + str(i) + "]", p, others, resname)) + else: + pdc.append(_format_ldc(sig, pins[0], others, resname)) + if named_pc: + pdc.append("\n".join(named_pc)) + + # Note: .pdc is only used post-synthesis, Synplify constraints clocks by default to 200MHz. + for clk, period in clocks.items(): + clk_name = vns.get_name(clk) + pdc.append("create_clock -period {} -name {} [{} {}];".format( + str(period), + clk_name, + "get_ports" if clk_name in [name for name, _, _, _ in named_sc] else "get_nets", + clk_name + )) + + tools.write_to_file(build_name + ".pdc", "\n".join(pdc)) + +# Project (.tcl) ----------------------------------------------------------------------------------- + +def _build_tcl(device, sources, vincpaths, build_name, pdc_file): + tcl = [] + # Create project + tcl.append(" ".join([ + "prj_create", + "-name \"{}\"".format(build_name), + "-impl \"impl\"", + "-dev {}".format(device), + "-synthesis \"synplify\"" + ])) + + def tcl_path(path): return path.replace("\\", "/") + + # Add include paths + vincpath = ";".join(map(lambda x: tcl_path(x), vincpaths)) + tcl.append("prj_set_impl_opt {include path} {\"" + vincpath + "\"}") + + # Add sources + for filename, language, library in sources: + tcl.append("prj_add_source \"{}\" -work {}".format(tcl_path(filename), library)) + + tcl.append("prj_add_source \"{}\" -work {}".format(tcl_path(pdc_file), library)) + + # Set top level + tcl.append("prj_set_impl_opt top \"{}\"".format(build_name)) + + # Save project + tcl.append("prj_save") + + # Build flow + tcl.append("prj_run Synthesis -impl impl -forceOne") + tcl.append("prj_run Map -impl impl") + tcl.append("prj_run PAR -impl impl") + tcl.append("prj_run Export -impl impl -task Bitgen") + + # Close project + tcl.append("prj_close") + + tools.write_to_file(build_name + ".tcl", "\n".join(tcl)) + +# Script ------------------------------------------------------------------------------------------- + +def _build_script(build_name, device): + if sys.platform in ("win32", "cygwin"): + tool = "pnmainc" + script_ext = ".bat" + script_contents = "@echo off\nrem Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n\n" + copy_stmt = "copy" + fail_stmt = " || exit /b" + else: + tool = "radiantc" + script_ext = ".sh" + script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\nset -e\n" + copy_stmt = "cp" + fail_stmt = "" + + script_contents += "{tool} {tcl_script}{fail_stmt}\n".format( + tool = tool, + tcl_script = build_name + ".tcl", + fail_stmt = fail_stmt) + + script_contents += "{copy_stmt} {radiant_product} {migen_product} {fail_stmt}\n".format( + copy_stmt = copy_stmt, + fail_stmt = fail_stmt, + radiant_product = os.path.join("impl", build_name + "_impl.bit"), + migen_product = build_name + ".bit") + + build_script_file = "build_" + build_name + script_ext + tools.write_to_file(build_script_file, script_contents, force_unix=False) + return build_script_file + +def _run_script(script): + if sys.platform in ("win32", "cygwin"): + shell = ["cmd", "/c"] + else: + shell = ["bash"] + + if subprocess.call(shell + [script]) != 0: + raise OSError("Subprocess failed") + +def _check_timing(build_name): + lines = open("impl/{}_impl.par".format(build_name), "r").readlines() + runs = [None, None] + for i in range(len(lines)-1): + if lines[i].startswith("Level/") and lines[i+1].startswith("Cost "): + runs[0] = i + 2 + if lines[i].startswith("* : Design saved.") and runs[0] is not None: + runs[1] = i + break + assert all(map(lambda x: x is not None, runs)) + + p = re.compile(r"(^\s*\S+\s+\*?\s+[0-9]+\s+)(\S+)(\s+\S+\s+)(\S+)(\s+.*)") + for l in lines[runs[0]:runs[1]]: + m = p.match(l) + if m is None: continue + limit = 1e-8 + setup = m.group(2) + hold = m.group(4) + # If there were no freq constraints in ldc, ratings will be dashed. + # results will likely be terribly unreliable, so bail + assert not setup == hold == "-", "No timing constraints were provided" + setup, hold = map(float, (setup, hold)) + if setup > limit and hold > limit: + # At least one run met timing + # XXX is this necessarily the run from which outputs will be used? + return + raise Exception("Failed to meet timing") + +# LatticeRadiantToolchain -------------------------------------------------------------------------- + +class LatticeRadiantToolchain: + attr_translate = { + # FIXME: document + "keep": ("syn_keep", "true"), + "no_retiming": ("syn_no_retiming", "true"), + "async_reg": None, + "mr_ff": None, + "mr_false_path": None, + "ars_ff1": None, + "ars_ff2": None, + "ars_false_path": None, + "no_shreg_extract": None + } + + special_overrides = common.lattice_NX_special_overrides + + def __init__(self): + self.clocks = {} + self.false_paths = set() # FIXME: use it + + def build(self, platform, fragment, + build_dir = "build", + build_name = "top", + run = True, + timingstrict = True, + **kwargs): + + # Create build directory + os.makedirs(build_dir, exist_ok=True) + cwd = os.getcwd() + os.chdir(build_dir) + + # Finalize design + if not isinstance(fragment, _Fragment): + fragment = fragment.get_fragment() + platform.finalize(fragment) + + # Generate verilog + v_output = platform.get_verilog(fragment, name=build_name, **kwargs) + named_sc, named_pc = platform.resolve_signals(v_output.ns) + v_file = build_name + ".v" + v_output.write(v_file) + platform.add_source(v_file) + + # Generate design constraints file (.pdc) + _build_pdc(named_sc, named_pc, self.clocks, v_output.ns, build_name) + pdc_file = build_dir + "\\" + build_name + ".pdc" + + # Generate design script file (.tcl) + _build_tcl(platform.device, platform.sources, platform.verilog_include_paths, build_name, pdc_file) + + # Generate build script + script = _build_script(build_name, platform.device) + + # Run + if run: + _run_script(script) + if timingstrict: + _check_timing(build_name) + + os.chdir(cwd) + + return v_output.ns + + def add_period_constraint(self, platform, clk, period): + clk.attr.add("keep") + period = math.floor(period*1e3)/1e3 # round to lowest picosecond + if clk in self.clocks: + if period != self.clocks[clk]: + raise ValueError("Clock already constrained to {:.2f}ns, new constraint to {:.2f}ns" + .format(self.clocks[clk], period)) + self.clocks[clk] = period + + def add_false_path_constraint(self, platform, from_, to): + from_.attr.add("keep") + to.attr.add("keep") + if (to, from_) not in self.false_paths: + self.false_paths.add((from_, to))