From 9b782bd7da28b00b5f8b292ca0741503484b7ed7 Mon Sep 17 00:00:00 2001 From: Shawn Hoffman Date: Fri, 8 May 2020 21:00:24 -0700 Subject: [PATCH] diamond: clock constraint improvements Specify NET or PORT for freq constraints Add equivalent timing closure check that diamond ui uses, and default to asserting check has passed --- litex/build/lattice/diamond.py | 71 ++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/litex/build/lattice/diamond.py b/litex/build/lattice/diamond.py index cf44d096..e48f4852 100644 --- a/litex/build/lattice/diamond.py +++ b/litex/build/lattice/diamond.py @@ -4,6 +4,7 @@ # License: BSD import os +import re import sys import subprocess import shutil @@ -40,18 +41,29 @@ def _format_lpf(signame, pin, others, resname): return "\n".join(lpf) -def _build_lpf(named_sc, named_pc, build_name): +def _build_lpf(named_sc, named_pc, cc, build_name): lpf = [] lpf.append("BLOCK RESETPATHS;") lpf.append("BLOCK ASYNCPATHS;") + clk_ports = set() for sig, pins, others, resname in named_sc: if len(pins) > 1: for i, p in enumerate(pins): lpf.append(_format_lpf(sig + "[" + str(i) + "]", p, others, resname)) else: lpf.append(_format_lpf(sig, pins[0], others, resname)) + if sig in cc.keys(): + clk_ports.add(sig) if named_pc: - lpf.append("\n\n".join(named_pc)) + lpf.append("\n".join(named_pc)) + + # NOTE: The lpf is only used post-synthesis. Currently Synplify is fed no constraints, + # so defaults to inferring clocks and trying to hit 200MHz on all of them. + # NOTE: For additional options (PAR_ADJ, etc), use add_platform_command + for clk, freq in cc.items(): + sig_type = "PORT" if clk in clk_ports else "NET" + lpf.append("FREQUENCY {} \"{}\" {} MHz;".format(sig_type, clk, freq)) + tools.write_to_file(build_name + ".lpf", "\n".join(lpf)) # Project (.tcl) ----------------------------------------------------------------------------------- @@ -67,13 +79,15 @@ def _build_tcl(device, sources, vincpaths, build_name): "-synthesis \"synplify\"" ])) + def tcl_path(path): return path.replace("\\", "/") + # Add include paths - vincpath = ';'.join(map(lambda x: x.replace('\\', '/'), vincpaths)) + vincpath = ";".join(map(lambda x: tcl_path(x), vincpaths)) tcl.append("prj_impl option {include path} {\"" + vincpath + "\"}") # Add sources for filename, language, library in sources: - tcl.append("prj_src add \"" + filename.replace("\\", "/") + "\" -work " + library) + tcl.append("prj_src add \"{}\" -work {}".format(tcl_path(filename), library)) # Set top level tcl.append("prj_impl option top \"{}\"".format(build_name)) @@ -89,6 +103,7 @@ def _build_tcl(device, sources, vincpaths, build_name): tcl.append("prj_run Export -impl impl -task Bitgen") if _produces_jedec(device): tcl.append("prj_run Export -impl impl -task Jedecgen") + tools.write_to_file(build_name + ".tcl", "\n".join(tcl)) # Script ------------------------------------------------------------------------------------------- @@ -130,6 +145,35 @@ def _run_script(script): if subprocess.call(shell + [script]) != 0: raise OSError("Subprocess failed") +# This operates the same way tmcheck does, but tmcheck isn't usable without gui +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 lpf, 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") + # LatticeDiamondToolchain -------------------------------------------------------------------------- class LatticeDiamondToolchain: @@ -149,12 +193,14 @@ class LatticeDiamondToolchain: special_overrides = common.lattice_ecp5_special_overrides def __init__(self): + self.period_constraints = [] 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 @@ -174,8 +220,15 @@ class LatticeDiamondToolchain: v_output.write(v_file) platform.add_source(v_file) + cc = {} + for (clk, freq) in self.period_constraints: + clk_name = v_output.ns.get_name(clk) + if clk_name in cc and cc[clk_name] != freq: + raise ConstraintError("Differing period constraints on {}".format(clk_name)) + cc[clk_name] = freq + # Generate design constraints file (.lpf) - _build_lpf(named_sc, named_pc, build_name) + _build_lpf(named_sc, named_pc, cc, build_name) # Generate design script file (.tcl) _build_tcl(platform.device, platform.sources, platform.verilog_include_paths, build_name) @@ -186,16 +239,18 @@ class LatticeDiamondToolchain: # 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") # TODO: handle differential clk - platform.add_platform_command("""FREQUENCY PORT "{clk}" {freq} MHz;""".format( - freq=str(float(1/period)*1000), clk="{clk}"), clk=clk) + clk.attr.add("keep") + freq = str(float(1/period)*1000) + self.period_constraints.append((clk, freq)) def add_false_path_constraint(self, platform, from_, to): from_.attr.add("keep") -- 2.30.2