From 9db1d9e49f39ee4ada7ce48628e53b8b1c662e47 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 23 Jun 2022 08:13:07 +0200 Subject: [PATCH] toolchain: implement generic toolchain and devices toolchain refactoring --- litex/build/altera/quartus.py | 387 ++++++++++++++----------------- litex/build/generic_toolchain.py | 96 ++++++++ litex/build/lattice/icestorm.py | 303 +++++++++++------------- litex/build/lattice/trellis.py | 358 +++++++++++++--------------- 4 files changed, 565 insertions(+), 579 deletions(-) create mode 100644 litex/build/generic_toolchain.py diff --git a/litex/build/altera/quartus.py b/litex/build/altera/quartus.py index c4bf96ac7..986920ddf 100644 --- a/litex/build/altera/quartus.py +++ b/litex/build/altera/quartus.py @@ -15,253 +15,206 @@ from shutil import which from migen.fhdl.structure import _Fragment from litex.build.generic_platform import Pins, IOStandard, Misc +from litex.build.generic_toolchain import GenericToolchain from litex.build import tools -# IO/Placement Constraints (.qsf) ------------------------------------------------------------------ +# AlteraQuartusToolchain --------------------------------------------------------------------------- -def _format_constraint(c, signame, fmt_r): - # IO location constraints - if isinstance(c, Pins): - tpl = "set_location_assignment -comment \"{name}\" -to {signame} Pin_{pin}" - return tpl.format(signame=signame, name=fmt_r, pin=c.identifiers[0]) +class AlteraQuartusToolchain(GenericToolchain): + attr_translate = {} - # IO standard constraints - elif isinstance(c, IOStandard): - tpl = "set_instance_assignment -name io_standard -comment \"{name}\" \"{std}\" -to {signame}" - return tpl.format(signame=signame, name=fmt_r, std=c.name) + def __init__(self): + super().__init__() + self.additional_sdc_commands = [] + self.additional_qsf_commands = [] + self.cst = [] - # Others constraints - elif isinstance(c, Misc): - if not isinstance(c.misc, str) and len(c.misc) == 2: - tpl = "set_instance_assignment -comment \"{name}\" -name {misc[0]} \"{misc[1]}\" -to {signame}" - return tpl.format(signame=signame, name=fmt_r, misc=c.misc) - else: - tpl = "set_instance_assignment -comment \"{name}\" -name {misc} -to {signame}" - return tpl.format(signame=signame, name=fmt_r, misc=c.misc) + def build(self, platform, fragment, **kwargs): -def _format_qsf_constraint(signame, pin, others, resname): - fmt_r = "{}:{}".format(*resname[:2]) - if resname[2] is not None: - fmt_r += "." + resname[2] - fmt_c = [_format_constraint(c, signame, fmt_r) for c in ([Pins(pin)] + others)] - return '\n'.join(fmt_c) + return self._build(platform, fragment, **kwargs) -def _is_virtual_pin(pin_name): - return pin_name in ( - "altera_reserved_tms", - "altera_reserved_tck", - "altera_reserved_tdi", - "altera_reserved_tdo", - ) + # IO/Placement Constraints (.qsf) ------------------------------------------------------------------ -def _build_qsf_constraints(named_sc, named_pc): - qsf = [] - for sig, pins, others, resname in named_sc: - if len(pins) > 1: - for i, p in enumerate(pins): - if _is_virtual_pin(p): - continue - qsf.append(_format_qsf_constraint("{}[{}]".format(sig, i), p, others, resname)) - else: - if _is_virtual_pin(pins[0]): - continue - qsf.append(_format_qsf_constraint(sig, pins[0], others, resname)) - if named_pc: - qsf.append("\n\n".join(named_pc)) - return "\n".join(qsf) + def _format_constraint(self, c, signame, fmt_r): + # IO location constraints + if isinstance(c, Pins): + tpl = "set_location_assignment -comment \"{name}\" -to {signame} Pin_{pin}" + return tpl.format(signame=signame, name=fmt_r, pin=c.identifiers[0]) -# Timing Constraints (.sdc) ------------------------------------------------------------------------ + # IO standard constraints + elif isinstance(c, IOStandard): + tpl = "set_instance_assignment -name io_standard -comment \"{name}\" \"{std}\" -to {signame}" + return tpl.format(signame=signame, name=fmt_r, std=c.name) -def _build_sdc(clocks, false_paths, vns, named_sc, build_name, additional_sdc_commands): - sdc = [] + # Others constraints + elif isinstance(c, Misc): + if not isinstance(c.misc, str) and len(c.misc) == 2: + tpl = "set_instance_assignment -comment \"{name}\" -name {misc[0]} \"{misc[1]}\" -to {signame}" + return tpl.format(signame=signame, name=fmt_r, misc=c.misc) + else: + tpl = "set_instance_assignment -comment \"{name}\" -name {misc} -to {signame}" + return tpl.format(signame=signame, name=fmt_r, misc=c.misc) - # Clock constraints - for clk, period in sorted(clocks.items(), key=lambda x: x[0].duid): - is_port = False + def _format_qsf_constraint(self, signame, pin, others, resname): + fmt_r = "{}:{}".format(*resname[:2]) + if resname[2] is not None: + fmt_r += "." + resname[2] + fmt_c = [self._format_constraint(c, signame, fmt_r) for c in ([Pins(pin)] + others)] + return '\n'.join(fmt_c) + + def _is_virtual_pin(self, pin_name): + return pin_name in ( + "altera_reserved_tms", + "altera_reserved_tck", + "altera_reserved_tdi", + "altera_reserved_tdo", + ) + + def build_constr_file(self, named_sc, named_pc): for sig, pins, others, resname in named_sc: - if sig == vns.get_name(clk): - is_port = True - if is_port: - tpl = "create_clock -name {clk} -period {period} [get_ports {{{clk}}}]" - sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period))) + if len(pins) > 1: + for i, p in enumerate(pins): + if self._is_virtual_pin(p): + continue + self.cst.append(self._format_qsf_constraint("{}[{}]".format(sig, i), p, others, resname)) + else: + if self._is_virtual_pin(pins[0]): + continue + self.cst.append(self._format_qsf_constraint(sig, pins[0], others, resname)) + if named_pc: + self.cst.append("\n\n".join(named_pc)) + + # Timing Constraints (.sdc) ------------------------------------------------------------------------ + + def build_timing_constr(self, vns, clocks): + sdc = [] + + # Clock constraints + for clk, period in sorted(clocks.items(), key=lambda x: x[0].duid): + is_port = False + for sig, pins, others, resname in self.named_sc: + if sig == vns.get_name(clk): + is_port = True + if is_port: + tpl = "create_clock -name {clk} -period {period} [get_ports {{{clk}}}]" + sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period))) + else: + tpl = "create_clock -name {clk} -period {period} [get_nets {{{clk}}}]" + sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period))) + + # Enable automatical constraint generation for PLLs + sdc.append("derive_pll_clocks") + + # False path constraints + for from_, to in sorted(self.false_paths, key=lambda x: (x[0].duid, x[1].duid)): + tpl = "set_false_path -from [get_clocks {{{from_}}}] -to [get_clocks {{{to}}}]" + sdc.append(tpl.format(from_=vns.get_name(from_), to=vns.get_name(to))) + + # Add additional commands + sdc += self.additional_sdc_commands + + # Generate .sdc + tools.write_to_file("{}.sdc".format(self._build_name), "\n".join(sdc)) + + # Project (.qsf) ----------------------------------------------------------------------------------- + + def build_project(self): + qsf = [] + + # Set device + qsf.append("set_global_assignment -name DEVICE {}".format(self.platform.device)) + + # Add sources + for filename, language, library, *copy in self.platform.sources: + if language == "verilog": language = "systemverilog" # Enforce use of SystemVerilog + tpl = "set_global_assignment -name {lang}_FILE {path} -library {lib}" + # Do not add None type files + if language is not None: + qsf.append(tpl.format(lang=language.upper(), path=filename.replace("\\", "/"), lib=library)) + # Check if the file is a header. Those should not be explicitly added to qsf, + # but rather included in include search_path + else: + if filename.endswith(".svh") or filename.endswith(".vh"): + fpath = os.path.dirname(filename) + if fpath not in platform.verilog_include_paths: + platform.verilog_include_paths.append(fpath) + + # Add ips + for filename in self.platform.ips: + tpl = "set_global_assignment -name QSYS_FILE {filename}" + qsf.append(tpl.replace(filename=filename.replace("\\", "/"))) + + # Add include paths + for path in self.platform.verilog_include_paths: + qsf.append("set_global_assignment -name SEARCH_PATH {}".format(path.replace("\\", "/"))) + + # Set top level + qsf.append("set_global_assignment -name top_level_entity " + self._build_name) + + # Add io, placement constraints + qsf.append("\n".join(self.cst)) + + # Set timing constraints + qsf.append("set_global_assignment -name SDC_FILE {}.sdc".format(self._build_name)) + + # Add additional commands + qsf += self.additional_qsf_commands + + # Generate .qsf + tools.write_to_file("{}.qsf".format(self._build_name), "\n".join(qsf)) + + # Script ------------------------------------------------------------------------------------------- + + def build_script(self): + build_name = self._build_name + + self._build_qsf() + + if sys.platform in ["win32", "cygwin"]: + script_file = "build_" + build_name + ".bat" + script_contents = "REM Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n" else: - tpl = "create_clock -name {clk} -period {period} [get_nets {{{clk}}}]" - sdc.append(tpl.format(clk=vns.get_name(clk), period=str(period))) - - # Enable automatical constraint generation for PLLs - sdc.append("derive_pll_clocks") - - # False path constraints - for from_, to in sorted(false_paths, key=lambda x: (x[0].duid, x[1].duid)): - tpl = "set_false_path -from [get_clocks {{{from_}}}] -to [get_clocks {{{to}}}]" - sdc.append(tpl.format(from_=vns.get_name(from_), to=vns.get_name(to))) - - # Add additional commands - sdc += additional_sdc_commands - - # Generate .sdc - tools.write_to_file("{}.sdc".format(build_name), "\n".join(sdc)) - -# Project (.qsf) ----------------------------------------------------------------------------------- - -def _build_qsf(device, ips, sources, vincpaths, named_sc, named_pc, build_name, additional_qsf_commands): - qsf = [] - - # Set device - qsf.append("set_global_assignment -name DEVICE {}".format(device)) - - # Add sources - for filename, language, library, *copy in sources: - if language == "verilog": language = "systemverilog" # Enforce use of SystemVerilog - tpl = "set_global_assignment -name {lang}_FILE {path} -library {lib}" - # Do not add None type files - if language is not None: - qsf.append(tpl.format(lang=language.upper(), path=filename.replace("\\", "/"), lib=library)) - # Check if the file is a header. Those should not be explicitly added to qsf, - # but rather included in include search_path - else: - if filename.endswith(".svh") or filename.endswith(".vh"): - fpath = os.path.dirname(filename) - if fpath not in vincpaths: - vincpaths.append(fpath) - - # Add ips - for filename in ips: - tpl = "set_global_assignment -name QSYS_FILE {filename}" - qsf.append(tpl.replace(filename=filename.replace("\\", "/"))) - - # Add include paths - for path in vincpaths: - qsf.append("set_global_assignment -name SEARCH_PATH {}".format(path.replace("\\", "/"))) - - # Set top level - qsf.append("set_global_assignment -name top_level_entity " + build_name) - - # Add io, placement constraints - qsf.append(_build_qsf_constraints(named_sc, named_pc)) - - # Set timing constraints - qsf.append("set_global_assignment -name SDC_FILE {}.sdc".format(build_name)) - - # Add additional commands - qsf += additional_qsf_commands - - # Generate .qsf - tools.write_to_file("{}.qsf".format(build_name), "\n".join(qsf)) - -# Script ------------------------------------------------------------------------------------------- - -def _build_script(build_name, create_rbf): - if sys.platform in ["win32", "cygwin"]: - script_file = "build_" + build_name + ".bat" - script_contents = "REM Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n" - else: - script_file = "build_" + build_name + ".sh" - script_contents = "#!/usr/bin/env bash\n" - script_contents += "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n" - script_contents += "set -e -u -x -o pipefail\n" - script_contents += """ + script_file = "build_" + build_name + ".sh" + script_contents = "#!/usr/bin/env bash\n" + script_contents += "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n" + script_contents += "set -e -u -x -o pipefail\n" + script_contents += """ quartus_map --read_settings_files=on --write_settings_files=off {build_name} -c {build_name} quartus_fit --read_settings_files=off --write_settings_files=off {build_name} -c {build_name} quartus_asm --read_settings_files=off --write_settings_files=off {build_name} -c {build_name} quartus_sta {build_name} -c {build_name}""" - if create_rbf: - if sys.platform in ["win32", "cygwin"]: - script_contents += """ + if self.platform.create_rbf: + if sys.platform in ["win32", "cygwin"]: + script_contents += """ if exist "{build_name}.sof" ( quartus_cpf -c {build_name}.sof {build_name}.rbf ) """ - else: - script_contents += """ + else: + script_contents += """ if [ -f "{build_name}.sof" ] then quartus_cpf -c {build_name}.sof {build_name}.rbf fi """ - script_contents = script_contents.format(build_name=build_name) - tools.write_to_file(script_file, script_contents, force_unix=True) + script_contents = script_contents.format(build_name=build_name) + tools.write_to_file(script_file, script_contents, force_unix=True) - return script_file + return script_file -def _run_script(script): - if sys.platform in ["win32", "cygwin"]: - shell = ["cmd", "/c"] - else: - shell = ["bash"] + def run_script(self, script): + if sys.platform in ["win32", "cygwin"]: + shell = ["cmd", "/c"] + else: + shell = ["bash"] - if which("quartus_map") is None: - msg = "Unable to find Quartus toolchain, please:\n" - msg += "- Add Quartus toolchain to your $PATH." - raise OSError(msg) + if which("quartus_map") is None: + msg = "Unable to find Quartus toolchain, please:\n" + msg += "- Add Quartus toolchain to your $PATH." + raise OSError(msg) - if subprocess.call(shell + [script]) != 0: - raise OSError("Error occured during Quartus's script execution.") - -# AlteraQuartusToolchain --------------------------------------------------------------------------- - -class AlteraQuartusToolchain: - attr_translate = {} - - def __init__(self): - self.clocks = dict() - self.false_paths = set() - self.additional_sdc_commands = [] - self.additional_qsf_commands = [] - - def build(self, platform, fragment, - build_dir = "build", - build_name = "top", - run = True, - **kwargs): - - # Create build directory - cwd = os.getcwd() - os.makedirs(build_dir, exist_ok=True) - 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 timing constraints file (.sdc) - _build_sdc( - clocks = self.clocks, - false_paths = self.false_paths, - vns = v_output.ns, - named_sc = named_sc, - build_name = build_name, - additional_sdc_commands = self.additional_sdc_commands) - - # Generate design project and location constraints file (.qsf) - _build_qsf( - device = platform.device, - ips = platform.ips, - sources = platform.sources, - vincpaths = platform.verilog_include_paths, - named_sc = named_sc, - named_pc = named_pc, - build_name = build_name, - additional_qsf_commands = self.additional_qsf_commands) - - # Generate build script - script = _build_script(build_name, platform.create_rbf) - - # Run - if run: - _run_script(script) - - os.chdir(cwd) - - return v_output.ns + if subprocess.call(shell + [script]) != 0: + raise OSError("Error occured during Quartus's script execution.") def add_period_constraint(self, platform, clk, period): clk.attr.add("keep") diff --git a/litex/build/generic_toolchain.py b/litex/build/generic_toolchain.py new file mode 100644 index 000000000..7d208d3e8 --- /dev/null +++ b/litex/build/generic_toolchain.py @@ -0,0 +1,96 @@ +# +# This file is part of LiteX. +# +# Copyright (c) 2017-2018 William D. Jones +# Copyright (c) 2019 Florent Kermarrec +# Copyright (c) 2022 Gwenhael Goavec-Merou +# SPDX-License-Identifier: BSD-2-Clause + + +import os +import sys +import subprocess +from shutil import which + +from migen.fhdl.structure import _Fragment + +from litex.build.generic_platform import * +from litex.build import tools + + +# GenericToolchain ------------------------------------------------------------------------- + +class GenericToolchain: + attr_translate = { + "keep": ("keep", "true"), + } + + def __init__(self): + self.clocks = dict() + self.false_paths = set() # FIXME: use it + self.named_pc = [] + self.named_sc = [] + + # method use to build timing params + def build_timing_constr(self, vns, clocks): + pass + + # method use to build project (if required) + def build_timing_constr(self, vns, clocks): + pass + + def _build(self, platform, fragment, + build_dir = "build", + build_name = "top", + synth_opts = "", + run = True, + **kwargs): + + self._build_name = build_name + self._synth_opts = synth_opts + self.platform = platform + + # 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) + self.named_sc, self.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 io constraints file + self.build_constr_file(self.named_sc, self.named_pc) + + # Generate design timing constraints file (in timing_constr file) + self.build_timing_constr(v_output.ns, self.clocks) + + # Generate project + self.build_project() + + # Generate build script + script = self.build_script() + + # Run + if run: + self.run_script(script) + + os.chdir(cwd) + + return v_output.ns + + def add_period_constraint(self, platform, clk, period): + clk.attr.add("keep") + 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 diff --git a/litex/build/lattice/icestorm.py b/litex/build/lattice/icestorm.py index 6e5efd92d..beb8d70c5 100644 --- a/litex/build/lattice/icestorm.py +++ b/litex/build/lattice/icestorm.py @@ -16,138 +16,11 @@ from migen.fhdl.structure import _Fragment from litex.build.generic_platform import * from litex.build import tools from litex.build.lattice import common - -# IO Constraints (.pcf) ---------------------------------------------------------------------------- - -def _build_pcf(named_sc, named_pc): - r = "" - for sig, pins, others, resname in named_sc: - if len(pins) > 1: - for bit, pin in enumerate(pins): - r += "set_io {}[{}] {}\n".format(sig, bit, pin) - else: - r += "set_io {} {}\n".format(sig, pins[0]) - if named_pc: - r += "\n" + "\n\n".join(named_pc) - return r - -# Timing Constraints (in pre_pack file) ------------------------------------------------------------ - -def _build_pre_pack(vns, clocks): - r = "" - for clk, period in clocks.items(): - r += """ctx.addClock("{}", {})\n""".format(vns.get_name(clk), 1e3/period) - return r - -# Yosys/Nextpnr Helpers/Templates ------------------------------------------------------------------ - -_yosys_template = [ - "verilog_defaults -push", - "verilog_defaults -add -defer", - "{read_files}", - "verilog_defaults -pop", - "attrmap -tocase keep -imap keep=\"true\" keep=1 -imap keep=\"false\" keep=0 -remove keep=0", - "synth_ice40 {synth_opts} -json {build_name}.json -top {build_name} -dsp", -] - -def _yosys_import_sources(platform): - includes = "" - reads = [] - for path in platform.verilog_include_paths: - includes += " -I" + path - for filename, language, library, *copy in platform.sources: - # yosys has no such function read_systemverilog - if language == "systemverilog": - language = "verilog -sv" - reads.append("read_{}{} {}".format( - language, includes, filename)) - return "\n".join(reads) - -def _build_yosys(template, platform, build_name, synth_opts): - ys = [] - for l in template: - ys.append(l.format( - build_name = build_name, - read_files = _yosys_import_sources(platform), - synth_opts = synth_opts - )) - tools.write_to_file(build_name + ".ys", "\n".join(ys)) - -def parse_device(device): - packages = { - "lp384": ["qn32", "cm36", "cm49"], - "lp1k": ["swg16tr", "cm36", "cm49", "cm81", "cb81", "qn84", "cm121", "cb121"], - "hx1k": ["vq100", "cb132", "tq144"], - "lp8k": ["cm81", "cm81:4k", "cm121", "cm121:4k", "cm225", "cm225:4k"], - "hx8k": ["bg121", "bg121:4k", "cb132", "cb132:4k", "cm121", - "cm121:4k", "cm225", "cm225:4k", "cm81", "cm81:4k", - "ct256", "tq144:4k"], - "up3k": ["sg48", "uwg30"], - "up5k": ["sg48", "uwg30"], - "u4k": ["sg48"], - } - - (family, architecture, package) = device.split("-") - if family not in ["ice40"]: - raise ValueError("Unknown device family {}".format(family)) - if architecture not in packages.keys(): - raise ValueError("Invalid device architecture {}".format(architecture)) - if package not in packages[architecture]: - raise ValueError("Invalid device package {}".format(package)) - return (family, architecture, package) - -# Script ------------------------------------------------------------------------------------------- - -_build_template = [ - "yosys -l {build_name}.rpt {build_name}.ys", - "nextpnr-ice40 --json {build_name}.json --pcf {build_name}.pcf --asc {build_name}.txt \ - --pre-pack {build_name}_pre_pack.py --{architecture} --package {package} {timefailarg} {ignoreloops} --seed {seed}", - "icepack -s {build_name}.txt {build_name}.bin" -] - -def _build_script(build_template, build_name, architecture, package, timingstrict, ignoreloops, seed): - if sys.platform in ("win32", "cygwin"): - script_ext = ".bat" - script_contents = "@echo off\nrem Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n\n" - fail_stmt = " || exit /b" - else: - script_ext = ".sh" - script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\nset -e\n" - fail_stmt = "" - - for s in build_template: - s_fail = s + "{fail_stmt}\n" # Required so Windows scripts fail early. - script_contents += s_fail.format( - build_name = build_name, - architecture = architecture, - package = package, - timefailarg = "--timing-allow-fail" if not timingstrict else "", - ignoreloops = "--ignore-loops" if ignoreloops else "", - fail_stmt = fail_stmt, - seed = seed) - - script_file = "build_" + build_name + script_ext - tools.write_to_file(script_file, script_contents, force_unix=False) - - return script_file - -def _run_script(script): - if sys.platform in ("win32", "cygwin"): - shell = ["cmd", "/c"] - else: - shell = ["bash"] - - if which("yosys") is None or which("nextpnr-ice40") is None: - msg = "Unable to find Yosys/Nextpnr toolchain, please:\n" - msg += "- Add Yosys/Nextpnr toolchain to your $PATH." - raise OSError(msg) - - if subprocess.call(shell + [script]) != 0: - raise OSError("Error occured during Yosys/Nextpnr's script execution.") +from litex.build.generic_toolchain import GenericToolchain # LatticeIceStormToolchain ------------------------------------------------------------------------- -class LatticeIceStormToolchain: +class LatticeIceStormToolchain(GenericToolchain): attr_translate = { "keep": ("keep", "true"), } @@ -155,67 +28,155 @@ class LatticeIceStormToolchain: special_overrides = common.lattice_ice40_special_overrides def __init__(self): - self.yosys_template = _yosys_template - self.build_template = _build_template - self.clocks = dict() + super().__init__() + self.yosys_template = self._yosys_template + self.build_template = self._build_template def build(self, platform, fragment, - build_dir = "build", - build_name = "top", - synth_opts = "", - run = True, timingstrict = False, ignoreloops = False, seed = 1, **kwargs): - # Create build directory - os.makedirs(build_dir, exist_ok=True) - cwd = os.getcwd() - os.chdir(build_dir) + self.timingstrict = timingstrict + self.ignoreloops = ignoreloops + self.seed = seed - # Finalize design - if not isinstance(fragment, _Fragment): - fragment = fragment.get_fragment() - platform.finalize(fragment) + return self._build(platform, fragment, **kwargs) - # 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) + # IO Constraints (.pcf) ------------------------------------------------------------------------ - # Generate design io constraints file (.pcf) - tools.write_to_file(build_name + ".pcf",_build_pcf(named_sc, named_pc)) + def build_constr_file(self, named_sc, named_pc): + r = "" + for sig, pins, others, resname in named_sc: + if len(pins) > 1: + for bit, pin in enumerate(pins): + r += "set_io {}[{}] {}\n".format(sig, bit, pin) + else: + r += "set_io {} {}\n".format(sig, pins[0]) + if named_pc: + r += "\n" + "\n\n".join(named_pc) + tools.write_to_file(self._build_name + "pcf", r) - # Generate design timing constraints file (in pre_pack file) - tools.write_to_file(build_name + "_pre_pack.py", _build_pre_pack(v_output.ns, self.clocks)) + # Timing Constraints (in pre_pack file) ------------------------------------------------------------ - # Generate Yosys script - _build_yosys(self.yosys_template, platform, build_name, synth_opts=synth_opts) + def build_timing_constr(self, vns, clocks): + r = "" + for clk, period in clocks.items(): + r += """ctx.addClock("{}", {})\n""".format(vns.get_name(clk), 1e3/period) + tools.write_to_file(self._build_name + "_pre_pack.py", r) + # Yosys/Nextpnr Helpers/Templates ------------------------------------------------------------------ + + def _yosys_import_sources(self): + includes = "" + reads = [] + for path in self.platform.verilog_include_paths: + includes += " -I" + path + for filename, language, library, *copy in self.platform.sources: + # yosys has no such function read_systemverilog + if language == "systemverilog": + language = "verilog -sv" + reads.append("read_{}{} {}".format( + language, includes, filename)) + return "\n".join(reads) + + # Project (ys) ------------------------------------------------------------------------------------- + + _yosys_template = [ + "verilog_defaults -push", + "verilog_defaults -add -defer", + "{read_files}", + "verilog_defaults -pop", + "attrmap -tocase keep -imap keep=\"true\" keep=1 -imap keep=\"false\" keep=0 -remove keep=0", + "synth_ice40 {synth_opts} -json {build_name}.json -top {build_name} -dsp", + ] + + def build_project(self): + ys = [] + for l in self._yosys_template: + ys.append(l.format( + build_name = self._build_name, + read_files = self._yosys_import_sources(), + synth_opts = self._synth_opts + )) + tools.write_to_file(self._build_name + ".ys", "\n".join(ys)) + + # Script ------------------------------------------------------------------------------------------- + + _build_template = [ + "yosys -l {build_name}.rpt {build_name}.ys", + "nextpnr-ice40 --json {build_name}.json --pcf {build_name}.pcf --asc {build_name}.txt \ + --pre-pack {build_name}_pre_pack.py --{architecture} --package {package} {timefailarg} {ignoreloops} --seed {seed}", + "icepack -s {build_name}.txt {build_name}.bin" + ] + + def build_script(self): # Translate device to Nextpnr architecture/package - (family, architecture, package) = parse_device(platform.device) + (family, architecture, package) = self.parse_device() - # Generate build script - script = _build_script(self.build_template, build_name, architecture, package, timingstrict, ignoreloops, seed) + if sys.platform in ("win32", "cygwin"): + script_ext = ".bat" + script_contents = "@echo off\nrem Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n\n" + fail_stmt = " || exit /b" + else: + script_ext = ".sh" + script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\nset -e\n" + fail_stmt = "" - # Run - if run: - _run_script(script) + for s in self.build_template: + s_fail = s + "{fail_stmt}\n" # Required so Windows scripts fail early. + script_contents += s_fail.format( + build_name = self._build_name, + architecture = architecture, + package = package, + timefailarg = "--timing-allow-fail" if not self.timingstrict else "", + ignoreloops = "--ignore-loops" if self.ignoreloops else "", + fail_stmt = fail_stmt, + seed = self.seed) - os.chdir(cwd) + script_file = "build_" + self._build_name + script_ext + tools.write_to_file(script_file, script_contents, force_unix=False) - return v_output.ns + return script_file + + def run_script(script): + if sys.platform in ("win32", "cygwin"): + shell = ["cmd", "/c"] + else: + shell = ["bash"] + + if which("yosys") is None or which("nextpnr-ice40") is None: + msg = "Unable to find Yosys/Nextpnr toolchain, please:\n" + msg += "- Add Yosys/Nextpnr toolchain to your $PATH." + raise OSError(msg) + + if subprocess.call(shell + [script]) != 0: + raise OSError("Error occured during Yosys/Nextpnr's script execution.") + + def parse_device(self): + packages = { + "lp384": ["qn32", "cm36", "cm49"], + "lp1k": ["swg16tr", "cm36", "cm49", "cm81", "cb81", "qn84", "cm121", "cb121"], + "hx1k": ["vq100", "cb132", "tq144"], + "lp8k": ["cm81", "cm81:4k", "cm121", "cm121:4k", "cm225", "cm225:4k"], + "hx8k": ["bg121", "bg121:4k", "cb132", "cb132:4k", "cm121", + "cm121:4k", "cm225", "cm225:4k", "cm81", "cm81:4k", + "ct256", "tq144:4k"], + "up3k": ["sg48", "uwg30"], + "up5k": ["sg48", "uwg30"], + "u4k": ["sg48"], + } + + (family, architecture, package) = self.platform.device.split("-") + if family not in ["ice40"]: + raise ValueError("Unknown device family {}".format(family)) + if architecture not in packages.keys(): + raise ValueError("Invalid device architecture {}".format(architecture)) + if package not in packages[architecture]: + raise ValueError("Invalid device package {}".format(package)) + return (family, architecture, package) - def add_period_constraint(self, platform, clk, period): - clk.attr.add("keep") - 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 icestorm_args(parser): toolchain_group = parser.add_argument_group(title="Toolchain options") diff --git a/litex/build/lattice/trellis.py b/litex/build/lattice/trellis.py index 5dc255554..abd213617 100644 --- a/litex/build/lattice/trellis.py +++ b/litex/build/lattice/trellis.py @@ -16,166 +16,11 @@ from migen.fhdl.structure import _Fragment from litex.build.generic_platform import * from litex.build import tools from litex.build.lattice import common - -# IO Constraints (.lpf) ---------------------------------------------------------------------------- - -def _format_constraint(c): - if isinstance(c, Pins): - return ("LOCATE COMP ", " SITE " + "\"" + c.identifiers[0] + "\"") - elif isinstance(c, IOStandard): - return ("IOBUF PORT ", " IO_TYPE=" + c.name) - elif isinstance(c, Misc): - return ("IOBUF PORT ", " " + c.misc) - - -def _format_lpf(signame, pin, others, resname): - fmt_c = [_format_constraint(c) for c in ([Pins(pin)] + others)] - lpf = [] - for pre, suf in fmt_c: - lpf.append(pre + "\"" + signame + "\"" + suf + ";") - return "\n".join(lpf) - - -def _build_lpf(named_sc, named_pc, build_name): - lpf = [] - lpf.append("BLOCK RESETPATHS;") - lpf.append("BLOCK ASYNCPATHS;") - 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 named_pc: - lpf.append("\n\n".join(named_pc)) - tools.write_to_file(build_name + ".lpf", "\n".join(lpf)) - -# Yosys/Nextpnr Helpers/Templates ------------------------------------------------------------------ - -_yosys_template = [ - "verilog_defaults -push", - "verilog_defaults -add -defer", - "{read_files}", - "verilog_defaults -pop", - "attrmap -tocase keep -imap keep=\"true\" keep=1 -imap keep=\"false\" keep=0 -remove keep=0", - "synth_ecp5 {nwl} {abc} -json {build_name}.json -top {build_name}", -] - -def _yosys_import_sources(platform): - includes = "" - reads = [] - for path in platform.verilog_include_paths: - includes += " -I" + path - for filename, language, library, *copy in platform.sources: - # yosys has no such function read_systemverilog - if language == "systemverilog": - language = "verilog -sv" - reads.append("read_{}{} {}".format( - language, includes, filename)) - return "\n".join(reads) - -def _build_yosys(template, platform, nowidelut, abc9, build_name): - ys = [] - for l in template: - ys.append(l.format( - build_name = build_name, - nwl = "-nowidelut" if nowidelut else "", - abc = "-abc9" if abc9 else "", - read_files = _yosys_import_sources(platform) - )) - tools.write_to_file(build_name + ".ys", "\n".join(ys)) - -def nextpnr_ecp5_parse_device(device): - device = device.lower() - family = device.split("-")[0] - size = device.split("-")[1] - speed_grade = device.split("-")[2][0] - if speed_grade not in ["6", "7", "8"]: - raise ValueError("Invalid speed grade {}".format(speed_grade)) - package = device.split("-")[2][1:] - if "256" in package: - package = "CABGA256" - elif "285" in package: - package = "CSFBGA285" - elif "381" in package: - package = "CABGA381" - elif "554" in package: - package = "CABGA554" - elif "756" in package: - package = "CABGA756" - else: - raise ValueError("Invalid package {}".format(package)) - return (family, size, speed_grade, package) - -nextpnr_ecp5_architectures = { - "lfe5u-12f" : "12k", - "lfe5u-25f" : "25k", - "lfe5u-45f" : "45k", - "lfe5u-85f" : "85k", - "lfe5um-25f" : "um-25k", - "lfe5um-45f" : "um-45k", - "lfe5um-85f" : "um-85k", - "lfe5um5g-25f": "um5g-25k", - "lfe5um5g-45f": "um5g-45k", - "lfe5um5g-85f": "um5g-85k", -} - -# Script ------------------------------------------------------------------------------------------- - -_build_template = [ - "yosys -l {build_name}.rpt {build_name}.ys", - "nextpnr-ecp5 --json {build_name}.json --lpf {build_name}.lpf --textcfg {build_name}.config \ - --{architecture} --package {package} --speed {speed_grade} {timefailarg} {ignoreloops} --seed {seed}", - "ecppack {build_name}.config --svf {build_name}.svf --bit {build_name}.bit --bootaddr {bootaddr} {spimode} {compress}" -] - -def _build_script(source, build_template, build_name, architecture, package, speed_grade, timingstrict, ignoreloops, bootaddr, seed, spimode, compress): - if sys.platform in ("win32", "cygwin"): - script_ext = ".bat" - script_contents = "@echo off\nrem Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n\n" - fail_stmt = " || exit /b" - else: - script_ext = ".sh" - script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\nset -e\n" - fail_stmt = "" - - for s in build_template: - s_fail = s + "{fail_stmt}\n" # Required so Windows scripts fail early. - script_contents += s_fail.format( - build_name = build_name, - architecture = architecture, - package = package, - speed_grade = speed_grade, - timefailarg = "--timing-allow-fail" if not timingstrict else "", - ignoreloops = "--ignore-loops" if ignoreloops else "", - bootaddr = bootaddr, - fail_stmt = fail_stmt, - seed = seed, - spimode = "" if spimode is None else "--spimode {}".format(spimode), - compress = "" if not compress else "--compress") - - script_file = "build_" + build_name + script_ext - tools.write_to_file(script_file, script_contents, force_unix=False) - - return script_file - -def _run_script(script): - if sys.platform in ("win32", "cygwin"): - shell = ["cmd", "/c"] - else: - shell = ["bash"] - - if which("yosys") is None or which("nextpnr-ecp5") is None: - msg = "Unable to find Yosys/Nextpnr toolchain, please:\n" - msg += "- Add Yosys/Nextpnr toolchain to your $PATH." - raise OSError(msg) - - if subprocess.call(shell + [script]) != 0: - raise OSError("Error occured during Yosys/Nextpnr's script execution.") +from litex.build.generic_toolchain import GenericToolchain # LatticeTrellisToolchain -------------------------------------------------------------------------- -class LatticeTrellisToolchain: +class LatticeTrellisToolchain(GenericToolchain): attr_translate = { "keep": ("keep", "true"), } @@ -183,14 +28,11 @@ class LatticeTrellisToolchain: special_overrides = common.lattice_ecp5_trellis_special_overrides def __init__(self): - self.yosys_template = _yosys_template - self.build_template = _build_template - self.false_paths = set() # FIXME: use it + super().__init__() + self.yosys_template = self._yosys_template + self.build_template = self._build_template def build(self, platform, fragment, - build_dir = "build", - build_name = "top", - run = True, nowidelut = False, abc9 = False, timingstrict = False, @@ -201,43 +43,177 @@ class LatticeTrellisToolchain: compress = True, **kwargs): - # Create build directory - os.makedirs(build_dir, exist_ok=True) - cwd = os.getcwd() - os.chdir(build_dir) + self._nowidelut = nowidelut + self._abc9 = abc9 + self._timingstrict = timingstrict + self._ignoreloops = ignoreloops + self._bootaddr = bootaddr + self._seed = seed + self._spimode = spimode + self._compress = compress - # Finalize design - if not isinstance(fragment, _Fragment): - fragment = fragment.get_fragment() - platform.finalize(fragment) + return self._build(platform, fragment, **kwargs) - # Generate verilog - v_output = platform.get_verilog(fragment, name=build_name, **kwargs) - named_sc, named_pc = platform.resolve_signals(v_output.ns) - top_file = build_name + ".v" - v_output.write(top_file) - platform.add_source(top_file) + # IO Constraints (.lpf) ---------------------------------------------------------------------------- - # Generate design constraints file (.lpf) - _build_lpf(named_sc, named_pc, build_name) + @classmethod + def _format_constraint(cls, c): + if isinstance(c, Pins): + return ("LOCATE COMP ", " SITE " + "\"" + c.identifiers[0] + "\"") + elif isinstance(c, IOStandard): + return ("IOBUF PORT ", " IO_TYPE=" + c.name) + elif isinstance(c, Misc): + return ("IOBUF PORT ", " " + c.misc) + def _format_lpf(self, signame, pin, others, resname): + fmt_c = [self._format_constraint(c) for c in ([Pins(pin)] + others)] + lpf = [] + for pre, suf in fmt_c: + lpf.append(pre + "\"" + signame + "\"" + suf + ";") + return "\n".join(lpf) + + def build_constr_file(self, named_sc, named_pc): + lpf = [] + lpf.append("BLOCK RESETPATHS;") + lpf.append("BLOCK ASYNCPATHS;") + for sig, pins, others, resname in named_sc: + if len(pins) > 1: + for i, p in enumerate(pins): + lpf.append(self._format_lpf(sig + "[" + str(i) + "]", p, others, resname)) + else: + lpf.append(self._format_lpf(sig, pins[0], others, resname)) + if named_pc: + lpf.append("\n\n".join(named_pc)) + tools.write_to_file(self._build_name + ".lpf", "\n".join(lpf)) + + # Yosys/Nextpnr Helpers/Templates ------------------------------------------------------------------ + + _yosys_template = [ + "verilog_defaults -push", + "verilog_defaults -add -defer", + "{read_files}", + "verilog_defaults -pop", + "attrmap -tocase keep -imap keep=\"true\" keep=1 -imap keep=\"false\" keep=0 -remove keep=0", + "synth_ecp5 {nwl} {abc} -json {build_name}.json -top {build_name}", + ] + + def _yosys_import_sources(self): + includes = "" + reads = [] + for path in self.platform.verilog_include_paths: + includes += " -I" + path + for filename, language, library, *copy in self.platform.sources: + # yosys has no such function read_systemverilog + if language == "systemverilog": + language = "verilog -sv" + reads.append("read_{}{} {}".format( + language, includes, filename)) + return "\n".join(reads) + + def _build_yosys(self): + ys = [] + for l in self._yosys_template: + ys.append(l.format( + build_name = self._build_name, + nwl = "-nowidelut" if self._nowidelut else "", + abc = "-abc9" if self._abc9 else "", + read_files = self._yosys_import_sources() + )) + tools.write_to_file(self._build_name + ".ys", "\n".join(ys)) + + def nextpnr_ecp5_parse_device(self, device): + device = device.lower() + family = device.split("-")[0] + size = device.split("-")[1] + speed_grade = device.split("-")[2][0] + if speed_grade not in ["6", "7", "8"]: + raise ValueError("Invalid speed grade {}".format(speed_grade)) + package = device.split("-")[2][1:] + if "256" in package: + package = "CABGA256" + elif "285" in package: + package = "CSFBGA285" + elif "381" in package: + package = "CABGA381" + elif "554" in package: + package = "CABGA554" + elif "756" in package: + package = "CABGA756" + else: + raise ValueError("Invalid package {}".format(package)) + return (family, size, speed_grade, package) + + nextpnr_ecp5_architectures = { + "lfe5u-12f" : "12k", + "lfe5u-25f" : "25k", + "lfe5u-45f" : "45k", + "lfe5u-85f" : "85k", + "lfe5um-25f" : "um-25k", + "lfe5um-45f" : "um-45k", + "lfe5um-85f" : "um-85k", + "lfe5um5g-25f": "um5g-25k", + "lfe5um5g-45f": "um5g-45k", + "lfe5um5g-85f": "um5g-85k", + } + + # Script ------------------------------------------------------------------------------------------- + + _build_template = [ + "yosys -l {build_name}.rpt {build_name}.ys", + "nextpnr-ecp5 --json {build_name}.json --lpf {build_name}.lpf --textcfg {build_name}.config \ + --{architecture} --package {package} --speed {speed_grade} {timefailarg} {ignoreloops} --seed {seed}", + "ecppack {build_name}.config --svf {build_name}.svf --bit {build_name}.bit --bootaddr {bootaddr} {spimode} {compress}" + ] + + def build_script(self): # Generate Yosys script - _build_yosys(self.yosys_template, platform, nowidelut, abc9, build_name) + self._build_yosys() + # Translate device to Nextpnr architecture/package + (family, size, speed_grade, package) = self.nextpnr_ecp5_parse_device(self.platform.device) + architecture = self.nextpnr_ecp5_architectures[(family + "-" + size)] - # Translate device to Nextpnr architecture/package/speed_grade - (family, size, speed_grade, package) = nextpnr_ecp5_parse_device(platform.device) - architecture = nextpnr_ecp5_architectures[(family + "-" + size)] + if sys.platform in ("win32", "cygwin"): + script_ext = ".bat" + script_contents = "@echo off\nrem Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\n\n" + fail_stmt = " || exit /b" + else: + script_ext = ".sh" + script_contents = "# Autogenerated by LiteX / git: " + tools.get_litex_git_revision() + "\nset -e\n" + fail_stmt = "" - # Generate build script - script = _build_script(False, self.build_template, build_name, architecture, package, - speed_grade, timingstrict, ignoreloops, bootaddr, seed, spimode, compress) - # Run - if run: - _run_script(script) + for s in self._build_template: + s_fail = s + "{fail_stmt}\n" # Required so Windows scripts fail early. + script_contents += s_fail.format( + build_name = self._build_name, + architecture = architecture, + package = package, + speed_grade = speed_grade, + timefailarg = "--timing-allow-fail" if not self._timingstrict else "", + ignoreloops = "--ignore-loops" if self._ignoreloops else "", + bootaddr = self._bootaddr, + fail_stmt = fail_stmt, + seed = self._seed, + spimode = "" if self._spimode is None else f"--spimode {self._spimode}", + compress = "" if not self._compress else "--compress") - os.chdir(cwd) + script_file = "build_" + self._build_name + script_ext + tools.write_to_file(script_file, script_contents, force_unix=False) - return v_output.ns + return script_file + + def run_script(script): + if sys.platform in ("win32", "cygwin"): + shell = ["cmd", "/c"] + else: + shell = ["bash"] + + if which("yosys") is None or which("nextpnr-ecp5") is None: + msg = "Unable to find Yosys/Nextpnr toolchain, please:\n" + msg += "- Add Yosys/Nextpnr toolchain to your $PATH." + raise OSError(msg) + + if subprocess.call(shell + [script]) != 0: + raise OSError("Error occured during Yosys/Nextpnr's script execution.") def add_period_constraint(self, platform, clk, period): platform.add_platform_command("""FREQUENCY PORT "{clk}" {freq} MHz;""".format(