diamond: clock constraint improvements
authorShawn Hoffman <godisgovernment@gmail.com>
Sat, 9 May 2020 04:00:24 +0000 (21:00 -0700)
committerShawn Hoffman <godisgovernment@gmail.com>
Sat, 9 May 2020 09:28:00 +0000 (02:28 -0700)
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

index cf44d096b94753ca8dfdd77b3953fa62336f3fd2..e48f4852bf383ec528302bc00607fdb2640495d3 100644 (file)
@@ -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")